From d74cd7f25e187c7e0c81e1ada181120b6c2a2e91 Mon Sep 17 00:00:00 2001 From: Fadi Shawki Date: Fri, 24 Apr 2026 23:12:22 +0200 Subject: [PATCH] Boilerplate for a remastered FoG/Prison/SkyBlcok --- .../minecraft/bungeecord/BungeeCord.java | 2 +- .../archive/minecraft/spigot/Spigot.java | 2 + .../spigot/_2019/libs/spigot/OMServer.java | 8 +- .../libs/spigot/guis/ServerSelectorGUI.java | 3 +- .../_2019/libs/spigot/utils/ServerUtils.java | 2 + .../minecraft/spigot/servers/fog/FoG.java | 215 +++++++++++ .../spigot/servers/fog/FoGChatHandler.java | 15 + .../spigot/servers/fog/FoGLootHandler.java | 28 ++ .../spigot/servers/fog/FoGPlayer.java | 232 ++++++++++++ .../spigot/servers/fog/ability/Ability.java | 26 ++ .../servers/fog/ability/AbilityRegistry.java | 26 ++ .../servers/fog/beehive/BeehiveHandler.java | 113 ++++++ .../spigot/servers/fog/boss/Boss.java | 42 +++ .../spigot/servers/fog/boss/UndeadKnight.java | 100 +++++ .../spigot/servers/fog/choice/Choice.java | 113 ++++++ .../servers/fog/choice/ChoicePicker.java | 111 ++++++ .../servers/fog/choice/ChoiceState.java | 14 + .../spigot/servers/fog/choice/Rarity.java | 34 ++ .../servers/fog/commands/ChoiceArgument.java | 79 ++++ .../servers/fog/commands/CommandChoice.java | 69 ++++ .../servers/fog/commands/CommandCodex.java | 24 ++ .../fog/commands/CommandCoopAccept.java | 46 +++ .../servers/fog/commands/CommandFactory.java | 24 ++ .../servers/fog/commands/CommandRun.java | 22 ++ .../servers/fog/commands/CommandShop.java | 24 ++ .../servers/fog/commands/CommandWorld.java | 44 +++ .../spigot/servers/fog/crate/Crate.java | 35 ++ .../spigot/servers/fog/crate/CrateType.java | 36 ++ .../servers/fog/database/FoGPlayerModel.java | 85 +++++ .../servers/fog/database/FoGRunModel.java | 103 ++++++ .../spigot/servers/fog/drone/Drone.java | 83 +++++ .../servers/fog/drone/DroneFactory.java | 40 ++ .../spigot/servers/fog/drone/DroneModule.java | 20 + .../spigot/servers/fog/drone/ModuleType.java | 41 +++ .../servers/fog/enchant/CustomEnchant.java | 65 ++++ .../servers/fog/enchant/EnchantRegistry.java | 31 ++ .../fog/events/BlockBreakListener.java | 57 +++ .../servers/fog/events/DamageListener.java | 53 +++ .../servers/fog/events/DeathListener.java | 110 ++++++ .../fog/events/ExperienceSuppressor.java | 50 +++ .../servers/fog/events/FoGCommandEvents.java | 12 + .../fog/events/HologramCleanupListener.java | 30 ++ .../servers/fog/events/ItemDropListener.java | 30 ++ .../fog/events/LockedSlotsListener.java | 71 ++++ .../fog/events/RunGUICloseListener.java | 33 ++ .../spigot/servers/fog/faction/Faction.java | 38 ++ .../spigot/servers/fog/gui/CoopJoinGUI.java | 111 ++++++ .../servers/fog/gui/DifficultySelector.java | 34 ++ .../servers/fog/gui/DroneFactoryGUI.java | 60 +++ .../servers/fog/gui/HologramSelector.java | 205 +++++++++++ .../spigot/servers/fog/gui/RecipeBookGUI.java | 35 ++ .../spigot/servers/fog/gui/RunSelector.java | 75 ++++ .../spigot/servers/fog/gui/ShopGUI.java | 31 ++ .../servers/fog/gui/SpectatorJoinGUI.java | 82 +++++ .../spigot/servers/fog/item/Backpack.java | 28 ++ .../spigot/servers/fog/item/Pickaxe.java | 34 ++ .../spigot/servers/fog/kit/FoGLobbyKit.java | 150 ++++++++ .../servers/fog/level/LevelFormulas.java | 34 ++ .../spigot/servers/fog/level/LevelUpFlow.java | 98 +++++ .../minecraft/spigot/servers/fog/mob/Bee.java | 29 ++ .../spigot/servers/fog/mob/Bull.java | 29 ++ .../spigot/servers/fog/mob/DebuffMage.java | 78 ++++ .../spigot/servers/fog/mob/FireMage.java | 66 ++++ .../spigot/servers/fog/mob/FoGMob.java | 46 +++ .../spigot/servers/fog/mob/FoGZombie.java | 32 ++ .../minecraft/spigot/servers/fog/ore/Ore.java | 40 ++ .../spigot/servers/fog/quest/Quest.java | 28 ++ .../servers/fog/quest/QuestGenerator.java | 35 ++ .../spigot/servers/fog/quest/QuestType.java | 6 + .../spigot/servers/fog/raid/Raid.java | 83 +++++ .../spigot/servers/fog/raid/RaidManager.java | 44 +++ .../spigot/servers/fog/recipe/RecipeBook.java | 25 ++ .../servers/fog/recipe/RecipeEntry.java | 26 ++ .../spigot/servers/fog/run/Difficulty.java | 45 +++ .../minecraft/spigot/servers/fog/run/Run.java | 89 +++++ .../spigot/servers/fog/run/RunManager.java | 307 ++++++++++++++++ .../spigot/servers/fog/run/RunState.java | 19 + .../spigot/servers/fog/run/RunStore.java | 341 ++++++++++++++++++ .../servers/fog/scoreboard/FoGScoreboard.java | 35 ++ .../spigot/servers/fog/shop/Shop.java | 27 ++ .../spigot/servers/fog/shop/ShopOffer.java | 25 ++ .../spigot/servers/fog/skill/Skill.java | 18 + .../spigot/servers/fog/stats/Stats.java | 145 ++++++++ .../spigot/servers/fog/stats/StatsHolder.java | 17 + .../servers/fog/stats/ToolStatsStamp.java | 51 +++ .../spigot/servers/fog/stats/ToolType.java | 28 ++ .../servers/fog/structure/Compartment.java | 68 ++++ .../fog/structure/CompartmentType.java | 30 ++ .../spigot/servers/fog/tree/Tree.java | 35 ++ .../spigot/servers/fog/util/Fogi18n.java | 23 ++ .../spigot/servers/fog/util/HologramTag.java | 61 ++++ .../servers/fog/util/InventoryBlob.java | 64 ++++ .../spigot/servers/fog/world/CaveScorer.java | 76 ++++ .../spigot/servers/fog/world/CityWall.java | 44 +++ .../spigot/servers/fog/world/FoGWorld.java | 43 +++ .../servers/fog/world/MapExpansion.java | 37 ++ .../fog/world/StainedGlassBarrier.java | 39 ++ .../src/main/resources/fog_en_US.properties | 313 ++++++++++++++++ .../src/main/resources/fog_nl_NL.properties | 313 ++++++++++++++++ .../main/resources/spigot_en_US.properties | 3 + .../main/resources/spigot_nl_NL.properties | 3 + 101 files changed, 6281 insertions(+), 3 deletions(-) create mode 100644 @orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/FoG.java create mode 100644 @orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/FoGChatHandler.java create mode 100644 @orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/FoGLootHandler.java create mode 100644 @orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/FoGPlayer.java create mode 100644 @orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/ability/Ability.java create mode 100644 @orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/ability/AbilityRegistry.java create mode 100644 @orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/beehive/BeehiveHandler.java create mode 100644 @orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/boss/Boss.java create mode 100644 @orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/boss/UndeadKnight.java create mode 100644 @orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/choice/Choice.java create mode 100644 @orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/choice/ChoicePicker.java create mode 100644 @orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/choice/ChoiceState.java create mode 100644 @orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/choice/Rarity.java create mode 100644 @orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/commands/ChoiceArgument.java create mode 100644 @orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/commands/CommandChoice.java create mode 100644 @orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/commands/CommandCodex.java create mode 100644 @orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/commands/CommandCoopAccept.java create mode 100644 @orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/commands/CommandFactory.java create mode 100644 @orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/commands/CommandRun.java create mode 100644 @orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/commands/CommandShop.java create mode 100644 @orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/commands/CommandWorld.java create mode 100644 @orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/crate/Crate.java create mode 100644 @orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/crate/CrateType.java create mode 100644 @orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/database/FoGPlayerModel.java create mode 100644 @orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/database/FoGRunModel.java create mode 100644 @orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/drone/Drone.java create mode 100644 @orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/drone/DroneFactory.java create mode 100644 @orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/drone/DroneModule.java create mode 100644 @orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/drone/ModuleType.java create mode 100644 @orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/enchant/CustomEnchant.java create mode 100644 @orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/enchant/EnchantRegistry.java create mode 100644 @orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/events/BlockBreakListener.java create mode 100644 @orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/events/DamageListener.java create mode 100644 @orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/events/DeathListener.java create mode 100644 @orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/events/ExperienceSuppressor.java create mode 100644 @orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/events/FoGCommandEvents.java create mode 100644 @orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/events/HologramCleanupListener.java create mode 100644 @orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/events/ItemDropListener.java create mode 100644 @orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/events/LockedSlotsListener.java create mode 100644 @orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/events/RunGUICloseListener.java create mode 100644 @orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/faction/Faction.java create mode 100644 @orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/gui/CoopJoinGUI.java create mode 100644 @orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/gui/DifficultySelector.java create mode 100644 @orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/gui/DroneFactoryGUI.java create mode 100644 @orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/gui/HologramSelector.java create mode 100644 @orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/gui/RecipeBookGUI.java create mode 100644 @orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/gui/RunSelector.java create mode 100644 @orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/gui/ShopGUI.java create mode 100644 @orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/gui/SpectatorJoinGUI.java create mode 100644 @orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/item/Backpack.java create mode 100644 @orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/item/Pickaxe.java create mode 100644 @orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/kit/FoGLobbyKit.java create mode 100644 @orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/level/LevelFormulas.java create mode 100644 @orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/level/LevelUpFlow.java create mode 100644 @orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/mob/Bee.java create mode 100644 @orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/mob/Bull.java create mode 100644 @orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/mob/DebuffMage.java create mode 100644 @orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/mob/FireMage.java create mode 100644 @orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/mob/FoGMob.java create mode 100644 @orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/mob/FoGZombie.java create mode 100644 @orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/ore/Ore.java create mode 100644 @orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/quest/Quest.java create mode 100644 @orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/quest/QuestGenerator.java create mode 100644 @orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/quest/QuestType.java create mode 100644 @orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/raid/Raid.java create mode 100644 @orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/raid/RaidManager.java create mode 100644 @orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/recipe/RecipeBook.java create mode 100644 @orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/recipe/RecipeEntry.java create mode 100644 @orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/run/Difficulty.java create mode 100644 @orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/run/Run.java create mode 100644 @orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/run/RunManager.java create mode 100644 @orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/run/RunState.java create mode 100644 @orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/run/RunStore.java create mode 100644 @orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/scoreboard/FoGScoreboard.java create mode 100644 @orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/shop/Shop.java create mode 100644 @orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/shop/ShopOffer.java create mode 100644 @orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/skill/Skill.java create mode 100644 @orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/stats/Stats.java create mode 100644 @orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/stats/StatsHolder.java create mode 100644 @orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/stats/ToolStatsStamp.java create mode 100644 @orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/stats/ToolType.java create mode 100644 @orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/structure/Compartment.java create mode 100644 @orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/structure/CompartmentType.java create mode 100644 @orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/tree/Tree.java create mode 100644 @orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/util/Fogi18n.java create mode 100644 @orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/util/HologramTag.java create mode 100644 @orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/util/InventoryBlob.java create mode 100644 @orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/world/CaveScorer.java create mode 100644 @orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/world/CityWall.java create mode 100644 @orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/world/FoGWorld.java create mode 100644 @orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/world/MapExpansion.java create mode 100644 @orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/world/StainedGlassBarrier.java create mode 100644 @orbitmines/minecraft/remastered/spigot/src/main/resources/fog_en_US.properties create mode 100644 @orbitmines/minecraft/remastered/spigot/src/main/resources/fog_nl_NL.properties diff --git a/@orbitmines/minecraft/remastered/bungeecord/src/main/java/com/orbitmines/archive/minecraft/bungeecord/BungeeCord.java b/@orbitmines/minecraft/remastered/bungeecord/src/main/java/com/orbitmines/archive/minecraft/bungeecord/BungeeCord.java index 7fb7ac997..a94e110ee 100644 --- a/@orbitmines/minecraft/remastered/bungeecord/src/main/java/com/orbitmines/archive/minecraft/bungeecord/BungeeCord.java +++ b/@orbitmines/minecraft/remastered/bungeecord/src/main/java/com/orbitmines/archive/minecraft/bungeecord/BungeeCord.java @@ -29,7 +29,7 @@ public void onLoad() { public void onEnable() { bungeecord.onEnable(); - for (Server type : List.of(Server.HUB, Server.KITPVP, Server.SURVIVAL, Server.CREATIVE)) { + for (Server type : List.of(Server.HUB, Server.KITPVP, Server.SURVIVAL, Server.CREATIVE, Server.FOG)) { MinecraftServer server = new MinecraftServer(type, "26.1.2", Environment.get("OM_RAM_" + type.toString(), Environment.get("OM_RAM_DEFAULT", "2G")), findAvailablePort()); server.run(); this.registerServer(server); diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/archive/minecraft/spigot/Spigot.java b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/archive/minecraft/spigot/Spigot.java index 460e9eb8e..92e97c366 100644 --- a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/archive/minecraft/spigot/Spigot.java +++ b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/archive/minecraft/spigot/Spigot.java @@ -8,6 +8,7 @@ import com.orbitmines.archive.minecraft.spigot._2019.servers.kitpvp.KitPvP; import com.orbitmines.archive.minecraft.spigot._2019.servers.survival.Survival; import com.orbitmines.archive.minecraft.spigot._2019.utils.spigot.placeholders.SpigotServer; +import com.orbitmines.minecraft.spigot.servers.fog.FoG; import org.bukkit.plugin.java.JavaPlugin; public class Spigot extends JavaPlugin { @@ -23,6 +24,7 @@ public void onLoad() { case HUB -> new Hub(this); case KITPVP -> new KitPvP(this); case CREATIVE -> new Creative(this); + case FOG -> new FoG(this); default -> throw new IllegalArgumentException("Unsupported server type: " + type); }; diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/archive/minecraft/spigot/_2019/libs/spigot/OMServer.java b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/archive/minecraft/spigot/_2019/libs/spigot/OMServer.java index 04baca826..7471ae31f 100644 --- a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/archive/minecraft/spigot/_2019/libs/spigot/OMServer.java +++ b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/archive/minecraft/spigot/_2019/libs/spigot/OMServer.java @@ -19,6 +19,8 @@ import com.orbitmines.archive.minecraft.spigot._2019.libs.spigot.database.models.survival.*; import com.orbitmines.archive.minecraft.spigot._2019.libs.spigot.database.models.creative.CreativePlayerModel; import com.orbitmines.archive.minecraft.spigot._2019.servers.creative.CreativeWorldMember; +import com.orbitmines.minecraft.spigot.servers.fog.database.FoGPlayerModel; +import com.orbitmines.minecraft.spigot.servers.fog.database.FoGRunModel; import com.orbitmines.archive.minecraft.spigot._2019.libs.spigot.discord.SpigotDiscordBot; import com.orbitmines.archive.minecraft.spigot._2019.libs.spigot.events.*; import com.orbitmines.archive.minecraft.spigot._2019.utils.spigot.prevention.Prevention; @@ -221,7 +223,11 @@ public void onStart() { /* Creative */ CreativePlayerModel.TABLE, - CreativeWorldMember.TABLE + CreativeWorldMember.TABLE, + + /* FoG */ + FoGPlayerModel.TABLE, + FoGRunModel.TABLE ); /* Register to bungeecord startup subscriber first, in case it is starting up/down */ diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/archive/minecraft/spigot/_2019/libs/spigot/guis/ServerSelectorGUI.java b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/archive/minecraft/spigot/_2019/libs/spigot/guis/ServerSelectorGUI.java index e5a82c9eb..49916303a 100644 --- a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/archive/minecraft/spigot/_2019/libs/spigot/guis/ServerSelectorGUI.java +++ b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/archive/minecraft/spigot/_2019/libs/spigot/guis/ServerSelectorGUI.java @@ -21,7 +21,6 @@ public class ServerSelectorGUI, P extends OMPlayer unknownGalaxies = Arrays.asList( (3 * 9) + 1, - (3 * 9) + 5, (3 * 9) + 7, (4 * 9) + 2, (4 * 9) + 6 @@ -39,6 +38,8 @@ else if (i == (3 * 9) + 5) setItem(i, Server.CREATIVE); else if (i == (3 * 9) + 3) setItem(i, Server.KITPVP); + else if (i == (2 * 9) + 4) + setItem(i, Server.FOG); else if (unknownGalaxies.contains(i)) set(i, new Item(() -> new ItemBuilder(Material.RED_STAINED_GLASS_PANE, 1, "§c" + viewer.translate("spigot", "player.unknown_galaxies")))); else diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/archive/minecraft/spigot/_2019/libs/spigot/utils/ServerUtils.java b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/archive/minecraft/spigot/_2019/libs/spigot/utils/ServerUtils.java index 31b97a264..dc59931c8 100644 --- a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/archive/minecraft/spigot/_2019/libs/spigot/utils/ServerUtils.java +++ b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/archive/minecraft/spigot/_2019/libs/spigot/utils/ServerUtils.java @@ -22,6 +22,8 @@ public static ItemBuilder getItemBuilder(Server server) { return new ItemBuilder(Material.IRON_SWORD).addFlag(ItemFlag.HIDE_ATTRIBUTES); case CREATIVE: return new ItemBuilder(Material.WOODEN_AXE).addFlag(ItemFlag.HIDE_ATTRIBUTES); + case FOG: + return new ItemBuilder(Material.NETHER_STAR).addFlag(ItemFlag.HIDE_ATTRIBUTES); default: throw new IllegalStateException(); } diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/FoG.java b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/FoG.java new file mode 100644 index 000000000..18bf20f8a --- /dev/null +++ b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/FoG.java @@ -0,0 +1,215 @@ +package com.orbitmines.minecraft.spigot.servers.fog; + +import com.orbitmines.archive.minecraft._2019.libs.Server; +import com.orbitmines.archive.minecraft._2019.libs.player.PlayerInstance; +import com.orbitmines.archive.minecraft.spigot._2019.libs.spigot.ChatHandler; +import com.orbitmines.archive.minecraft.spigot._2019.libs.spigot.OMServer; +import com.orbitmines.archive.minecraft.spigot._2019.libs.spigot.TabListHandler; +import com.orbitmines.archive.minecraft.spigot._2019.libs.spigot.commands.brigadier.Command; +import com.orbitmines.archive.minecraft.spigot._2019.libs.spigot.database.models.OMMap; +import com.orbitmines.archive.minecraft.spigot._2019.libs.spigot.events.CommandEvents; +import com.orbitmines.archive.minecraft.spigot._2019.libs.spigot.loot.LootHandler; +import com.orbitmines.archive.minecraft.spigot._2019.utils.spigot.freezer.events.FreezeEvents; +import com.orbitmines.archive.minecraft.spigot._2019.utils.spigot.prevention.Prevention; +import com.orbitmines.archive.minecraft.spigot._2019.utils.spigot.worlds.maps.datapoints.DataPointHandler; +import com.orbitmines.minecraft.spigot.servers.fog.ability.AbilityRegistry; +import com.orbitmines.minecraft.spigot.servers.fog.commands.CommandChoice; +import com.orbitmines.minecraft.spigot.servers.fog.commands.CommandCodex; +import com.orbitmines.minecraft.spigot.servers.fog.commands.CommandCoopAccept; +import com.orbitmines.minecraft.spigot.servers.fog.commands.CommandFactory; +import com.orbitmines.minecraft.spigot.servers.fog.commands.CommandRun; +import com.orbitmines.minecraft.spigot.servers.fog.commands.CommandShop; +import com.orbitmines.minecraft.spigot.servers.fog.commands.CommandWorld; +import com.orbitmines.minecraft.spigot.servers.fog.events.BlockBreakListener; +import com.orbitmines.minecraft.spigot.servers.fog.events.DamageListener; +import com.orbitmines.minecraft.spigot.servers.fog.events.DeathListener; +import com.orbitmines.minecraft.spigot.servers.fog.events.ExperienceSuppressor; +import com.orbitmines.minecraft.spigot.servers.fog.events.FoGCommandEvents; +import com.orbitmines.minecraft.spigot.servers.fog.events.ItemDropListener; +import com.orbitmines.minecraft.spigot.servers.fog.events.LockedSlotsListener; +import com.orbitmines.minecraft.spigot.servers.fog.kit.FoGLobbyKit; +import com.orbitmines.minecraft.spigot.servers.fog.level.LevelUpFlow; +import com.orbitmines.minecraft.spigot.servers.fog.raid.RaidManager; +import com.orbitmines.minecraft.spigot.servers.fog.run.Run; +import com.orbitmines.minecraft.spigot.servers.fog.run.RunManager; +import lombok.Getter; +import org.bukkit.Location; +import org.bukkit.plugin.java.JavaPlugin; +import org.bukkit.scheduler.BukkitRunnable; + +public class FoG extends OMServer { + + @Getter private RunManager runManager; + @Getter private LevelUpFlow levelUpFlow; + @Getter private AbilityRegistry abilityRegistry; + @Getter private FoGLobbyKit lobbyKit; + + public FoG(JavaPlugin plugin) { + super(plugin); + } + + @Override + public FoGPlayer newPlayerInstance(org.bukkit.entity.Player player) { + return new FoGPlayer(player, this); + } + + @Override + public void afterStartupSync() { + this.runManager = new RunManager(this); + this.levelUpFlow = new LevelUpFlow(this); + this.abilityRegistry = new AbilityRegistry(); + this.lobbyKit = new FoGLobbyKit(this); + + /* Hologram tag setup + sweep any worlds already loaded at startup. */ + com.orbitmines.minecraft.spigot.servers.fog.util.HologramTag.init(plugin); + int removed = com.orbitmines.minecraft.spigot.servers.fog.util.HologramTag.cleanupAllLoaded(); + if (removed > 0) { + org.bukkit.Bukkit.getLogger().info("[fog] Startup swept " + removed + " stale hologram entities across loaded worlds."); + } + + registerCommands( + new CommandRun(this), + new CommandWorld(this), + new CommandShop(this), + new CommandCodex(this), + new CommandFactory(this), + new CommandChoice(this), + new CommandCoopAccept(this) + ); + + registerEvents( + new ExperienceSuppressor(this), + new DeathListener(this), + new DamageListener(this), + new BlockBreakListener(this), + new ItemDropListener(this), + new LockedSlotsListener(this), + new FreezeEvents(this), + new com.orbitmines.minecraft.spigot.servers.fog.events.HologramCleanupListener(this), + new com.orbitmines.minecraft.spigot.servers.fog.events.RunGUICloseListener(this) + ); + + /* Per-run raid + mob ticking */ + new BukkitRunnable() { + @Override + public void run() { + for (FoGPlayer player : getPlayers()) { + Run run = player.getActiveRun(); + if (run == null) continue; + RaidManager rm = player.getRaidManager(); + if (rm != null) rm.tick(); + } + } + }.runTaskTimer(plugin, 20L, 20L); + + /* Periodic stats flush for all online players (async thread) */ + new BukkitRunnable() { + @Override + public void run() { + for (FoGPlayer player : getPlayers()) { + com.orbitmines.minecraft.spigot.servers.fog.stats.Stats s = player.getStats(); + if (s == null) continue; + try { s.flush(); } catch (Exception ignored) {} + } + } + }.runTaskTimerAsynchronously(plugin, 20L * 30, 20L * 30); + } + + @Override + public CommandEvents newCommandEventsInstance() { + return new FoGCommandEvents(this); + } + + /** + * Skip the base class's {@code PatchNotes.build()} — FoG has no patch notes + * registered yet, and the default builder NPEs on {@code getLatest(FOG)}. + */ + @Override + public void afterStartupAsync() { + /* intentionally empty */ + } + + private void registerCommands(Command... commands) { + for (Command command : commands) { + command.register(); + } + } + + /** + * Creative-style elevated spawn for players who have no active run yet, so + * the run selector holograms are visible in open air. Uses the default + * world, positions the player high above its spawn, and enables flight. + */ + public void teleportToSkyAnchor(FoGPlayer player) { + org.bukkit.World world = org.bukkit.Bukkit.getWorlds().get(0); + org.bukkit.Location spawn = world.getSpawnLocation(); + org.bukkit.Location anchor = new org.bukkit.Location(world, spawn.getX(), 200, spawn.getZ(), 0f, 0f); + player.setGameMode(org.bukkit.GameMode.ADVENTURE); + player.teleport(anchor); + /* Enable flight one tick later — Bukkit may reset allow-flight during a + cross-world teleport, and calling setFlying(true) before that settles + throws IllegalArgumentException. Also clear the onJoin invulnerability + flag once the player is safely in the sky with flight. */ + new org.bukkit.scheduler.BukkitRunnable() { + @Override public void run() { + if (!player.bukkit().isOnline()) return; + player.setAllowFlight(true); + try { + player.setFlying(true); + } catch (IllegalArgumentException ignored) { + /* Gamemode changed between the teleport and this tick — accept it. */ + } + player.bukkit().setInvulnerable(false); + } + }.runTaskLater(plugin, 1L); + } + + @Override + public Server getType() { + return Server.FOG; + } + + @Override + protected FoG instance() { + return this; + } + + @Override + public ChatHandler newChatHandler(PlayerInstance sender, ChatHandler.Type type, String message) { + return new FoGChatHandler(this, type, sender, message); + } + + @Override + public TabListHandler newTabListHandler(FoGPlayer player) { + return new TabListHandler<>(this, player); + } + + @Override + public LootHandler newLootHandler(FoGPlayer player) { + return new FoGLootHandler(player); + } + + @Override + public boolean clearPlayerData() { return false; } + + @Override + public boolean saveChunksOnRestart() { return true; } + + @Override + public boolean broadcastWhenSaving() { return true; } + + @Override + public boolean shouldSetupLobby() { return false; } + + @Override + public DataPointHandler createDataPointHandler(OMMap.Type type) { return null; } + + @Override + public Prevention[] getLobbyPreventions() { return new Prevention[0]; } + + @Override + public void setupNpc(String string, Location location) { } + + @Override + protected void instantiateLeaderBoards() { } +} diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/FoGChatHandler.java b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/FoGChatHandler.java new file mode 100644 index 000000000..1d78cb2ab --- /dev/null +++ b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/FoGChatHandler.java @@ -0,0 +1,15 @@ +package com.orbitmines.minecraft.spigot.servers.fog; + +import com.orbitmines.archive.minecraft._2019.libs.player.PlayerInstance; +import com.orbitmines.archive.minecraft.spigot._2019.libs.spigot.ChatHandler; + +public class FoGChatHandler extends ChatHandler { + + public FoGChatHandler(FoG server, Type type, PlayerInstance sender) { + super(server, type, sender); + } + + public FoGChatHandler(FoG server, Type type, PlayerInstance sender, String message) { + super(server, type, sender, message); + } +} diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/FoGLootHandler.java b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/FoGLootHandler.java new file mode 100644 index 000000000..5df52212a --- /dev/null +++ b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/FoGLootHandler.java @@ -0,0 +1,28 @@ +package com.orbitmines.minecraft.spigot.servers.fog; + +import com.orbitmines.archive.minecraft._2019.libs.database.models.loot.LootItem; +import com.orbitmines.archive.minecraft._2019.libs.database.models.loot.PeriodLootItem; +import com.orbitmines.archive.minecraft.spigot._2019.libs.spigot.loot.LootHandler; +import com.orbitmines.archive.minecraft.spigot._2019.libs.spigot.loot.LootItemType; + +public class FoGLootHandler extends LootHandler { + + public FoGLootHandler(FoGPlayer player) { + super(player); + } + + @Override + protected void give(LootItemType type, LootItem... items) { + /* POC: nothing — vote/donation rewards are not wired yet. */ + } + + @Override + protected void give(PeriodLootItem item) { + /* POC: nothing. */ + } + + @Override + protected boolean canReceive(PeriodLootItem.Type type) { + return true; + } +} diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/FoGPlayer.java b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/FoGPlayer.java new file mode 100644 index 000000000..dab37c565 --- /dev/null +++ b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/FoGPlayer.java @@ -0,0 +1,232 @@ +package com.orbitmines.minecraft.spigot.servers.fog; + +import com.orbitmines.archive.minecraft.spigot._2019.libs.spigot.OMPlayer; +import com.orbitmines.minecraft.spigot.servers.fog.database.FoGPlayerModel; +import com.orbitmines.minecraft.spigot.servers.fog.drone.Drone; +import com.orbitmines.minecraft.spigot.servers.fog.gui.RunSelector; +import com.orbitmines.minecraft.spigot.servers.fog.level.LevelFormulas; +import com.orbitmines.minecraft.spigot.servers.fog.raid.RaidManager; +import com.orbitmines.minecraft.spigot.servers.fog.run.Difficulty; +import com.orbitmines.minecraft.spigot.servers.fog.run.Run; +import com.orbitmines.minecraft.spigot.servers.fog.scoreboard.FoGScoreboard; +import com.orbitmines.minecraft.spigot.servers.fog.stats.Stats; +import com.orbitmines.minecraft.spigot.servers.fog.stats.StatsHolder; +import com.orbitmines.minecraft.spigot.servers.fog.util.InventoryBlob; +import lombok.Getter; +import lombok.Setter; +import org.bukkit.attribute.Attribute; +import org.bukkit.attribute.AttributeInstance; +import org.bukkit.entity.Player; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +public class FoGPlayer extends OMPlayer implements StatsHolder { + + @Getter private FoGPlayerModel playerModel; + @Getter @Setter private Run activeRun; + @Getter private RaidManager raidManager; + @Getter private Stats stats; + @Getter private final List drones = new ArrayList<>(); + @Getter @Setter private boolean dronesActive = false; + + /** Pending coop join requests: requester raw name → run id. */ + @Getter private final Map coopRequestsFrom = new HashMap<>(); + + /** Captured on quit (sync) so {@link #afterQuitAsync()} can write to the DB. */ + private transient String pendingInventoryBlob; + private transient Run pendingInventoryRun; + + public FoGPlayer(Player player, FoG server) { + super(player, server); + } + + @Override + protected void register() { + server.registerPlayer(this); + } + + @Override + protected void unregister() { + server.unregisterPlayer(this); + } + + @Override + public FoGPlayer getInstance() { + return this; + } + + @Override + public boolean onJoin() { + super.onJoin(); + this.playerModel = FoGPlayerModel.findOrInitializeBy(getUUID()); + if (!this.playerModel.isInserted()) this.playerModel.insert(); + + server.runSync(() -> { + /* Fall-damage safety net — the player may land mid-air if their last + logout location was the sky anchor. Invulnerability only here; the + downstream paths (joinRun / teleportToSkyAnchor) own flight state. + Setting flight during the initial join handshake causes client + desync (player can't move, tab/chat silent, no error). */ + bukkit().setInvulnerable(true); + + resetScoreboard(); + setScoreboard(new FoGScoreboard(server, this)); + + /* Always-on kit (Switch Run + Spectate) so the player has an escape + route regardless of state (spectating, no run, or in their run). */ + server.getLobbyKit().copyAlwaysOnToInventory(this); + + if (playerModel.getActiveRunId() != null) { + server.getRunManager().joinRun(this, playerModel.getActiveRunId()); + } else { + /* No active run — elevate the player to a creative-style sky + anchor in the default world and open the run selector. */ + server.teleportToSkyAnchor(this); + RunSelector.open(server, this); + } + }); + + return true; + } + + @Override + public void beforeQuitSync() { + /* Sync thread: safe to serialize the inventory, NOT to write the DB. + Capture the blob for afterQuitAsync to persist. */ + if (activeRun != null) { + pendingInventoryRun = activeRun; + pendingInventoryBlob = InventoryBlob.serialize(bukkit()); + } + for (Drone d : drones) d.despawn(); + super.beforeQuitSync(); + } + + @Override + public void afterQuitAsync() { + if (pendingInventoryRun != null) { + pendingInventoryRun.getStore().setMemberInventoryBlob(getUUID(), pendingInventoryBlob); + } + super.afterQuitAsync(); + } + + @Override + public void onFirstLogin() { + /* The sky-anchor + RunSelector.open flow runs in onJoin for everyone + without an active run, so first-login has nothing additional to do. */ + } + + /** Give the freshly-created-run spawn inventory. Only call on run start / first join. */ + public void giveRunStartInventory() { + server.getLobbyKit().copyToInventory(this); + } + + public void toggleDroneMode() { + setDronesActive(!dronesActive); + for (Drone d : drones) d.setMode(dronesActive ? Drone.Mode.ACTIVE : Drone.Mode.IDLE); + /* Kit item material flips LEVER / REDSTONE_TORCH — refresh the slot. + The ItemHoverActionBar re-renders every tick, so the ACTIVE/idle label + updates on its own — no chat message. */ + server.getLobbyKit().refreshDroneToggle(this); + } + + /** In-memory cache of the player's total run XP. Populated on initForRun from a + pre-fetched async read; kept in sync by addExperience. Sync-safe to read. */ + private volatile long cachedTotalXp; + + public int getLevel() { + if (activeRun == null) return 0; + return LevelFormulas.computeLevel(cachedTotalXp); + } + + public long getTotalXp() { + return cachedTotalXp; + } + + /** Async-only — writes to the RunStore. */ + public void addExperience(long amount) { + Run run = activeRun; + if (run == null) return; + int before = getLevel(); + long newTotal = cachedTotalXp + amount; + cachedTotalXp = newTotal; + run.getStore().setMemberExperience(getUUID(), newTotal); + int after = LevelFormulas.computeLevel(newTotal); + run.getStore().setMemberLevel(getUUID(), after); + server.runSync(this::updateExperienceBar); + if (after > before) { + int fBefore = before, fAfter = after; + server.runSync(() -> { + for (int lvl = fBefore + 1; lvl <= fAfter; lvl++) { + server.getLevelUpFlow().start(this, lvl, run); + } + }); + } + } + + /** Sync-safe — reads only the cached XP. */ + public void updateExperienceBar() { + if (activeRun == null) return; + int level = getLevel(); + long into = LevelFormulas.xpIntoLevel(cachedTotalXp, level); + long required = LevelFormulas.required(level); + bukkit().setLevel(level); + bukkit().setExp(required == 0 ? 0f : Math.min(1f, (float) into / (float) required)); + } + + /** Sync-safe: reads only the in-memory stats map. */ + public void applyDerivedStats() { + if (activeRun == null || stats == null) return; + AttributeInstance maxHp = bukkit().getAttribute(Attribute.MAX_HEALTH); + if (maxHp != null) maxHp.setBaseValue(Math.max(2.0, 20.0 + stats.getExtraHearts() * 2.0)); + AttributeInstance speed = bukkit().getAttribute(Attribute.MOVEMENT_SPEED); + if (speed != null) speed.setBaseValue(0.1 * stats.getSpeedMultiplier()); + } + + /** + * Sync-only. Applies a new run to this player. DB reads/writes are the caller's + * responsibility — see {@link com.orbitmines.minecraft.spigot.servers.fog.run.RunManager}. + * + * @param newRun new run (or null to leave) + * @param restoreFromBlob pre-fetched inventory blob; null means don't touch inventory + * @param preloadedStats Stats already loaded async by the caller; becomes this + * player's stats handle. Pass a fresh empty Stats for new runs. + * @param preloadedTotalXp Member experience pre-fetched from the store (async). + */ + public void initForRun(Run newRun, String restoreFromBlob, Stats preloadedStats, long preloadedTotalXp) { + this.activeRun = newRun; + if (newRun != null) { + this.raidManager = new RaidManager(newRun); + this.stats = preloadedStats; + this.cachedTotalXp = preloadedTotalXp; + if (restoreFromBlob != null) InventoryBlob.deserializeInto(bukkit(), restoreFromBlob); + applyDerivedStats(); + updateExperienceBar(); + } else { + this.raidManager = null; + this.stats = null; + this.cachedTotalXp = 0L; + } + } + + @Override + public UUID getOwnerUuid() { return getUUID(); } + + @Override + public String getStatsPrefix() { return "member:" + getUUID(); } + + @Override + public Stats getStats() { + /* Returns the already-loaded Stats handle, or null if no run is active. + Never allocates here — a new Stats would be empty until load() runs async, + which would silently report zeros. Callers must tolerate null. */ + return stats; + } + + public Difficulty getDifficulty() { + return activeRun != null ? activeRun.getDifficulty() : Difficulty.NORMAL; + } +} diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/ability/Ability.java b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/ability/Ability.java new file mode 100644 index 000000000..0acdc7880 --- /dev/null +++ b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/ability/Ability.java @@ -0,0 +1,26 @@ +package com.orbitmines.minecraft.spigot.servers.fog.ability; + +import com.orbitmines.archive.minecraft._2019.libs.player.Languageable; +import com.orbitmines.minecraft.spigot.servers.fog.choice.Choice; +import lombok.Getter; + +public enum Ability { + + SHIELD_BASH(Choice.ABILITY_SHIELD, 8000, 3.0), + SWEEP_ATTACK(Choice.ABILITY_SWEEP, 4000, 2.0), + LEAP(Choice.ABILITY_LEAP, 6000, 0.0); + + @Getter private final Choice unlockChoice; + @Getter private final long cooldownMs; + @Getter private final double damage; + + Ability(Choice unlockChoice, long cooldownMs, double damage) { + this.unlockChoice = unlockChoice; + this.cooldownMs = cooldownMs; + this.damage = damage; + } + + public String nameKey() { return "ability." + name().toLowerCase() + ".name"; } + + public String getDisplayName(Languageable viewer) { return viewer.translate("fog", nameKey()); } +} diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/ability/AbilityRegistry.java b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/ability/AbilityRegistry.java new file mode 100644 index 000000000..4378acf17 --- /dev/null +++ b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/ability/AbilityRegistry.java @@ -0,0 +1,26 @@ +package com.orbitmines.minecraft.spigot.servers.fog.ability; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +/** Per-player ability cooldown tracker. */ +public class AbilityRegistry { + + private final Map> cooldowns = new HashMap<>(); + + public boolean tryTrigger(UUID uuid, Ability ability) { + Map map = cooldowns.computeIfAbsent(uuid, k -> new HashMap<>()); + long now = System.currentTimeMillis(); + long ready = map.getOrDefault(ability, 0L); + if (now < ready) return false; + map.put(ability, now + ability.getCooldownMs()); + return true; + } + + public long remaining(UUID uuid, Ability ability) { + Map map = cooldowns.get(uuid); + if (map == null) return 0; + return Math.max(0, map.getOrDefault(ability, 0L) - System.currentTimeMillis()); + } +} diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/beehive/BeehiveHandler.java b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/beehive/BeehiveHandler.java new file mode 100644 index 000000000..4dfaaa466 --- /dev/null +++ b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/beehive/BeehiveHandler.java @@ -0,0 +1,113 @@ +package com.orbitmines.minecraft.spigot.servers.fog.beehive; + +import com.orbitmines.minecraft.spigot.servers.fog.run.Run; +import com.orbitmines.minecraft.spigot.servers.fog.run.RunStore; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.World; +import org.bukkit.block.Block; +import org.bukkit.block.Campfire; +import org.bukkit.entity.Bee; +import org.bukkit.entity.EntityType; + +import java.util.Random; + +/** + * Scatters bee-hive "nests" around trees. Each nest is: + * - a Beehive block placed on top of the highest solid block; + * - a lit campfire one block below the hive, producing signal smoke; + * - a ring of random flowers (dandelion, poppy, allium, etc.) on the ground; + * - a small swarm of Bees spawned near each hive. + * + * Each placed hive's block coordinates are remembered in the {@link RunStore} so + * {@link com.orbitmines.minecraft.spigot.servers.fog.events.BlockBreakListener} + * can keep the hive unbreakable (part of the world's natural decor). + */ +public class BeehiveHandler { + + private static final Random RANDOM = new Random(); + + private static final Material[] FLOWERS = { + Material.DANDELION, Material.POPPY, Material.BLUE_ORCHID, Material.ALLIUM, + Material.AZURE_BLUET, Material.RED_TULIP, Material.ORANGE_TULIP, Material.WHITE_TULIP, + Material.PINK_TULIP, Material.OXEYE_DAISY, Material.CORNFLOWER, Material.LILY_OF_THE_VALLEY + }; + + public static boolean isProtectedHive(Run run, Location location) { + if (run == null || location == null) return false; + return run.getStore().isHoneyTree(location.getBlockX(), location.getBlockY(), location.getBlockZ()); + } + + /** + * Populate the area around {@code origin} with up to {@code count} bee-hive nests. + * Only attaches to trees (highest block is a log). Idempotent in the sense that + * repeated calls may skip positions that already have a hive recorded. + */ + public static void decorateNearbyTrees(Run run, Location origin, int radius, int count) { + RunStore store = run.getStore(); + World world = origin.getWorld(); + int placed = 0; + for (int tries = 0; tries < 60 && placed < count; tries++) { + int dx = RANDOM.nextInt(radius * 2) - radius; + int dz = RANDOM.nextInt(radius * 2) - radius; + int x = origin.getBlockX() + dx; + int z = origin.getBlockZ() + dz; + int groundY = world.getHighestBlockYAt(x, z); + Block ground = world.getBlockAt(x, groundY, z); + if (!ground.getType().name().endsWith("_LOG")) continue; + + /* Hive goes one block above the tree top; campfire directly below the hive. */ + Block hiveBlock = world.getBlockAt(x, groundY + 1, z); + Block campfireBlock = world.getBlockAt(x, groundY, z); + + placeCampfire(campfireBlock); + placeHive(hiveBlock); + scatterFlowers(world, x, groundY, z, 3); + spawnBees(world, hiveBlock.getLocation(), 2 + RANDOM.nextInt(2)); + + store.setHoneyTree(hiveBlock.getX(), hiveBlock.getY(), hiveBlock.getZ()); + placed++; + } + } + + private static void placeHive(Block block) { + block.setType(Material.BEE_NEST, false); + } + + private static void placeCampfire(Block block) { + block.setType(Material.CAMPFIRE, false); + if (block.getState() instanceof Campfire fire) { + if (fire.getBlockData() instanceof org.bukkit.block.data.type.Campfire cd) { + cd.setLit(true); + cd.setSignalFire(true); + fire.setBlockData(cd); + } + fire.update(true, false); + } + } + + private static void scatterFlowers(World world, int cx, int groundY, int cz, int ringRadius) { + for (int dx = -ringRadius; dx <= ringRadius; dx++) { + for (int dz = -ringRadius; dz <= ringRadius; dz++) { + if (Math.abs(dx) + Math.abs(dz) < ringRadius - 1) continue; + if (RANDOM.nextInt(3) != 0) continue; + int fx = cx + dx, fz = cz + dz; + int fy = world.getHighestBlockYAt(fx, fz); + Block under = world.getBlockAt(fx, fy - 1, fz); + Block at = world.getBlockAt(fx, fy, fz); + if (!at.getType().isAir()) continue; + if (!under.getType().isSolid()) continue; + at.setType(FLOWERS[RANDOM.nextInt(FLOWERS.length)], false); + } + } + } + + private static void spawnBees(World world, Location at, int count) { + for (int i = 0; i < count; i++) { + Location spawn = at.clone().add(RANDOM.nextInt(4) - 2, 1, RANDOM.nextInt(4) - 2); + Bee bee = (Bee) world.spawnEntity(spawn, EntityType.BEE); + bee.setRemoveWhenFarAway(false); + bee.setAnger(0); + } + } +} diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/boss/Boss.java b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/boss/Boss.java new file mode 100644 index 000000000..ef4294abc --- /dev/null +++ b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/boss/Boss.java @@ -0,0 +1,42 @@ +package com.orbitmines.minecraft.spigot.servers.fog.boss; + +import com.orbitmines.minecraft.spigot.servers.fog.mob.FoGMob; +import com.orbitmines.minecraft.spigot.servers.fog.run.Run; +import lombok.Getter; +import org.bukkit.boss.BarColor; +import org.bukkit.boss.BarStyle; +import org.bukkit.boss.BossBar; +import org.bukkit.entity.Player; + +/** Extends FoGMob with a BossBar so the client sees progressive health. */ +public abstract class Boss extends FoGMob { + + @Getter protected BossBar bossBar; + + protected Boss(Run run) { + super(run); + } + + protected void initBossBar(String title, BarColor color) { + this.bossBar = org.bukkit.Bukkit.createBossBar(title, color, BarStyle.SOLID); + this.bossBar.setProgress(1.0); + } + + public void addViewer(Player player) { + if (bossBar != null) bossBar.addPlayer(player); + } + + public void removeViewer(Player player) { + if (bossBar != null) bossBar.removePlayer(player); + } + + public void updateBar() { + if (bossBar == null || !isAlive()) return; + bossBar.setProgress(Math.max(0.0, Math.min(1.0, entity.getHealth() / entity.getMaxHealth()))); + } + + public void destroy() { + if (bossBar != null) bossBar.removeAll(); + super.remove(); + } +} diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/boss/UndeadKnight.java b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/boss/UndeadKnight.java new file mode 100644 index 000000000..853b12bae --- /dev/null +++ b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/boss/UndeadKnight.java @@ -0,0 +1,100 @@ +package com.orbitmines.minecraft.spigot.servers.fog.boss; + +import com.orbitmines.minecraft.spigot.servers.fog.mob.DebuffMage; +import com.orbitmines.minecraft.spigot.servers.fog.mob.FireMage; +import com.orbitmines.minecraft.spigot.servers.fog.mob.FoGZombie; +import com.orbitmines.minecraft.spigot.servers.fog.run.Run; +import org.bukkit.Location; +import org.bukkit.boss.BarColor; +import org.bukkit.entity.EntityType; +import org.bukkit.entity.Player; +import org.bukkit.entity.PiglinBrute; + +import java.util.Random; + +/** + * First boss. Base Piglin Brute stand-in. + * - Every 30s: summons a random undead mage (FireMage or DebuffMage) + * - Every 15s: summons 2/3/4/5 zombies depending on hp % + * - Every 10s: pulls players in front of him + * - Below 50%: every 15s charges a random player knocking them up + */ +public class UndeadKnight extends Boss { + + private static final Random RANDOM = new Random(); + + private long lastSummon; + private long lastZombies; + private long lastPull; + private long lastCharge; + + public UndeadKnight(Run run) { + super(run); + } + + @Override public double baseHealth() { return 300.0; } + @Override public String translationKey() { return "boss.undead_knight.name"; } + @Override public String colorPrefix() { return "§4"; } + + @Override + public void spawn(Location at) { + PiglinBrute brute = (PiglinBrute) at.getWorld().spawnEntity(at, EntityType.PIGLIN_BRUTE); + brute.setMaxHealth(baseHealth()); + brute.setHealth(baseHealth()); + brute.setCustomName(displayName()); + brute.setCustomNameVisible(true); + this.entity = brute; + this.entityUuid = brute.getUniqueId(); + initBossBar(displayName(), BarColor.RED); + for (Player p : at.getWorld().getPlayers()) addViewer(p); + } + + @Override + public void tick() { + if (!isAlive()) return; + updateBar(); + long now = entity.getWorld().getFullTime(); + double ratio = entity.getHealth() / entity.getMaxHealth(); + + if (now - lastSummon > 600) { + if (RANDOM.nextBoolean()) + new FireMage(run).spawn(entity.getLocation()); + else + new DebuffMage(run).spawn(entity.getLocation()); + lastSummon = now; + } + + if (now - lastZombies > 300) { + int count = ratio > 0.75 ? 2 : ratio > 0.5 ? 3 : ratio > 0.25 ? 4 : 5; + for (int i = 0; i < count; i++) { + FoGZombie z = new FoGZombie(run); + z.spawn(entity.getLocation().add(RANDOM.nextInt(6) - 3, 0, RANDOM.nextInt(6) - 3)); + } + lastZombies = now; + } + + if (now - lastPull > 200) { + for (Player p : entity.getWorld().getPlayers()) { + if (p.getLocation().distance(entity.getLocation()) > 10) continue; + org.bukkit.util.Vector dir = entity.getLocation().toVector().subtract(p.getLocation().toVector()).normalize().multiply(0.8); + p.setVelocity(dir); + } + lastPull = now; + } + + if (ratio < 0.5 && now - lastCharge > 300) { + Player best = null; + double bestDist = 16 * 16; + for (Player p : entity.getWorld().getPlayers()) { + double d = p.getLocation().distanceSquared(entity.getLocation()); + if (d < bestDist) { bestDist = d; best = p; } + } + if (best != null) { + org.bukkit.util.Vector dir = best.getLocation().toVector().subtract(entity.getLocation().toVector()).normalize().multiply(2.0).setY(0.9); + entity.setVelocity(dir); + best.setVelocity(new org.bukkit.util.Vector(0, 1.4, 0)); + } + lastCharge = now; + } + } +} diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/choice/Choice.java b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/choice/Choice.java new file mode 100644 index 000000000..987f43e13 --- /dev/null +++ b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/choice/Choice.java @@ -0,0 +1,113 @@ +package com.orbitmines.minecraft.spigot.servers.fog.choice; + +import com.orbitmines.archive.minecraft._2019.libs.player.Languageable; +import lombok.Getter; +import org.bukkit.Material; + +/** + * Master enum of all level-up choices. Display name + description come from + * {@code fog/choice..{name,description}}. + */ +public enum Choice { + + /* === FACTIONS === (unique, one-per-player once-per-run) */ + FACTION_OMEGA (Rarity.LEGENDARY, true, Category.FACTION, Material.ENDER_EYE, 1, 1), + FACTION_ALPHA (Rarity.LEGENDARY, true, Category.FACTION, Material.BLAZE_POWDER, 1, 1), + FACTION_BETA (Rarity.LEGENDARY, true, Category.FACTION, Material.PRISMARINE_CRYSTALS, 1, 1), + + /* === STATS === */ + EXTRA_HP_4 (Rarity.COMMON, false, Category.STAT, Material.RED_DYE, 1, 10), + DAMAGE_5 (Rarity.COMMON, false, Category.STAT, Material.IRON_SWORD, 1, 10), + SPEED_5 (Rarity.COMMON, false, Category.STAT, Material.FEATHER, 1, 5), + MINING_SPEED_5 (Rarity.COMMON, false, Category.STAT, Material.IRON_PICKAXE, 1, 10), + + /* === SKILLS === */ + SKILL_LUMBERJACK (Rarity.RARE, false, Category.SKILL, Material.IRON_AXE, 1, 1), + SKILL_FISHING (Rarity.RARE, false, Category.SKILL, Material.FISHING_ROD, 1, 1), + SKILL_BOTANY (Rarity.RARE, false, Category.SKILL, Material.OAK_SAPLING, 1, 5), + SKILL_BEEKEEPING (Rarity.RARE, false, Category.SKILL, Material.HONEY_BOTTLE, 1, 1), + + /* === ORES === */ + ORE_COPPER (Rarity.COMMON, false, Category.ORE, Material.COPPER_INGOT, 1, 1), + ORE_COBALT (Rarity.RARE, false, Category.ORE, Material.LIGHT_BLUE_DYE, 3, 1), + ORE_STRONTIUM (Rarity.RARE, false, Category.ORE, Material.GLOWSTONE_DUST, 5, 1), + ORE_AMETHYST (Rarity.RARE, false, Category.ORE, Material.AMETHYST_SHARD, 5, 1), + ORE_URANIUM (Rarity.EPIC, false, Category.ORE, Material.LIME_DYE, 8, 1), + ORE_IRIDIUM (Rarity.EPIC, false, Category.ORE, Material.LIGHT_GRAY_DYE, 10, 1), + ORE_FRANCIUM (Rarity.LEGENDARY, false, Category.ORE, Material.MAGENTA_DYE, 15, 1), + + /* === TREES === */ + TREE_TAR (Rarity.COMMON, false, Category.TREE, Material.COAL, 1, 1), + TREE_GLOWWOOD (Rarity.RARE, false, Category.TREE, Material.GLOW_INK_SAC, 3, 1), + TREE_VOIDOAK (Rarity.EPIC, false, Category.TREE, Material.SCULK, 8, 1), + + /* === DRONES === */ + DRONE_UNLOCK (Rarity.EPIC, false, Category.DRONE, Material.ALLAY_SPAWN_EGG, 10, 9), + DRONE_FACTORY (Rarity.LEGENDARY, true, Category.STRUCTURE,Material.SMITHING_TABLE, 10, 1), + + /* === STRUCTURES === */ + STRUCTURE_FARM (Rarity.EPIC, true, Category.STRUCTURE,Material.WHEAT, 1, 1), + STRUCTURE_ENCHANT (Rarity.EPIC, true, Category.STRUCTURE,Material.ENCHANTING_TABLE, 1, 1), + STRUCTURE_CRATES (Rarity.LEGENDARY, true, Category.STRUCTURE,Material.CHEST, 1, 1), + + /* === MAP EXPANSION === */ + MAP_EXPANSION (Rarity.LEGENDARY, false, Category.MAP, Material.MAP, 1, 20), + + /* === ENCHANTS === */ + ENCHANT_AUTO_REPLANT (Rarity.RARE, false, Category.ENCHANT, Material.ENCHANTED_BOOK, 1, 1), + ENCHANT_HOMING_ARROW (Rarity.EPIC, false, Category.ENCHANT, Material.ENCHANTED_BOOK, 3, 1), + ENCHANT_RAIN_ARROWS (Rarity.LEGENDARY, false, Category.ENCHANT, Material.ENCHANTED_BOOK, 5, 1), + ENCHANT_ABSORPTION (Rarity.RARE, false, Category.ENCHANT, Material.ENCHANTED_BOOK, 3, 1), + ENCHANT_CHOP_TREE (Rarity.EPIC, false, Category.ENCHANT, Material.ENCHANTED_BOOK, 3, 1), + + /* === RECIPES === */ + RECIPE_ORE_SUIT (Rarity.RARE, false, Category.RECIPE, Material.IRON_CHESTPLATE, 2, 1), + RECIPE_PICKAXE_UP (Rarity.RARE, false, Category.RECIPE, Material.DIAMOND_PICKAXE, 2, 1), + RECIPE_BACKPACK (Rarity.RARE, false, Category.RECIPE, Material.BUNDLE, 2, 1), + + /* === ABILITIES === */ + ABILITY_SHIELD (Rarity.EPIC, false, Category.ABILITY, Material.SHIELD, 5, 1), + ABILITY_SWEEP (Rarity.RARE, false, Category.ABILITY, Material.IRON_AXE, 3, 1), + ABILITY_LEAP (Rarity.RARE, false, Category.ABILITY, Material.RABBIT_FOOT, 3, 1), + + /* === QUESTS === */ + QUEST_COLLECT (Rarity.COMMON, false, Category.QUEST, Material.WRITABLE_BOOK, 1, 1), + QUEST_DEFEAT (Rarity.COMMON, false, Category.QUEST, Material.WRITABLE_BOOK, 1, 1); + + public enum Category { + FACTION, STAT, SKILL, ORE, TREE, DRONE, STRUCTURE, MAP, ENCHANT, RECIPE, ABILITY, QUEST + } + + @Getter private final Rarity rarity; + @Getter private final boolean unique; + @Getter private final Category category; + @Getter private final Material icon; + @Getter private final int minLevel; + @Getter private final int maxStacks; + + Choice(Rarity rarity, boolean unique, Category category, Material icon, int minLevel, int maxStacks) { + this.rarity = rarity; + this.unique = unique; + this.category = category; + this.icon = icon; + this.minLevel = minLevel; + this.maxStacks = maxStacks; + } + + public String nameKey() { return "choice." + name().toLowerCase() + ".name"; } + public String descriptionKey() { return "choice." + name().toLowerCase() + ".description"; } + + public String getDisplayName(Languageable viewer) { return viewer.translate("fog", nameKey()); } + public String[] getDescriptionLines(Languageable viewer) { + return viewer.getLanguage().getStringArray("fog", descriptionKey()); + } + + public String getStorageKey() { + return name().toLowerCase(); + } + + public static Choice parse(String s) { + if (s == null) return null; + try { return Choice.valueOf(s.toUpperCase()); } catch (IllegalArgumentException e) { return null; } + } +} diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/choice/ChoicePicker.java b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/choice/ChoicePicker.java new file mode 100644 index 000000000..43dc75941 --- /dev/null +++ b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/choice/ChoicePicker.java @@ -0,0 +1,111 @@ +package com.orbitmines.minecraft.spigot.servers.fog.choice; + +import com.orbitmines.minecraft.spigot.servers.fog.faction.Faction; +import com.orbitmines.minecraft.spigot.servers.fog.run.Run; +import com.orbitmines.minecraft.spigot.servers.fog.run.RunStore; +import com.orbitmines.minecraft.spigot.servers.fog.stats.Stats; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Random; +import java.util.Set; +import java.util.UUID; + +/** + * Picks three {@link Choice}s for a level-up prompt, applying rarity weights and + * exclusion rules. Sync-safe: reads only from the in-memory caches on + * {@link Stats} and {@link Run} — never touches the store directly. + * + *

{@link #recordChoice(Run, UUID, int, Choice)} writes to the store + * (async-only) and returns without updating in-memory caches. Callers + * should have already bumped {@link Stats#onChoiceActivated(Choice)} and + * {@link Run#markUniqueTaken(Choice)} on the sync thread before scheduling the + * record — see {@link #applyInMemory(Run, Stats, Choice)}.

+ */ +public class ChoicePicker { + + private static final Random RANDOM = new Random(); + + public static List pickThree(Run run, Stats stats, int level) { + List candidates = new ArrayList<>(); + + Set excluded = new HashSet<>(); + for (Choice c : Choice.values()) { + if (c.isUnique() && run.isUniqueTaken(c)) excluded.add(c); + if (c.getMaxStacks() > 0 && stats.getChoiceStackCount(c) >= c.getMaxStacks()) excluded.add(c); + } + Faction current = stats.getFaction(); + if (current != null) { + if (current != Faction.OMEGA) excluded.add(Choice.FACTION_OMEGA); + if (current != Faction.ALPHA) excluded.add(Choice.FACTION_ALPHA); + if (current != Faction.BETA) excluded.add(Choice.FACTION_BETA); + } + + for (Choice c : Choice.values()) { + if (excluded.contains(c)) continue; + if (level < c.getMinLevel()) continue; + candidates.add(c); + } + + /* A previously-DAMAGED choice on this level is guaranteed to reappear. */ + Choice previouslyMade = stats.getDamagedChoiceAt(level); + List chosen = new ArrayList<>(); + if (previouslyMade != null && candidates.contains(previouslyMade)) { + chosen.add(previouslyMade); + candidates.remove(previouslyMade); + } + + /* Guaranteed Drone Factory at lvl 10 if not yet unlocked. */ + if (level == 10 && !run.isUniqueTaken(Choice.DRONE_FACTORY) && candidates.contains(Choice.DRONE_FACTORY)) { + chosen.add(Choice.DRONE_FACTORY); + candidates.remove(Choice.DRONE_FACTORY); + } + + while (chosen.size() < 3 && !candidates.isEmpty()) { + Choice roll = weightedPick(candidates); + chosen.add(roll); + candidates.remove(roll); + } + + while (chosen.size() < 3) { + chosen.add(Choice.EXTRA_HP_4); + } + + return Collections.unmodifiableList(chosen); + } + + private static Choice weightedPick(List pool) { + int totalWeight = 0; + for (Choice c : pool) totalWeight += c.getRarity().getWeight(); + int r = RANDOM.nextInt(totalWeight); + int cum = 0; + for (Choice c : pool) { + cum += c.getRarity().getWeight(); + if (r < cum) return c; + } + return pool.get(0); + } + + /** + * Sync-safe. Mutates the in-memory caches to reflect a just-applied choice. + * Pair this with {@link #recordChoice(Run, UUID, int, Choice)} on async to + * keep the store in sync. + */ + public static void applyInMemory(Run run, Stats stats, Choice choice) { + if (stats != null) stats.onChoiceActivated(choice); + if (run != null) run.markUniqueTaken(choice); + } + + /** Async only. Persists the choice to the store. Does not touch caches. */ + public static void recordChoice(Run run, UUID uuid, int level, Choice choice) { + RunStore store = run.getStore(); + store.setChoiceState(uuid, level, choice, ChoiceState.ACTIVE); + store.incrementChoiceStackCount(uuid, choice); + if (choice.isUnique()) store.setUniqueHolder(choice, uuid); + if (choice == Choice.FACTION_OMEGA) store.setMemberFaction(uuid, Faction.OMEGA); + if (choice == Choice.FACTION_ALPHA) store.setMemberFaction(uuid, Faction.ALPHA); + if (choice == Choice.FACTION_BETA) store.setMemberFaction(uuid, Faction.BETA); + } +} diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/choice/ChoiceState.java b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/choice/ChoiceState.java new file mode 100644 index 000000000..c7b8931df --- /dev/null +++ b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/choice/ChoiceState.java @@ -0,0 +1,14 @@ +package com.orbitmines.minecraft.spigot.servers.fog.choice; + +public enum ChoiceState { + /** Choice is active — effects apply, items/enchants normal. */ + ACTIVE, + /** Choice was tied to a level that was lost on death. Item/enchant/unlock + is suppressed and visually marked (strikethrough + red "DAMAGED"). */ + DAMAGED; + + public static ChoiceState parse(String s) { + if (s == null) return ACTIVE; + try { return ChoiceState.valueOf(s.toUpperCase()); } catch (IllegalArgumentException e) { return ACTIVE; } + } +} diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/choice/Rarity.java b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/choice/Rarity.java new file mode 100644 index 000000000..ce3aea0bc --- /dev/null +++ b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/choice/Rarity.java @@ -0,0 +1,34 @@ +package com.orbitmines.minecraft.spigot.servers.fog.choice; + +import com.orbitmines.archive.minecraft._2019.libs.Color; +import com.orbitmines.archive.minecraft._2019.libs.player.Languageable; +import lombok.Getter; + +/** Choice rarity; weights sum to 100. Display name comes from `fog/rarity..name`. */ +public enum Rarity { + + COMMON(Color.WHITE, 50), + RARE(Color.AQUA, 30), + EPIC(Color.PURPLE, 15), + LEGENDARY(Color.YELLOW, 5); + + @Getter private final Color color; + @Getter private final int weight; + + Rarity(Color color, int weight) { + this.color = color; + this.weight = weight; + } + + public String getTranslationKey() { + return "rarity." + name().toLowerCase() + ".name"; + } + + public String getDisplayName(Languageable viewer) { + return viewer.translate("fog", getTranslationKey()); + } + + public String getColoredName(Languageable viewer) { + return color.getCc() + "§l" + getDisplayName(viewer); + } +} diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/commands/ChoiceArgument.java b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/commands/ChoiceArgument.java new file mode 100644 index 000000000..8a4a43f7c --- /dev/null +++ b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/commands/ChoiceArgument.java @@ -0,0 +1,79 @@ +package com.orbitmines.minecraft.spigot.servers.fog.commands; + +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import com.orbitmines.archive.minecraft.spigot._2019.libs.spigot.commands.brigadier.Argument; +import com.orbitmines.minecraft.spigot.servers.fog.FoG; +import com.orbitmines.minecraft.spigot.servers.fog.FoGPlayer; +import com.orbitmines.minecraft.spigot.servers.fog.choice.Choice; +import lombok.Getter; + +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.concurrent.CompletableFuture; + +/** Brigadier argument that autocompletes all {@link Choice} enum names. */ +public class ChoiceArgument extends Argument { + + @Getter private final String name = "choice"; + + @Override + public String getDescription(FoGPlayer player) { + return player.translate("fog", "player.command.choice.argument.description"); + } + + @Override + public Choice getValue(FoGPlayer player, String string) { + Choice value = Choice.parse(string); + if (value == null) { + player.sendMessage("FoG", com.orbitmines.archive.minecraft._2019.libs.Color.RED, + "fog", "player.command.choice.invalid", string); + } + return value; + } + + @Override + public String invalidReason(FoGPlayer player, String string, Choice value) { + return player.translate("fog", "player.command.choice.invalid", string); + } + + @Override + public String getValidTooltip(FoGPlayer player, Choice value) { + return value.getRarity().getColor().getCc() + value.getDisplayName(player); + } + + @Override + public Set getExamples(FoGPlayer player, int limit) { + Set out = new LinkedHashSet<>(); + int n = 0; + for (Choice c : Choice.values()) { + out.add(c.name()); + if (++n >= limit) break; + } + return out; + } + + @Override + public CompletableFuture getSuggestions(FoGPlayer player, CommandContext context, SuggestionsBuilder builder) throws CommandSyntaxException { + String remaining = builder.getRemaining().toUpperCase(); + for (Choice c : Choice.values()) { + if (!c.name().startsWith(remaining)) continue; + String display = c.getRarity().getColor().getCc() + c.getDisplayName(player); + builder.suggest(c.name(), () -> display); + } + return builder.buildFuture(); + } + + @Override + public StringArgumentType.StringType getType() { + return StringArgumentType.StringType.SINGLE_WORD; + } + + @Override + protected ChoiceArgument getInstance() { + return this; + } +} diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/commands/CommandChoice.java b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/commands/CommandChoice.java new file mode 100644 index 000000000..edf965388 --- /dev/null +++ b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/commands/CommandChoice.java @@ -0,0 +1,69 @@ +package com.orbitmines.minecraft.spigot.servers.fog.commands; + +import com.orbitmines.archive.minecraft._2019.libs.Color; +import com.orbitmines.archive.minecraft._2019.libs.Server; +import com.orbitmines.archive.minecraft._2019.libs.rank.StaffRank; +import com.orbitmines.archive.minecraft.spigot._2019.libs.spigot.commands.brigadier.Command; +import com.orbitmines.archive.minecraft.spigot._2019.libs.spigot.commands.brigadier.executors.Executor0; +import com.orbitmines.archive.minecraft.spigot._2019.libs.spigot.commands.brigadier.executors.Executor1; +import com.orbitmines.minecraft.spigot.servers.fog.FoG; +import com.orbitmines.minecraft.spigot.servers.fog.FoGPlayer; +import com.orbitmines.minecraft.spigot.servers.fog.choice.Choice; +import com.orbitmines.minecraft.spigot.servers.fog.choice.ChoicePicker; +import com.orbitmines.minecraft.spigot.servers.fog.run.Run; + +/** + * DEV+ command for triggering level-up prompts / directly applying a specific choice. + * + * /choice → open the normal 3-option level-up prompt at current level + 1 + * /choice DRONE_UNLOCK → apply that specific choice at current level + 1, skipping the prompt + * + *

Brigadier command executors run on the async DB thread, so any Bukkit-side + * work (entity spawns, attribute writes, chat to the player) must be wrapped in + * {@code server.runSync}.

+ */ +public class CommandChoice extends Command { + + public CommandChoice(FoG server) { + super(server, Server.FOG, "choice"); + + requires(StaffRank.DEVELOPER); + + executes((Executor0) player -> { + if (!player.isEligible(StaffRank.DEVELOPER)) return; + Run run = player.getActiveRun(); + if (run == null) { + player.sendMessage("FoG", Color.RED, "fog", "player.command.choice.no_run"); + return; + } + /* LevelUpFlow.start spawns armor stands + floating items — must be sync. */ + server.runSync(() -> server.getLevelUpFlow().start(player, player.getLevel() + 1, run)); + }); + + withArg(new ChoiceArgument().executes( + (Executor1) (player, choice) -> { + if (!player.isEligible(StaffRank.DEVELOPER)) return; + Run run = player.getActiveRun(); + if (run == null) { + player.sendMessage("FoG", Color.RED, "fog", "player.command.choice.no_run"); + return; + } + int level = player.getLevel() + 1; + server.runSync(() -> { + ChoicePicker.applyInMemory(run, player.getStats(), choice); + player.applyDerivedStats(); + player.sendMessage("FoG", Color.LIME, "fog", "player.command.choice.applied", + choice.getDisplayName(player), level); + }); + /* This executor is already on an async thread — persist directly. */ + ChoicePicker.recordChoice(run, player.getUUID(), level, choice); + run.getStore().setMemberLevel(player.getUUID(), level); + } + )); + } + + @Override + public String getDescription(FoGPlayer player) { + return player.translate("fog", "player.command.choice.description"); + } +} diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/commands/CommandCodex.java b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/commands/CommandCodex.java new file mode 100644 index 000000000..e400979d2 --- /dev/null +++ b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/commands/CommandCodex.java @@ -0,0 +1,24 @@ +package com.orbitmines.minecraft.spigot.servers.fog.commands; + +import com.orbitmines.archive.minecraft._2019.libs.Server; +import com.orbitmines.archive.minecraft.spigot._2019.libs.spigot.commands.brigadier.Command; +import com.orbitmines.archive.minecraft.spigot._2019.libs.spigot.commands.brigadier.executors.Executor0; +import com.orbitmines.minecraft.spigot.servers.fog.FoG; +import com.orbitmines.minecraft.spigot.servers.fog.FoGPlayer; +import com.orbitmines.minecraft.spigot.servers.fog.gui.RecipeBookGUI; + +public class CommandCodex extends Command { + + public CommandCodex(FoG server) { + super(server, Server.FOG, "codex", "recipes"); + + executes((Executor0) player -> { + new RecipeBookGUI(server, player).open(); + }); + } + + @Override + public String getDescription(FoGPlayer player) { + return player.translate("fog", "player.command.codex.description"); + } +} diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/commands/CommandCoopAccept.java b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/commands/CommandCoopAccept.java new file mode 100644 index 000000000..67b40b9a9 --- /dev/null +++ b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/commands/CommandCoopAccept.java @@ -0,0 +1,46 @@ +package com.orbitmines.minecraft.spigot.servers.fog.commands; + +import com.orbitmines.archive.minecraft._2019.libs.Color; +import com.orbitmines.archive.minecraft._2019.libs.Server; +import com.orbitmines.archive.minecraft._2019.libs.jedis.OnlinePlayer; +import com.orbitmines.archive.minecraft.spigot._2019.libs.spigot.commands.arguments.GlobalPlayerArgument; +import com.orbitmines.archive.minecraft.spigot._2019.libs.spigot.commands.brigadier.Command; +import com.orbitmines.archive.minecraft.spigot._2019.libs.spigot.commands.brigadier.executors.Executor1; +import com.orbitmines.minecraft.spigot.servers.fog.FoG; +import com.orbitmines.minecraft.spigot.servers.fog.FoGPlayer; + +import java.util.Map; + +/** {@code /coopaccept } — accept a pending coop-join request. */ +public class CommandCoopAccept extends Command { + + public CommandCoopAccept(FoG server) { + super(server, Server.FOG, "coopaccept"); + + withArg(new GlobalPlayerArgument(false).executes( + (Executor1>) (host, requester) -> { + Map reqs = host.getCoopRequestsFrom(); + Long runId = reqs.remove(requester.getRawName()); + if (runId == null) { + host.sendMessage("Coop", Color.RED, "fog", "coop.request.none", requester.getRawName()); + return; + } + host.sendMessage("Coop", Color.LIME, "fog", "coop.request.accepted", requester.getRawName()); + + /* Look up the requester as a FoGPlayer by UUID and join them into the run. */ + FoGPlayer requesterPlayer = server.getPlayer(org.bukkit.Bukkit.getPlayer(requester.getUUID())); + if (requesterPlayer == null) { + host.sendMessage("Coop", Color.RED, "fog", "coop.request.requester_offline", requester.getRawName()); + return; + } + requesterPlayer.sendMessage("Coop", Color.LIME, "fog", "coop.request.you_were_accepted", host.getRawName()); + server.getRunManager().joinRun(requesterPlayer, runId); + } + )); + } + + @Override + public String getDescription(FoGPlayer player) { + return player.translate("fog", "player.command.coopaccept.description"); + } +} diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/commands/CommandFactory.java b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/commands/CommandFactory.java new file mode 100644 index 000000000..7b8b7d3ab --- /dev/null +++ b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/commands/CommandFactory.java @@ -0,0 +1,24 @@ +package com.orbitmines.minecraft.spigot.servers.fog.commands; + +import com.orbitmines.archive.minecraft._2019.libs.Server; +import com.orbitmines.archive.minecraft.spigot._2019.libs.spigot.commands.brigadier.Command; +import com.orbitmines.archive.minecraft.spigot._2019.libs.spigot.commands.brigadier.executors.Executor0; +import com.orbitmines.minecraft.spigot.servers.fog.FoG; +import com.orbitmines.minecraft.spigot.servers.fog.FoGPlayer; +import com.orbitmines.minecraft.spigot.servers.fog.gui.DroneFactoryGUI; + +public class CommandFactory extends Command { + + public CommandFactory(FoG server) { + super(server, Server.FOG, "factory", "drone"); + + executes((Executor0) player -> { + new DroneFactoryGUI(server, player).open(); + }); + } + + @Override + public String getDescription(FoGPlayer player) { + return player.translate("fog", "player.command.factory.description"); + } +} diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/commands/CommandRun.java b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/commands/CommandRun.java new file mode 100644 index 000000000..c5b2319c4 --- /dev/null +++ b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/commands/CommandRun.java @@ -0,0 +1,22 @@ +package com.orbitmines.minecraft.spigot.servers.fog.commands; + +import com.orbitmines.archive.minecraft._2019.libs.Server; +import com.orbitmines.archive.minecraft.spigot._2019.libs.spigot.commands.brigadier.Command; +import com.orbitmines.archive.minecraft.spigot._2019.libs.spigot.commands.brigadier.executors.Executor0; +import com.orbitmines.minecraft.spigot.servers.fog.FoG; +import com.orbitmines.minecraft.spigot.servers.fog.FoGPlayer; +import com.orbitmines.minecraft.spigot.servers.fog.gui.RunSelector; + +public class CommandRun extends Command { + + public CommandRun(FoG server) { + super(server, Server.FOG, "run"); + + executes((Executor0) player -> RunSelector.open(server, player)); + } + + @Override + public String getDescription(FoGPlayer player) { + return player.translate("fog", "player.command.run.description"); + } +} diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/commands/CommandShop.java b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/commands/CommandShop.java new file mode 100644 index 000000000..0e4b0bee3 --- /dev/null +++ b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/commands/CommandShop.java @@ -0,0 +1,24 @@ +package com.orbitmines.minecraft.spigot.servers.fog.commands; + +import com.orbitmines.archive.minecraft._2019.libs.Server; +import com.orbitmines.archive.minecraft.spigot._2019.libs.spigot.commands.brigadier.Command; +import com.orbitmines.archive.minecraft.spigot._2019.libs.spigot.commands.brigadier.executors.Executor0; +import com.orbitmines.minecraft.spigot.servers.fog.FoG; +import com.orbitmines.minecraft.spigot.servers.fog.FoGPlayer; +import com.orbitmines.minecraft.spigot.servers.fog.gui.ShopGUI; + +public class CommandShop extends Command { + + public CommandShop(FoG server) { + super(server, Server.FOG, "fogshop"); + + executes((Executor0) player -> { + new ShopGUI(server, player).open(); + }); + } + + @Override + public String getDescription(FoGPlayer player) { + return player.translate("fog", "player.command.shop.description"); + } +} diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/commands/CommandWorld.java b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/commands/CommandWorld.java new file mode 100644 index 000000000..eb23a9b9d --- /dev/null +++ b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/commands/CommandWorld.java @@ -0,0 +1,44 @@ +package com.orbitmines.minecraft.spigot.servers.fog.commands; + +import com.orbitmines.archive.minecraft._2019.libs.Server; +import com.orbitmines.archive.minecraft._2019.libs.jedis.OnlinePlayer; +import com.orbitmines.archive.minecraft.spigot._2019.libs.spigot.commands.arguments.GlobalPlayerArgument; +import com.orbitmines.archive.minecraft.spigot._2019.libs.spigot.commands.brigadier.Command; +import com.orbitmines.archive.minecraft.spigot._2019.libs.spigot.commands.brigadier.executors.Executor0; +import com.orbitmines.archive.minecraft.spigot._2019.libs.spigot.commands.brigadier.executors.Executor1; +import com.orbitmines.minecraft.spigot.servers.fog.FoG; +import com.orbitmines.minecraft.spigot.servers.fog.FoGPlayer; +import com.orbitmines.minecraft.spigot.servers.fog.gui.SpectatorJoinGUI; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +/** Spectate another player's active run: `/world [PLAYER]`. */ +public class CommandWorld extends Command { + + public CommandWorld(FoG server) { + super(server, Server.FOG, "world"); + + executes((Executor0) viewer -> SpectatorJoinGUI.open(server, viewer)); + + withArg(new GlobalPlayerArgument(true).executes( + (Executor1>) (viewer, target) -> { + Player bukkit = Bukkit.getPlayer(target.getUUID()); + if (bukkit == null) { + viewer.sendMessage("FoG", com.orbitmines.archive.minecraft._2019.libs.Color.RED, "fog", "command.world.not_online", target.getRawName()); + return; + } + FoGPlayer targetPlayer = server.getPlayer(bukkit); + if (targetPlayer == null || targetPlayer.getActiveRun() == null) { + viewer.sendMessage("FoG", com.orbitmines.archive.minecraft._2019.libs.Color.RED, "fog", "command.world.not_in_run", target.getRawName()); + return; + } + server.getRunManager().spectateRun(viewer, targetPlayer.getActiveRun().getId()); + }) + ); + } + + @Override + public String getDescription(FoGPlayer player) { + return player.translate("fog", "player.command.world.description"); + } +} diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/crate/Crate.java b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/crate/Crate.java new file mode 100644 index 000000000..034a7462d --- /dev/null +++ b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/crate/Crate.java @@ -0,0 +1,35 @@ +package com.orbitmines.minecraft.spigot.servers.fog.crate; + +import com.orbitmines.minecraft.spigot.servers.fog.choice.Rarity; + +import java.util.Map; +import java.util.Random; + +/** Opens a crate and produces a "Level-up trigger" of a particular rarity. */ +public class Crate { + + private static final Random RANDOM = new Random(); + + /** Returns a rarity to use when pushing tracked XP / level-up choice. */ + public static Rarity roll(CrateType crate) { + int total = 0; + for (int w : crate.getRarityWeights().values()) total += w; + int r = RANDOM.nextInt(total); + int cum = 0; + for (Map.Entry e : crate.getRarityWeights().entrySet()) { + cum += e.getValue(); + if (r < cum) return e.getKey(); + } + return Rarity.COMMON; + } + + /** Amount of tracked XP a rarity grants when converted from a crate. */ + public static int xpReward(Rarity rarity) { + return switch (rarity) { + case COMMON -> 50; + case RARE -> 150; + case EPIC -> 400; + case LEGENDARY -> 1000; + }; + } +} diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/crate/CrateType.java b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/crate/CrateType.java new file mode 100644 index 000000000..a16ff9ace --- /dev/null +++ b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/crate/CrateType.java @@ -0,0 +1,36 @@ +package com.orbitmines.minecraft.spigot.servers.fog.crate; + +import com.orbitmines.archive.minecraft._2019.libs.player.Languageable; +import com.orbitmines.minecraft.spigot.servers.fog.choice.Rarity; +import lombok.Getter; +import org.bukkit.Material; + +import java.util.Map; + +public enum CrateType { + + BASIC(Material.CHEST, Material.TRIPWIRE_HOOK, Map.of( + Rarity.COMMON, 70, + Rarity.RARE, 25, + Rarity.EPIC, 4, + Rarity.LEGENDARY, 1)), + EPIC(Material.TRAPPED_CHEST, Material.LAPIS_LAZULI, Map.of( + Rarity.COMMON, 10, + Rarity.RARE, 50, + Rarity.EPIC, 30, + Rarity.LEGENDARY, 10)); + + @Getter private final Material icon; + @Getter private final Material keyMaterial; + @Getter private final Map rarityWeights; + + CrateType(Material icon, Material keyMaterial, Map rarityWeights) { + this.icon = icon; + this.keyMaterial = keyMaterial; + this.rarityWeights = Map.copyOf(rarityWeights); + } + + public String nameKey() { return "crate." + name().toLowerCase() + ".name"; } + + public String getDisplayName(Languageable viewer) { return viewer.translate("fog", nameKey()); } +} diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/database/FoGPlayerModel.java b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/database/FoGPlayerModel.java new file mode 100644 index 000000000..2d7025d15 --- /dev/null +++ b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/database/FoGPlayerModel.java @@ -0,0 +1,85 @@ +package com.orbitmines.minecraft.spigot.servers.fog.database; + +import com.orbitmines.archive.minecraft._2019.libs.database.mysql.OMMySQLModel; +import com.orbitmines.archive.minecraft._2019.utils.database.lib.Column; +import com.orbitmines.archive.minecraft._2019.utils.database.lib.ColumnKey; +import com.orbitmines.archive.minecraft._2019.utils.database.lib.froms.MySQLColumn; +import com.orbitmines.archive.minecraft._2019.utils.database.lib.froms.MySQLColumnInstance; +import com.orbitmines.archive.minecraft._2019.utils.database.lib.froms.MySQLColumnKey; +import com.orbitmines.archive.minecraft._2019.utils.database.lib.froms.MySQLTable; +import com.orbitmines.archive.minecraft._2019.utils.database.model.ModelSelector; +import com.orbitmines.archive.minecraft._2019.utils.database.model.mysql.MySQLModelColumn; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + +import java.util.UUID; + +public class FoGPlayerModel extends OMMySQLModel { + + public static MySQLTable TABLE = new MySQLTable("fog_players", MySQLModelColumn.toColumns(column.values())); + + @Getter private UUID uuid; + @Getter @Setter private Long activeRunId; + + public FoGPlayerModel() { + } + + public FoGPlayerModel(UUID uuid) { + this.uuid = uuid; + this.activeRunId = null; + } + + @Override + protected void load() { + this.uuid = getUUID(column.UUID); + this.activeRunId = getLong(column.ACTIVE_RUN_ID); + } + + @Override + protected MySQLTable getTable() { + return TABLE; + } + + @Override + protected ModelSelector[] getUniqueIdentifier() { + return localIdentifiers(column.UUID); + } + + @Override + protected column getSortedIdentifier() { + throw new UnsupportedOperationException(); + } + + @Override + protected String stringifyValue(column column) { + switch (column) { + case UUID: + return stringify(this.uuid); + case ACTIVE_RUN_ID: + return stringify(this.activeRunId); + default: + throw new UnsupportedOperationException(); + } + } + + @Override + protected column[] getColumns() { + return column.values(); + } + + @AllArgsConstructor + public enum column implements MySQLModelColumn { + UUID(new MySQLColumnKey("uuid", Column.Type.VARCHAR, ColumnKey.Key.PRIMARY, 36)), + ACTIVE_RUN_ID(new MySQLColumn("active_run_id", Column.Type.BIGINT)); + + @Getter private final MySQLColumnInstance column; + } + + public static FoGPlayerModel findOrInitializeBy(UUID uuid) { + FoGPlayerModel model = findBy(FoGPlayerModel.class, column.UUID.is(uuid)); + if (model != null) + return model; + return new FoGPlayerModel(uuid); + } +} diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/database/FoGRunModel.java b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/database/FoGRunModel.java new file mode 100644 index 000000000..de7a478b0 --- /dev/null +++ b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/database/FoGRunModel.java @@ -0,0 +1,103 @@ +package com.orbitmines.minecraft.spigot.servers.fog.database; + +import com.orbitmines.archive.minecraft._2019.libs.database.mysql.OMMySQLModel; +import com.orbitmines.archive.minecraft._2019.utils.DateUtils; +import com.orbitmines.archive.minecraft._2019.utils.database.lib.Column; +import com.orbitmines.archive.minecraft._2019.utils.database.lib.ColumnKey; +import com.orbitmines.archive.minecraft._2019.utils.database.lib.froms.MySQLColumn; +import com.orbitmines.archive.minecraft._2019.utils.database.lib.froms.MySQLColumnInstance; +import com.orbitmines.archive.minecraft._2019.utils.database.lib.froms.MySQLColumnKey; +import com.orbitmines.archive.minecraft._2019.utils.database.lib.froms.MySQLTable; +import com.orbitmines.archive.minecraft._2019.utils.database.model.ModelSelector; +import com.orbitmines.archive.minecraft._2019.utils.database.model.mysql.MySQLModelColumn; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + +import java.util.Date; +import java.util.UUID; + +/** Spawn coordinates are persisted on the world itself via {@code World#setSpawnLocation}; not in the DB. */ +public class FoGRunModel extends OMMySQLModel { + + public static MySQLTable TABLE = new MySQLTable("fog_runs", MySQLModelColumn.toColumns(column.values())); + + @Getter private Long id; + @Getter @Setter private UUID ownerUuid; + @Getter @Setter private String difficulty; + @Getter @Setter private String worldFileName; + @Getter @Setter private String state; + @Getter private Date createdAt; + + public FoGRunModel() { + } + + public FoGRunModel(UUID ownerUuid, String difficulty, String worldFileName, String state) { + this.ownerUuid = ownerUuid; + this.difficulty = difficulty; + this.worldFileName = worldFileName; + this.state = state; + this.createdAt = DateUtils.now(); + } + + @Override + protected void load() { + this.id = getLong(column.ID); + this.ownerUuid = getUUID(column.OWNER_UUID); + this.difficulty = getString(column.DIFFICULTY); + this.worldFileName = getString(column.WORLD_FILE_NAME); + this.state = getString(column.STATE); + this.createdAt = getDate(column.CREATED_AT, DateUtils.DATE_TIME_FORMAT); + } + + @Override + public void insert() { + setupReloadAfterInsert(localIdentifiers(column.OWNER_UUID, column.WORLD_FILE_NAME)); + super.insert(); + } + + @Override + protected MySQLTable getTable() { + return TABLE; + } + + @Override + protected ModelSelector[] getUniqueIdentifier() { + return localIdentifiers(column.ID); + } + + @Override + protected column getSortedIdentifier() { + return column.CREATED_AT; + } + + @Override + protected String stringifyValue(column column) { + switch (column) { + case ID: return stringify(this.id); + case OWNER_UUID: return stringify(this.ownerUuid); + case DIFFICULTY: return this.difficulty; + case WORLD_FILE_NAME: return this.worldFileName; + case STATE: return this.state; + case CREATED_AT: return stringify(this.createdAt, DateUtils.DATE_TIME_FORMAT); + default: throw new UnsupportedOperationException(); + } + } + + @Override + protected column[] getColumns() { + return column.values(); + } + + @AllArgsConstructor + public enum column implements MySQLModelColumn { + ID(new MySQLColumnKey("id", Column.Type.BIGINT, ColumnKey.Key.PRIMARY).autoIncrement()), + OWNER_UUID(new MySQLColumn("owner_uuid", Column.Type.VARCHAR, 36).indexed()), + DIFFICULTY(new MySQLColumn("difficulty", Column.Type.VARCHAR).notNull()), + WORLD_FILE_NAME(new MySQLColumn("world_file_name", Column.Type.VARCHAR).notNull()), + STATE(new MySQLColumn("state", Column.Type.VARCHAR).notNull()), + CREATED_AT(new MySQLColumn("created_at", Column.Type.DATETIME).indexed()); + + @Getter private final MySQLColumnInstance column; + } +} diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/drone/Drone.java b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/drone/Drone.java new file mode 100644 index 000000000..80d272ca8 --- /dev/null +++ b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/drone/Drone.java @@ -0,0 +1,83 @@ +package com.orbitmines.minecraft.spigot.servers.fog.drone; + +import com.orbitmines.minecraft.spigot.servers.fog.run.Run; +import com.orbitmines.minecraft.spigot.servers.fog.stats.Stats; +import com.orbitmines.minecraft.spigot.servers.fog.stats.StatsHolder; +import com.orbitmines.minecraft.spigot.servers.fog.util.Fogi18n; +import lombok.Getter; +import lombok.Setter; +import org.bukkit.Location; +import org.bukkit.entity.Allay; +import org.bukkit.entity.EntityType; +import org.bukkit.entity.LivingEntity; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +/** + * A single drone belonging to a player. Modelled on an Allay; up to 9 per player. + */ +public class Drone implements StatsHolder { + + public enum Mode { IDLE, ACTIVE } + + @Getter private final int id; + @Getter private final UUID ownerUuid; + @Getter @Setter private Mode mode = Mode.IDLE; + @Getter private final List modules = new ArrayList<>(); + + @Getter @Setter private Allay entity; + @Getter @Setter private double health = 20.0; + @Getter @Setter private double maxHealth = 20.0; + + private final Stats stats; + + public Drone(int id, UUID ownerUuid, Run run) { + this.id = id; + this.ownerUuid = ownerUuid; + this.stats = new Stats(run.getStore(), "member:" + ownerUuid + ":drone:" + id); + } + + public boolean addModule(DroneModule module) { + if (modules.size() >= 4) return false; /* POC cap */ + modules.add(module); + return true; + } + + public boolean removeModule(ModuleType type) { + return modules.removeIf(m -> m.getType() == type); + } + + public boolean hasModule(ModuleType type) { + return modules.stream().anyMatch(m -> m.getType() == type); + } + + @Override + public UUID getOwnerUuid() { return ownerUuid; } + + @Override + public String getStatsPrefix() { return "member:" + ownerUuid + ":drone:" + id; } + + @Override + public Stats getStats() { return stats; } + + /** Spawn the Allay entity at the given location (main thread). */ + public void spawn(Location at) { + if (entity != null && entity.isValid()) return; + LivingEntity e = (LivingEntity) at.getWorld().spawnEntity(at, EntityType.ALLAY); + if (e instanceof Allay) { + this.entity = (Allay) e; + this.entity.setCustomName("§b§l" + Fogi18n.defaultText("drone.entity_name", id)); + this.entity.setCustomNameVisible(true); + this.entity.setRemoveWhenFarAway(false); + } + } + + public void despawn() { + if (entity != null && entity.isValid()) { + entity.remove(); + } + entity = null; + } +} diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/drone/DroneFactory.java b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/drone/DroneFactory.java new file mode 100644 index 000000000..5bb79f217 --- /dev/null +++ b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/drone/DroneFactory.java @@ -0,0 +1,40 @@ +package com.orbitmines.minecraft.spigot.servers.fog.drone; + +import com.orbitmines.minecraft.spigot.servers.fog.ore.Ore; +import com.orbitmines.minecraft.spigot.servers.fog.run.Run; + +/** Ore-based repair & module-removal economics for drones. */ +public class DroneFactory { + + /** Cost to fully repair a drone from 0 HP, in units per listed ore. */ + public static int repairCost(Drone drone) { + int total = 0; + for (DroneModule module : drone.getModules()) { + for (Ore o : module.getType().getRepairOres()) { + total += module.getType().getRepairCostPerOre() * Math.max(1, module.getTier()); + } + } + return total; + } + + /** + * Cost to remove a module. Charged in the same ores that would be needed + * to repair — keeps the recipe relationship consistent. + */ + public static int removeCost(DroneModule module) { + int total = 0; + for (Ore o : module.getType().getRepairOres()) { + total += module.getType().getRepairCostPerOre() * 2 * Math.max(1, module.getTier()); + } + return total; + } + + public static void repair(Drone drone) { + drone.setHealth(drone.getMaxHealth()); + } + + public static boolean canCraftDrone(Run run) { + /* POC: infinite. Real build: consume resources. */ + return true; + } +} diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/drone/DroneModule.java b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/drone/DroneModule.java new file mode 100644 index 000000000..fdf7fc629 --- /dev/null +++ b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/drone/DroneModule.java @@ -0,0 +1,20 @@ +package com.orbitmines.minecraft.spigot.servers.fog.drone; + +import lombok.Getter; +import lombok.Setter; + +/** One equipped module on a drone. Not a Bukkit item — just a record. */ +public class DroneModule { + + @Getter private final ModuleType type; + @Getter @Setter private int tier; + + public DroneModule(ModuleType type, int tier) { + this.type = type; + this.tier = tier; + } + + public DroneModule(ModuleType type) { + this(type, 1); + } +} diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/drone/ModuleType.java b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/drone/ModuleType.java new file mode 100644 index 000000000..84bb9a68c --- /dev/null +++ b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/drone/ModuleType.java @@ -0,0 +1,41 @@ +package com.orbitmines.minecraft.spigot.servers.fog.drone; + +import com.orbitmines.archive.minecraft._2019.libs.Color; +import com.orbitmines.archive.minecraft._2019.libs.player.Languageable; +import com.orbitmines.minecraft.spigot.servers.fog.ore.Ore; +import lombok.Getter; +import org.bukkit.Material; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +public enum ModuleType { + + COLLECT (Color.YELLOW, Material.HOPPER, Arrays.asList(Ore.IRON, Ore.COPPER), 2), + SKILL (Color.GREEN, Material.IRON_AXE, Arrays.asList(Ore.IRON, Ore.COBALT), 3), + ENCHANTMENT(Color.AQUA, Material.ENCHANTED_BOOK, Arrays.asList(Ore.AMETHYST, Ore.COBALT), 3), + COMBAT (Color.RED, Material.IRON_SWORD, Arrays.asList(Ore.IRON, Ore.URANIUM), 4), + SHIELD (Color.BLUE, Material.SHIELD, Arrays.asList(Ore.IRIDIUM), 3), + INSPIRE (Color.PURPLE, Material.BELL, Arrays.asList(Ore.AMETHYST, Ore.FRANCIUM), 5); + + @Getter private final Color color; + @Getter private final Material icon; + @Getter private final List repairOres; + @Getter private final int repairCostPerOre; + + ModuleType(Color color, Material icon, List repairOres, int repairCostPerOre) { + this.color = color; + this.icon = icon; + this.repairOres = Collections.unmodifiableList(repairOres); + this.repairCostPerOre = repairCostPerOre; + } + + public String nameKey() { return "module." + name().toLowerCase() + ".name"; } + public String descriptionKey() { return "module." + name().toLowerCase() + ".description"; } + + public String getDisplayName(Languageable viewer) { return viewer.translate("fog", nameKey()); } + public String[] getDescriptionLines(Languageable viewer) { + return viewer.getLanguage().getStringArray("fog", descriptionKey()); + } +} diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/enchant/CustomEnchant.java b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/enchant/CustomEnchant.java new file mode 100644 index 000000000..6c4a9c3e4 --- /dev/null +++ b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/enchant/CustomEnchant.java @@ -0,0 +1,65 @@ +package com.orbitmines.minecraft.spigot.servers.fog.enchant; + +import com.orbitmines.archive.minecraft._2019.libs.Color; +import com.orbitmines.archive.minecraft._2019.libs.player.Languageable; +import com.orbitmines.minecraft.spigot.servers.fog.choice.Choice; +import com.orbitmines.minecraft.spigot.servers.fog.choice.ChoiceState; +import lombok.Getter; +import net.md_5.bungee.api.ChatColor; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; + +import java.util.ArrayList; +import java.util.List; + +public enum CustomEnchant { + + AUTO_REPLANT(Choice.ENCHANT_AUTO_REPLANT, Color.LIME), + HOMING_ARROW(Choice.ENCHANT_HOMING_ARROW, Color.AQUA), + RAIN_ARROWS (Choice.ENCHANT_RAIN_ARROWS, Color.ORANGE), + ABSORPTION (Choice.ENCHANT_ABSORPTION, Color.BLUE), + TREEFELLER (Choice.ENCHANT_CHOP_TREE, Color.GREEN); + + @Getter private final Choice unlockChoice; + @Getter private final Color color; + + CustomEnchant(Choice unlockChoice, Color color) { + this.unlockChoice = unlockChoice; + this.color = color; + } + + public String nameKey() { return "enchant." + name().toLowerCase() + ".name"; } + public String damagedSuffixKey() { return "enchant.damaged_suffix"; } + + public String getDisplayName(Languageable viewer) { return viewer.translate("fog", nameKey()); } + + /** Format enchant name for lore, applying strikethrough/red when DAMAGED. */ + public String loreLine(Languageable viewer, ChoiceState state, int level) { + String displayName = getDisplayName(viewer); + String levelStr = level <= 1 ? "" : " " + roman(level); + if (state == ChoiceState.DAMAGED) { + return ChatColor.DARK_RED + "§m" + displayName + levelStr + "§r " + ChatColor.RED + "§l" + viewer.translate("fog", damagedSuffixKey()); + } + return color.getCc() + displayName + levelStr; + } + + public static List describe(ItemStack stack) { + List out = new ArrayList<>(); + if (stack == null) return out; + ItemMeta meta = stack.getItemMeta(); + if (meta == null || !meta.hasLore()) return out; + out.addAll(meta.getLore()); + return out; + } + + private static String roman(int n) { + return switch (n) { + case 1 -> "I"; + case 2 -> "II"; + case 3 -> "III"; + case 4 -> "IV"; + case 5 -> "V"; + default -> Integer.toString(n); + }; + } +} diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/enchant/EnchantRegistry.java b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/enchant/EnchantRegistry.java new file mode 100644 index 000000000..78a65dd19 --- /dev/null +++ b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/enchant/EnchantRegistry.java @@ -0,0 +1,31 @@ +package com.orbitmines.minecraft.spigot.servers.fog.enchant; + +import com.orbitmines.archive.minecraft._2019.libs.player.Languageable; +import com.orbitmines.minecraft.spigot.servers.fog.choice.ChoiceState; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; + +import java.util.ArrayList; +import java.util.List; + +/** Simple re-renderer for enchantment lore. Real impl uses persistent data. */ +public class EnchantRegistry { + + public static ItemStack apply(Languageable viewer, ItemStack stack, CustomEnchant enchant, int level, ChoiceState state) { + if (stack == null) return null; + ItemMeta meta = stack.getItemMeta(); + if (meta == null) return stack; + + List lore = meta.hasLore() ? new ArrayList<>(meta.getLore()) : new ArrayList<>(); + String currentName = enchant.getDisplayName(viewer).toLowerCase(); + lore.removeIf(line -> line != null && line.toLowerCase().contains(currentName)); + lore.add(enchant.loreLine(viewer, state, level)); + meta.setLore(lore); + stack.setItemMeta(meta); + return stack; + } + + public static ItemStack markDamaged(Languageable viewer, ItemStack stack, CustomEnchant enchant) { + return apply(viewer, stack, enchant, 1, ChoiceState.DAMAGED); + } +} diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/events/BlockBreakListener.java b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/events/BlockBreakListener.java new file mode 100644 index 000000000..710a3a689 --- /dev/null +++ b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/events/BlockBreakListener.java @@ -0,0 +1,57 @@ +package com.orbitmines.minecraft.spigot.servers.fog.events; + +import com.orbitmines.minecraft.spigot.servers.fog.FoG; +import com.orbitmines.minecraft.spigot.servers.fog.FoGPlayer; +import com.orbitmines.minecraft.spigot.servers.fog.beehive.BeehiveHandler; +import com.orbitmines.minecraft.spigot.servers.fog.run.Run; +import com.orbitmines.minecraft.spigot.servers.fog.stats.Stats; +import com.orbitmines.minecraft.spigot.servers.fog.stats.ToolStatsStamp; +import com.orbitmines.minecraft.spigot.servers.fog.stats.ToolType; +import org.bukkit.Material; +import org.bukkit.block.Block; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.block.BlockBreakEvent; +import org.bukkit.inventory.ItemStack; + +/** + * Bumps the player's per-tool block-break counter on every break, Prison-style. + * Also protects FoG-placed bee-nest structures (unbreakable: nest, campfire + * underneath, and surrounding log they were attached to). + */ +public class BlockBreakListener implements Listener { + + private final FoG server; + + public BlockBreakListener(FoG server) { + this.server = server; + } + + @EventHandler + public void onBreak(BlockBreakEvent event) { + FoGPlayer player = server.getPlayer(event.getPlayer()); + if (player == null) return; + Run run = player.getActiveRun(); + if (run == null) return; + + Block block = event.getBlock(); + if (block.getType() == Material.BEE_NEST || block.getType() == Material.BEEHIVE || block.getType() == Material.CAMPFIRE) { + if (BeehiveHandler.isProtectedHive(run, block.getLocation()) + || BeehiveHandler.isProtectedHive(run, block.getLocation().clone().add(0, 1, 0))) { + event.setCancelled(true); + return; + } + } + + Stats stats = player.getStats(); + if (stats == null) return; // run joined mid-tick before stats finished loading + ToolType tool = ToolType.fromItem(event.getPlayer().getInventory().getItemInMainHand().getType()); + stats.incrementBlocksBroken(tool); + + /* Stamp the updated count onto the tool's metadata + lore so the player sees it. */ + ItemStack stack = event.getPlayer().getInventory().getItemInMainHand(); + long total = stats.getBlocksBroken(tool); + ItemStack updated = ToolStatsStamp.stamp(server, player, stack, tool, total); + if (updated != null) event.getPlayer().getInventory().setItemInMainHand(updated); + } +} diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/events/DamageListener.java b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/events/DamageListener.java new file mode 100644 index 000000000..70d1ea353 --- /dev/null +++ b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/events/DamageListener.java @@ -0,0 +1,53 @@ +package com.orbitmines.minecraft.spigot.servers.fog.events; + +import com.orbitmines.minecraft.spigot.servers.fog.FoG; +import org.bukkit.entity.Bee; +import org.bukkit.entity.LivingEntity; +import org.bukkit.entity.Player; +import org.bukkit.entity.Ravager; +import org.bukkit.entity.Zombie; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.entity.EntityDamageByEntityEvent; +import org.bukkit.potion.PotionEffect; +import org.bukkit.potion.PotionEffectType; +import org.bukkit.util.Vector; + +import java.util.Random; + +/** Applies FoG-specific damage side-effects (hunger, knock-up, poison). */ +public class DamageListener implements Listener { + + private static final Random RANDOM = new Random(); + private final FoG server; + + public DamageListener(FoG server) { + this.server = server; + } + + @EventHandler + public void onHit(EntityDamageByEntityEvent event) { + if (!(event.getEntity() instanceof Player victim)) return; + if (!(event.getDamager() instanceof LivingEntity damager)) return; + + /* Zombie → chance of hunger if food bar is empty */ + if (damager instanceof Zombie) { + if (victim.getFoodLevel() <= 0 && RANDOM.nextInt(100) < 20) { + victim.addPotionEffect(new PotionEffect(PotionEffectType.HUNGER, 20 * 5, 0)); + victim.addPotionEffect(new PotionEffect(PotionEffectType.SLOWNESS, 20 * 3, 2)); + } + } + + /* Ravager (Bull) → knock up */ + if (damager instanceof Ravager) { + victim.setVelocity(victim.getVelocity().add(new Vector(0, 1.4, 0))); + } + + /* Bee → 20% poison chance */ + if (damager instanceof Bee) { + if (RANDOM.nextInt(100) < 20) { + victim.addPotionEffect(new PotionEffect(PotionEffectType.POISON, 20 * 2, 0)); + } + } + } +} diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/events/DeathListener.java b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/events/DeathListener.java new file mode 100644 index 000000000..f4563efc4 --- /dev/null +++ b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/events/DeathListener.java @@ -0,0 +1,110 @@ +package com.orbitmines.minecraft.spigot.servers.fog.events; + +import com.orbitmines.minecraft.spigot.servers.fog.FoG; +import com.orbitmines.minecraft.spigot.servers.fog.FoGPlayer; +import com.orbitmines.minecraft.spigot.servers.fog.choice.Choice; +import com.orbitmines.minecraft.spigot.servers.fog.choice.ChoiceState; +import com.orbitmines.minecraft.spigot.servers.fog.level.LevelFormulas; +import com.orbitmines.minecraft.spigot.servers.fog.run.Difficulty; +import com.orbitmines.minecraft.spigot.servers.fog.run.Run; +import com.orbitmines.minecraft.spigot.servers.fog.run.RunStore; +import com.orbitmines.minecraft.spigot.servers.fog.structure.Compartment; +import com.orbitmines.minecraft.spigot.servers.fog.structure.CompartmentType; +import org.bukkit.GameMode; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.entity.PlayerDeathEvent; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +/** + * On death: lose up to N levels ({@link Difficulty#getMaxLevelsLostOnDeath()}). Choices + * made at those levels flip to DAMAGED state, tracked XP rolls back to the start of + * the target level, and Hardcore enters permanent spectator mode. + */ +public class DeathListener implements Listener { + + private final FoG server; + + public DeathListener(FoG server) { + this.server = server; + } + + @EventHandler + public void onDeath(PlayerDeathEvent event) { + Player bukkit = event.getEntity(); + FoGPlayer player = server.getPlayer(bukkit); + if (player == null) return; + Run run = player.getActiveRun(); + if (run == null) return; + + Difficulty diff = run.getDifficulty(); + int currentLevel = player.getLevel(); + int target = Math.max(0, currentLevel - diff.getMaxLevelsLostOnDeath()); + UUID uuid = player.getUUID(); + + /* Sync bits — tweak the death event immediately so vanilla doesn't drop XP. */ + event.setKeepInventory(!diff.isPermadeath()); + event.setKeepLevel(true); + event.setDroppedExp(0); + + if (diff.isPermadeath()) { + server.runSync(() -> player.setGameMode(GameMode.SPECTATOR)); + } + + player.sendMessage("FoG", com.orbitmines.archive.minecraft._2019.libs.Color.RED, "fog", "death.lost_levels", + currentLevel - target); + + /* Async: walk the choice grid, flip DAMAGED, roll back XP. */ + final int fTarget = target; + server.runAsync(() -> { + RunStore store = run.getStore(); + List toSeal = new ArrayList<>(); + for (int lvl = fTarget + 1; lvl <= currentLevel; lvl++) { + for (Choice c : Choice.values()) { + if (store.getChoiceState(uuid, lvl, c) == ChoiceState.ACTIVE) { + store.setChoiceState(uuid, lvl, c, ChoiceState.DAMAGED); + if (player.getStats() != null) player.getStats().onChoiceDamaged(lvl, c); + if (c.getCategory() == Choice.Category.STRUCTURE) { + CompartmentType ct = guessCompartmentType(c); + if (ct != null) toSeal.add(ct); + } + } + } + } + + long newTotalXp = 0; + for (int i = 0; i < fTarget; i++) newTotalXp += LevelFormulas.required(i); + store.setMemberExperience(uuid, newTotalXp); + store.setMemberLevel(uuid, fTarget); + if (diff.isPermadeath()) store.setMemberDead(uuid, true); + + /* Sync-seal each damaged compartment (block ops). */ + for (CompartmentType ct : toSeal) { + Integer cx = store.getCompartmentX(ct); + Integer cy = store.getCompartmentY(ct); + Integer cz = store.getCompartmentZ(ct); + if (cx == null || cy == null || cz == null) continue; + int fx = cx, fy = cy, fz = cz; + Compartment.markDamagedPersist(store, ct); + server.runSync(() -> Compartment.markDamagedBlocks(run, ct, fx, fy, fz)); + } + + /* Refresh derived stats after the rollback, sync. */ + server.runSync(player::applyDerivedStats); + }); + } + + private CompartmentType guessCompartmentType(Choice c) { + return switch (c) { + case STRUCTURE_FARM -> CompartmentType.FARM; + case STRUCTURE_ENCHANT -> CompartmentType.ENCHANTING; + case STRUCTURE_CRATES -> CompartmentType.CRATES; + case DRONE_FACTORY -> CompartmentType.DRONE_FACTORY; + default -> null; + }; + } +} diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/events/ExperienceSuppressor.java b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/events/ExperienceSuppressor.java new file mode 100644 index 000000000..ab6712e23 --- /dev/null +++ b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/events/ExperienceSuppressor.java @@ -0,0 +1,50 @@ +package com.orbitmines.minecraft.spigot.servers.fog.events; + +import com.orbitmines.minecraft.spigot.servers.fog.FoG; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.entity.EntityTargetLivingEntityEvent; +import org.bukkit.event.entity.ExpBottleEvent; +import org.bukkit.event.entity.PlayerDeathEvent; +import org.bukkit.event.player.PlayerExpChangeEvent; +import org.bukkit.entity.ExperienceOrb; + +/** + * Blocks all vanilla XP sources. The bar is managed via + * {@link com.orbitmines.minecraft.spigot.servers.fog.FoGPlayer#updateExperienceBar()}. + * XP bottles from crates push tracked XP via a different path. + */ +public class ExperienceSuppressor implements Listener { + + private final FoG server; + + public ExperienceSuppressor(FoG server) { + this.server = server; + } + + @EventHandler + public void onExpChange(PlayerExpChangeEvent event) { + event.setAmount(0); + } + + @EventHandler + public void onOrbTarget(EntityTargetLivingEntityEvent event) { + /* Prevent XP orbs from homing on players (they're destroyed when ignored). */ + if (event.getEntity() instanceof ExperienceOrb) { + event.setCancelled(true); + event.getEntity().remove(); + } + } + + @EventHandler + public void onExpBottle(ExpBottleEvent event) { + event.setExperience(0); + event.setShowEffect(true); + } + + @EventHandler + public void onDeath(PlayerDeathEvent event) { + event.setDroppedExp(0); + event.setKeepLevel(true); + } +} diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/events/FoGCommandEvents.java b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/events/FoGCommandEvents.java new file mode 100644 index 000000000..f201b16cd --- /dev/null +++ b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/events/FoGCommandEvents.java @@ -0,0 +1,12 @@ +package com.orbitmines.minecraft.spigot.servers.fog.events; + +import com.orbitmines.archive.minecraft.spigot._2019.libs.spigot.events.CommandEvents; +import com.orbitmines.minecraft.spigot.servers.fog.FoG; +import com.orbitmines.minecraft.spigot.servers.fog.FoGPlayer; + +public class FoGCommandEvents extends CommandEvents { + + public FoGCommandEvents(FoG server) { + super(server); + } +} diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/events/HologramCleanupListener.java b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/events/HologramCleanupListener.java new file mode 100644 index 000000000..c40ccf8cf --- /dev/null +++ b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/events/HologramCleanupListener.java @@ -0,0 +1,30 @@ +package com.orbitmines.minecraft.spigot.servers.fog.events; + +import com.orbitmines.minecraft.spigot.servers.fog.FoG; +import com.orbitmines.minecraft.spigot.servers.fog.util.HologramTag; +import org.bukkit.Bukkit; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.world.WorldLoadEvent; + +/** + * Removes leftover FoG holograms (tagged armor stands + floating items) whenever a + * world is loaded. This covers world-reloads mid-session and any run-worlds that + * come back with stale holograms saved in their level.dat from a prior session. + */ +public class HologramCleanupListener implements Listener { + + private final FoG server; + + public HologramCleanupListener(FoG server) { + this.server = server; + } + + @EventHandler + public void onWorldLoad(WorldLoadEvent event) { + int removed = HologramTag.cleanupWorld(event.getWorld()); + if (removed > 0) { + Bukkit.getLogger().info("[fog] Removed " + removed + " stale hologram entities in " + event.getWorld().getName()); + } + } +} diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/events/ItemDropListener.java b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/events/ItemDropListener.java new file mode 100644 index 000000000..61119c70b --- /dev/null +++ b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/events/ItemDropListener.java @@ -0,0 +1,30 @@ +package com.orbitmines.minecraft.spigot.servers.fog.events; + +import com.orbitmines.minecraft.spigot.servers.fog.FoG; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerDropItemEvent; +import org.bukkit.inventory.ItemStack; + +/** + * Blocks FoG lobby-kit items from being dropped. Those items are stamped with + * {@code interactive_kit:item_id} by {@link + * com.orbitmines.archive.minecraft.spigot._2019.utils.spigot.kits.interactive.InteractiveKit.Interaction#applyId}. + */ +public class ItemDropListener implements Listener { + + private final FoG server; + + public ItemDropListener(FoG server) { + this.server = server; + } + + @EventHandler + public void onDrop(PlayerDropItemEvent event) { + ItemStack stack = event.getItemDrop().getItemStack(); + Long id = server.getNms().customItem().getMetaDataLong(stack, "interactive_kit", "item_id"); + if (id != null && id >= 0) { + event.setCancelled(true); + } + } +} diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/events/LockedSlotsListener.java b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/events/LockedSlotsListener.java new file mode 100644 index 000000000..c441083f0 --- /dev/null +++ b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/events/LockedSlotsListener.java @@ -0,0 +1,71 @@ +package com.orbitmines.minecraft.spigot.servers.fog.events; + +import com.orbitmines.minecraft.spigot.servers.fog.FoG; +import com.orbitmines.minecraft.spigot.servers.fog.FoGPlayer; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.inventory.InventoryAction; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.event.inventory.InventoryDragEvent; +import org.bukkit.inventory.Inventory; + +/** + * Keeps the lobby-kit items pinned to their fixed slots (the player can't move, + * swap, or drop them) AND treats a plain click on one of those slots as a + * trigger — firing the same action that right-clicking the item in hand would. + * + *

Needed because slots 16 / 17 (top-right of the main inventory) can't be + * right-clicked in the world without first moving the item to the hotbar, which + * we also forbid. So the only sensible UX is to dispatch from the inventory + * click itself.

+ */ +public class LockedSlotsListener implements Listener { + + private final FoG server; + + public LockedSlotsListener(FoG server) { + this.server = server; + } + + @EventHandler + public void onClick(InventoryClickEvent event) { + if (!(event.getWhoClicked() instanceof org.bukkit.entity.Player bp)) return; + /* Only police the player's own inventory, not other inventories. */ + Inventory top = event.getView().getTopInventory(); + boolean clickingPlayerInv = event.getClickedInventory() == bp.getInventory(); + boolean shiftingIntoPlayerInv = event.getAction() == InventoryAction.MOVE_TO_OTHER_INVENTORY + && event.getClickedInventory() == top; + if (!clickingPlayerInv && !shiftingIntoPlayerInv) return; + + int slot = event.getSlot(); + if (!server.getLobbyKit().getLockedSlots().contains(slot)) return; + /* Always cancel so the item can't be picked up / swapped out. */ + event.setCancelled(true); + + /* Only dispatch the action when the player's own inventory is clicked + directly — shift-click-from-top-inventory shouldn't open another GUI. */ + if (!clickingPlayerInv) return; + + FoGPlayer player = server.getPlayer(bp); + if (player == null) return; + /* Close the inventory first on the next tick if the action opens another GUI. + The kit's own fireSlotAction handles no-ops when a hologram selector is up. */ + int finalSlot = slot; + server.runSync(() -> { + bp.closeInventory(); + server.getLobbyKit().fireSlotAction(finalSlot, player); + }); + } + + @EventHandler + public void onDrag(InventoryDragEvent event) { + if (!(event.getWhoClicked() instanceof org.bukkit.entity.Player bp)) return; + for (int raw : event.getRawSlots()) { + int slot = event.getView().convertSlot(raw); + if (server.getLobbyKit().getLockedSlots().contains(slot)) { + event.setCancelled(true); + return; + } + } + } +} diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/events/RunGUICloseListener.java b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/events/RunGUICloseListener.java new file mode 100644 index 000000000..3450f728e --- /dev/null +++ b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/events/RunGUICloseListener.java @@ -0,0 +1,33 @@ +package com.orbitmines.minecraft.spigot.servers.fog.events; + +import com.orbitmines.minecraft.spigot.servers.fog.FoG; +import com.orbitmines.minecraft.spigot.servers.fog.FoGPlayer; +import com.orbitmines.minecraft.spigot.servers.fog.gui.CoopJoinGUI; +import com.orbitmines.minecraft.spigot.servers.fog.gui.SpectatorJoinGUI; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.inventory.InventoryCloseEvent; + +/** + * Re-opens the top-level RunSelector when a player closes a run-related GUI + * (coop / spectate) without actually committing to their action — cancelling + * drops them back at the top-level choice rather than leaving them stranded. + */ +public class RunGUICloseListener implements Listener { + + private final FoG server; + + public RunGUICloseListener(FoG server) { + this.server = server; + } + + @EventHandler + public void onClose(InventoryCloseEvent event) { + if (!(event.getPlayer() instanceof Player bukkit)) return; + FoGPlayer player = server.getPlayer(bukkit); + if (player == null) return; + CoopJoinGUI.onInventoryClose(server, player, event.getInventory()); + SpectatorJoinGUI.onInventoryClose(server, player, event.getInventory()); + } +} diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/faction/Faction.java b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/faction/Faction.java new file mode 100644 index 000000000..9f9d32072 --- /dev/null +++ b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/faction/Faction.java @@ -0,0 +1,38 @@ +package com.orbitmines.minecraft.spigot.servers.fog.faction; + +import com.orbitmines.archive.minecraft._2019.libs.Color; +import com.orbitmines.archive.minecraft._2019.libs.player.Languageable; +import lombok.Getter; +import org.bukkit.Material; + +public enum Faction { + + OMEGA(Color.PURPLE, Material.ENDER_EYE), + ALPHA(Color.RED, Material.BLAZE_POWDER), + BETA (Color.AQUA, Material.PRISMARINE_CRYSTALS); + + @Getter private final Color color; + @Getter private final Material icon; + + Faction(Color color, Material icon) { + this.color = color; + this.icon = icon; + } + + public String nameKey() { return "faction." + name().toLowerCase() + ".name"; } + public String descriptionKey() { return "faction." + name().toLowerCase() + ".description"; } + + public String getDisplayName(Languageable viewer) { return viewer.translate("fog", nameKey()); } + public String[] getDescriptionLines(Languageable viewer) { + return viewer.getLanguage().getStringArray("fog", descriptionKey()); + } + + public String getColoredName(Languageable viewer) { + return color.getCc() + "§l" + getDisplayName(viewer); + } + + public static Faction parse(String s) { + if (s == null) return null; + try { return Faction.valueOf(s.toUpperCase()); } catch (IllegalArgumentException e) { return null; } + } +} diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/gui/CoopJoinGUI.java b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/gui/CoopJoinGUI.java new file mode 100644 index 000000000..5a4866929 --- /dev/null +++ b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/gui/CoopJoinGUI.java @@ -0,0 +1,111 @@ +package com.orbitmines.minecraft.spigot.servers.fog.gui; + +import com.orbitmines.archive.minecraft._2019.libs.Color; +import com.orbitmines.archive.minecraft._2019.libs.player.Name; +import com.orbitmines.archive.minecraft._2019.libs.utils.Message; +import com.orbitmines.archive.minecraft.spigot._2019.utils.spigot.builders.chat.text.TextBuilder; +import com.orbitmines.archive.minecraft.spigot._2019.utils.spigot.builders.item.ItemBuilder; +import com.orbitmines.archive.minecraft.spigot._2019.utils.spigot.builders.item.mutable.MutableItemBuilder; +import com.orbitmines.archive.minecraft.spigot._2019.utils.spigot.guis.GUI; +import com.orbitmines.minecraft.spigot.servers.fog.FoG; +import com.orbitmines.minecraft.spigot.servers.fog.FoGPlayer; +import com.orbitmines.minecraft.spigot.servers.fog.run.Run; +import net.md_5.bungee.api.chat.ClickEvent; +import net.md_5.bungee.api.chat.HoverEvent; +import org.bukkit.Material; +import org.bukkit.inventory.Inventory; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +/** + * List of online players currently in a run. Clicking sends a coop-join request + * to the target (like /tphere). The target accepts via a clickable chat message + * or {@code /coopaccept }. + * + *

If the viewer closes this GUI without clicking any player, the RunSelector + * is re-opened — cancelling coop puts them back at the top-level choice.

+ */ +public class CoopJoinGUI extends GUI { + + /** UUID → currently-open GUI; used by {@link com.orbitmines.minecraft.spigot.servers.fog.events.CoopCloseListener}. */ + private static final Map OPEN = new HashMap<>(); + + private final FoG fog; + private boolean requestSent; + + public static void open(FoG server, FoGPlayer viewer) { + CoopJoinGUI gui = new CoopJoinGUI(server, viewer); + OPEN.put(viewer.getUUID(), gui); + gui.open(); + } + + /** + * Called from {@link com.orbitmines.minecraft.spigot.servers.fog.events.CoopCloseListener} + * when the viewer closes any inventory. If the closed one is the registered + * CoopJoinGUI and no request was sent, re-open RunSelector on the next tick. + */ + public static void onInventoryClose(FoG server, FoGPlayer viewer, Inventory closed) { + CoopJoinGUI gui = OPEN.get(viewer.getUUID()); + if (gui == null) return; + if (gui.getInventory() != closed) return; + OPEN.remove(viewer.getUUID()); + if (!gui.requestSent) { + server.runSync(() -> RunSelector.open(server, viewer)); + } + } + + private CoopJoinGUI(FoG server, FoGPlayer viewer) { + super(54, "§0§l" + viewer.translate("fog", "gui.coop_join.title"), viewer); + this.fog = server; + + int slot = 0; + for (FoGPlayer other : server.getPlayers()) { + if (other == viewer) continue; + Run run = other.getActiveRun(); + if (run == null) continue; + final Run finalRun = run; + final String otherName = other.getName(); + final FoGPlayer finalOther = other; + set(slot++, new Item(() -> + new ItemBuilder(Material.PLAYER_HEAD, 1, "§e§l" + otherName) + .addLore(viewer.translate("fog", "gui.coop_join.run", finalRun.getId())) + .addLore(viewer.translate("fog", "gui.coop_join.difficulty", finalRun.getDifficulty().getColoredName(viewer))) + .addLore(" ") + .addLore("§a§l" + viewer.translate("fog", "gui.coop_join.click_to_request")), + event -> { + this.requestSent = true; + OPEN.remove(viewer.getUUID()); + viewer.closeInventory(); + sendRequest(server, viewer, finalOther, finalRun); + } + )); + if (slot >= 45) break; + } + + for (int s = slot; s < getInventory().getSize(); s++) { + set(s, new Item(() -> + new ItemBuilder(Material.BLACK_STAINED_GLASS_PANE, 1, "§f"))); + } + } + + /** Send a coop request to {@code target}. Target sees a click-to-accept chat message. */ + private static void sendRequest(FoG server, FoGPlayer requester, FoGPlayer target, Run run) { + target.getCoopRequestsFrom().put(requester.getRawName(), run.getId()); + + requester.sendMessage("Coop", Color.LIME, "fog", "coop.request.sent", target.getName(Name.RAW_COLORED) + "§7"); + + target.sendRawMessage(""); + target.sendMessage("Coop", Color.BLUE, "fog", "coop.request.received", requester.getName(Name.RAW_COLORED) + "§7"); + + TextBuilder builder = new TextBuilder<>(); + builder.add(Color.SILVER, p -> Message.format("Coop", Color.LIME, + " " + target.translate("fog", "coop.request.click_to_accept", + "§a" + target.translate("fog", "coop.request.accept")))) + .click(ClickEvent.Action.RUN_COMMAND, p -> "/coopaccept " + requester.getRawName()) + .hover(HoverEvent.Action.SHOW_TEXT, p -> "§7" + target.translate("fog", "coop.request.hover", + requester.getName(Name.RAW_COLORED) + "§7")); + builder.send(target); + } +} diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/gui/DifficultySelector.java b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/gui/DifficultySelector.java new file mode 100644 index 000000000..7a9d6d63c --- /dev/null +++ b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/gui/DifficultySelector.java @@ -0,0 +1,34 @@ +package com.orbitmines.minecraft.spigot.servers.fog.gui; + +import com.orbitmines.minecraft.spigot.servers.fog.FoG; +import com.orbitmines.minecraft.spigot.servers.fog.FoGPlayer; +import com.orbitmines.minecraft.spigot.servers.fog.run.Difficulty; + +import java.util.ArrayList; +import java.util.List; + +/** + * Hologram-based difficulty picker. Like the RunSelector, each option shows a + * title (Normal / Hard / Hardcore) above the floating icon — descriptions are + * omitted. + */ +public class DifficultySelector { + + private static final String[] NO_DESCRIPTION = new String[0]; + + public static void open(FoG server, FoGPlayer viewer) { + List> options = new ArrayList<>(); + for (Difficulty d : Difficulty.values()) { + options.add(new HologramSelector.Option<>( + d.getIcon(), + d.getColoredName(viewer), + NO_DESCRIPTION, + d + )); + } + + new HologramSelector<>(server, viewer, options, + (sel, difficulty) -> server.getRunManager().startNewRun(viewer, difficulty) + ).open(); + } +} diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/gui/DroneFactoryGUI.java b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/gui/DroneFactoryGUI.java new file mode 100644 index 000000000..165ecc8c6 --- /dev/null +++ b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/gui/DroneFactoryGUI.java @@ -0,0 +1,60 @@ +package com.orbitmines.minecraft.spigot.servers.fog.gui; + +import com.orbitmines.archive.minecraft.spigot._2019.utils.spigot.builders.item.ItemBuilder; +import com.orbitmines.archive.minecraft.spigot._2019.utils.spigot.builders.item.mutable.MutableItemBuilder; +import com.orbitmines.archive.minecraft.spigot._2019.utils.spigot.guis.GUI; +import com.orbitmines.minecraft.spigot.servers.fog.FoG; +import com.orbitmines.minecraft.spigot.servers.fog.FoGPlayer; +import com.orbitmines.minecraft.spigot.servers.fog.drone.Drone; +import com.orbitmines.minecraft.spigot.servers.fog.drone.DroneFactory; +import com.orbitmines.minecraft.spigot.servers.fog.drone.DroneModule; +import com.orbitmines.minecraft.spigot.servers.fog.drone.ModuleType; + +public class DroneFactoryGUI extends GUI { + + public DroneFactoryGUI(FoG server, FoGPlayer viewer) { + super(54, "§0§l" + viewer.translate("fog", "gui.drone_factory.title"), viewer); + + int slot = 0; + for (Drone drone : viewer.getDrones()) { + final Drone finalDrone = drone; + set(slot++, new Item(() -> + new ItemBuilder(org.bukkit.Material.ALLAY_SPAWN_EGG, 1, + "§b§l" + viewer.translate("fog", "gui.drone_factory.drone_title", finalDrone.getId())) + .addLore(viewer.translate("fog", "gui.drone_factory.hp", (int) finalDrone.getHealth(), (int) finalDrone.getMaxHealth())) + .addLore(viewer.translate("fog", "gui.drone_factory.modules", finalDrone.getModules().size())) + .addLore(" ") + .addLore("§e§l" + viewer.translate("fog", "gui.drone_factory.click_to_repair", DroneFactory.repairCost(finalDrone))), + event -> { + DroneFactory.repair(finalDrone); + viewer.sendMessage("FoG", com.orbitmines.archive.minecraft._2019.libs.Color.LIME, "fog", "drone.repaired", finalDrone.getId()); + } + )); + if (slot >= 9) break; + } + + slot = 18; + for (ModuleType module : ModuleType.values()) { + final ModuleType finalModule = module; + set(slot++, new Item(() -> + new ItemBuilder(finalModule.getIcon(), 1, + finalModule.getColor().getCc() + "§l" + finalModule.getDisplayName(viewer)) + .addLore(" ") + .addLore("§e§l" + viewer.translate("fog", "gui.drone_factory.click_to_attach")), + event -> { + if (viewer.getDrones().isEmpty()) { + viewer.sendMessage("FoG", com.orbitmines.archive.minecraft._2019.libs.Color.RED, "fog", "drone.none"); + return; + } + Drone first = viewer.getDrones().get(0); + if (!first.addModule(new DroneModule(finalModule))) { + viewer.sendMessage("FoG", com.orbitmines.archive.minecraft._2019.libs.Color.RED, "fog", "drone.modules_full"); + } else { + viewer.sendMessage("FoG", com.orbitmines.archive.minecraft._2019.libs.Color.LIME, "fog", "drone.module_attached", + finalModule.getDisplayName(viewer), first.getId()); + } + } + )); + } + } +} diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/gui/HologramSelector.java b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/gui/HologramSelector.java new file mode 100644 index 000000000..87eaabb7b --- /dev/null +++ b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/gui/HologramSelector.java @@ -0,0 +1,205 @@ +package com.orbitmines.minecraft.spigot.servers.fog.gui; + +import com.orbitmines.archive.minecraft.spigot._2019.utils.spigot.builders.item.ItemBuilder; +import com.orbitmines.archive.minecraft.spigot._2019.utils.spigot.freezer.Freezer; +import com.orbitmines.archive.minecraft.spigot._2019.utils.spigot.npcs.FloatingItem; +import com.orbitmines.minecraft.spigot.servers.fog.FoG; +import com.orbitmines.minecraft.spigot.servers.fog.FoGPlayer; +import com.orbitmines.minecraft.spigot.servers.fog.util.HologramTag; +import lombok.Getter; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.entity.Entity; +import org.bukkit.inventory.ItemStack; +import org.bukkit.util.Vector; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.function.BiConsumer; + +/** + * Reusable hologram-based choice selector. + * + *

Spawns N {@link FloatingItem}s in a horizontal arc in front of the viewer at + * eye level and freezes the viewer in place (head-turn only) until one is clicked. + * Click dispatch goes through {@code NpcEvents}+{@code Npc.InteractAction}.

+ * + *

Chain-of-selectors behaviour: opening a selector while another is already + * up re-uses the existing freeze; resolving only releases the freeze if no new + * selector follows in the same tick. This prevents the player from being + * repeatedly teleported between prompts.

+ */ +public class HologramSelector { + + /** Horizontal radius of the arc — all options sit this distance from the player. + Kept well within survival reach (~3 blocks) so the player can right-click + any option without moving. */ + public static final double OPTION_RADIUS = 3.0; + /** Angular separation between adjacent options, in degrees. Wider = more visual + breathing room without pushing options out of reach. */ + public static final double OPTION_ANGLE_STEP_DEG = 50.0; + /** + * Y-offset from the viewer's feet to the FloatingItem's user-anchor. + * + *

{@code FloatingItem} (2-arg constructor) places the visible item 1.75 + * above the anchor you pass it, and its invisible clickbox hitbox spans + * from anchor+0.75 to anchor+2.75 — which wraps the item visual. If we + * deviate from this geometry (e.g. by passing {@code yOff=0}), the clickbox + * no longer overlaps the item and right-clicks don't fire.

+ * + *

A negative value here keeps the item at chest/belly height while + * preserving that clickbox/visual relationship. With -0.5 the item appears + * at feet+1.25 and the clickbox covers feet+0.25 to feet+2.25.

+ */ + public static final double OPTION_HEIGHT = -0.5; + + /** UUID → currently-open selector. A player can only have one open at a time. */ + private static final Map> OPEN = new HashMap<>(); + + public static class Option { + public final Material icon; + public final String title; // coloured display name + public final String[] description; + public final T value; + + public Option(Material icon, String title, String[] description, T value) { + this.icon = icon; + this.title = title; + this.description = description == null ? new String[0] : description; + this.value = value; + } + } + + @Getter private final FoG server; + @Getter private final FoGPlayer viewer; + @Getter private final List> options; + @Getter private final BiConsumer, T> onSelect; + + private final List> items = new ArrayList<>(); + private final Set ourEntityIds = new HashSet<>(); + private boolean resolved; + + public HologramSelector(FoG server, FoGPlayer viewer, List> options, + BiConsumer, T> onSelect) { + this.server = server; + this.viewer = viewer; + this.options = options; + this.onSelect = onSelect; + } + + public static boolean hasOpen(UUID uuid) { + HologramSelector sel = OPEN.get(uuid); + return sel != null && !sel.resolved; + } + + /** Opens the selector. Must be called on the main thread. */ + public void open() { + /* If another selector was already up for this player, close it first. */ + HologramSelector prev = OPEN.get(viewer.getUUID()); + if (prev != null && prev != this) prev.destroy(); + + OPEN.put(viewer.getUUID(), this); + + /* Freeze the viewer in place (head-turn only). No teleport, no float-up. */ + if (!viewer.isFrozen()) { + viewer.freeze(Freezer.MOVE); + } + + Location anchor = viewer.getLocation().clone().add(0, OPTION_HEIGHT, 0); + + /* Arc layout: all options sit at OPTION_RADIUS from the viewer, spread + across an angular range centred on the viewer's facing direction. + Bukkit yaw: yaw=0 faces +Z ("south"); forward = (-sin(yaw), 0, cos(yaw)). */ + double baseYawRad = Math.toRadians(viewer.getLocation().getYaw()); + double stepRad = Math.toRadians(OPTION_ANGLE_STEP_DEG); + + int n = options.size(); + double mid = (n - 1) / 2.0; + for (int i = 0; i < n; i++) { + Option opt = options.get(i); + double angle = baseYawRad + (i - mid) * stepRad; + double dx = -Math.sin(angle) * OPTION_RADIUS; + double dz = Math.cos(angle) * OPTION_RADIUS; + Location optLoc = anchor.clone().add(new Vector(dx, 0, dz)); + render(opt, optLoc); + } + } + + private void render(Option opt, Location at) { + /* Apply the title to the ItemStack itself (shown on hover). If title is + null, the item is a bare floating block/item with no name. */ + ItemStack stack = opt.title == null + ? new ItemBuilder(opt.icon).build() + : new ItemBuilder(opt.icon, 1, opt.title).build(); + + /* 2-arg FloatingItem — its internal yOff=1.75 is what keeps the invisible + clickbox aligned over the item visual. See OPTION_HEIGHT's javadoc. */ + FloatingItem floating = new FloatingItem<>(() -> stack, at); + floating.setInteractAction((event, player) -> { + if (resolved) return; + if (!player.getUUID().equals(viewer.getUUID())) return; + resolve(opt.value); + }); + + /* Title line above the item — skipped entirely for "label-less" options. */ + if (opt.title != null) { + floating.addLine(() -> opt.title, false); + } + for (String line : opt.description) { + if (line == null) continue; + floating.addLine(() -> "§7" + line, false); + } + floating.create(viewer.bukkit()); + + /* Tag every entity (armor stands + the floating item) so the world-load + cleanup can remove any leftovers without touching player-placed ones. */ + for (Entity e : floating.getEntities()) { + HologramTag.tag(e); + } + + items.add(floating); + ourEntityIds.add(floating.getItem() == null ? -1 : floating.getItem().getEntityId()); + } + + private void resolve(T value) { + if (resolved) return; + resolved = true; + + destroyRenderedOnly(); + + if (onSelect != null) onSelect.accept(this, value); + + /* If the callback didn't open a successor selector, release the freeze. + If it did, the new selector owns the freeze and we leave it alone. */ + HologramSelector now = OPEN.get(viewer.getUUID()); + if (now == this) { + OPEN.remove(viewer.getUUID()); + if (viewer.isFrozen()) viewer.clearFreeze(); + } + } + + /** Destroy without firing the callback (used when a newer selector replaces us). */ + public void destroy() { + if (resolved) return; + resolved = true; + destroyRenderedOnly(); + if (OPEN.get(viewer.getUUID()) == this) OPEN.remove(viewer.getUUID()); + } + + public void cancel() { + destroy(); + if (viewer.isFrozen()) viewer.clearFreeze(); + } + + private void destroyRenderedOnly() { + for (FloatingItem it : items) { + try { it.destroy(); } catch (Exception ignored) {} + } + items.clear(); + } +} diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/gui/RecipeBookGUI.java b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/gui/RecipeBookGUI.java new file mode 100644 index 000000000..f023a12c9 --- /dev/null +++ b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/gui/RecipeBookGUI.java @@ -0,0 +1,35 @@ +package com.orbitmines.minecraft.spigot.servers.fog.gui; + +import com.orbitmines.archive.minecraft.spigot._2019.utils.spigot.builders.item.ItemBuilder; +import com.orbitmines.archive.minecraft.spigot._2019.utils.spigot.builders.item.mutable.MutableItemBuilder; +import com.orbitmines.archive.minecraft.spigot._2019.utils.spigot.guis.GUI; +import com.orbitmines.minecraft.spigot.servers.fog.FoG; +import com.orbitmines.minecraft.spigot.servers.fog.FoGPlayer; +import com.orbitmines.minecraft.spigot.servers.fog.recipe.RecipeBook; +import com.orbitmines.minecraft.spigot.servers.fog.recipe.RecipeEntry; +import com.orbitmines.minecraft.spigot.servers.fog.stats.Stats; +import org.bukkit.Material; + +public class RecipeBookGUI extends GUI { + + public RecipeBookGUI(FoG server, FoGPlayer viewer) { + super(54, "§0§l" + viewer.translate("fog", "gui.codex.title"), viewer); + + /* Unlocked = the player has ever picked the gating choice. Reads the + stats cache — no DB access, so the GUI can be built on any thread. */ + Stats stats = viewer.getStats(); + int slot = 0; + for (RecipeEntry entry : RecipeBook.ENTRIES) { + boolean unlocked = entry.getUnlockChoice() == null + || (stats != null && stats.getChoiceStackCount(entry.getUnlockChoice()) > 0); + Material icon = unlocked ? entry.getIcon() : Material.GRAY_DYE; + String name = unlocked + ? ("§a§l" + entry.getDisplayName(viewer)) + : ("§7§l" + entry.getDisplayName(viewer) + " §c" + viewer.translate("fog", "gui.codex.locked_suffix")); + set(slot++, new Item(() -> + new ItemBuilder(icon, 1, name).addLore("§7" + entry.getDescription(viewer)) + )); + if (slot >= getInventory().getSize()) break; + } + } +} diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/gui/RunSelector.java b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/gui/RunSelector.java new file mode 100644 index 000000000..cc6717f70 --- /dev/null +++ b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/gui/RunSelector.java @@ -0,0 +1,75 @@ +package com.orbitmines.minecraft.spigot.servers.fog.gui; + +import com.orbitmines.minecraft.spigot.servers.fog.FoG; +import com.orbitmines.minecraft.spigot.servers.fog.FoGPlayer; +import com.orbitmines.minecraft.spigot.servers.fog.run.Run; +import org.bukkit.Material; + +import java.util.ArrayList; +import java.util.List; + +/** + * Top-level run selector. Shown when the player joins FoG without an active + * run, or when they right-click / inventory-click the Switch Run kit item. + * + *

If the player already has an active run, a leftmost "Continue" option + * appears (orange, spruce hanging sign) so they can resume it without picking + * one of the other flows.

+ */ +public class RunSelector { + + public enum Action { CONTINUE, NEW_RUN, COOP_JOIN, SPECTATE_RUN } + + private static final String[] NO_DESCRIPTION = new String[0]; + + public static void open(FoG server, FoGPlayer viewer) { + List> options = new ArrayList<>(); + + boolean hasActiveRun = viewer.getActiveRun() != null + || (viewer.getPlayerModel() != null && viewer.getPlayerModel().getActiveRunId() != null); + if (hasActiveRun) { + options.add(new HologramSelector.Option<>( + Material.SPRUCE_HANGING_SIGN, + "§6§l" + viewer.translate("fog", "run.selector.continue.name"), + NO_DESCRIPTION, + Action.CONTINUE)); + } + + options.add(new HologramSelector.Option<>( + Material.GRASS_BLOCK, + "§a§l" + viewer.translate("fog", "run.selector.new.name"), + NO_DESCRIPTION, + Action.NEW_RUN)); + options.add(new HologramSelector.Option<>( + Material.DARK_OAK_BOAT, + "§2§l" + viewer.translate("fog", "run.selector.coop.name"), + NO_DESCRIPTION, + Action.COOP_JOIN)); + options.add(new HologramSelector.Option<>( + Material.ENDER_EYE, + "§5§l" + viewer.translate("fog", "run.selector.spectate.name"), + NO_DESCRIPTION, + Action.SPECTATE_RUN)); + + new HologramSelector<>(server, viewer, options, (sel, action) -> { + switch (action) { + case CONTINUE: { + Run current = viewer.getActiveRun(); + Long runId = current != null ? current.getId() + : viewer.getPlayerModel() != null ? viewer.getPlayerModel().getActiveRunId() : null; + if (runId != null) server.getRunManager().joinRun(viewer, runId); + break; + } + case NEW_RUN: + DifficultySelector.open(server, viewer); + break; + case COOP_JOIN: + CoopJoinGUI.open(server, viewer); + break; + case SPECTATE_RUN: + SpectatorJoinGUI.open(server, viewer); + break; + } + }).open(); + } +} diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/gui/ShopGUI.java b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/gui/ShopGUI.java new file mode 100644 index 000000000..55a617d42 --- /dev/null +++ b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/gui/ShopGUI.java @@ -0,0 +1,31 @@ +package com.orbitmines.minecraft.spigot.servers.fog.gui; + +import com.orbitmines.archive.minecraft.spigot._2019.utils.spigot.builders.item.ItemBuilder; +import com.orbitmines.archive.minecraft.spigot._2019.utils.spigot.builders.item.mutable.MutableItemBuilder; +import com.orbitmines.archive.minecraft.spigot._2019.utils.spigot.guis.GUI; +import com.orbitmines.minecraft.spigot.servers.fog.FoG; +import com.orbitmines.minecraft.spigot.servers.fog.FoGPlayer; +import com.orbitmines.minecraft.spigot.servers.fog.shop.Shop; +import com.orbitmines.minecraft.spigot.servers.fog.shop.ShopOffer; + +public class ShopGUI extends GUI { + + public ShopGUI(FoG server, FoGPlayer viewer) { + super(54, "§0§l" + viewer.translate("fog", "gui.shop.title"), viewer); + + int slot = 0; + for (ShopOffer offer : Shop.DEFAULT_OFFERS) { + set(slot++, new Item(() -> + new ItemBuilder(offer.getIcon(), 1, "§e§l" + offer.getDisplayName(viewer)) + .addLore("§7" + offer.getDescription(viewer)) + .addLore(" ") + .addLore("§6§l" + viewer.translate("fog", "gui.shop.price", offer.getPriceCredits())), + event -> viewer.sendMessage("FoG", + com.orbitmines.archive.minecraft._2019.libs.Color.YELLOW, + "fog", "shop.purchase_stub", + offer.getDisplayName(viewer)) + )); + if (slot >= getInventory().getSize()) break; + } + } +} diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/gui/SpectatorJoinGUI.java b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/gui/SpectatorJoinGUI.java new file mode 100644 index 000000000..9f8cf6c57 --- /dev/null +++ b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/gui/SpectatorJoinGUI.java @@ -0,0 +1,82 @@ +package com.orbitmines.minecraft.spigot.servers.fog.gui; + +import com.orbitmines.archive.minecraft.spigot._2019.utils.spigot.builders.item.ItemBuilder; +import com.orbitmines.archive.minecraft.spigot._2019.utils.spigot.builders.item.mutable.MutableItemBuilder; +import com.orbitmines.archive.minecraft.spigot._2019.utils.spigot.guis.GUI; +import com.orbitmines.minecraft.spigot.servers.fog.FoG; +import com.orbitmines.minecraft.spigot.servers.fog.FoGPlayer; +import com.orbitmines.minecraft.spigot.servers.fog.run.Run; +import org.bukkit.Material; +import org.bukkit.inventory.Inventory; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +/** + * List of running players' runs. A scrollable inventory list fits better than + * holograms here because there may be many candidates. + * + *

If the viewer closes this GUI without clicking anyone, the RunSelector is + * re-opened — cancelling spectate drops them back at the top-level choice, same + * UX as cancelling the coop GUI.

+ */ +public class SpectatorJoinGUI extends GUI { + + private static final Map OPEN = new HashMap<>(); + + private boolean spectateLaunched; + + public static void open(FoG server, FoGPlayer viewer) { + SpectatorJoinGUI gui = new SpectatorJoinGUI(server, viewer); + OPEN.put(viewer.getUUID(), gui); + gui.open(); + } + + /** + * Called from {@link com.orbitmines.minecraft.spigot.servers.fog.events.RunGUICloseListener}. + * If the closed inventory matches the registered SpectatorJoinGUI and the + * viewer didn't commit to spectating anyone, re-open the RunSelector. + */ + public static void onInventoryClose(FoG server, FoGPlayer viewer, Inventory closed) { + SpectatorJoinGUI gui = OPEN.get(viewer.getUUID()); + if (gui == null) return; + if (gui.getInventory() != closed) return; + OPEN.remove(viewer.getUUID()); + if (!gui.spectateLaunched) { + server.runSync(() -> RunSelector.open(server, viewer)); + } + } + + private SpectatorJoinGUI(FoG server, FoGPlayer viewer) { + super(54, "§0§l" + viewer.translate("fog", "gui.spectator_join.title"), viewer); + + int slot = 0; + for (FoGPlayer other : server.getPlayers()) { + if (other == viewer) continue; + Run run = other.getActiveRun(); + if (run == null) continue; + final Run finalRun = run; + final String otherName = other.getName(); + set(slot++, new Item(() -> + new ItemBuilder(Material.PLAYER_HEAD, 1, "§e§l" + otherName) + .addLore(viewer.translate("fog", "gui.spectator_join.run", finalRun.getId())) + .addLore(viewer.translate("fog", "gui.spectator_join.difficulty", finalRun.getDifficulty().getColoredName(viewer))) + .addLore(" ") + .addLore("§a§l" + viewer.translate("fog", "gui.spectator_join.click_to_spectate")), + event -> { + this.spectateLaunched = true; + OPEN.remove(viewer.getUUID()); + viewer.closeInventory(); + server.getRunManager().spectateRun(viewer, finalRun.getId()); + } + )); + if (slot >= 45) break; + } + + for (int s = slot; s < getInventory().getSize(); s++) { + set(s, new Item(() -> + new ItemBuilder(Material.BLACK_STAINED_GLASS_PANE, 1, "§f"))); + } + } +} diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/item/Backpack.java b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/item/Backpack.java new file mode 100644 index 000000000..d72ce0c44 --- /dev/null +++ b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/item/Backpack.java @@ -0,0 +1,28 @@ +package com.orbitmines.minecraft.spigot.servers.fog.item; + +import com.orbitmines.archive.minecraft._2019.libs.player.Languageable; +import com.orbitmines.minecraft.spigot.servers.fog.FoG; +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.BundleMeta; + +/** Custom "Backpack" — a stamped bundle that can hold more than a vanilla stack. */ +public class Backpack { + + public static final String NS = "fog"; + public static final String KEY_TIER = "backpack_tier"; + + public static ItemStack create(FoG server, Languageable viewer, int tier) { + ItemStack stack = new ItemStack(Material.BUNDLE); + BundleMeta meta = (BundleMeta) stack.getItemMeta(); + if (meta != null) { + meta.setDisplayName("§6§l" + viewer.translate("fog", "item.backpack.name", tier)); + stack.setItemMeta(meta); + } + return server.getNms().customItem().setMetaData(stack, NS, KEY_TIER, tier); + } + + public static Integer getTier(FoG server, ItemStack stack) { + return server.getNms().customItem().getMetaDataInt(stack, NS, KEY_TIER); + } +} diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/item/Pickaxe.java b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/item/Pickaxe.java new file mode 100644 index 000000000..962945dfc --- /dev/null +++ b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/item/Pickaxe.java @@ -0,0 +1,34 @@ +package com.orbitmines.minecraft.spigot.servers.fog.item; + +import com.orbitmines.archive.minecraft._2019.libs.player.Languageable; +import com.orbitmines.minecraft.spigot.servers.fog.FoG; +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; + +/** Pickaxe upgrade chain — crafted by combining a current pickaxe with ores. */ +public class Pickaxe { + + public static final String NS = "fog"; + public static final String KEY_TIER = "pickaxe_tier"; + + public static ItemStack tier(FoG server, Languageable viewer, int tier) { + Material mat = switch (tier) { + case 2 -> Material.IRON_PICKAXE; + case 3 -> Material.DIAMOND_PICKAXE; + case 4 -> Material.NETHERITE_PICKAXE; + default -> Material.STONE_PICKAXE; + }; + ItemStack stack = new ItemStack(mat); + ItemMeta meta = stack.getItemMeta(); + if (meta != null) { + meta.setDisplayName("§b§l" + viewer.translate("fog", "item.pickaxe.name", tier)); + stack.setItemMeta(meta); + } + return server.getNms().customItem().setMetaData(stack, NS, KEY_TIER, tier); + } + + public static Integer getTier(FoG server, ItemStack stack) { + return server.getNms().customItem().getMetaDataInt(stack, NS, KEY_TIER); + } +} diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/kit/FoGLobbyKit.java b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/kit/FoGLobbyKit.java new file mode 100644 index 000000000..03b40f772 --- /dev/null +++ b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/kit/FoGLobbyKit.java @@ -0,0 +1,150 @@ +package com.orbitmines.minecraft.spigot.servers.fog.kit; + +import com.orbitmines.archive.minecraft.spigot._2019.utils.spigot.builders.item.ItemBuilder; +import com.orbitmines.archive.minecraft.spigot._2019.utils.spigot.kits.interactive.InteractiveKit; +import com.orbitmines.minecraft.spigot.servers.fog.FoG; +import com.orbitmines.minecraft.spigot.servers.fog.FoGPlayer; +import com.orbitmines.minecraft.spigot.servers.fog.gui.HologramSelector; +import com.orbitmines.minecraft.spigot.servers.fog.gui.RecipeBookGUI; +import com.orbitmines.minecraft.spigot.servers.fog.gui.RunSelector; +import com.orbitmines.minecraft.spigot.servers.fog.gui.SpectatorJoinGUI; +import lombok.Getter; +import org.bukkit.Material; +import org.bukkit.event.player.PlayerInteractEvent; +import org.bukkit.inventory.ItemStack; + +import java.util.HashSet; +import java.util.Set; + +/** + * Kit given to FoG players: a drone toggle + codex on the hotbar, and + * Spectate + Switch Run pinned to the top-right of the main inventory. + * + *

Interactions have two dispatch paths — both fire the same action: + *

    + *
  1. Right-clicking the item while it's held, via + * {@link com.orbitmines.archive.minecraft.spigot._2019.utils.spigot.item_handlers.ItemInteraction} + * and the already-registered ItemHandlerEvents.
  2. + *
  3. Clicking the item in the inventory view on one of the locked slots, + * handled by {@link com.orbitmines.minecraft.spigot.servers.fog.events.LockedSlotsListener} + * which calls {@link #fireSlotAction(int, FoGPlayer)}.
  4. + *
+ * + *

Both paths route through the private {@link #fire} helper so the behavior + * stays symmetric.

+ */ +public class FoGLobbyKit extends InteractiveKit { + + public static final int SLOT_DRONE_TOGGLE = 2; + public static final int SLOT_RECIPE_BOOK = 4; + public static final int SLOT_SPECTATE = 16; + public static final int SLOT_SWITCH_RUN = 17; + + @Getter private final Set lockedSlots = new HashSet<>(); + + private final FoG fog; + + public FoGLobbyKit(FoG server) { + this.fog = server; + + lockedSlots.add(SLOT_DRONE_TOGGLE); + lockedSlots.add(SLOT_RECIPE_BOOK); + lockedSlots.add(SLOT_SPECTATE); + lockedSlots.add(SLOT_SWITCH_RUN); + + /* Drone toggle — material flips based on player.isDronesActive(). */ + set(SLOT_DRONE_TOGGLE, + player -> new ItemBuilder( + player.isDronesActive() ? Material.REDSTONE_TORCH : Material.LEVER, 1, + "§b§l" + player.translate("fog", "item.drone_toggle.name")), + new InteractiveKit.Interaction(server) { + @Override + public void onInteract(FoGPlayer player, PlayerInteractEvent event, ItemStack item) { + fire(SLOT_DRONE_TOGGLE, player); + } + }.onActionBarHover((player, item) -> + "§b§l" + player.translate("fog", "item.drone_toggle.name") + + "§r §8- " + + (player.isDronesActive() + ? "§a§l" + player.translate("fog", "drone.active") + : "§6§l" + player.translate("fog", "drone.idle")) + + " §8- §e§l" + player.translate("spigot", "player.mouse.right_click")) + ); + + set(SLOT_RECIPE_BOOK, + player -> new ItemBuilder(Material.KNOWLEDGE_BOOK, 1, + "§a§l" + player.translate("fog", "item.recipe_book.name")), + new InteractiveKit.Interaction(server) { + @Override + public void onInteract(FoGPlayer player, PlayerInteractEvent event, ItemStack item) { + fire(SLOT_RECIPE_BOOK, player); + } + }.onActionBarHover((player, item) -> + "§a§l" + player.translate("fog", "item.recipe_book.name") + + "§r §8- §e§l" + player.translate("spigot", "player.mouse.right_click")) + ); + + set(SLOT_SPECTATE, + player -> new ItemBuilder(Material.ENDER_EYE, 1, + "§5§l" + player.translate("fog", "item.spectator_join.name")), + new InteractiveKit.Interaction(server) { + @Override + public void onInteract(FoGPlayer player, PlayerInteractEvent event, ItemStack item) { + fire(SLOT_SPECTATE, player); + } + }.onActionBarHover((player, item) -> + "§5§l" + player.translate("fog", "item.spectator_join.name") + + "§r §8- §e§l" + player.translate("spigot", "player.mouse.right_click")) + ); + + set(SLOT_SWITCH_RUN, + player -> new ItemBuilder(Material.COMPASS, 1, + "§2§l" + player.translate("fog", "item.run_switcher.name")), + new InteractiveKit.Interaction(server) { + @Override + public void onInteract(FoGPlayer player, PlayerInteractEvent event, ItemStack item) { + fire(SLOT_SWITCH_RUN, player); + } + }.onActionBarHover((player, item) -> + "§2§l" + player.translate("fog", "item.run_switcher.name") + + "§r §8- §e§l" + player.translate("spigot", "player.mouse.right_click")) + ); + } + + /** Re-copy just the drone-toggle slot so its material (LEVER↔REDSTONE_TORCH) refreshes. */ + public void refreshDroneToggle(FoGPlayer player) { + player.getInventory().setItem(SLOT_DRONE_TOGGLE, build(get(SLOT_DRONE_TOGGLE), player)); + } + + /** + * Set only the always-on top-right items (Switch Run, Spectate). Used on + * every join / state change so the player always has an escape route — even + * while spectating someone else's run or when they have no active run at all. + */ + public void copyAlwaysOnToInventory(FoGPlayer player) { + player.getInventory().setItem(SLOT_SWITCH_RUN, build(get(SLOT_SWITCH_RUN), player)); + player.getInventory().setItem(SLOT_SPECTATE, build(get(SLOT_SPECTATE), player)); + } + + /** Fire the slot's action from an inventory click. */ + public void fireSlotAction(int slot, FoGPlayer player) { + fire(slot, player); + } + + private void fire(int slot, FoGPlayer player) { + /* Blocked while a hologram selector is up — can't double-trigger selectors. */ + if (HologramSelector.hasOpen(player.getUUID())) return; + switch (slot) { + case SLOT_DRONE_TOGGLE: player.toggleDroneMode(); break; + case SLOT_RECIPE_BOOK: new RecipeBookGUI(fog, player).open(); break; + case SLOT_SPECTATE: SpectatorJoinGUI.open(fog, player); break; + case SLOT_SWITCH_RUN: + /* Lift the player to the sky anchor first so the selector hologram + is visible in open air (same UX as the initial join). */ + fog.teleportToSkyAnchor(player); + fog.runSync(() -> RunSelector.open(fog, player)); + break; + default: /* no-op */ + } + } +} diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/level/LevelFormulas.java b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/level/LevelFormulas.java new file mode 100644 index 000000000..db33dc621 --- /dev/null +++ b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/level/LevelFormulas.java @@ -0,0 +1,34 @@ +package com.orbitmines.minecraft.spigot.servers.fog.level; + +/** Level/XP curves for FoG. Mirrors KitPvP's feel. */ +public class LevelFormulas { + + public static final int MAX_LEVEL = 100; + + /** XP required *to reach* level (level+1) from the start of level. */ + public static long required(int level) { + return (level + 1L) * (300 + 8L * level); + } + + /** Given total XP earned in the run, compute current level and leftover XP. */ + public static int computeLevel(long totalXp) { + long remaining = totalXp; + for (int i = 0; i <= MAX_LEVEL; i++) { + long req = required(i); + if (remaining >= req) { + remaining -= req; + } else { + return i; + } + } + return MAX_LEVEL; + } + + public static long xpIntoLevel(long totalXp, int level) { + long remaining = totalXp; + for (int i = 0; i < level; i++) { + remaining -= required(i); + } + return Math.max(0, remaining); + } +} diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/level/LevelUpFlow.java b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/level/LevelUpFlow.java new file mode 100644 index 000000000..1ad7e96d0 --- /dev/null +++ b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/level/LevelUpFlow.java @@ -0,0 +1,98 @@ +package com.orbitmines.minecraft.spigot.servers.fog.level; + +import com.orbitmines.minecraft.spigot.servers.fog.FoG; +import com.orbitmines.minecraft.spigot.servers.fog.FoGPlayer; +import com.orbitmines.minecraft.spigot.servers.fog.choice.Choice; +import com.orbitmines.minecraft.spigot.servers.fog.choice.ChoicePicker; +import com.orbitmines.minecraft.spigot.servers.fog.gui.HologramSelector; +import com.orbitmines.minecraft.spigot.servers.fog.run.Run; +import com.orbitmines.minecraft.spigot.servers.fog.stats.Stats; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +/** + * Level-up prompt. Builds a {@link HologramSelector} with the three {@link Choice}s + * rolled by {@link ChoicePicker}. All state reads come from in-memory caches + * (Stats + Run); store writes happen async in the resolve callback. + */ +public class LevelUpFlow { + + private final FoG server; + private final Map> active = new HashMap<>(); + + public LevelUpFlow(FoG server) { + this.server = server; + } + + public boolean hasPrompt(UUID uuid) { + return active.containsKey(uuid) || HologramSelector.hasOpen(uuid); + } + + public void cleanup(UUID uuid) { + HologramSelector selector = active.remove(uuid); + if (selector != null) selector.cancel(); + } + + /** Begin a level-up prompt. Main-thread only. */ + public void start(FoGPlayer player, int level, Run run) { + start(player, level, run, null); + } + + /** + * Begin a level-up prompt with a post-resolve hook. The hook fires after the + * choice has been applied in-memory and scheduled for persistence. + * Main-thread only. + */ + public void start(FoGPlayer player, int level, Run run, Runnable afterResolve) { + if (active.containsKey(player.getUUID())) return; + Stats stats = player.getStats(); + if (stats == null) return; + + List rolled = ChoicePicker.pickThree(run, stats, level); + Choice damagedAtThisLevel = stats.getDamagedChoiceAt(level); + + List> options = new ArrayList<>(); + for (Choice choice : rolled) { + String[] description = choice.getDescriptionLines(player); + String[] finalDescription = description; + if (choice == damagedAtThisLevel) { + finalDescription = new String[description.length + 1]; + System.arraycopy(description, 0, finalDescription, 0, description.length); + finalDescription[description.length] = "§c" + player.translate("fog", "level.previously"); + } + options.add(new HologramSelector.Option<>( + choice.getIcon(), + choice.getRarity().getColor().getCc() + "§l" + choice.getDisplayName(player), + finalDescription, + choice + )); + } + + HologramSelector selector = new HologramSelector<>( + server, player, options, + (sel, choice) -> { + /* In-memory caches first, so applyDerivedStats / further pickers + observe the new count immediately. */ + ChoicePicker.applyInMemory(run, player.getStats(), choice); + player.applyDerivedStats(); + player.sendMessage("FoG", + com.orbitmines.archive.minecraft._2019.libs.Color.YELLOW, + "fog", "level.chosen", choice.getDisplayName(player)); + server.runAsync(() -> { + ChoicePicker.recordChoice(run, player.getUUID(), level, choice); + run.getStore().setPendingChoiceLevel(player.getUUID(), null); + }); + active.remove(player.getUUID()); + if (afterResolve != null) afterResolve.run(); + } + ); + active.put(player.getUUID(), selector); + /* Persist that a prompt is pending — rejoin re-opens the same prompt. */ + server.runAsync(() -> run.getStore().setPendingChoiceLevel(player.getUUID(), level)); + selector.open(); + } +} diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/mob/Bee.java b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/mob/Bee.java new file mode 100644 index 000000000..3f44867c4 --- /dev/null +++ b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/mob/Bee.java @@ -0,0 +1,29 @@ +package com.orbitmines.minecraft.spigot.servers.fog.mob; + +import com.orbitmines.minecraft.spigot.servers.fog.run.Run; +import org.bukkit.Location; +import org.bukkit.entity.EntityType; + +/** Aggressive bee-like mob. Poison chance on sting handled in DamageListener. */ +public class Bee extends FoGMob { + + public Bee(Run run) { + super(run); + } + + @Override public double baseHealth() { return 10.0; } + @Override public String translationKey() { return "mob.bee.name"; } + @Override public String colorPrefix() { return "§e"; } + + @Override + public void spawn(Location at) { + org.bukkit.entity.Bee b = (org.bukkit.entity.Bee) at.getWorld().spawnEntity(at, EntityType.BEE); + b.setMaxHealth(baseHealth()); + b.setHealth(baseHealth()); + b.setAnger(Integer.MAX_VALUE); + b.setCustomName(displayName()); + b.setCustomNameVisible(true); + this.entity = b; + this.entityUuid = b.getUniqueId(); + } +} diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/mob/Bull.java b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/mob/Bull.java new file mode 100644 index 000000000..b912e4e19 --- /dev/null +++ b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/mob/Bull.java @@ -0,0 +1,29 @@ +package com.orbitmines.minecraft.spigot.servers.fog.mob; + +import com.orbitmines.minecraft.spigot.servers.fog.run.Run; +import org.bukkit.Location; +import org.bukkit.entity.EntityType; +import org.bukkit.entity.Ravager; + +/** Bull that charges at players and knocks them into the air on hit. */ +public class Bull extends FoGMob { + + public Bull(Run run) { + super(run); + } + + @Override public double baseHealth() { return 60.0; } + @Override public String translationKey() { return "mob.bull.name"; } + @Override public String colorPrefix() { return "§6"; } + + @Override + public void spawn(Location at) { + Ravager r = (Ravager) at.getWorld().spawnEntity(at, EntityType.RAVAGER); + r.setMaxHealth(baseHealth()); + r.setHealth(baseHealth()); + r.setCustomName(displayName()); + r.setCustomNameVisible(true); + this.entity = r; + this.entityUuid = r.getUniqueId(); + } +} diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/mob/DebuffMage.java b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/mob/DebuffMage.java new file mode 100644 index 000000000..3d2a3044e --- /dev/null +++ b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/mob/DebuffMage.java @@ -0,0 +1,78 @@ +package com.orbitmines.minecraft.spigot.servers.fog.mob; + +import com.orbitmines.minecraft.spigot.servers.fog.run.Run; +import org.bukkit.Location; +import org.bukkit.Particle; +import org.bukkit.entity.EntityType; +import org.bukkit.entity.LivingEntity; +import org.bukkit.entity.Player; +import org.bukkit.entity.WitherSkull; +import org.bukkit.entity.Witch; +import org.bukkit.potion.PotionEffect; +import org.bukkit.potion.PotionEffectType; +import org.bukkit.util.Vector; + +/** Undead mage that casts lingering slow fields (30s), withering skulls (5s cd). */ +public class DebuffMage extends FoGMob { + + private long lastSkullTick; + private long lastSlowTick; + + public DebuffMage(Run run) { + super(run); + } + + @Override public double baseHealth() { return 36.0; } + @Override public String translationKey() { return "mob.debuff_mage.name"; } + @Override public String colorPrefix() { return "§5"; } + + @Override + public void spawn(Location at) { + Witch witch = (Witch) at.getWorld().spawnEntity(at, EntityType.WITCH); + witch.setMaxHealth(baseHealth()); + witch.setHealth(baseHealth()); + witch.setCustomName(displayName()); + witch.setCustomNameVisible(true); + this.entity = witch; + this.entityUuid = witch.getUniqueId(); + } + + @Override + public void tick() { + if (!isAlive()) return; + long now = entity.getWorld().getFullTime(); + Location loc = entity.getLocation(); + + if (now - lastSlowTick > 400) { + for (Player p : entity.getWorld().getPlayers()) { + if (p.getLocation().distance(loc) < 10) { + p.addPotionEffect(new PotionEffect(PotionEffectType.SLOWNESS, 20 * 30, 1)); + } + } + entity.getWorld().spawnParticle(Particle.WITCH, loc, 30, 2, 1, 2, 0); + lastSlowTick = now; + } + + if (now - lastSkullTick > 100) { + LivingEntity target = nearestPlayer(20.0); + if (target != null) { + WitherSkull skull = entity.getWorld().spawn(loc.add(0, 1, 0), WitherSkull.class); + Vector dir = target.getEyeLocation().toVector().subtract(loc.toVector()).normalize().multiply(1.2); + skull.setDirection(dir); + skull.setShooter(entity); + skull.setCharged(false); + } + lastSkullTick = now; + } + } + + private LivingEntity nearestPlayer(double range) { + Player best = null; + double bestDistSq = range * range; + for (Player p : entity.getWorld().getPlayers()) { + double d = p.getLocation().distanceSquared(entity.getLocation()); + if (d < bestDistSq) { bestDistSq = d; best = p; } + } + return best; + } +} diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/mob/FireMage.java b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/mob/FireMage.java new file mode 100644 index 000000000..03c12a0fe --- /dev/null +++ b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/mob/FireMage.java @@ -0,0 +1,66 @@ +package com.orbitmines.minecraft.spigot.servers.fog.mob; + +import com.orbitmines.minecraft.spigot.servers.fog.run.Run; +import org.bukkit.Location; +import org.bukkit.Particle; +import org.bukkit.entity.Blaze; +import org.bukkit.entity.EntityType; +import org.bukkit.entity.LivingEntity; +import org.bukkit.entity.Player; +import org.bukkit.entity.SmallFireball; +import org.bukkit.util.Vector; + +/** + * Undead mage that throws fire balls (4s cd), leaves a fire trail for 10s, + * and explodes on death. POC: blaze-based stand-in. + */ +public class FireMage extends FoGMob { + + private long lastFireballTick; + + public FireMage(Run run) { + super(run); + } + + @Override public double baseHealth() { return 40.0; } + @Override public String translationKey() { return "mob.fire_mage.name"; } + @Override public String colorPrefix() { return "§c"; } + + @Override + public void spawn(Location at) { + Blaze blaze = (Blaze) at.getWorld().spawnEntity(at, EntityType.BLAZE); + blaze.setMaxHealth(baseHealth()); + blaze.setHealth(baseHealth()); + blaze.setCustomName(displayName()); + blaze.setCustomNameVisible(true); + this.entity = blaze; + this.entityUuid = blaze.getUniqueId(); + } + + @Override + public void tick() { + if (!isAlive()) return; + Location loc = entity.getLocation(); + loc.getWorld().spawnParticle(Particle.FLAME, loc, 4, 0.2, 0.1, 0.2, 0.01); + + long now = loc.getWorld().getFullTime(); + if (now - lastFireballTick < 80) return; + LivingEntity target = nearestPlayer(24.0); + if (target == null) return; + Vector dir = target.getEyeLocation().toVector().subtract(loc.toVector()).normalize().multiply(1.4); + SmallFireball fb = loc.getWorld().spawn(loc.add(0, 1, 0), SmallFireball.class); + fb.setDirection(dir); + fb.setShooter((Blaze) entity); + lastFireballTick = now; + } + + private LivingEntity nearestPlayer(double range) { + Player best = null; + double bestDistSq = range * range; + for (Player p : entity.getWorld().getPlayers()) { + double d = p.getLocation().distanceSquared(entity.getLocation()); + if (d < bestDistSq) { bestDistSq = d; best = p; } + } + return best; + } +} diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/mob/FoGMob.java b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/mob/FoGMob.java new file mode 100644 index 000000000..85c297c41 --- /dev/null +++ b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/mob/FoGMob.java @@ -0,0 +1,46 @@ +package com.orbitmines.minecraft.spigot.servers.fog.mob; + +import com.orbitmines.minecraft.spigot.servers.fog.run.Run; +import com.orbitmines.minecraft.spigot.servers.fog.util.Fogi18n; +import lombok.Getter; +import org.bukkit.Location; +import org.bukkit.entity.LivingEntity; + +import java.util.UUID; + +/** Base class for all FoG mobs. Owns a spawn hook + a per-tick behavior hook. */ +public abstract class FoGMob { + + @Getter protected final Run run; + @Getter protected LivingEntity entity; + @Getter protected UUID entityUuid; + + protected FoGMob(Run run) { + this.run = run; + } + + public abstract double baseHealth(); + + /** Namespace key for this mob's display name (under `fog/mob..name`). */ + public abstract String translationKey(); + + /** Chat-colour prefix applied to the default-locale display name. */ + public abstract String colorPrefix(); + + public final String displayName() { + return colorPrefix() + "§l" + Fogi18n.defaultText(translationKey()); + } + + public abstract void spawn(Location at); + + /** Invoked periodically while the mob is alive. Override to express AI. */ + public void tick() { /* default no-op */ } + + public boolean isAlive() { + return entity != null && entity.isValid() && !entity.isDead(); + } + + public void remove() { + if (entity != null && entity.isValid()) entity.remove(); + } +} diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/mob/FoGZombie.java b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/mob/FoGZombie.java new file mode 100644 index 000000000..31de0b5f6 --- /dev/null +++ b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/mob/FoGZombie.java @@ -0,0 +1,32 @@ +package com.orbitmines.minecraft.spigot.servers.fog.mob; + +import com.orbitmines.minecraft.spigot.servers.fog.run.Run; +import org.bukkit.Location; +import org.bukkit.entity.EntityType; +import org.bukkit.entity.Zombie; + +/** + * Zombie variant whose melee attacks can apply the hunger debuff. Hunger application + * logic lives in events/DamageListener so it can observe EntityDamageByEntityEvent cleanly. + */ +public class FoGZombie extends FoGMob { + + public FoGZombie(Run run) { + super(run); + } + + @Override public double baseHealth() { return 24.0; } + @Override public String translationKey() { return "mob.zombie.name"; } + @Override public String colorPrefix() { return "§7"; } + + @Override + public void spawn(Location at) { + Zombie z = (Zombie) at.getWorld().spawnEntity(at, EntityType.ZOMBIE); + z.setMaxHealth(baseHealth()); + z.setHealth(baseHealth()); + z.setCustomName(displayName()); + z.setCustomNameVisible(true); + this.entity = z; + this.entityUuid = z.getUniqueId(); + } +} diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/ore/Ore.java b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/ore/Ore.java new file mode 100644 index 000000000..bc86ea5d3 --- /dev/null +++ b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/ore/Ore.java @@ -0,0 +1,40 @@ +package com.orbitmines.minecraft.spigot.servers.fog.ore; + +import com.orbitmines.minecraft.spigot.servers.fog.choice.Choice; +import lombok.Getter; +import org.bukkit.Material; + +public enum Ore { + + /* Vanilla — always available */ + COAL(null, Material.COAL_ORE, Material.COAL, 1), + IRON(null, Material.IRON_ORE, Material.RAW_IRON, 3), + GOLD(null, Material.GOLD_ORE, Material.RAW_GOLD, 5), + LAPIS(null, Material.LAPIS_ORE, Material.LAPIS_LAZULI, 6), + REDSTONE(null, Material.REDSTONE_ORE, Material.REDSTONE, 4), + DIAMOND(null, Material.DIAMOND_ORE, Material.DIAMOND, 10), + EMERALD(null, Material.EMERALD_ORE, Material.EMERALD, 12), + + /* Unlocked via choices */ + COPPER(Choice.ORE_COPPER, Material.COPPER_ORE, Material.RAW_COPPER, 3), + COBALT(Choice.ORE_COBALT, Material.LIGHT_BLUE_CONCRETE_POWDER, Material.LIGHT_BLUE_DYE, 6), + STRONTIUM(Choice.ORE_STRONTIUM, Material.YELLOW_CONCRETE_POWDER, Material.GLOWSTONE_DUST, 8), + AMETHYST(Choice.ORE_AMETHYST, Material.AMETHYST_BLOCK, Material.AMETHYST_SHARD, 7), + URANIUM(Choice.ORE_URANIUM, Material.LIME_CONCRETE_POWDER, Material.LIME_DYE, 15), + IRIDIUM(Choice.ORE_IRIDIUM, Material.LIGHT_GRAY_CONCRETE_POWDER, Material.LIGHT_GRAY_DYE, 18), + FRANCIUM(Choice.ORE_FRANCIUM, Material.MAGENTA_CONCRETE_POWDER, Material.MAGENTA_DYE, 25); + + @Getter private final Choice unlockChoice; + @Getter private final Material oreMaterial; + @Getter private final Material dropMaterial; + @Getter private final int value; // abstract value for shop pricing / drone repair costs + + Ore(Choice unlockChoice, Material oreMaterial, Material dropMaterial, int value) { + this.unlockChoice = unlockChoice; + this.oreMaterial = oreMaterial; + this.dropMaterial = dropMaterial; + this.value = value; + } + + public boolean alwaysAvailable() { return unlockChoice == null; } +} diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/quest/Quest.java b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/quest/Quest.java new file mode 100644 index 000000000..65214e515 --- /dev/null +++ b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/quest/Quest.java @@ -0,0 +1,28 @@ +package com.orbitmines.minecraft.spigot.servers.fog.quest; + +import lombok.Getter; +import lombok.Setter; + +/** One active quest scoped to a (run, player) pair. */ +public class Quest { + + @Getter private final String id; + @Getter private final QuestType type; + @Getter private final String target; // Material name or EntityType name + @Getter private final int requiredAmount; + @Getter @Setter private int progress; + @Getter private final int creditsReward; + @Getter private final int xpReward; + + public Quest(String id, QuestType type, String target, int requiredAmount, int creditsReward, int xpReward) { + this.id = id; + this.type = type; + this.target = target; + this.requiredAmount = requiredAmount; + this.creditsReward = creditsReward; + this.xpReward = xpReward; + this.progress = 0; + } + + public boolean isDone() { return progress >= requiredAmount; } +} diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/quest/QuestGenerator.java b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/quest/QuestGenerator.java new file mode 100644 index 000000000..4bed3926e --- /dev/null +++ b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/quest/QuestGenerator.java @@ -0,0 +1,35 @@ +package com.orbitmines.minecraft.spigot.servers.fog.quest; + +import org.bukkit.Material; +import org.bukkit.entity.EntityType; + +import java.util.Random; + +/** Generates SkyBlock-style quests. Keeps the pool intentionally small for POC. */ +public class QuestGenerator { + + private static final Random RANDOM = new Random(); + + private static final Material[] COLLECT_POOL = { + Material.COAL, Material.IRON_INGOT, Material.GOLD_INGOT, Material.DIAMOND, + Material.OAK_LOG, Material.COBBLESTONE, Material.WHEAT, Material.CARROT + }; + + private static final EntityType[] DEFEAT_POOL = { + EntityType.ZOMBIE, EntityType.SKELETON, EntityType.CREEPER, EntityType.SPIDER, + EntityType.WITCH, EntityType.BLAZE + }; + + public static Quest random(int level) { + boolean collect = RANDOM.nextBoolean(); + if (collect) { + Material m = COLLECT_POOL[RANDOM.nextInt(COLLECT_POOL.length)]; + int req = 16 + RANDOM.nextInt(48) + level * 4; + return new Quest("q_" + System.currentTimeMillis(), QuestType.COLLECT, m.name(), req, 50 + level * 20, 40 + level * 10); + } else { + EntityType e = DEFEAT_POOL[RANDOM.nextInt(DEFEAT_POOL.length)]; + int req = 3 + RANDOM.nextInt(12) + level; + return new Quest("q_" + System.currentTimeMillis(), QuestType.DEFEAT, e.name(), req, 80 + level * 30, 60 + level * 15); + } + } +} diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/quest/QuestType.java b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/quest/QuestType.java new file mode 100644 index 000000000..26e0cb65c --- /dev/null +++ b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/quest/QuestType.java @@ -0,0 +1,6 @@ +package com.orbitmines.minecraft.spigot.servers.fog.quest; + +public enum QuestType { + COLLECT, + DEFEAT +} diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/raid/Raid.java b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/raid/Raid.java new file mode 100644 index 000000000..2112019e2 --- /dev/null +++ b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/raid/Raid.java @@ -0,0 +1,83 @@ +package com.orbitmines.minecraft.spigot.servers.fog.raid; + +import com.orbitmines.minecraft.spigot.servers.fog.mob.Bee; +import com.orbitmines.minecraft.spigot.servers.fog.mob.Bull; +import com.orbitmines.minecraft.spigot.servers.fog.mob.FireMage; +import com.orbitmines.minecraft.spigot.servers.fog.mob.FoGMob; +import com.orbitmines.minecraft.spigot.servers.fog.mob.FoGZombie; +import com.orbitmines.minecraft.spigot.servers.fog.run.Run; +import com.orbitmines.minecraft.spigot.servers.fog.util.Fogi18n; +import lombok.Getter; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.boss.BarColor; +import org.bukkit.boss.BarStyle; +import org.bukkit.boss.BossBar; +import org.bukkit.entity.Player; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +/** + * A time-boxed wave of mobs scaled by average online player level. + * Progress bar is the ratio of (mobs remaining / mobs spawned). + */ +public class Raid { + + private static final Random RANDOM = new Random(); + + @Getter private final Run run; + @Getter private final int level; + @Getter private final List mobs = new ArrayList<>(); + @Getter private final BossBar bossBar; + private final int initialCount; + + public Raid(Run run, int level) { + this.run = run; + this.level = level; + this.bossBar = Bukkit.createBossBar(Fogi18n.defaultText("raid.title", level), BarColor.RED, BarStyle.SEGMENTED_10); + int base = 6 + level; + this.initialCount = base; + this.bossBar.setProgress(1.0); + } + + public void start(Location centre) { + for (Player p : centre.getWorld().getPlayers()) bossBar.addPlayer(p); + for (int i = 0; i < initialCount; i++) { + Location at = centre.clone().add(RANDOM.nextInt(30) - 15, 0, RANDOM.nextInt(30) - 15); + FoGMob mob = pickMob(); + mob.spawn(at); + mobs.add(mob); + } + } + + private FoGMob pickMob() { + int roll = RANDOM.nextInt(100); + if (roll < 50) return new FoGZombie(run); + if (roll < 75) return new FireMage(run); + if (roll < 90) return new Bee(run); + return new Bull(run); + } + + public void tick() { + int alive = 0; + for (FoGMob m : mobs) { + if (m.isAlive()) { alive++; m.tick(); } + } + bossBar.setProgress((double) alive / (double) Math.max(1, initialCount)); + if (alive == 0) { + bossBar.removeAll(); + } + } + + public boolean isOver() { + for (FoGMob m : mobs) if (m.isAlive()) return false; + return true; + } + + public void cleanup() { + for (FoGMob m : mobs) m.remove(); + bossBar.removeAll(); + } +} diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/raid/RaidManager.java b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/raid/RaidManager.java new file mode 100644 index 000000000..22152bdcc --- /dev/null +++ b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/raid/RaidManager.java @@ -0,0 +1,44 @@ +package com.orbitmines.minecraft.spigot.servers.fog.raid; + +import com.orbitmines.minecraft.spigot.servers.fog.run.Run; +import org.bukkit.Location; +import org.bukkit.entity.Player; + +import java.util.ArrayList; +import java.util.List; + +/** Tracks active raids per run. */ +public class RaidManager { + + private final Run run; + private final List active = new ArrayList<>(); + + public RaidManager(Run run) { + this.run = run; + } + + /** Compute raid level from online players in this run. Lower-level players contribute less. */ + public int computeLevel(List onlineMembers, int[] levels) { + int total = 0; + for (int l : levels) total += Math.max(1, l); + int n = Math.max(1, onlineMembers.size()); + return Math.max(1, total / n); + } + + public Raid startRaid(Location at, int level) { + Raid r = new Raid(run, level); + r.start(at); + active.add(r); + return r; + } + + public void tick() { + active.removeIf(r -> { + r.tick(); + if (r.isOver()) { r.cleanup(); return true; } + return false; + }); + } + + public List getActive() { return active; } +} diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/recipe/RecipeBook.java b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/recipe/RecipeBook.java new file mode 100644 index 000000000..6c35efd39 --- /dev/null +++ b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/recipe/RecipeBook.java @@ -0,0 +1,25 @@ +package com.orbitmines.minecraft.spigot.servers.fog.recipe; + +import com.orbitmines.minecraft.spigot.servers.fog.choice.Choice; +import org.bukkit.Material; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** Greenbook-style codex of every recipe/mechanic the player has encountered. */ +public class RecipeBook { + + public static final List ENTRIES; + + static { + List entries = new ArrayList<>(); + entries.add(new RecipeEntry("ore_suit", Material.IRON_CHESTPLATE, Choice.RECIPE_ORE_SUIT)); + entries.add(new RecipeEntry("pickaxe_upgrade", Material.DIAMOND_PICKAXE, Choice.RECIPE_PICKAXE_UP)); + entries.add(new RecipeEntry("backpack", Material.BUNDLE, Choice.RECIPE_BACKPACK)); + entries.add(new RecipeEntry("drone_factory_intro", Material.SMITHING_TABLE, null)); + entries.add(new RecipeEntry("crate_schedule", Material.CHEST, Choice.STRUCTURE_CRATES)); + entries.add(new RecipeEntry("factions", Material.ENDER_EYE, null)); + ENTRIES = Collections.unmodifiableList(entries); + } +} diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/recipe/RecipeEntry.java b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/recipe/RecipeEntry.java new file mode 100644 index 000000000..76460dd82 --- /dev/null +++ b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/recipe/RecipeEntry.java @@ -0,0 +1,26 @@ +package com.orbitmines.minecraft.spigot.servers.fog.recipe; + +import com.orbitmines.archive.minecraft._2019.libs.player.Languageable; +import com.orbitmines.minecraft.spigot.servers.fog.choice.Choice; +import lombok.Getter; +import org.bukkit.Material; + +/** One entry in the codex. Text comes from {@code fog/recipe..{name,description}}. */ +public class RecipeEntry { + + @Getter private final String id; + @Getter private final Material icon; + @Getter private final Choice unlockChoice; // null = always available + + public RecipeEntry(String id, Material icon, Choice unlockChoice) { + this.id = id; + this.icon = icon; + this.unlockChoice = unlockChoice; + } + + public String nameKey() { return "recipe." + id + ".name"; } + public String descriptionKey() { return "recipe." + id + ".description"; } + + public String getDisplayName(Languageable viewer) { return viewer.translate("fog", nameKey()); } + public String getDescription(Languageable viewer) { return viewer.translate("fog", descriptionKey()); } +} diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/run/Difficulty.java b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/run/Difficulty.java new file mode 100644 index 000000000..8a6e9029c --- /dev/null +++ b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/run/Difficulty.java @@ -0,0 +1,45 @@ +package com.orbitmines.minecraft.spigot.servers.fog.run; + +import com.orbitmines.archive.minecraft._2019.libs.Color; +import com.orbitmines.archive.minecraft._2019.libs.player.Languageable; +import lombok.Getter; +import org.bukkit.Material; + +/** Difficulty knobs. Text comes from `fog/difficulty..{name,description}`. */ +public enum Difficulty { + + NORMAL (Color.LIME, Material.LIME_CONCRETE, 5, 1.0, false), + HARD (Color.ORANGE, Material.ORANGE_CONCRETE, 10, 1.5, false), + HARDCORE(Color.RED, Material.RED_CONCRETE, 10, 1.5, true); + + @Getter private final Color color; + @Getter private final Material icon; + @Getter private final int maxLevelsLostOnDeath; + @Getter private final double mobDifficultyMultiplier; + @Getter private final boolean permadeath; + + Difficulty(Color color, Material icon, int maxLevelsLostOnDeath, double mobDifficultyMultiplier, boolean permadeath) { + this.color = color; + this.icon = icon; + this.maxLevelsLostOnDeath = maxLevelsLostOnDeath; + this.mobDifficultyMultiplier = mobDifficultyMultiplier; + this.permadeath = permadeath; + } + + public String nameKey() { return "difficulty." + name().toLowerCase() + ".name"; } + public String descriptionKey() { return "difficulty." + name().toLowerCase() + ".description"; } + + public String getDisplayName(Languageable viewer) { return viewer.translate("fog", nameKey()); } + public String[] getDescriptionLines(Languageable viewer) { + return viewer.getLanguage().getStringArray("fog", descriptionKey()); + } + + public String getColoredName(Languageable viewer) { + return color.getCc() + "§l" + getDisplayName(viewer); + } + + public static Difficulty parse(String s) { + if (s == null) return NORMAL; + try { return Difficulty.valueOf(s.toUpperCase()); } catch (IllegalArgumentException e) { return NORMAL; } + } +} diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/run/Run.java b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/run/Run.java new file mode 100644 index 000000000..05fbf8f2c --- /dev/null +++ b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/run/Run.java @@ -0,0 +1,89 @@ +package com.orbitmines.minecraft.spigot.servers.fog.run; + +import com.orbitmines.minecraft.spigot.servers.fog.choice.Choice; +import com.orbitmines.minecraft.spigot.servers.fog.database.FoGRunModel; +import lombok.Getter; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.World; + +import java.util.Date; +import java.util.EnumSet; +import java.util.Set; +import java.util.UUID; + +/** + * Runtime wrapper around a {@link FoGRunModel} + {@link RunStore}. The spawn + * location is owned by the world's level.dat (via {@link World#setSpawnLocation}) + * — not persisted in the DB. + * + *

The "uniques taken" set is the in-memory authoritative view for the lifetime + * of the server process. It's seeded from the store on the async side when a + * {@link RunManager} first loads the run, and mutated by choice-recording flows. + * Sync-thread callers must only read it — writes happen alongside the async + * DB persistence.

+ */ +public class Run { + + @Getter private final FoGRunModel model; + @Getter private final RunStore store; + private final Set uniquesTaken = EnumSet.noneOf(Choice.class); + + public Run(FoGRunModel model) { + this.model = model; + this.store = new RunStore(model.getId() == null ? 0 : model.getId()); + } + + public long getId() { return model.getId() == null ? 0 : model.getId(); } + public UUID getOwnerUuid() { return model.getOwnerUuid(); } + public String getWorldFileName() { return model.getWorldFileName(); } + public Difficulty getDifficulty() { return Difficulty.parse(model.getDifficulty()); } + public RunState getState() { return RunState.parse(model.getState()); } + + public void setState(RunState state) { + model.setState(state.toString()); + } + + public World getWorld() { + if (model.getWorldFileName() == null) return null; + return Bukkit.getWorld(model.getWorldFileName()); + } + + public Location getSpawn() { + World world = getWorld(); + if (world == null) return null; + return world.getSpawnLocation(); + } + + public boolean isOwner(UUID uuid) { + return uuid != null && uuid.equals(model.getOwnerUuid()); + } + + /** Global run start date (when the owner created it). */ + public Date getCreatedAt() { + return model.getCreatedAt(); + } + + /** Per-player "joined this run" date, or null if the player has never joined. */ + public Date getMemberJoinedAt(UUID uuid) { + Long millis = store.getMemberJoinedAtMillis(uuid); + return millis == null ? null : new Date(millis); + } + + /* === In-memory uniques-taken set === */ + + public boolean isUniqueTaken(Choice choice) { + return uniquesTaken.contains(choice); + } + + public void markUniqueTaken(Choice choice) { + if (choice != null && choice.isUnique()) uniquesTaken.add(choice); + } + + /** Async-only. Seeds the uniques-taken set from the store. Idempotent. */ + public void loadUniquesFromStore() { + for (Choice c : Choice.values()) { + if (c.isUnique() && store.isUniqueTaken(c)) uniquesTaken.add(c); + } + } +} diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/run/RunManager.java b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/run/RunManager.java new file mode 100644 index 000000000..269841de2 --- /dev/null +++ b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/run/RunManager.java @@ -0,0 +1,307 @@ +package com.orbitmines.minecraft.spigot.servers.fog.run; + +import com.orbitmines.archive.minecraft._2019.libs.Color; +import com.orbitmines.archive.minecraft.spigot._2019.utils.spigot.builders.chat.ActionBar; +import com.orbitmines.minecraft.spigot.servers.fog.FoG; +import com.orbitmines.minecraft.spigot.servers.fog.FoGPlayer; +import com.orbitmines.minecraft.spigot.servers.fog.database.FoGPlayerModel; +import com.orbitmines.minecraft.spigot.servers.fog.database.FoGRunModel; +import com.orbitmines.minecraft.spigot.servers.fog.structure.Compartment; +import com.orbitmines.minecraft.spigot.servers.fog.structure.CompartmentType; +import com.orbitmines.minecraft.spigot.servers.fog.world.CaveScorer; +import com.orbitmines.minecraft.spigot.servers.fog.world.CityWall; +import com.orbitmines.minecraft.spigot.servers.fog.world.FoGWorld; +import org.bukkit.GameMode; +import org.bukkit.Location; +import org.bukkit.World; + +import java.util.Map; +import java.util.Random; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Lifecycle of {@link Run} instances. Sync/async discipline: + * + *
    + *
  • DB reads/writes (RunStore, models) happen only on async threads.
  • + *
  • World block operations, teleports, inventory edits happen only on the main thread.
  • + *
+ * + * All public methods here hide that split from callers. + */ +public class RunManager { + + private final FoG server; + private final Map loaded = new ConcurrentHashMap<>(); + + public RunManager(FoG server) { + this.server = server; + } + + public Run get(long id) { return loaded.get(id); } + + public Run registerLoaded(Run run) { + loaded.put(run.getId(), run); + return run; + } + + public Run runForModel(FoGRunModel model) { + Run existing = loaded.get(model.getId()); + if (existing != null) return existing; + return new Run(model); + } + + /** Create a fresh run for the given player. Async cave-scoring, then teleport. */ + public void startNewRun(FoGPlayer owner, Difficulty difficulty) { + final UUID ownerUuid = owner.getUUID(); + + owner.sendMessage("FoG", Color.YELLOW, "fog", "run.preparing"); + new ActionBar(owner.bukkit(), () -> "§e§l" + owner.translate("fog", "run.preparing"), 20 * 60) { + @Override public void onRun() { + if (owner.getActiveRun() != null) forceStop(); + } + }.send(); + + /* 1) Async: insert run model to get an id, compute world file name. */ + server.runAsync(() -> { + FoGRunModel model = new FoGRunModel(ownerUuid, difficulty.name(), "pending", RunState.PREPARING.name()); + model.insert(); + + String worldFileName = ownerUuid.toString().toLowerCase() + "_fog_" + model.getId(); + model.setWorldFileName(worldFileName); + model.update(FoGRunModel.column.WORLD_FILE_NAME); + + long seed = FoGWorld.randomSeed(); + + /* 2) Sync: load the world. */ + server.runSync(() -> { + World world = FoGWorld.createOrLoad(worldFileName, seed); + if (world == null) { + owner.sendMessage("FoG", Color.RED, "fog", "run.generation_failed"); + return; + } + + /* 3) Async: cave-score (forces chunk gen, slow). */ + server.runAsync(() -> { + Random r = new Random(seed); + int startCx = r.nextInt(40) - 20; + int startCz = r.nextInt(40) - 20; + Location spawn = CaveScorer.findBestSpawn(world, startCx, startCz, 8); + + /* 4) Sync: place blocks, teleport, give inventory, start level-up. */ + server.runSync(() -> { + world.setSpawnLocation(spawn); + world.save(); + + CityWall.build(world, spawn.getBlockX(), spawn.getBlockY(), spawn.getBlockZ(), 24, 5); + + int[] farmC = Compartment.place(world, CompartmentType.FARM, spawn.getBlockX() + 12, spawn.getBlockY(), spawn.getBlockZ() + 12); + int[] enchC = Compartment.place(world, CompartmentType.ENCHANTING, spawn.getBlockX() - 14, spawn.getBlockY(), spawn.getBlockZ() + 10); + int[] crateC = Compartment.place(world, CompartmentType.CRATES, spawn.getBlockX() + 14, spawn.getBlockY(), spawn.getBlockZ() - 10); + int[] droneFC = Compartment.place(world, CompartmentType.DRONE_FACTORY, spawn.getBlockX() - 12, spawn.getBlockY(), spawn.getBlockZ() - 12); + + Run run = runForModel(model); + registerLoaded(run); + + /* Fresh run — no prior data, so Stats stays all zeros; no load needed. */ + com.orbitmines.minecraft.spigot.servers.fog.stats.Stats freshStats = + new com.orbitmines.minecraft.spigot.servers.fog.stats.Stats( + run.getStore(), "member:" + ownerUuid); + + /* Teleport FIRST so the downgrade to SURVIVAL + no-flight happens + on solid ground (avoids fall damage if the player is still at + the sky anchor when this fires). */ + owner.teleport(spawn); + owner.bukkit().setInvulnerable(false); + owner.setGameMode(GameMode.SURVIVAL); + owner.setAllowFlight(false); + owner.setFlying(false); + owner.bukkit().setCollidable(true); + + owner.initForRun(run, null, freshStats, 0L); + owner.giveRunStartInventory(); + owner.sendMessage("FoG", Color.LIME, "fog", "run.ready"); + + /* Immediately confront the player with a level 0 choice. */ + server.getLevelUpFlow().start(owner, 0, run); + + /* 5) Async: persist all DB writes (compartments, membership, model state, player pointer). */ + server.runAsync(() -> { + Compartment.persist(run.getStore(), CompartmentType.FARM, farmC[0], farmC[1], farmC[2]); + Compartment.persist(run.getStore(), CompartmentType.ENCHANTING, enchC[0], enchC[1], enchC[2]); + Compartment.persist(run.getStore(), CompartmentType.CRATES, crateC[0], crateC[1], crateC[2]); + Compartment.persist(run.getStore(), CompartmentType.DRONE_FACTORY, droneFC[0], droneFC[1], droneFC[2]); + + run.getStore().registerMember(ownerUuid); + + model.setState(RunState.ACTIVE.name()); + model.update(FoGRunModel.column.STATE); + + owner.getPlayerModel().setActiveRunId(model.getId()); + if (!owner.getPlayerModel().isInserted()) owner.getPlayerModel().insert(); + else owner.getPlayerModel().update(FoGPlayerModel.column.ACTIVE_RUN_ID); + }); + }); + }); + }); + }); + } + + /** Join a run as a full member. Restores the saved per-run inventory if any. */ + public void joinRun(FoGPlayer player, long runId) { + server.runAsync(() -> { + FoGRunModel model = FoGRunModel.findBy(FoGRunModel.class, FoGRunModel.column.ID.is(runId)); + if (model == null) { + player.sendMessage("FoG", Color.RED, "fog", "run.not_found"); + return; + } + /* Read everything we need async first. */ + String blob = null; + boolean firstTimeMember; + Run run; + synchronized (loaded) { + run = runForModel(model); + } + /* Seed the run's in-memory uniques set from the store (async-only). */ + run.loadUniquesFromStore(); + firstTimeMember = !run.getStore().isMember(player.getUUID()); + run.getStore().registerMember(player.getUUID()); + blob = run.getStore().getMemberInventoryBlob(player.getUUID()); + + /* Pre-load stats async — constructor does no DB, load() walks the store. */ + com.orbitmines.minecraft.spigot.servers.fog.stats.Stats stats = + new com.orbitmines.minecraft.spigot.servers.fog.stats.Stats( + run.getStore(), "member:" + player.getUUID()); + stats.load(); + long totalXp = run.getStore().getMemberExperience(player.getUUID()); + Integer pendingChoiceLevel = run.getStore().getPendingChoiceLevel(player.getUUID()); + + player.getPlayerModel().setActiveRunId(run.getId()); + if (!player.getPlayerModel().isInserted()) player.getPlayerModel().insert(); + else player.getPlayerModel().update(FoGPlayerModel.column.ACTIVE_RUN_ID); + + final String fBlob = blob; + final boolean fFirstTimeMember = firstTimeMember; + final Run fRun = run; + final com.orbitmines.minecraft.spigot.servers.fog.stats.Stats fStats = stats; + final long fTotalXp = totalXp; + final Integer fPendingChoiceLevel = pendingChoiceLevel; + + /* Now sync: load world, teleport, apply inventory. */ + server.runSync(() -> { + World world = FoGWorld.createOrLoad(model.getWorldFileName(), 0L); + if (world == null) { + player.sendMessage("FoG", Color.RED, "fog", "run.load_failed"); + return; + } + registerLoaded(fRun); + + /* Initialise stats/xp first — sync-safe, no DB. */ + player.initForRun(fRun, fBlob, fStats, fTotalXp); + + if (fPendingChoiceLevel != null) { + /* Rejoin mid-prompt: elevate to sky above run spawn, grant + flight, open the prompt. When the player resolves it, + drop them back to the run spawn in SURVIVAL. */ + openPendingChoiceInSky(player, fRun, fPendingChoiceLevel); + return; + } + + /* Normal rejoin: teleport FIRST, then downgrade to SURVIVAL + no flight. + The onJoin safety net (ADVENTURE + flight + invulnerable) is cleared + here, once the player is on solid ground. */ + Location spawn = fRun.getSpawn(); + if (spawn != null) player.teleport(spawn); + player.bukkit().setInvulnerable(false); + player.setGameMode(GameMode.SURVIVAL); + player.setAllowFlight(false); + player.setFlying(false); + player.bukkit().setCollidable(true); + + if (fFirstTimeMember && fBlob == null) { + player.giveRunStartInventory(); + } else { + /* Even on a returning member, make sure the always-on items are present. */ + server.getLobbyKit().copyAlwaysOnToInventory(player); + } + }); + }); + } + + /** Teleport the player to the sky above their run's spawn and open the + pending-choice prompt there. On resolve, drop them to the ground in + SURVIVAL. */ + private void openPendingChoiceInSky(FoGPlayer player, Run run, int level) { + Location runSpawn = run.getSpawn(); + if (runSpawn == null) return; + Location sky = runSpawn.clone(); + sky.setY(Math.min(runSpawn.getWorld().getMaxHeight() - 10, runSpawn.getY() + 120)); + + player.setGameMode(GameMode.ADVENTURE); + player.teleport(sky); + /* Flight setup deferred one tick so Bukkit's post-teleport state settles + (matches the teleportToSkyAnchor pattern). */ + new org.bukkit.scheduler.BukkitRunnable() { + @Override public void run() { + if (!player.bukkit().isOnline()) return; + player.setAllowFlight(true); + try { player.setFlying(true); } catch (IllegalArgumentException ignored) {} + server.getLobbyKit().copyAlwaysOnToInventory(player); + server.getLevelUpFlow().start(player, level, run, () -> { + /* After the choice resolves, transition to normal play. */ + Location ground = run.getSpawn(); + if (ground != null) player.teleport(ground); + player.bukkit().setInvulnerable(false); + player.setGameMode(GameMode.SURVIVAL); + player.setAllowFlight(false); + player.setFlying(false); + player.bukkit().setCollidable(true); + }); + } + }.runTaskLater(server.getPlugin(), 1L); + } + + /** + * Spectate another player's run. Doesn't register the viewer as a member. + * + *

Uses ADVENTURE + flight (not GameMode.SPECTATOR) so the viewer's + * top-right kit items remain right-clickable — essential for escaping the + * spectate session without a relog. The viewer is set non-collidable so + * they don't obstruct active players.

+ */ + public void spectateRun(FoGPlayer viewer, long runId) { + server.runAsync(() -> { + FoGRunModel model = FoGRunModel.findBy(FoGRunModel.class, FoGRunModel.column.ID.is(runId)); + if (model == null) { + viewer.sendMessage("FoG", Color.RED, "fog", "run.not_found"); + return; + } + /* Construct + load uniques on the async side. */ + Run run; + synchronized (loaded) { + run = runForModel(model); + } + run.loadUniquesFromStore(); + final Run fRun = run; + + server.runSync(() -> { + World world = FoGWorld.createOrLoad(model.getWorldFileName(), 0L); + if (world == null) { + viewer.sendMessage("FoG", Color.RED, "fog", "run.load_failed"); + return; + } + registerLoaded(fRun); + Location spawn = fRun.getSpawn(); + if (spawn != null) viewer.teleport(spawn); + viewer.bukkit().setInvulnerable(false); + viewer.setGameMode(GameMode.ADVENTURE); + viewer.setAllowFlight(true); + viewer.setFlying(true); + viewer.bukkit().setCollidable(false); + /* Always-on kit must be present so the viewer can escape. */ + server.getLobbyKit().copyAlwaysOnToInventory(viewer); + }); + }); + } +} diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/run/RunState.java b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/run/RunState.java new file mode 100644 index 000000000..9fe8f08b8 --- /dev/null +++ b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/run/RunState.java @@ -0,0 +1,19 @@ +package com.orbitmines.minecraft.spigot.servers.fog.run; + +public enum RunState { + /** Cave scoring / world generation in progress. */ + PREPARING, + /** Run is live and playable. */ + ACTIVE, + /** Run is over — everyone in permadeath, or owner ended it. Spectators still allowed. */ + ENDED; + + public static RunState parse(String s) { + if (s == null) return PREPARING; + try { + return RunState.valueOf(s.toUpperCase()); + } catch (IllegalArgumentException e) { + return PREPARING; + } + } +} diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/run/RunStore.java b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/run/RunStore.java new file mode 100644 index 000000000..e06a0a745 --- /dev/null +++ b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/run/RunStore.java @@ -0,0 +1,341 @@ +package com.orbitmines.minecraft.spigot.servers.fog.run; + +import com.orbitmines.archive.minecraft._2019.utils.state.StateProvider; +import com.orbitmines.minecraft.spigot.servers.fog.choice.Choice; +import com.orbitmines.minecraft.spigot.servers.fog.choice.ChoiceState; +import com.orbitmines.minecraft.spigot.servers.fog.faction.Faction; +import com.orbitmines.minecraft.spigot.servers.fog.stats.ToolType; +import com.orbitmines.minecraft.spigot.servers.fog.structure.CompartmentType; +import com.orbitmines.minecraft.spigot.servers.fog.world.MapExpansion; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +/** + * Typed facade over the {@link StateProvider} key-value store, scoped to a run. + * + *

Every method here hides the underlying key shape so call sites never build + * colon-separated keys by hand. If the storage layout changes, the change is + * localized here.

+ * + *

Base prefix is {@code fog:run:<runId>:}.

+ */ +public class RunStore { + + private static final String KEY_MEMBER = "member:"; + private static final String KEY_MEMBERS_CSV = "members"; + private static final String KEY_UNIQUE = "unique:"; + private static final String KEY_COMPARTMENT = "compartment:"; + private static final String KEY_MAP_EXPANSIONS = "map:expansions"; + private static final String KEY_MAP_EXPANSION = "map:expansion:"; + private static final String KEY_HONEY_TREE = "honey:tree:"; + + private final long runId; + private final String prefix; + + public RunStore(long runId) { + this.runId = runId; + this.prefix = "fog:run:" + runId + ":"; + } + + public long getRunId() { return runId; } + + /* + * Primitive access (kept package-private-ish; prefer the typed helpers below). + */ + + private String raw(String suffix) { + return StateProvider.getInstance().getString(prefix + suffix); + } + + private void rawSet(String suffix, String value) { + if (value == null) { + StateProvider.getInstance().deleteString(prefix + suffix); + return; + } + StateProvider.getInstance().setString(prefix + suffix, value); + } + + private void rawDelete(String suffix) { + StateProvider.getInstance().deleteString(prefix + suffix); + } + + private int rawInt(String suffix, int fallback) { + String v = raw(suffix); + if (v == null) return fallback; + try { return Integer.parseInt(v); } catch (NumberFormatException e) { return fallback; } + } + + private long rawLong(String suffix, long fallback) { + String v = raw(suffix); + if (v == null) return fallback; + try { return Long.parseLong(v); } catch (NumberFormatException e) { return fallback; } + } + + /* + * Members. + */ + + public boolean isMember(UUID uuid) { + return raw(KEY_MEMBER + uuid) != null; + } + + public List getMembers() { + List uuids = new ArrayList<>(); + String csv = raw(KEY_MEMBERS_CSV); + if (csv == null || csv.isEmpty()) return uuids; + for (String part : csv.split(",")) { + try { uuids.add(UUID.fromString(part)); } catch (IllegalArgumentException ignored) {} + } + return uuids; + } + + public void registerMember(UUID uuid) { + List current = getMembers(); + if (current.contains(uuid)) return; + current.add(uuid); + StringBuilder csv = new StringBuilder(); + for (int i = 0; i < current.size(); i++) { + if (i > 0) csv.append(','); + csv.append(current.get(i)); + } + rawSet(KEY_MEMBERS_CSV, csv.toString()); + rawSet(KEY_MEMBER + uuid, "1"); + /* Stamp first-joined-at if we've never registered this member before. */ + if (getMemberJoinedAtMillis(uuid) == null) { + setMemberJoinedAtMillis(uuid, System.currentTimeMillis()); + } + } + + public void removeMember(UUID uuid) { + rawDelete(KEY_MEMBER + uuid); + } + + /* + * Per-member progression. + */ + + public int getMemberLevel(UUID uuid) { + return rawInt(KEY_MEMBER + uuid + ":level", 0); + } + + public void setMemberLevel(UUID uuid, int level) { + rawSet(KEY_MEMBER + uuid + ":level", Integer.toString(level)); + } + + public long getMemberExperience(UUID uuid) { + return rawLong(KEY_MEMBER + uuid + ":experience", 0L); + } + + public void setMemberExperience(UUID uuid, long xp) { + rawSet(KEY_MEMBER + uuid + ":experience", Long.toString(xp)); + } + + public long getMemberPlayTimeMs(UUID uuid) { + return rawLong(KEY_MEMBER + uuid + ":play-time", 0L); + } + + public void addMemberPlayTimeMs(UUID uuid, long deltaMs) { + rawSet(KEY_MEMBER + uuid + ":play-time", Long.toString(getMemberPlayTimeMs(uuid) + deltaMs)); + } + + /** Epoch millis at which this player was first registered as a member of this run. + Used for per-player "joined this run at" stats distinct from the run's own createdAt. */ + public Long getMemberJoinedAtMillis(UUID uuid) { + String v = raw(KEY_MEMBER + uuid + ":joined-at"); + if (v == null) return null; + try { return Long.parseLong(v); } catch (NumberFormatException e) { return null; } + } + + public void setMemberJoinedAtMillis(UUID uuid, long millis) { + rawSet(KEY_MEMBER + uuid + ":joined-at", Long.toString(millis)); + } + + public Faction getMemberFaction(UUID uuid) { + return Faction.parse(raw(KEY_MEMBER + uuid + ":faction")); + } + + public void setMemberFaction(UUID uuid, Faction faction) { + rawSet(KEY_MEMBER + uuid + ":faction", faction == null ? null : faction.name()); + } + + public boolean isMemberDead(UUID uuid) { + return "1".equals(raw(KEY_MEMBER + uuid + ":dead")); + } + + /** Level of the level-up choice currently in progress (if the player + disconnected mid-prompt), or {@code null} if no prompt is pending. */ + public Integer getPendingChoiceLevel(UUID uuid) { + String v = raw(KEY_MEMBER + uuid + ":pending-choice-level"); + if (v == null) return null; + try { return Integer.parseInt(v); } catch (NumberFormatException e) { return null; } + } + + public void setPendingChoiceLevel(UUID uuid, Integer level) { + rawSet(KEY_MEMBER + uuid + ":pending-choice-level", level == null ? null : level.toString()); + } + + public void setMemberDead(UUID uuid, boolean dead) { + rawSet(KEY_MEMBER + uuid + ":dead", dead ? "1" : null); + } + + public String getMemberInventoryBlob(UUID uuid) { + return raw(KEY_MEMBER + uuid + ":inventory"); + } + + public void setMemberInventoryBlob(UUID uuid, String blob) { + rawSet(KEY_MEMBER + uuid + ":inventory", blob); + } + + /* + * Per-member choice history. + */ + + public ChoiceState getChoiceState(UUID uuid, int level, Choice choice) { + String v = raw(KEY_MEMBER + uuid + ":choice:" + level + ":" + choice.getStorageKey()); + if (v == null) return null; + return ChoiceState.parse(v); + } + + public boolean hasChoiceAtLevel(UUID uuid, int level, Choice choice) { + return raw(KEY_MEMBER + uuid + ":choice:" + level + ":" + choice.getStorageKey()) != null; + } + + public void setChoiceState(UUID uuid, int level, Choice choice, ChoiceState state) { + rawSet(KEY_MEMBER + uuid + ":choice:" + level + ":" + choice.getStorageKey(), state == null ? null : state.name()); + } + + public int getChoiceStackCount(UUID uuid, Choice choice) { + return rawInt(KEY_MEMBER + uuid + ":choice_count:" + choice.getStorageKey(), 0); + } + + public void incrementChoiceStackCount(UUID uuid, Choice choice) { + rawSet(KEY_MEMBER + uuid + ":choice_count:" + choice.getStorageKey(), Integer.toString(getChoiceStackCount(uuid, choice) + 1)); + } + + /** Walk member choice levels to find a DAMAGED choice previously made at `level`. */ + public Choice findDamagedChoiceAtLevel(UUID uuid, int level) { + for (Choice c : Choice.values()) { + if (getChoiceState(uuid, level, c) == ChoiceState.DAMAGED) return c; + } + return null; + } + + /* + * Uniques (one-of-these-per-run). + */ + + public boolean isUniqueTaken(Choice choice) { + return choice.isUnique() && raw(KEY_UNIQUE + choice.getStorageKey()) != null; + } + + public UUID getUniqueHolder(Choice choice) { + String v = raw(KEY_UNIQUE + choice.getStorageKey()); + if (v == null) return null; + try { return UUID.fromString(v); } catch (IllegalArgumentException e) { return null; } + } + + public void setUniqueHolder(Choice choice, UUID uuid) { + rawSet(KEY_UNIQUE + choice.getStorageKey(), uuid == null ? null : uuid.toString()); + } + + /* + * Stats (shared by player + drone). The caller provides its own prefix + * (e.g. "member:" or "member::drone:"). + */ + + public long getBlocksBroken(String statsPrefix, ToolType tool) { + return rawLong(statsPrefix + ":stats:tool:" + tool.name().toLowerCase() + ":blocks_broken", 0L); + } + + public void setBlocksBroken(String statsPrefix, ToolType tool, long count) { + rawSet(statsPrefix + ":stats:tool:" + tool.name().toLowerCase() + ":blocks_broken", Long.toString(count)); + } + + /* + * Compartments. + */ + + public enum CompartmentState { DECAYED, CLAIMED, DAMAGED } + + public CompartmentState getCompartmentState(CompartmentType type) { + String v = raw(KEY_COMPARTMENT + type.name() + ":state"); + if (v == null) return null; + try { return CompartmentState.valueOf(v); } catch (IllegalArgumentException e) { return null; } + } + + public void setCompartmentState(CompartmentType type, CompartmentState state) { + rawSet(KEY_COMPARTMENT + type.name() + ":state", state == null ? null : state.name()); + } + + public void setCompartmentLocation(CompartmentType type, int x, int y, int z) { + rawSet(KEY_COMPARTMENT + type.name() + ":x", Integer.toString(x)); + rawSet(KEY_COMPARTMENT + type.name() + ":y", Integer.toString(y)); + rawSet(KEY_COMPARTMENT + type.name() + ":z", Integer.toString(z)); + } + + public Integer getCompartmentX(CompartmentType type) { + String v = raw(KEY_COMPARTMENT + type.name() + ":x"); + if (v == null) return null; + try { return Integer.parseInt(v); } catch (NumberFormatException e) { return null; } + } + + public Integer getCompartmentY(CompartmentType type) { + String v = raw(KEY_COMPARTMENT + type.name() + ":y"); + if (v == null) return null; + try { return Integer.parseInt(v); } catch (NumberFormatException e) { return null; } + } + + public Integer getCompartmentZ(CompartmentType type) { + String v = raw(KEY_COMPARTMENT + type.name() + ":z"); + if (v == null) return null; + try { return Integer.parseInt(v); } catch (NumberFormatException e) { return null; } + } + + /* + * Map expansions. + */ + + public List getUnlockedExpansions() { + List out = new ArrayList<>(); + String csv = raw(KEY_MAP_EXPANSIONS); + if (csv == null || csv.isEmpty()) return out; + for (String part : csv.split(",")) { + try { out.add(MapExpansion.valueOf(part)); } catch (IllegalArgumentException ignored) {} + } + return out; + } + + public void unlockExpansion(MapExpansion expansion) { + List unlocked = getUnlockedExpansions(); + if (unlocked.contains(expansion)) return; + unlocked.add(expansion); + StringBuilder csv = new StringBuilder(); + for (int i = 0; i < unlocked.size(); i++) { + if (i > 0) csv.append(','); + csv.append(unlocked.get(i).name()); + } + rawSet(KEY_MAP_EXPANSIONS, csv.toString()); + } + + public boolean isExpansionDamaged(MapExpansion expansion) { + return "1".equals(raw(KEY_MAP_EXPANSION + expansion.name() + ":damaged")); + } + + public void setExpansionDamaged(MapExpansion expansion, boolean damaged) { + rawSet(KEY_MAP_EXPANSION + expansion.name() + ":damaged", damaged ? "1" : null); + } + + /* + * Honey trees. + */ + + public boolean isHoneyTree(int x, int y, int z) { + return "1".equals(raw(KEY_HONEY_TREE + x + ":" + y + ":" + z)); + } + + public void setHoneyTree(int x, int y, int z) { + rawSet(KEY_HONEY_TREE + x + ":" + y + ":" + z, "1"); + } +} diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/scoreboard/FoGScoreboard.java b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/scoreboard/FoGScoreboard.java new file mode 100644 index 000000000..7829979ea --- /dev/null +++ b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/scoreboard/FoGScoreboard.java @@ -0,0 +1,35 @@ +package com.orbitmines.minecraft.spigot.servers.fog.scoreboard; + +import com.orbitmines.archive.minecraft.spigot._2019.libs.spigot.scoreboard.DefaultScoreboard; +import com.orbitmines.minecraft.spigot.servers.fog.FoG; +import com.orbitmines.minecraft.spigot.servers.fog.FoGPlayer; +import com.orbitmines.minecraft.spigot.servers.fog.run.Run; + +public class FoGScoreboard extends DefaultScoreboard { + + public FoGScoreboard(FoG server, FoGPlayer player) { + super(server, player, + () -> server.getScoreboardAnimation().get(), + () -> "§m--------------", + () -> "", + () -> "§e§l" + player.translate("fog", "scoreboard.level"), + () -> " §f" + player.getLevel(), + () -> " ", + () -> "§6§l" + player.translate("fog", "scoreboard.run"), + () -> { + Run run = player.getActiveRun(); + if (run == null) return " §7-"; + return " §f#" + run.getId() + " §7(" + run.getDifficulty().getColoredName(player) + ")"; + }, + () -> " ", + () -> "§b§l" + player.translate("fog", "scoreboard.drones"), + () -> " §f" + player.getDrones().size() + "§7/9", + () -> " " + ); + } + + @Override + public boolean canBypassSettings() { + return false; + } +} diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/shop/Shop.java b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/shop/Shop.java new file mode 100644 index 000000000..6cd7b200c --- /dev/null +++ b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/shop/Shop.java @@ -0,0 +1,27 @@ +package com.orbitmines.minecraft.spigot.servers.fog.shop; + +import org.bukkit.Material; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** Catalogue of shop offers. Translations live in fog_*.properties under `shop..{name,description}`. */ +public class Shop { + + public static final List DEFAULT_OFFERS; + + static { + List offers = new ArrayList<>(); + offers.add(new ShopOffer("key_basic", Material.TRIPWIRE_HOOK, 500)); + offers.add(new ShopOffer("key_epic", Material.LAPIS_LAZULI, 2500)); + offers.add(new ShopOffer("xp_bottle", Material.EXPERIENCE_BOTTLE, 300)); + offers.add(new ShopOffer("enchant_autoreplant",Material.ENCHANTED_BOOK, 4000)); + offers.add(new ShopOffer("enchant_homing", Material.ENCHANTED_BOOK, 6000)); + offers.add(new ShopOffer("recipe_pickaxe", Material.DIAMOND_PICKAXE, 8000)); + offers.add(new ShopOffer("recipe_suit", Material.IRON_CHESTPLATE, 9000)); + offers.add(new ShopOffer("module_upgrade", Material.SMITHING_TABLE, 12000)); + offers.add(new ShopOffer("enchant_remove", Material.GRINDSTONE, 2000)); + DEFAULT_OFFERS = Collections.unmodifiableList(offers); + } +} diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/shop/ShopOffer.java b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/shop/ShopOffer.java new file mode 100644 index 000000000..66c8e3f31 --- /dev/null +++ b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/shop/ShopOffer.java @@ -0,0 +1,25 @@ +package com.orbitmines.minecraft.spigot.servers.fog.shop; + +import com.orbitmines.archive.minecraft._2019.libs.player.Languageable; +import lombok.Getter; +import org.bukkit.Material; + +/** One offer in the shop. Text comes from {@code fog/shop..{name,description}}. */ +public class ShopOffer { + + @Getter private final String id; + @Getter private final Material icon; + @Getter private final int priceCredits; + + public ShopOffer(String id, Material icon, int priceCredits) { + this.id = id; + this.icon = icon; + this.priceCredits = priceCredits; + } + + public String nameKey() { return "shop." + id + ".name"; } + public String descriptionKey() { return "shop." + id + ".description"; } + + public String getDisplayName(Languageable viewer) { return viewer.translate("fog", nameKey()); } + public String getDescription(Languageable viewer) { return viewer.translate("fog", descriptionKey()); } +} diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/skill/Skill.java b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/skill/Skill.java new file mode 100644 index 000000000..c788d704d --- /dev/null +++ b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/skill/Skill.java @@ -0,0 +1,18 @@ +package com.orbitmines.minecraft.spigot.servers.fog.skill; + +import com.orbitmines.minecraft.spigot.servers.fog.choice.Choice; +import lombok.Getter; + +public enum Skill { + + LUMBERJACK(Choice.SKILL_LUMBERJACK), + FISHING(Choice.SKILL_FISHING), + BOTANY(Choice.SKILL_BOTANY), + BEEKEEPING(Choice.SKILL_BEEKEEPING); + + @Getter private final Choice unlockChoice; + + Skill(Choice unlockChoice) { + this.unlockChoice = unlockChoice; + } +} diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/stats/Stats.java b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/stats/Stats.java new file mode 100644 index 000000000..1c1eb60a6 --- /dev/null +++ b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/stats/Stats.java @@ -0,0 +1,145 @@ +package com.orbitmines.minecraft.spigot.servers.fog.stats; + +import com.orbitmines.minecraft.spigot.servers.fog.choice.Choice; +import com.orbitmines.minecraft.spigot.servers.fog.choice.ChoiceState; +import com.orbitmines.minecraft.spigot.servers.fog.faction.Faction; +import com.orbitmines.minecraft.spigot.servers.fog.run.RunStore; +import lombok.Getter; + +import java.util.EnumMap; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +/** + * In-memory stats for a {@link StatsHolder}, backed by a {@link RunStore}. + * + *

Strict sync/async discipline: + *

    + *
  • The constructor does zero DB access — safe on the sync thread.
  • + *
  • {@link #load()} seeds every counter, choice state, faction and damaged-level + * index from the store. Async only.
  • + *
  • {@link #flush()} writes the blocks-broken counter back. + * Async only.
  • + *
  • Event handlers call {@link #incrementBlocksBroken(ToolType)}, + * {@link #onChoiceActivated(Choice)}, {@link #onChoiceDamaged(int, Choice)} to + * mutate the cache on sync; no DB hits from those paths.
  • + *
+ * + *

All read-side methods (derived stats, stack counts, faction, damaged lookup) + * hit only in-memory maps and are therefore safe from any thread.

+ */ +public class Stats { + + @Getter private final RunStore store; + @Getter private final String prefix; + + private final Map blocksBroken = new EnumMap<>(ToolType.class); + /** Number of ACTIVE stacks of each choice — decreases on DAMAGED. */ + private final Map activeStacks = new EnumMap<>(Choice.class); + /** Total times each choice was ever picked — unaffected by DAMAGED flips. */ + private final Map choiceStackCount = new EnumMap<>(Choice.class); + /** level → the DAMAGED choice at that level (for the guaranteed re-offer). */ + private final Map damagedChoiceByLevel = new HashMap<>(); + + @Getter private volatile Faction faction; + @Getter private volatile boolean loaded; + + public Stats(RunStore store, String prefix) { + this.store = store; + this.prefix = prefix; + for (ToolType t : ToolType.values()) blocksBroken.put(t, 0L); + for (Choice c : Choice.values()) { + activeStacks.put(c, 0); + choiceStackCount.put(c, 0); + } + } + + /** Async only. Seeds the in-memory cache from the run store. */ + public void load() { + for (ToolType t : ToolType.values()) { + blocksBroken.put(t, store.getBlocksBroken(prefix, t)); + } + UUID uuid = extractUuid(prefix); + if (uuid != null) { + for (Choice c : Choice.values()) { + int active = 0; + for (int lvl = 0; lvl <= 100; lvl++) { + ChoiceState state = store.getChoiceState(uuid, lvl, c); + if (state == ChoiceState.ACTIVE) { + active++; + } else if (state == ChoiceState.DAMAGED) { + damagedChoiceByLevel.put(lvl, c); + } + } + activeStacks.put(c, active); + choiceStackCount.put(c, store.getChoiceStackCount(uuid, c)); + } + this.faction = store.getMemberFaction(uuid); + } + loaded = true; + } + + /** Async only. Writes the blocks-broken counter back to the store. */ + public void flush() { + for (Map.Entry e : blocksBroken.entrySet()) { + store.setBlocksBroken(prefix, e.getKey(), e.getValue()); + } + } + + /* === Mutations from sync threads (in-memory only) === */ + + public void incrementBlocksBroken(ToolType tool) { + blocksBroken.merge(tool, 1L, Long::sum); + } + + public void onChoiceActivated(Choice choice) { + activeStacks.merge(choice, 1, Integer::sum); + choiceStackCount.merge(choice, 1, Integer::sum); + switch (choice) { + case FACTION_OMEGA: this.faction = Faction.OMEGA; break; + case FACTION_ALPHA: this.faction = Faction.ALPHA; break; + case FACTION_BETA: this.faction = Faction.BETA; break; + default: /* no-op */ + } + } + + public void onChoiceDamaged(int level, Choice choice) { + activeStacks.merge(choice, -1, Integer::sum); + damagedChoiceByLevel.put(level, choice); + } + + public void setFaction(Faction faction) { + this.faction = faction; + } + + /* === Reads (always from memory) === */ + + public long getBlocksBroken(ToolType tool) { + return blocksBroken.getOrDefault(tool, 0L); + } + + public int getActiveStacks(Choice choice) { + return activeStacks.getOrDefault(choice, 0); + } + + public int getChoiceStackCount(Choice choice) { + return choiceStackCount.getOrDefault(choice, 0); + } + + public Choice getDamagedChoiceAt(int level) { + return damagedChoiceByLevel.get(level); + } + + public int getExtraHearts() { return 4 * getActiveStacks(Choice.EXTRA_HP_4); } + public double getDamageMultiplier() { return 1.0 + 0.05 * getActiveStacks(Choice.DAMAGE_5); } + public double getSpeedMultiplier() { return 1.0 + 0.05 * getActiveStacks(Choice.SPEED_5); } + public double getMiningSpeedMultiplier() { return 1.0 + 0.05 * getActiveStacks(Choice.MINING_SPEED_5); } + + private static UUID extractUuid(String prefix) { + if (prefix == null) return null; + String[] parts = prefix.split(":"); + if (parts.length < 2) return null; + try { return UUID.fromString(parts[1]); } catch (IllegalArgumentException e) { return null; } + } +} diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/stats/StatsHolder.java b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/stats/StatsHolder.java new file mode 100644 index 000000000..55ab08a0d --- /dev/null +++ b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/stats/StatsHolder.java @@ -0,0 +1,17 @@ +package com.orbitmines.minecraft.spigot.servers.fog.stats; + +import java.util.UUID; + +/** + * Anything in FoG that can accrue stats (a player, a drone, a party slot, …). + * Provides the key prefix used inside {@link com.orbitmines.minecraft.spigot.servers.fog.run.RunStore}. + */ +public interface StatsHolder { + + UUID getOwnerUuid(); + + /** Key prefix inside the RunStore. E.g. `member:<uuid>` or `member:<uuid>:drone:<id>`. */ + String getStatsPrefix(); + + Stats getStats(); +} diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/stats/ToolStatsStamp.java b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/stats/ToolStatsStamp.java new file mode 100644 index 000000000..93f56b305 --- /dev/null +++ b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/stats/ToolStatsStamp.java @@ -0,0 +1,51 @@ +package com.orbitmines.minecraft.spigot.servers.fog.stats; + +import com.orbitmines.archive.minecraft._2019.libs.player.Languageable; +import com.orbitmines.minecraft.spigot.servers.fog.FoG; +import com.orbitmines.minecraft.spigot.servers.fog.FoGPlayer; +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; + +import java.util.ArrayList; +import java.util.List; + +/** + * Persist the per-tool "blocks broken" counter on the tool itself: + * - NMS metadata under {@code fog:blocks_broken} (survives restarts); + * - a lore line "Blocks broken: N" (translated), so the player sees it. + */ +public class ToolStatsStamp { + + public static final String NS = "fog"; + public static final String KEY = "blocks_broken"; + + private static final String LORE_MARKER = "fog-stats:blocks_broken"; + + /** Returns a new ItemStack (may be the same instance) with updated meta + lore. */ + public static ItemStack stamp(FoG server, Languageable viewer, ItemStack stack, ToolType tool, long total) { + if (stack == null || stack.getType() == Material.AIR) return stack; + if (tool == ToolType.HAND) return stack; // skip bare-hand — nothing to stamp on + + ItemStack withNbt = server.getNms().customItem().setMetaData(stack, NS, KEY, total); + ItemMeta meta = withNbt.getItemMeta(); + if (meta == null) return withNbt; + + List lore = meta.hasLore() ? new ArrayList<>(meta.getLore()) : new ArrayList<>(); + /* Strip any prior fog-stats lines so we don't accumulate. */ + lore.removeIf(line -> line != null && line.contains(LORE_MARKER)); + + /* Visible summary + an invisible marker tag so we can find-and-replace next time. */ + String label = viewer.translate("fog", "stats.blocks_broken", total); + lore.add("§7" + label + "§0§o " + LORE_MARKER); + meta.setLore(lore); + withNbt.setItemMeta(meta); + return withNbt; + } + + public static long read(FoG server, ItemStack stack) { + if (stack == null || stack.getType() == Material.AIR) return 0; + Long v = server.getNms().customItem().getMetaDataLong(stack, NS, KEY); + return v == null ? 0L : v; + } +} diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/stats/ToolType.java b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/stats/ToolType.java new file mode 100644 index 000000000..4750acd00 --- /dev/null +++ b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/stats/ToolType.java @@ -0,0 +1,28 @@ +package com.orbitmines.minecraft.spigot.servers.fog.stats; + +import org.bukkit.Material; + +public enum ToolType { + + PICKAXE, + AXE, + SHOVEL, + HOE, + SWORD, + FISHING_ROD, + SHEARS, + HAND; + + public static ToolType fromItem(Material mat) { + if (mat == null) return HAND; + String n = mat.name(); + if (n.endsWith("_PICKAXE")) return PICKAXE; + if (n.endsWith("_AXE")) return AXE; + if (n.endsWith("_SHOVEL")) return SHOVEL; + if (n.endsWith("_HOE")) return HOE; + if (n.endsWith("_SWORD")) return SWORD; + if (n.equals("FISHING_ROD")) return FISHING_ROD; + if (n.equals("SHEARS")) return SHEARS; + return HAND; + } +} diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/structure/Compartment.java b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/structure/Compartment.java new file mode 100644 index 000000000..260e4e03b --- /dev/null +++ b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/structure/Compartment.java @@ -0,0 +1,68 @@ +package com.orbitmines.minecraft.spigot.servers.fog.structure; + +import com.orbitmines.minecraft.spigot.servers.fog.run.Run; +import com.orbitmines.minecraft.spigot.servers.fog.run.RunStore; +import com.orbitmines.minecraft.spigot.servers.fog.world.StainedGlassBarrier; +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.World; + +/** + * A generated city compartment (Farm, Enchanting, Crates, Drone Factory). + * + *

Responsibilities split to respect the sync/async contract: + *

    + *
  • {@link #place(World, CompartmentType, int, int, int)} — sync-only, places + * blocks. Returns the centre coords so the caller can persist them later.
  • + *
  • {@link #persist(RunStore, CompartmentType, int, int, int)} — async-only, + * writes the location + DECAYED state to the RunStore (which hits the DB).
  • + *
+ */ +public class Compartment { + + /** Place the compartment's blocks at (cx, cy, cz). Sync thread only. */ + public static int[] place(World world, CompartmentType type, int cx, int cy, int cz) { + if (world == null) return null; + int r = type.getRadius(); + for (int x = cx - r; x <= cx + r; x++) { + for (int z = cz - r; z <= cz + r; z++) { + int dx = x - cx, dz = z - cz; + if (dx * dx + dz * dz > r * r) continue; + world.getBlockAt(x, cy, z).setType(Material.STONE_BRICKS, false); + } + } + for (int dy = 1; dy <= 3; dy++) { + for (int x = cx - r; x <= cx + r; x++) { + for (int z = cz - r; z <= cz + r; z++) { + int ddx = x - cx, ddz = z - cz; + int distSq = ddx * ddx + ddz * ddz; + if (distSq < (r - 1) * (r - 1)) continue; + if (distSq > (r + 1) * (r + 1)) continue; + world.getBlockAt(x, cy + dy, z).setType(Material.MOSSY_STONE_BRICKS, false); + } + } + } + world.getBlockAt(cx, cy + 1, cz).setType(type.getMarker(), false); + Bukkit.getLogger().info("[fog] Generated compartment " + type + " at " + cx + "," + cy + "," + cz); + return new int[]{cx, cy, cz}; + } + + /** Persist the compartment's location and set its state to DECAYED. Async thread only. */ + public static void persist(RunStore store, CompartmentType type, int cx, int cy, int cz) { + store.setCompartmentLocation(type, cx, cy, cz); + store.setCompartmentState(type, RunStore.CompartmentState.DECAYED); + } + + /** Sync: read cached location (no DB — callers must have fetched coords on async), + seal with red stained glass. */ + public static void markDamagedBlocks(Run run, CompartmentType type, int cx, int cy, int cz) { + World world = run.getWorld(); + if (world == null) return; + StainedGlassBarrier.seal(world, cx, cy + 1, cz, type.getRadius() + 1, type.getHeight()); + } + + /** Async: flip compartment state to DAMAGED. */ + public static void markDamagedPersist(RunStore store, CompartmentType type) { + store.setCompartmentState(type, RunStore.CompartmentState.DAMAGED); + } +} diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/structure/CompartmentType.java b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/structure/CompartmentType.java new file mode 100644 index 000000000..6ccab670e --- /dev/null +++ b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/structure/CompartmentType.java @@ -0,0 +1,30 @@ +package com.orbitmines.minecraft.spigot.servers.fog.structure; + +import com.orbitmines.archive.minecraft._2019.libs.player.Languageable; +import com.orbitmines.minecraft.spigot.servers.fog.choice.Choice; +import lombok.Getter; +import org.bukkit.Material; + +public enum CompartmentType { + + FARM (Material.HAY_BLOCK, 12, 6, Choice.STRUCTURE_FARM), + ENCHANTING (Material.ENCHANTING_TABLE,10, 8, Choice.STRUCTURE_ENCHANT), + CRATES (Material.CHEST, 8, 6, Choice.STRUCTURE_CRATES), + DRONE_FACTORY(Material.SMITHING_TABLE, 14, 10, Choice.DRONE_FACTORY); + + @Getter private final Material marker; + @Getter private final int radius; + @Getter private final int height; + @Getter private final Choice unlockChoice; + + CompartmentType(Material marker, int radius, int height, Choice unlockChoice) { + this.marker = marker; + this.radius = radius; + this.height = height; + this.unlockChoice = unlockChoice; + } + + public String nameKey() { return "compartment." + name().toLowerCase() + ".name"; } + + public String getDisplayName(Languageable viewer) { return viewer.translate("fog", nameKey()); } +} diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/tree/Tree.java b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/tree/Tree.java new file mode 100644 index 000000000..a41ae3610 --- /dev/null +++ b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/tree/Tree.java @@ -0,0 +1,35 @@ +package com.orbitmines.minecraft.spigot.servers.fog.tree; + +import com.orbitmines.minecraft.spigot.servers.fog.choice.Choice; +import lombok.Getter; +import org.bukkit.Material; + +public enum Tree { + + /* Vanilla */ + OAK(null, Material.OAK_LOG, 1), + BIRCH(null, Material.BIRCH_LOG, 1), + SPRUCE(null, Material.SPRUCE_LOG, 2), + DARK_OAK(null, Material.DARK_OAK_LOG, 3), + ACACIA(null, Material.ACACIA_LOG, 2), + JUNGLE(null, Material.JUNGLE_LOG, 3), + MANGROVE(null, Material.MANGROVE_LOG, 3), + CHERRY(null, Material.CHERRY_LOG, 4), + + /* Choice-unlocked */ + TAR(Choice.TREE_TAR, Material.COAL_BLOCK, 5), + GLOWWOOD(Choice.TREE_GLOWWOOD, Material.GLOWSTONE, 8), + VOIDOAK(Choice.TREE_VOIDOAK, Material.SCULK, 15); + + @Getter private final Choice unlockChoice; + @Getter private final Material logMaterial; + @Getter private final int value; + + Tree(Choice unlockChoice, Material logMaterial, int value) { + this.unlockChoice = unlockChoice; + this.logMaterial = logMaterial; + this.value = value; + } + + public boolean alwaysAvailable() { return unlockChoice == null; } +} diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/util/Fogi18n.java b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/util/Fogi18n.java new file mode 100644 index 000000000..66d6aa49d --- /dev/null +++ b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/util/Fogi18n.java @@ -0,0 +1,23 @@ +package com.orbitmines.minecraft.spigot.servers.fog.util; + +import com.orbitmines.archive.minecraft._2019.utils.language.Language; + +/** + * Translation helpers for contexts where there is no specific viewer (entity custom + * names, global boss bars, log messages). Resolves against the server's default + * Language ({@link Language#ENGLISH}). + */ +public final class Fogi18n { + + private Fogi18n() {} + + public static final String NAMESPACE = "fog"; + + public static String defaultText(String key) { + return Language.ENGLISH.getString(NAMESPACE, key); + } + + public static String defaultText(String key, Object... args) { + return Language.ENGLISH.getString(NAMESPACE, key, args); + } +} diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/util/HologramTag.java b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/util/HologramTag.java new file mode 100644 index 000000000..7a20e8714 --- /dev/null +++ b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/util/HologramTag.java @@ -0,0 +1,61 @@ +package com.orbitmines.minecraft.spigot.servers.fog.util; + +import org.bukkit.Bukkit; +import org.bukkit.NamespacedKey; +import org.bukkit.World; +import org.bukkit.entity.Entity; +import org.bukkit.persistence.PersistentDataType; +import org.bukkit.plugin.java.JavaPlugin; + +/** + * Tags FoG-spawned hologram entities (ArmorStand markers + their ridden Item) with + * a plugin-scoped {@link NamespacedKey} so we can distinguish them from player-placed + * armor stands / items. + * + *

Cleanup runs on {@code WorldLoadEvent} and once on plugin startup. The tag + * is the only safe way to identify "our" entities — we must not touch untagged + * entities in any world.

+ */ +public class HologramTag { + + private static NamespacedKey KEY; + + public static void init(JavaPlugin plugin) { + KEY = new NamespacedKey(plugin, "fog_hologram"); + } + + public static NamespacedKey key() { return KEY; } + + public static void tag(Entity entity) { + if (entity == null || KEY == null) return; + entity.getPersistentDataContainer().set(KEY, PersistentDataType.BYTE, (byte) 1); + /* Also mark non-persistent so Minecraft doesn't save this to level.dat — even + if cleanup somehow misses one on shutdown, it won't come back next boot. */ + entity.setPersistent(false); + } + + public static boolean isTagged(Entity entity) { + if (entity == null || KEY == null) return false; + return entity.getPersistentDataContainer().has(KEY, PersistentDataType.BYTE); + } + + /** Remove every tagged entity in one world. */ + public static int cleanupWorld(World world) { + if (KEY == null || world == null) return 0; + int removed = 0; + for (Entity entity : world.getEntities()) { + if (isTagged(entity)) { + entity.remove(); + removed++; + } + } + return removed; + } + + /** Sweep every currently-loaded world. Call once on startup. */ + public static int cleanupAllLoaded() { + int total = 0; + for (World w : Bukkit.getWorlds()) total += cleanupWorld(w); + return total; + } +} diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/util/InventoryBlob.java b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/util/InventoryBlob.java new file mode 100644 index 000000000..2f4e49329 --- /dev/null +++ b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/util/InventoryBlob.java @@ -0,0 +1,64 @@ +package com.orbitmines.minecraft.spigot.servers.fog.util; + +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.PlayerInventory; + +import java.io.ByteArrayOutputStream; +import java.util.Base64; + +/** + * Serialize a player's inventory to a single string and back. Uses the built-in + * YAML serialization of ItemStack which handles NBT correctly for all vanilla + * items as well as persistent-data-container fields used by FoGItem. + * + * Stored as base64-encoded UTF-8 YAML text. + */ +public class InventoryBlob { + + public static String serialize(Player player) { + PlayerInventory inv = player.getInventory(); + YamlConfiguration yaml = new YamlConfiguration(); + for (int i = 0; i < inv.getSize(); i++) { + ItemStack stack = inv.getItem(i); + if (stack != null) yaml.set("slot." + i, stack); + } + yaml.set("helmet", inv.getHelmet()); + yaml.set("chestplate", inv.getChestplate()); + yaml.set("leggings", inv.getLeggings()); + yaml.set("boots", inv.getBoots()); + yaml.set("offhand", inv.getItemInOffHand()); + String s = yaml.saveToString(); + return Base64.getEncoder().encodeToString(s.getBytes()); + } + + public static void deserializeInto(Player player, String blob) { + if (blob == null || blob.isEmpty()) return; + String yaml = new String(Base64.getDecoder().decode(blob)); + YamlConfiguration conf = new YamlConfiguration(); + try { + conf.loadFromString(yaml); + } catch (Exception e) { + e.printStackTrace(); + return; + } + PlayerInventory inv = player.getInventory(); + inv.clear(); + if (conf.isConfigurationSection("slot")) { + for (String key : conf.getConfigurationSection("slot").getKeys(false)) { + try { + int idx = Integer.parseInt(key); + ItemStack stack = conf.getItemStack("slot." + key); + if (stack != null) inv.setItem(idx, stack); + } catch (NumberFormatException ignored) {} + } + } + inv.setHelmet(conf.getItemStack("helmet")); + inv.setChestplate(conf.getItemStack("chestplate")); + inv.setLeggings(conf.getItemStack("leggings")); + inv.setBoots(conf.getItemStack("boots")); + ItemStack off = conf.getItemStack("offhand"); + if (off != null) inv.setItemInOffHand(off); + } +} diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/world/CaveScorer.java b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/world/CaveScorer.java new file mode 100644 index 000000000..40873b9a3 --- /dev/null +++ b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/world/CaveScorer.java @@ -0,0 +1,76 @@ +package com.orbitmines.minecraft.spigot.servers.fog.world; + +import org.bukkit.Chunk; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.World; +import org.bukkit.block.Block; + +/** + * Evaluate a region of a world for the presence of a "big cave" beneath it. + * The POC uses a cheap sampling heuristic: count air blocks in the Y range + * [-30, 40] across the column; higher air count = better cave. + * + * Score is deliberately coarse — it doesn't have to be right, just good + * enough that the player's base ends up over a genuinely open cavern + * rather than a solid mountain. + */ +public class CaveScorer { + + /** Score a single column. Higher == more cave-like. */ + public static int scoreColumn(World world, int x, int z) { + int surface = world.getHighestBlockYAt(x, z); + int air = 0; + int yMin = Math.max(world.getMinHeight(), -40); + int yMax = Math.min(surface - 5, 50); + for (int y = yMin; y < yMax; y++) { + Block b = world.getBlockAt(x, y, z); + if (b.getType().isAir()) air++; + } + return air; + } + + /** Score a 16x16 chunk by summing column scores. */ + public static int scoreChunk(World world, int chunkX, int chunkZ) { + int sx = chunkX << 4, sz = chunkZ << 4; + int total = 0; + for (int dx = 0; dx < 16; dx += 4) { + for (int dz = 0; dz < 16; dz += 4) { + total += scoreColumn(world, sx + dx, sz + dz); + } + } + return total; + } + + /** + * Search a spiral of chunks around (centreChunkX, centreChunkZ) and return + * the best-scoring chunk location. `radius` is in chunks. Caller is expected + * to run this on an async thread — it forces chunk generation via + * {@link World#getChunkAt(int, int)}. + */ + public static Location findBestSpawn(World world, int centreChunkX, int centreChunkZ, int radius) { + int bestScore = Integer.MIN_VALUE; + Chunk best = null; + for (int r = 0; r <= radius; r++) { + for (int dx = -r; dx <= r; dx++) { + for (int dz = -r; dz <= r; dz++) { + if (Math.abs(dx) != r && Math.abs(dz) != r) continue; + Chunk c = world.getChunkAt(centreChunkX + dx, centreChunkZ + dz); + int s = scoreChunk(world, c.getX(), c.getZ()); + if (s > bestScore) { + bestScore = s; + best = c; + } + } + } + } + if (best == null) return world.getSpawnLocation(); + int wx = (best.getX() << 4) + 8, wz = (best.getZ() << 4) + 8; + int surface = world.getHighestBlockYAt(wx, wz); + /* Spawn on top of the surface, clear of trees */ + while (surface < world.getMaxHeight() && !world.getBlockAt(wx, surface, wz).getType().isAir()) { + surface++; + } + return new Location(world, wx + 0.5, surface + 1, wz + 0.5); + } +} diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/world/CityWall.java b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/world/CityWall.java new file mode 100644 index 000000000..60e9814fa --- /dev/null +++ b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/world/CityWall.java @@ -0,0 +1,44 @@ +package com.orbitmines.minecraft.spigot.servers.fog.world; + +import org.bukkit.Material; +import org.bukkit.World; +import org.bukkit.block.Block; + +/** + * Builds a simple stone-brick ring around the player's starting area. + * The wall constrains where the player can roam pre-map-expansion. + */ +public class CityWall { + + public static void build(World world, int cx, int cy, int cz, int radius, int height) { + for (int x = cx - radius; x <= cx + radius; x++) { + for (int z = cz - radius; z <= cz + radius; z++) { + int dx = x - cx, dz = z - cz; + int distSq = dx * dx + dz * dz; + if (distSq < (radius - 1) * (radius - 1)) continue; + if (distSq > (radius + 1) * (radius + 1)) continue; + for (int dy = 0; dy < height; dy++) { + Block b = world.getBlockAt(x, cy + dy, z); + b.setType(Material.STONE_BRICKS, false); + } + /* Top cap */ + world.getBlockAt(x, cy + height, z).setType(Material.SMOOTH_STONE_SLAB, false); + } + } + } + + /** Convert existing wall blocks into red stained glass to mark "damaged/inaccessible" boundaries. */ + public static void sealAsDamaged(World world, int cx, int cy, int cz, int radius, int height) { + for (int x = cx - radius; x <= cx + radius; x++) { + for (int z = cz - radius; z <= cz + radius; z++) { + int dx = x - cx, dz = z - cz; + int distSq = dx * dx + dz * dz; + if (distSq < (radius - 1) * (radius - 1)) continue; + if (distSq > (radius + 1) * (radius + 1)) continue; + for (int dy = 0; dy < height; dy++) { + world.getBlockAt(x, cy + dy, z).setType(Material.RED_STAINED_GLASS, false); + } + } + } + } +} diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/world/FoGWorld.java b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/world/FoGWorld.java new file mode 100644 index 000000000..8c8b6e52f --- /dev/null +++ b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/world/FoGWorld.java @@ -0,0 +1,43 @@ +package com.orbitmines.minecraft.spigot.servers.fog.world; + +import com.orbitmines.minecraft.spigot.servers.fog.run.Run; +import org.bukkit.Bukkit; +import org.bukkit.Difficulty; +import org.bukkit.GameRule; +import org.bukkit.World; +import org.bukkit.WorldCreator; +import org.bukkit.World.Environment; + +import java.util.Random; + +/** Runtime wrapper around the {@link World} for a specific {@link Run}. */ +public class FoGWorld { + + public static World createOrLoad(String worldFileName, long seed) { + World existing = Bukkit.getWorld(worldFileName); + if (existing != null) return existing; + + WorldCreator creator = new WorldCreator(worldFileName) + .environment(Environment.NORMAL) + .generateStructures(true) + .seed(seed); + World world = Bukkit.createWorld(creator); + if (world == null) return null; + + world.setDifficulty(Difficulty.NORMAL); + world.setGameRule(GameRule.KEEP_INVENTORY, false); + world.setGameRule(GameRule.MOB_DROPS, true); + world.setGameRule(GameRule.SPAWN_MOBS, true); + world.setGameRule(GameRule.RANDOM_TICK_SPEED, 3); + world.setAutoSave(true); + return world; + } + + public static long randomSeed() { + return new Random().nextLong(); + } + + public static String computeFileName(Run run) { + return run.getOwnerUuid().toString().toLowerCase() + "_fog_" + run.getId(); + } +} diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/world/MapExpansion.java b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/world/MapExpansion.java new file mode 100644 index 000000000..fa434a8ea --- /dev/null +++ b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/world/MapExpansion.java @@ -0,0 +1,37 @@ +package com.orbitmines.minecraft.spigot.servers.fog.world; + +import com.orbitmines.archive.minecraft._2019.libs.player.Languageable; +import lombok.Getter; + +/** + * Named regions the player can unlock via the MAP_EXPANSION choice. + * Persistence of the unlocked set lives on {@link com.orbitmines.minecraft.spigot.servers.fog.run.RunStore}. + */ +public enum MapExpansion { + + NORTH_HIGHLANDS(120, 0, 120, 40), + EAST_BADLANDS (0, 120, 120, 40), + SOUTH_DELTA (-120, 0, 120, 40), + WEST_DUNES (0, -120, 120, 40), + DEEP_CAVE_1 (0, 0, 80, 80); + + @Getter private final int offsetX; + @Getter private final int offsetZ; + @Getter private final int radius; + @Getter private final int height; + + MapExpansion(int offsetX, int offsetZ, int radius, int height) { + this.offsetX = offsetX; + this.offsetZ = offsetZ; + this.radius = radius; + this.height = height; + } + + public String nameKey() { return "expansion." + name().toLowerCase() + ".name"; } + public String descriptionKey() { return "expansion." + name().toLowerCase() + ".description"; } + + public String getDisplayName(Languageable viewer) { return viewer.translate("fog", nameKey()); } + public String[] getDescriptionLines(Languageable viewer) { + return viewer.getLanguage().getStringArray("fog", descriptionKey()); + } +} diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/world/StainedGlassBarrier.java b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/world/StainedGlassBarrier.java new file mode 100644 index 000000000..d35c796e9 --- /dev/null +++ b/@orbitmines/minecraft/remastered/spigot/src/main/java/com/orbitmines/minecraft/spigot/servers/fog/world/StainedGlassBarrier.java @@ -0,0 +1,39 @@ +package com.orbitmines.minecraft.spigot.servers.fog.world; + +import org.bukkit.Material; +import org.bukkit.World; + +/** Seals off a rectangular region with red stained-glass walls. Used when a + map expansion is marked DAMAGED or a compartment is inaccessible. */ +public class StainedGlassBarrier { + + public static void seal(World world, int cx, int cy, int cz, int radius, int height) { + for (int x = cx - radius; x <= cx + radius; x++) { + for (int z = cz - radius; z <= cz + radius; z++) { + int dx = x - cx, dz = z - cz; + int distSq = dx * dx + dz * dz; + if (distSq < (radius - 1) * (radius - 1)) continue; + if (distSq > (radius + 1) * (radius + 1)) continue; + for (int dy = 0; dy < height; dy++) { + world.getBlockAt(x, cy + dy, z).setType(Material.RED_STAINED_GLASS, false); + } + } + } + } + + public static void unseal(World world, int cx, int cy, int cz, int radius, int height) { + for (int x = cx - radius; x <= cx + radius; x++) { + for (int z = cz - radius; z <= cz + radius; z++) { + int dx = x - cx, dz = z - cz; + int distSq = dx * dx + dz * dz; + if (distSq < (radius - 1) * (radius - 1)) continue; + if (distSq > (radius + 1) * (radius + 1)) continue; + for (int dy = 0; dy < height; dy++) { + if (world.getBlockAt(x, cy + dy, z).getType() == Material.RED_STAINED_GLASS) { + world.getBlockAt(x, cy + dy, z).setType(Material.AIR, false); + } + } + } + } + } +} diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/resources/fog_en_US.properties b/@orbitmines/minecraft/remastered/spigot/src/main/resources/fog_en_US.properties new file mode 100644 index 000000000..8e8b6005a --- /dev/null +++ b/@orbitmines/minecraft/remastered/spigot/src/main/resources/fog_en_US.properties @@ -0,0 +1,313 @@ +# Fractals of the Galaxy — English + +# === Commands === +player.command.run.description = Manage your active run / switch runs. +player.command.world.description = Spectate another player's active run. +player.command.shop.description = Open the FoG shop. +player.command.codex.description = Open your recipe codex. +player.command.factory.description = Open the drone factory. +player.command.choice.description = (DEV) Trigger a level-up choice. +player.command.choice.argument.description = The specific choice to apply. +player.command.choice.invalid = No such choice: %s +player.command.choice.no_run = You are not currently in a run. +player.command.choice.applied = Applied %s at level %s. +player.command.coopaccept.description = Accept a pending coop-join request. + +# === Stats displayed on tools === +stats.blocks_broken = Blocks broken: %s + +# === Run lifecycle === +run.preparing = Preparing your run... this may take a moment. +run.ready = Your run is ready. Good luck. +run.generation_failed = Could not generate the world. Please try again. +run.load_failed = Could not load the run world. +run.not_found = Run not found. + +# === Level-up flow === +level.prompt = Choose one of three options. +level.chosen = You chose: %s +level.previously = You previously chose this on this level. + +# === Death === +death.lost_levels = You lost %s levels on death. +death.permadeath = Your character is dead. You may continue as spectator. + +# === Drones === +drone.active = ACTIVE +drone.idle = IDLE +drone.repaired = Drone #%s repaired. +drone.module_attached = Attached %s module to drone #%s. +drone.modules_full = That drone has no free module slots. +drone.none = You have no drones to modify. +drone.entity_name = Drone #%s + +# === Shop === +shop.purchase_stub = (POC) You would have bought %s. + +# === Commands (errors) === +command.world.not_online = %s is not online. +command.world.not_in_run = %s is not currently in a run. + +# === Run selector (hologram) === +run.selector.continue.name = Continue +run.selector.new.name = Start a new Run +run.selector.new.description = A fresh world is generated.|Pick a difficulty next. +run.selector.coop.name = Join a Coop +run.selector.coop.description = Request to join someone's run.|They have to accept your request. +run.selector.spectate.name = Spectate a Run +run.selector.spectate.description = Watch another player's run.|You cannot interact with blocks. + +# === Coop flow === +coop.request.sent = Coop request sent to %s. +coop.request.received = %s wants to join your run. +coop.request.click_to_accept = %s +coop.request.accept = Click here to accept +coop.request.hover = Accept %s's coop request. +coop.request.none = You have no pending coop request from %s. +coop.request.accepted = You accepted %s's coop request. +coop.request.you_were_accepted = %s accepted your coop request. +coop.request.requester_offline = %s went offline before you accepted. + +# === Difficulty selector (hologram) === +difficulty.normal.name = Normal +difficulty.normal.description = The standard challenge.|Lose max 5 levels on death. +difficulty.hard.name = Hard +difficulty.hard.description = Harder mobs.|Lose max 10 levels on death. +difficulty.hardcore.name = Hardcore +difficulty.hardcore.description = Hard + permadeath.|Your character cannot be revived. + +# === Factions === +faction.omega.name = Omega +faction.omega.description = Reality-weavers.|Unlocks Omega-only abilities. +faction.alpha.name = Alpha +faction.alpha.description = Conquerors.|Unlocks Alpha-only abilities. +faction.beta.name = Beta +faction.beta.description = Engineers.|Unlocks Beta-only drone upgrades. + +# === Rarities === +rarity.common.name = Common +rarity.rare.name = Rare +rarity.epic.name = Epic +rarity.legendary.name = Legendary + +# === Choices === +choice.faction_omega.name = Join Omega +choice.faction_omega.description = Reality-weavers.|Unlocks Omega-only abilities. +choice.faction_alpha.name = Join Alpha +choice.faction_alpha.description = Conquerors.|Unlocks Alpha-only abilities. +choice.faction_beta.name = Join Beta +choice.faction_beta.description = Engineers.|Unlocks Beta-only drone upgrades. + +choice.extra_hp_4.name = +4 Hearts HP +choice.extra_hp_4.description = Increases maximum HP|by 4 hearts. Stackable. +choice.damage_5.name = +5%% Damage +choice.damage_5.description = Increases melee damage|by 5%%. Stackable. +choice.speed_5.name = +5%% Speed +choice.speed_5.description = Increases movement speed|by 5%%. Stackable. +choice.mining_speed_5.name = +5%% Mining Speed +choice.mining_speed_5.description = Increases mining speed|by 5%%. Stackable. + +choice.skill_lumberjack.name = Lumberjack +choice.skill_lumberjack.description = Chop down whole trees|in one swing. +choice.skill_fishing.name = Fishing +choice.skill_fishing.description = Unlocks fishing-based|rewards and quests. +choice.skill_botany.name = Botany +choice.skill_botany.description = Increases tree and crop|growth speed. +choice.skill_beekeeping.name = Beekeeping +choice.skill_beekeeping.description = Unlocks honey-bearing|trees around the map. + +choice.ore_copper.name = Unlock Copper +choice.ore_copper.description = Copper ores begin to|generate in the map. +choice.ore_cobalt.name = Unlock Cobalt +choice.ore_cobalt.description = Cobalt ores begin to|generate in the map. +choice.ore_strontium.name = Unlock Strontium +choice.ore_strontium.description = Strontium ores begin to|generate in the map. +choice.ore_amethyst.name = Unlock Amethyst +choice.ore_amethyst.description = Amethyst ores begin to|generate in the map. +choice.ore_uranium.name = Unlock Uranium +choice.ore_uranium.description = Uranium ores begin to|generate in the map. +choice.ore_iridium.name = Unlock Iridium +choice.ore_iridium.description = Iridium ores begin to|generate in the map. +choice.ore_francium.name = Unlock Francium +choice.ore_francium.description = Francium ores begin to|generate in the map. + +choice.tree_tar.name = Unlock Tar Tree +choice.tree_tar.description = Tar trees generate|in your forests. +choice.tree_glowwood.name = Glowwood Tree +choice.tree_glowwood.description = Glowing wood trees|light nearby areas. +choice.tree_voidoak.name = Voidoak Tree +choice.tree_voidoak.description = Rare, yields valuable|void-oak logs. + +choice.drone_unlock.name = Unlock a Drone +choice.drone_unlock.description = Adds one Allay drone|to your swarm (max 9). +choice.drone_factory.name = Drone Factory +choice.drone_factory.description = Generates a Drone Factory|compartment in your city. + +choice.structure_farm.name = Farm +choice.structure_farm.description = Generates a Farm compartment|in your city. Claim it. +choice.structure_enchant.name = Enchanting Area +choice.structure_enchant.description = Generates an Enchanting|compartment. Unlocks custom enchants. +choice.structure_crates.name = 4h Crate +choice.structure_crates.description = Unlocks a 4-hourly crate|(stackable up to 5). + +choice.map_expansion.name = Map Expansion +choice.map_expansion.description = Expand your territory.|Go into spectate mode to pick. + +choice.enchant_auto_replant.name = Auto-Replant +choice.enchant_auto_replant.description = Crops replant themselves|when harvested. +choice.enchant_homing_arrow.name = Homing Arrow +choice.enchant_homing_arrow.description = Arrows track nearby|enemies. +choice.enchant_rain_arrows.name = Rain of Arrows +choice.enchant_rain_arrows.description = On kill, rain arrows|from above in an area. +choice.enchant_absorption.name = Shield Generator +choice.enchant_absorption.description = Regens fast out of combat;|provides an absorption shield. +choice.enchant_chop_tree.name = Treefeller +choice.enchant_chop_tree.description = Axes chop whole trees|at once. + +choice.recipe_ore_suit.name = Ore Suit +choice.recipe_ore_suit.description = Unlock recipe: craft an|armor suit from ores. +choice.recipe_pickaxe_up.name = Pickaxe Upgrade +choice.recipe_pickaxe_up.description = Unlock recipe: upgrade|your pickaxe with ores. +choice.recipe_backpack.name = Backpack +choice.recipe_backpack.description = Unlock recipe: craft|a backpack (extended bundle). + +choice.ability_shield.name = Shield Bash +choice.ability_shield.description = Active: raise shield|to absorb incoming damage. +choice.ability_sweep.name = Sweep Attack +choice.ability_sweep.description = AoE melee cleave|on right-click. +choice.ability_leap.name = Leap +choice.ability_leap.description = Double-jump into|a long horizontal leap. + +choice.quest_collect.name = Collect Quests +choice.quest_collect.description = Unlocks collect-type|quests SkyBlock-style. +choice.quest_defeat.name = Defeat Quests +choice.quest_defeat.description = Unlocks mob-defeat|quests. + +# === Modules === +module.collect.name = Collect Module +module.collect.description = Drone auto-picks-up|nearby items. +module.skill.name = Skill Module +module.skill.description = Drone performs the owner's|unlocked skill. +module.enchantment.name = Enchantment Module +module.enchantment.description = Applies a custom enchant|while equipped. +module.combat.name = Combat Module +module.combat.description = Drone attacks enemies|in range. +module.shield.name = Shield Module +module.shield.description = Absorbs damage for owner|within range. +module.inspire.name = Inspire Module +module.inspire.description = Buffs attack for|nearby allies. + +# === Crates === +crate.basic.name = Basic Crate +crate.epic.name = Epic Crate + +# === Abilities === +ability.shield_bash.name = Shield Bash +ability.sweep_attack.name = Sweep Attack +ability.leap.name = Leap + +# === Custom enchants === +enchant.auto_replant.name = Auto-Replant +enchant.homing_arrow.name = Homing Arrow +enchant.rain_arrows.name = Rain of Arrows +enchant.absorption.name = Shield Generator +enchant.treefeller.name = Treefeller +enchant.damaged_suffix = DAMAGED + +# === Compartments === +compartment.farm.name = Farm +compartment.enchanting.name = Enchanting Area +compartment.crates.name = Crates +compartment.drone_factory.name = Drone Factory + +# === Map expansions === +expansion.north_highlands.name = North Highlands +expansion.north_highlands.description = The frozen crescent|north of the city. +expansion.east_badlands.name = East Badlands +expansion.east_badlands.description = Orange buttes baked|under twin suns. +expansion.south_delta.name = South Delta +expansion.south_delta.description = Fog-soaked swamp deltas. +expansion.west_dunes.name = West Dunes +expansion.west_dunes.description = Rolling dune-seas. +expansion.deep_cave_1.name = Lower Cavern I +expansion.deep_cave_1.description = An enormous cave one|biome down. + +# === Mobs === +mob.fire_mage.name = Fire Mage +mob.debuff_mage.name = Debuff Mage +mob.zombie.name = FoG Zombie +mob.bull.name = Bull +mob.bee.name = Wasp + +# === Bosses === +boss.undead_knight.name = Undead Knight + +# === Raids === +raid.title = §c§lRaid §7(Level %s) + +# === Items === +item.run_switcher.name = Switch Run +item.recipe_book.name = Codex +item.drone_toggle.name = Drone Toggle +item.spectator_join.name = Spectate a Run +item.backpack.name = Backpack (Tier %s) +item.pickaxe.name = Pickaxe (Tier %s) + +# === Scoreboard === +scoreboard.level = Level +scoreboard.run = Run +scoreboard.drones = Drones + +# === GUIs === +gui.shop.title = Shop +gui.shop.price = %s credits +gui.codex.title = Codex +gui.codex.locked_suffix = (locked) +gui.drone_factory.title = Drone Factory +gui.drone_factory.drone_title = Drone #%s +gui.drone_factory.hp = §7HP: §a%s §7/ §a%s +gui.drone_factory.modules = §7Modules: §f%s +gui.drone_factory.click_to_repair = Click to repair §7(cost %s) +gui.drone_factory.click_to_attach = Click to attach to first drone +gui.spectator_join.title = Spectate a Run +gui.spectator_join.run = §7Run #%s +gui.spectator_join.difficulty = §7Difficulty: %s +gui.spectator_join.click_to_spectate = Click to spectate +gui.coop_join.title = Coop: Join an existing Run +gui.coop_join.run = §7Run #%s +gui.coop_join.difficulty = §7Difficulty: %s +gui.coop_join.click_to_request = Click to send coop request + +# === Shop offers === +shop.key_basic.name = Basic Crate Key +shop.key_basic.description = Unlocks one basic crate. +shop.key_epic.name = Epic Crate Key +shop.key_epic.description = Unlocks one epic crate. +shop.xp_bottle.name = XP Bottle +shop.xp_bottle.description = Pushes 100 tracked XP. +shop.enchant_autoreplant.name = Auto-Replant Enchant +shop.enchant_autoreplant.description = Applies to a hoe. +shop.enchant_homing.name = Homing Arrow Enchant +shop.enchant_homing.description = Applies to a bow. +shop.recipe_pickaxe.name = Pickaxe Upgrade Recipe +shop.recipe_pickaxe.description = Unlocks pickaxe upgrade recipe. +shop.recipe_suit.name = Ore Suit Recipe +shop.recipe_suit.description = Unlocks ore suit recipe. +shop.module_upgrade.name = Upgrade Enchant to Module +shop.module_upgrade.description = Promote an enchant to a drone module. +shop.enchant_remove.name = Strip Enchantment +shop.enchant_remove.description = Removes one enchant from a tool. + +# === Recipes (codex entries) === +recipe.ore_suit.name = Ore Suit +recipe.ore_suit.description = Craft full armor from ore blocks. +recipe.pickaxe_upgrade.name = Pickaxe Upgrade +recipe.pickaxe_upgrade.description = Combine your pickaxe with ores to upgrade. +recipe.backpack.name = Backpack +recipe.backpack.description = Extended-capacity bundle. +recipe.drone_factory_intro.name = Drone Factory +recipe.drone_factory_intro.description = Unlocked at level 10. Crafts and repairs drones. +recipe.crate_schedule.name = 4h Crate Schedule +recipe.crate_schedule.description = Crates regenerate every 4 hours, stack up to 5. +recipe.factions.name = Factions +recipe.factions.description = Pick one: Omega, Alpha, or Beta. Unique per-player. diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/resources/fog_nl_NL.properties b/@orbitmines/minecraft/remastered/spigot/src/main/resources/fog_nl_NL.properties new file mode 100644 index 000000000..c0cb87880 --- /dev/null +++ b/@orbitmines/minecraft/remastered/spigot/src/main/resources/fog_nl_NL.properties @@ -0,0 +1,313 @@ +# Fractals of the Galaxy — Nederlands + +# === Commando's === +player.command.run.description = Beheer je actieve run / wissel van run. +player.command.world.description = Bekijk de actieve run van een andere speler. +player.command.shop.description = Open de FoG-shop. +player.command.codex.description = Open je receptencodex. +player.command.factory.description = Open de drone-fabriek. +player.command.choice.description = (DEV) Trigger een level-up keuze. +player.command.choice.argument.description = De specifieke keuze om toe te passen. +player.command.choice.invalid = Onbekende keuze: %s +player.command.choice.no_run = Je zit momenteel niet in een run. +player.command.choice.applied = %s toegepast op level %s. +player.command.coopaccept.description = Accepteer een openstaand coop-verzoek. + +# === Stats op tool-metadata === +stats.blocks_broken = Blokken gebroken: %s + +# === Run-levenscyclus === +run.preparing = Je run wordt voorbereid... dit kan even duren. +run.ready = Je run is klaar. Veel succes. +run.generation_failed = Kon de wereld niet genereren. Probeer het opnieuw. +run.load_failed = Kon de run-wereld niet laden. +run.not_found = Run niet gevonden. + +# === Level-up keuzes === +level.prompt = Kies één van drie opties. +level.chosen = Je koos: %s +level.previously = Je koos dit eerder op dit level. + +# === Dood === +death.lost_levels = Je verloor %s levels bij je dood. +death.permadeath = Je personage is dood. Je kunt als toeschouwer verder. + +# === Drones === +drone.active = ACTIEF +drone.idle = INACTIEF +drone.repaired = Drone #%s is gerepareerd. +drone.module_attached = %s-module bevestigd aan drone #%s. +drone.modules_full = Die drone heeft geen vrije modulesleuven. +drone.none = Je hebt geen drones om aan te passen. +drone.entity_name = Drone #%s + +# === Shop === +shop.purchase_stub = (POC) Je zou %s gekocht hebben. + +# === Commando's (fouten) === +command.world.not_online = %s is niet online. +command.world.not_in_run = %s zit momenteel niet in een run. + +# === Run-selector (hologram) === +run.selector.continue.name = Ga verder +run.selector.new.name = Start een nieuwe Run +run.selector.new.description = Er wordt een nieuwe wereld|gegenereerd. Kies hierna een niveau. +run.selector.coop.name = Sluit je aan bij een Coop +run.selector.coop.description = Vraag iemand om in hun run|te spelen. Ze moeten accepteren. +run.selector.spectate.name = Bekijk een Run +run.selector.spectate.description = Bekijk de run van iemand anders.|Je kunt niet interacteren. + +# === Coop-flow === +coop.request.sent = Coop-verzoek gestuurd naar %s. +coop.request.received = %s wil in jouw run spelen. +coop.request.click_to_accept = %s +coop.request.accept = Klik hier om te accepteren +coop.request.hover = Accepteer het coop-verzoek van %s. +coop.request.none = Je hebt geen openstaand coop-verzoek van %s. +coop.request.accepted = Je hebt het coop-verzoek van %s geaccepteerd. +coop.request.you_were_accepted = %s heeft je coop-verzoek geaccepteerd. +coop.request.requester_offline = %s is offline gegaan voordat je accepteerde. + +# === Moeilijkheidsgraad (hologram) === +difficulty.normal.name = Normaal +difficulty.normal.description = De standaard uitdaging.|Verlies max 5 levels bij dood. +difficulty.hard.name = Zwaar +difficulty.hard.description = Moeilijkere mobs.|Verlies max 10 levels bij dood. +difficulty.hardcore.name = Hardcore +difficulty.hardcore.description = Zwaar + permadeath.|Je personage kan niet terugkeren. + +# === Facties === +faction.omega.name = Omega +faction.omega.description = Realiteitswevers.|Unlockt Omega-only vaardigheden. +faction.alpha.name = Alpha +faction.alpha.description = Veroveraars.|Unlockt Alpha-only vaardigheden. +faction.beta.name = Beta +faction.beta.description = Ingenieurs.|Unlockt Beta-only drone-upgrades. + +# === Zeldzaamheden === +rarity.common.name = Gewoon +rarity.rare.name = Zeldzaam +rarity.epic.name = Episch +rarity.legendary.name = Legendarisch + +# === Keuzes === +choice.faction_omega.name = Sluit je aan bij Omega +choice.faction_omega.description = Realiteitswevers.|Unlockt Omega-only vaardigheden. +choice.faction_alpha.name = Sluit je aan bij Alpha +choice.faction_alpha.description = Veroveraars.|Unlockt Alpha-only vaardigheden. +choice.faction_beta.name = Sluit je aan bij Beta +choice.faction_beta.description = Ingenieurs.|Unlockt Beta-only drone-upgrades. + +choice.extra_hp_4.name = +4 Harten HP +choice.extra_hp_4.description = Verhoogt maximale HP|met 4 harten. Stapelbaar. +choice.damage_5.name = +5%% Schade +choice.damage_5.description = Verhoogt melee-schade|met 5%%. Stapelbaar. +choice.speed_5.name = +5%% Snelheid +choice.speed_5.description = Verhoogt loopsnelheid|met 5%%. Stapelbaar. +choice.mining_speed_5.name = +5%% Mijnsnelheid +choice.mining_speed_5.description = Verhoogt mijnsnelheid|met 5%%. Stapelbaar. + +choice.skill_lumberjack.name = Houthakker +choice.skill_lumberjack.description = Vel hele bomen|in één klap. +choice.skill_fishing.name = Vissen +choice.skill_fishing.description = Unlockt visserij-gebaseerde|beloningen en quests. +choice.skill_botany.name = Botanie +choice.skill_botany.description = Verhoogt groeisnelheid|van bomen en gewassen. +choice.skill_beekeeping.name = Imkerij +choice.skill_beekeeping.description = Unlockt honingbomen|op de map. + +choice.ore_copper.name = Ontgrendel Koper +choice.ore_copper.description = Kopererts begint te|genereren op de map. +choice.ore_cobalt.name = Ontgrendel Kobalt +choice.ore_cobalt.description = Kobalterts begint te|genereren op de map. +choice.ore_strontium.name = Ontgrendel Strontium +choice.ore_strontium.description = Strontiumerts begint te|genereren op de map. +choice.ore_amethyst.name = Ontgrendel Amethist +choice.ore_amethyst.description = Amethisterts begint te|genereren op de map. +choice.ore_uranium.name = Ontgrendel Uranium +choice.ore_uranium.description = Uraniumerts begint te|genereren op de map. +choice.ore_iridium.name = Ontgrendel Iridium +choice.ore_iridium.description = Iridiumerts begint te|genereren op de map. +choice.ore_francium.name = Ontgrendel Francium +choice.ore_francium.description = Franciumerts begint te|genereren op de map. + +choice.tree_tar.name = Ontgrendel Teerboom +choice.tree_tar.description = Teerbomen genereren|in jouw bossen. +choice.tree_glowwood.name = Glimhoutboom +choice.tree_glowwood.description = Glimhoutbomen geven|licht aan de omgeving. +choice.tree_voidoak.name = Leegteik +choice.tree_voidoak.description = Zeldzaam, levert waardevolle|leegte-eikenhout. + +choice.drone_unlock.name = Ontgrendel een Drone +choice.drone_unlock.description = Voegt een Allay-drone toe|aan je zwerm (max 9). +choice.drone_factory.name = Drone-Fabriek +choice.drone_factory.description = Genereert een Drone-Fabriek|in je stad. + +choice.structure_farm.name = Boerderij +choice.structure_farm.description = Genereert een boerderij-compartiment|in je stad. Claim het. +choice.structure_enchant.name = Enchantruimte +choice.structure_enchant.description = Genereert een enchant-compartiment.|Unlockt eigen enchants. +choice.structure_crates.name = 4u Kist +choice.structure_crates.description = Unlockt een 4-uurs kist|(stapelbaar tot 5). + +choice.map_expansion.name = Kaartuitbreiding +choice.map_expansion.description = Breidt je gebied uit.|Ga naar spectator om te kiezen. + +choice.enchant_auto_replant.name = Auto-Herplanten +choice.enchant_auto_replant.description = Gewassen herplanten zichzelf|na het oogsten. +choice.enchant_homing_arrow.name = Zoekpijl +choice.enchant_homing_arrow.description = Pijlen zoeken vijanden|in de buurt. +choice.enchant_rain_arrows.name = Pijlregen +choice.enchant_rain_arrows.description = Bij kill regent het pijlen|uit de lucht. +choice.enchant_absorption.name = Schildgenerator +choice.enchant_absorption.description = Snel regenereren buiten gevecht;|geeft een absorptieschild. +choice.enchant_chop_tree.name = Boomveller +choice.enchant_chop_tree.description = Bijlen vellen hele bomen|in één keer. + +choice.recipe_ore_suit.name = Ertspak +choice.recipe_ore_suit.description = Unlockt recept: een wapenrusting|maken van erts. +choice.recipe_pickaxe_up.name = Houweel-Upgrade +choice.recipe_pickaxe_up.description = Unlockt recept: je houweel|upgraden met erts. +choice.recipe_backpack.name = Rugzak +choice.recipe_backpack.description = Unlockt recept: een rugzak|(uitgebreide bundel). + +choice.ability_shield.name = Schildstoot +choice.ability_shield.description = Actief: hef je schild|om schade te absorberen. +choice.ability_sweep.name = Uithaal +choice.ability_sweep.description = Gebied-melee-aanval|op rechtsklik. +choice.ability_leap.name = Sprong +choice.ability_leap.description = Dubbele sprong in|een lange horizontale sprong. + +choice.quest_collect.name = Verzamel-Quests +choice.quest_collect.description = Unlockt verzamel-type|quests SkyBlock-stijl. +choice.quest_defeat.name = Versla-Quests +choice.quest_defeat.description = Unlockt mob-versla|quests. + +# === Modules === +module.collect.name = Verzamel-Module +module.collect.description = Drone pakt automatisch|items in de buurt op. +module.skill.name = Vaardigheid-Module +module.skill.description = Drone voert de vaardigheid|van de eigenaar uit. +module.enchantment.name = Enchantment-Module +module.enchantment.description = Past een eigen enchant toe|terwijl uitgerust. +module.combat.name = Gevechts-Module +module.combat.description = Drone valt vijanden|in de buurt aan. +module.shield.name = Schild-Module +module.shield.description = Absorbeert schade voor de|eigenaar in bereik. +module.inspire.name = Inspiratie-Module +module.inspire.description = Verhoogt aanval voor|bondgenoten in de buurt. + +# === Kisten === +crate.basic.name = Basis-Kist +crate.epic.name = Epische Kist + +# === Vaardigheden === +ability.shield_bash.name = Schildstoot +ability.sweep_attack.name = Uithaal +ability.leap.name = Sprong + +# === Eigen enchants === +enchant.auto_replant.name = Auto-Herplanten +enchant.homing_arrow.name = Zoekpijl +enchant.rain_arrows.name = Pijlregen +enchant.absorption.name = Schildgenerator +enchant.treefeller.name = Boomveller +enchant.damaged_suffix = BESCHADIGD + +# === Compartimenten === +compartment.farm.name = Boerderij +compartment.enchanting.name = Enchantruimte +compartment.crates.name = Kisten +compartment.drone_factory.name = Drone-Fabriek + +# === Kaartuitbreidingen === +expansion.north_highlands.name = Noordelijke Hooglanden +expansion.north_highlands.description = De bevroren sikkel|ten noorden van de stad. +expansion.east_badlands.name = Oostelijke Woestenij +expansion.east_badlands.description = Oranje rotsen gebakken|onder twee zonnen. +expansion.south_delta.name = Zuidelijke Delta +expansion.south_delta.description = Mistige moerasdelta's. +expansion.west_dunes.name = Westelijke Duinen +expansion.west_dunes.description = Rollende duinenzee. +expansion.deep_cave_1.name = Onderste Grot I +expansion.deep_cave_1.description = Een enorme grot één|bioom omlaag. + +# === Mobs === +mob.fire_mage.name = Vuurmagiër +mob.debuff_mage.name = Debuff-Magiër +mob.zombie.name = FoG-Zombie +mob.bull.name = Stier +mob.bee.name = Wesp + +# === Bazen === +boss.undead_knight.name = Ondode Ridder + +# === Raids === +raid.title = §c§lRaid §7(Level %s) + +# === Items === +item.run_switcher.name = Wissel Run +item.recipe_book.name = Codex +item.drone_toggle.name = Drone-Schakelaar +item.spectator_join.name = Bekijk een Run +item.backpack.name = Rugzak (Tier %s) +item.pickaxe.name = Houweel (Tier %s) + +# === Scoreboard === +scoreboard.level = Level +scoreboard.run = Run +scoreboard.drones = Drones + +# === GUI's === +gui.shop.title = Shop +gui.shop.price = %s credits +gui.codex.title = Codex +gui.codex.locked_suffix = (vergrendeld) +gui.drone_factory.title = Drone-Fabriek +gui.drone_factory.drone_title = Drone #%s +gui.drone_factory.hp = §7HP: §a%s §7/ §a%s +gui.drone_factory.modules = §7Modules: §f%s +gui.drone_factory.click_to_repair = Klik om te repareren §7(kost %s) +gui.drone_factory.click_to_attach = Klik om aan eerste drone te bevestigen +gui.spectator_join.title = Bekijk een Run +gui.spectator_join.run = §7Run #%s +gui.spectator_join.difficulty = §7Moeilijkheid: %s +gui.spectator_join.click_to_spectate = Klik om te bekijken +gui.coop_join.title = Coop: Sluit je aan bij een Run +gui.coop_join.run = §7Run #%s +gui.coop_join.difficulty = §7Moeilijkheid: %s +gui.coop_join.click_to_request = Klik om een coop-verzoek te sturen + +# === Shop-aanbiedingen === +shop.key_basic.name = Basis-Kistsleutel +shop.key_basic.description = Opent één basiskist. +shop.key_epic.name = Epische Kistsleutel +shop.key_epic.description = Opent één epische kist. +shop.xp_bottle.name = XP-Fles +shop.xp_bottle.description = Duwt 100 getelde XP. +shop.enchant_autoreplant.name = Auto-Herplant-Enchant +shop.enchant_autoreplant.description = Toepasbaar op een hak. +shop.enchant_homing.name = Zoekpijl-Enchant +shop.enchant_homing.description = Toepasbaar op een boog. +shop.recipe_pickaxe.name = Houweel-Upgrade Recept +shop.recipe_pickaxe.description = Ontgrendelt houweel-upgrade-recept. +shop.recipe_suit.name = Ertspak Recept +shop.recipe_suit.description = Ontgrendelt ertspak-recept. +shop.module_upgrade.name = Enchant naar Module +shop.module_upgrade.description = Promoveer een enchant tot drone-module. +shop.enchant_remove.name = Enchant Verwijderen +shop.enchant_remove.description = Verwijdert één enchant van een tool. + +# === Recepten (codex) === +recipe.ore_suit.name = Ertspak +recipe.ore_suit.description = Maak een volledige wapenrusting van ertsblokken. +recipe.pickaxe_upgrade.name = Houweel-Upgrade +recipe.pickaxe_upgrade.description = Combineer je houweel met erts om te upgraden. +recipe.backpack.name = Rugzak +recipe.backpack.description = Bundel met uitgebreide capaciteit. +recipe.drone_factory_intro.name = Drone-Fabriek +recipe.drone_factory_intro.description = Ontgrendeld op level 10. Maakt en repareert drones. +recipe.crate_schedule.name = 4u Kist-Schema +recipe.crate_schedule.description = Kisten regenereren elke 4 uur, stapelen tot 5. +recipe.factions.name = Facties +recipe.factions.description = Kies één: Omega, Alpha of Beta. Uniek per speler. diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/resources/spigot_en_US.properties b/@orbitmines/minecraft/remastered/spigot/src/main/resources/spigot_en_US.properties index a9a890df1..9c7f1b3b1 100644 --- a/@orbitmines/minecraft/remastered/spigot/src/main/resources/spigot_en_US.properties +++ b/@orbitmines/minecraft/remastered/spigot/src/main/resources/spigot_en_US.properties @@ -118,6 +118,9 @@ player.server_selector.kitpvp.features = Kits|Kit Upgrades|Abilities|Custom Ench player.server_selector.creative.underline = Galactic Design Archives player.server_selector.creative.description = It is rumored this is where the|galactic authority stores all|their designs. player.server_selector.creative.features = Create Lobbies|Create Maps|WorldEdit +player.server_selector.fog.underline = Galactic Prison Planet +player.server_selector.fog.description = A maximum-security prison world|combining the best of Prison and|SkyBlock: escape, mine, survive. +player.server_selector.fog.features = Rogue-like Runs|Mine & Expand|Drones|Custom Enchantments|Raids|Bosses # Stats player.stats.back_to_stats = Back to General Stats diff --git a/@orbitmines/minecraft/remastered/spigot/src/main/resources/spigot_nl_NL.properties b/@orbitmines/minecraft/remastered/spigot/src/main/resources/spigot_nl_NL.properties index 794277da5..dd85da85d 100644 --- a/@orbitmines/minecraft/remastered/spigot/src/main/resources/spigot_nl_NL.properties +++ b/@orbitmines/minecraft/remastered/spigot/src/main/resources/spigot_nl_NL.properties @@ -115,6 +115,9 @@ player.server_selector.survival.features = Regions|Land Claims|30,000x30,000 Wer player.server_selector.creative.underline = Galactisch Ontwerp Archief player.server_selector.creative.description = Het gerucht gaat dat de galactische|autoriteit hier al hun ontwerpen|opslaat. player.server_selector.creative.features = Maak Lobby's|Maak Maps|WorldEdit +player.server_selector.fog.underline = Galactische Gevangenisplaneet +player.server_selector.fog.description = Een maximaal-beveiligde gevangeniswereld|met het beste uit Prison en SkyBlock:|ontsnap, mijn, overleef. +player.server_selector.fog.features = Rogue-like Runs|Mijnen & Uitbreiden|Drones|Eigen Enchantments|Raids|Bazen player.server_selector.kitpvp.underline = Geheime Gladiatoren Arena player.server_selector.kitpvp.description = Een gladiatoren arena opgezet door een|onbekende generaal en admiraal van OrbitMines|vloot, bevindt zich op Militaire Basis #27. player.server_selector.kitpvp.features = Kits|Kit Upgrades|Abilities|Custom Enchantments|Level Systeem|Geen item drops