From 16433a893fa6452cc5ad8601259b24c00e2b1fb9 Mon Sep 17 00:00:00 2001 From: Cortex Builder Date: Thu, 11 Jun 2026 20:14:58 +0200 Subject: [PATCH] premier commit --- .gitignore | 15 + build.gradle.kts | 33 + rust/.cargo/config.toml | 2 + rust/Cargo.lock | 640 ++++++++++ rust/Cargo.toml | 14 + rust/src/arena.rs | 36 + rust/src/commands.rs | 195 ++++ rust/src/config.rs | 70 ++ rust/src/events.rs | 155 +++ rust/src/game.rs | 201 ++++ rust/src/generator.rs | 79 ++ rust/src/lib.rs | 54 + rust/src/shop.rs | 118 ++ rust/src/state.rs | 6 + rust/src/team.rs | 70 ++ settings.gradle.kts | 1 + src/main/java/com/bedwars/ArenaManager.java | 381 ++++++ src/main/java/com/bedwars/BedwarsCommand.java | 304 +++++ src/main/java/com/bedwars/BedwarsPlugin.java | 132 +++ src/main/java/com/bedwars/BedwarsTeam.java | 49 + src/main/java/com/bedwars/GameListener.java | 823 +++++++++++++ src/main/java/com/bedwars/GameManager.java | 1040 +++++++++++++++++ src/main/java/com/bedwars/GameState.java | 7 + src/main/java/com/bedwars/Generator.java | 191 +++ .../java/com/bedwars/GeneratorManager.java | 131 +++ src/main/java/com/bedwars/GeneratorType.java | 32 + src/main/java/com/bedwars/LastDamageType.java | 9 + .../java/com/bedwars/ScoreboardManager.java | 159 +++ src/main/java/com/bedwars/ShopListener.java | 107 ++ src/main/java/com/bedwars/ShopManager.java | 790 +++++++++++++ .../java/com/bedwars/UpgradesManager.java | 254 ++++ src/main/resources/config.yml | 247 ++++ src/main/resources/plugin.yml | 11 + 33 files changed, 6356 insertions(+) create mode 100644 .gitignore create mode 100644 build.gradle.kts create mode 100644 rust/.cargo/config.toml create mode 100644 rust/Cargo.lock create mode 100644 rust/Cargo.toml create mode 100644 rust/src/arena.rs create mode 100644 rust/src/commands.rs create mode 100644 rust/src/config.rs create mode 100644 rust/src/events.rs create mode 100644 rust/src/game.rs create mode 100644 rust/src/generator.rs create mode 100644 rust/src/lib.rs create mode 100644 rust/src/shop.rs create mode 100644 rust/src/state.rs create mode 100644 rust/src/team.rs create mode 100644 settings.gradle.kts create mode 100644 src/main/java/com/bedwars/ArenaManager.java create mode 100644 src/main/java/com/bedwars/BedwarsCommand.java create mode 100644 src/main/java/com/bedwars/BedwarsPlugin.java create mode 100644 src/main/java/com/bedwars/BedwarsTeam.java create mode 100644 src/main/java/com/bedwars/GameListener.java create mode 100644 src/main/java/com/bedwars/GameManager.java create mode 100644 src/main/java/com/bedwars/GameState.java create mode 100644 src/main/java/com/bedwars/Generator.java create mode 100644 src/main/java/com/bedwars/GeneratorManager.java create mode 100644 src/main/java/com/bedwars/GeneratorType.java create mode 100644 src/main/java/com/bedwars/LastDamageType.java create mode 100644 src/main/java/com/bedwars/ScoreboardManager.java create mode 100644 src/main/java/com/bedwars/ShopListener.java create mode 100644 src/main/java/com/bedwars/ShopManager.java create mode 100644 src/main/java/com/bedwars/UpgradesManager.java create mode 100644 src/main/resources/config.yml create mode 100644 src/main/resources/plugin.yml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..daad22f --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +# Gradle +.gradle/ +.gradle_home/ +build/ +bin/ +out/ +._build + +# Rust/Cargo +/rust/target/ + +# OS-specific +.DS_Store +Thumbs.db +._* diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..632430a --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,33 @@ +plugins { + java +} + +group = "com.bedwars" +version = "1.0.0" + +repositories { + mavenCentral() + maven { + name = "papermc" + url = uri("https://repo.papermc.io/repository/maven-public/") + } +} + +dependencies { + compileOnly("io.papermc.paper:paper-api:1.21.4-R0.1-SNAPSHOT") +} + +tasks.withType { + options.encoding = "UTF-8" + options.release.set(21) +} + +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(21)) + } +} + +tasks.jar { + archiveFileName.set("Bedwars.jar") +} diff --git a/rust/.cargo/config.toml b/rust/.cargo/config.toml new file mode 100644 index 0000000..f68f33c --- /dev/null +++ b/rust/.cargo/config.toml @@ -0,0 +1,2 @@ +[build] +target = "wasm32-wasip2" diff --git a/rust/Cargo.lock b/rust/Cargo.lock new file mode 100644 index 0000000..f8495cd --- /dev/null +++ b/rust/Cargo.lock @@ -0,0 +1,640 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "atomic-polyfill" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" +dependencies = [ + "critical-section", +] + +[[package]] +name = "bedwars_pumpkin" +version = "1.0.0" +dependencies = [ + "pumpkin-plugin-api", + "rand", + "serde", + "serde_yaml", + "tracing", +] + +[[package]] +name = "bitflags" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror", +] + +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "hash32" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "foldhash", +] + +[[package]] +name = "heapless" +version = "0.7.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" +dependencies = [ + "atomic-polyfill", + "hash32", + "rustc_version", + "serde", + "spin", + "stable_deref_trait", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown", + "serde", + "serde_core", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" + +[[package]] +name = "memchr" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "postcard" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "serde", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pumpkin-plugin-api" +version = "0.1.0" +source = "git+https://github.com/Pumpkin-MC/Pumpkin#30303a8cbd9c3dedac4ff13abd1de64537c6203b" +dependencies = [ + "postcard", + "serde_json", + "tracing", + "tracing-serde-structured", + "wit-bindgen", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tracing-serde-structured" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0832510e9838a4ff7e45e278602ab0533686f9507bc6189e024e488602f29820" +dependencies = [ + "hash32", + "heapless", + "serde", + "tracing-core", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-encoder" +version = "0.247.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30b6733b8b91d010a6ac5b0fb237dc46a19650bc4c67db66857e2e787d437204" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.247.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "665fe59e56cc9b419ca6fcca56673e3421d1a5011e3b65caf6b726fd9e041d10" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.247.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e6fb4c2bee46c5ea4d40f8cdb5c131725cd976718ec56f1c8e82fbde5fa2a80" +dependencies = [ + "bitflags", + "hashbrown", + "indexmap", + "semver", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02dee27a2dc20d1008016c742ec9fc6ea498492994ba3750be7454cbc97ff04c" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5007dae772945b7a5003d69d90a3a4a78929d41f19d004e980c4259a6af4484" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af9237d678e3513ad24e96fe98beacdc0db6405284ba2a2400418cf0d42caa89" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.247.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d567162a6b9843080e5e0053f696623ff694bae8ae017c9ec536d1873bbe3d8" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.247.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ffe4064318cdf3c08cb99343b44c039fcefe61ccdf58aa9975285f13d74d1fc" +dependencies = [ + "anyhow", + "hashbrown", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "zerocopy" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/rust/Cargo.toml b/rust/Cargo.toml new file mode 100644 index 0000000..1c8d4b2 --- /dev/null +++ b/rust/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "bedwars_pumpkin" +version = "1.0.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +pumpkin-plugin-api = { git = "https://github.com/Pumpkin-MC/Pumpkin", package = "pumpkin-plugin-api" } +serde = { version = "1.0", features = ["derive"] } +serde_yaml = "0.9" +tracing = "0.1" +rand = "0.8" diff --git a/rust/src/arena.rs b/rust/src/arena.rs new file mode 100644 index 0000000..da18228 --- /dev/null +++ b/rust/src/arena.rs @@ -0,0 +1,36 @@ +use std::collections::HashSet; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct BlockPos { + pub x: i32, + pub y: i32, + pub z: i32, +} + +pub struct ArenaManager { + pub player_placed_blocks: HashSet, +} + +impl ArenaManager { + pub fn new() -> Self { + Self { + player_placed_blocks: HashSet::new(), + } + } + + pub fn track_block(&mut self, pos: BlockPos) { + self.player_placed_blocks.insert(pos); + } + + pub fn untrack_block(&mut self, pos: &BlockPos) -> bool { + self.player_placed_blocks.remove(pos) + } + + pub fn is_player_placed(&self, pos: &BlockPos) -> bool { + self.player_placed_blocks.contains(pos) + } + + pub fn clear(&mut self) { + self.player_placed_blocks.clear(); + } +} diff --git a/rust/src/commands.rs b/rust/src/commands.rs new file mode 100644 index 0000000..14ee276 --- /dev/null +++ b/rust/src/commands.rs @@ -0,0 +1,195 @@ +use crate::config::{BedwarsConfig, Loc, TeamConfig, GeneratorConfig}; +use crate::team::BedwarsTeam; +use crate::game::GameManager; + +pub enum BedwarsCommand { + SetLobby, + SetSpawn { team: BedwarsTeam }, + SetBed { team: BedwarsTeam }, + SetShop { team: BedwarsTeam }, + SetUpgrades { team: BedwarsTeam }, + SetGenerator { team: BedwarsTeam }, + SetEnderchest { team: BedwarsTeam }, + AddGenerator { gen_type: String }, + Save, + Start, + Help, +} + +impl BedwarsCommand { + pub fn parse(args: &[String]) -> Result { + if args.is_empty() { + return Ok(Self::Help); + } + + match args[0].to_lowercase().as_str() { + "setlobby" => Ok(Self::SetLobby), + "setspawn" => { + if args.len() < 2 { + return Err("Usage: /bw setspawn ".to_string()); + } + let team = BedwarsTeam::from_str(&args[1]).ok_or("Invalid team name")?; + Ok(Self::SetSpawn { team }) + } + "setbed" => { + if args.len() < 2 { + return Err("Usage: /bw setbed ".to_string()); + } + let team = BedwarsTeam::from_str(&args[1]).ok_or("Invalid team name")?; + Ok(Self::SetBed { team }) + } + "setshop" => { + if args.len() < 2 { + return Err("Usage: /bw setshop ".to_string()); + } + let team = BedwarsTeam::from_str(&args[1]).ok_or("Invalid team name")?; + Ok(Self::SetShop { team }) + } + "setupgrades" => { + if args.len() < 2 { + return Err("Usage: /bw setupgrades ".to_string()); + } + let team = BedwarsTeam::from_str(&args[1]).ok_or("Invalid team name")?; + Ok(Self::SetUpgrades { team }) + } + "setgenerator" => { + if args.len() < 2 { + return Err("Usage: /bw setgenerator ".to_string()); + } + let team = BedwarsTeam::from_str(&args[1]).ok_or("Invalid team name")?; + Ok(Self::SetGenerator { team }) + } + "setenderchest" => { + if args.len() < 2 { + return Err("Usage: /bw setenderchest ".to_string()); + } + let team = BedwarsTeam::from_str(&args[1]).ok_or("Invalid team name")?; + Ok(Self::SetEnderchest { team }) + } + "addgenerator" => { + if args.len() < 2 { + return Err("Usage: /bw addgenerator ".to_string()); + } + let gen_type = args[1].to_lowercase(); + if gen_type != "diamond" && gen_type != "emerald" { + return Err("Generator type must be 'diamond' or 'emerald'".to_string()); + } + Ok(Self::AddGenerator { gen_type }) + } + "save" => Ok(Self::Save), + "start" => Ok(Self::Start), + "help" => Ok(Self::Help), + _ => Err("Unknown command. Type /bw help for options.".to_string()), + } + } + + pub fn execute( + &self, + config: &mut BedwarsConfig, + game_manager: &mut GameManager, + player_loc: Loc, + target_block_loc: Option, + ) -> Result { + match self { + Self::SetLobby => { + config.locations.lobby = player_loc; + Ok("Lobby location updated in config".to_string()) + } + Self::SetSpawn { team } => { + let team_name = team.get_name().to_lowercase(); + let team_conf = config.locations.teams.entry(team_name.clone()).or_insert_with(|| TeamConfig { + enabled: true, + spawn: player_loc.clone(), + bed: player_loc.clone(), + generator: player_loc.clone(), + shop: player_loc.clone(), + upgrades: player_loc.clone(), + enderchest: player_loc.clone(), + }); + team_conf.spawn = player_loc; + team_conf.enabled = true; + game_manager.enable_team(*team); + Ok(format!("Spawn for team {} set and enabled", team.get_name())) + } + Self::SetBed { team } => { + let block_loc = target_block_loc.ok_or("You must look at a bed block")?; + let team_name = team.get_name().to_lowercase(); + if let Some(t_conf) = config.locations.teams.get_mut(&team_name) { + t_conf.bed = block_loc; + Ok(format!("Bed for team {} set", team.get_name())) + } else { + Err("Team not configured yet. Set spawn first.".to_string()) + } + } + Self::SetShop { team } => { + let team_name = team.get_name().to_lowercase(); + if let Some(t_conf) = config.locations.teams.get_mut(&team_name) { + t_conf.shop = player_loc; + Ok(format!("Shop for team {} set", team.get_name())) + } else { + Err("Team not configured yet. Set spawn first.".to_string()) + } + } + Self::SetUpgrades { team } => { + let team_name = team.get_name().to_lowercase(); + if let Some(t_conf) = config.locations.teams.get_mut(&team_name) { + t_conf.upgrades = player_loc; + Ok(format!("Upgrades for team {} set", team.get_name())) + } else { + Err("Team not configured yet. Set spawn first.".to_string()) + } + } + Self::SetGenerator { team } => { + let team_name = team.get_name().to_lowercase(); + if let Some(t_conf) = config.locations.teams.get_mut(&team_name) { + t_conf.generator = player_loc; + Ok(format!("Generator for team {} set", team.get_name())) + } else { + Err("Team not configured yet. Set spawn first.".to_string()) + } + } + Self::SetEnderchest { team } => { + let team_name = team.get_name().to_lowercase(); + if let Some(t_conf) = config.locations.teams.get_mut(&team_name) { + t_conf.enderchest = player_loc; + Ok(format!("Ender Chest for team {} set", team.get_name())) + } else { + Err("Team not configured yet. Set spawn first.".to_string()) + } + } + Self::AddGenerator { gen_type } => { + config.locations.generators.push(GeneratorConfig { + x: player_loc.x, + y: player_loc.y, + z: player_loc.z, + generator_type: gen_type.to_uppercase(), + }); + Ok(format!("Added a {} generator at your location", gen_type.to_uppercase())) + } + Self::Save => { + // Return string representation to write to file + Ok("Config ready to save".to_string()) + } + Self::Start => { + game_manager.start_game()?; + Ok("Match starting now!".to_string()) + } + Self::Help => { + let mut help = String::new(); + help.push_str("§d§m================ §b§lBEDWARS SETUP (RUST) §d§m================\n"); + help.push_str("§d/bw setlobby §7- Set waiting lobby coordinate\n"); + help.push_str("§d/bw setspawn §7- Set team player spawn point\n"); + help.push_str("§d/bw setbed §7- Register team bed (look at block)\n"); + help.push_str("§d/bw setshop §7- Set shop NPC position\n"); + help.push_str("§d/bw setupgrades §7- Set upgrades NPC position\n"); + help.push_str("§d/bw setgenerator §7- Set base spawner location\n"); + help.push_str("§d/bw setenderchest §7- Set team Ender Chest\n"); + help.push_str("§d/bw addgenerator §7- Add generator\n"); + help.push_str("§d/bw save §7- Save config file\n"); + help.push_str("§d/bw start §7- Force start game immediately\n"); + help.push_str("§d§m================================================\n"); + Ok(help) + } + } + } +} diff --git a/rust/src/config.rs b/rust/src/config.rs new file mode 100644 index 0000000..24efb0e --- /dev/null +++ b/rust/src/config.rs @@ -0,0 +1,70 @@ +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct Loc { + pub x: f64, + pub y: f64, + pub z: f64, + pub yaw: Option, + pub pitch: Option, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct TeamConfig { + pub enabled: bool, + pub spawn: Loc, + pub bed: Loc, + pub generator: Loc, + pub shop: Loc, + pub upgrades: Loc, + pub enderchest: Loc, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct GeneratorConfig { + pub x: f64, + pub y: f64, + pub z: f64, + #[serde(rename = "type")] + pub generator_type: String, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct RatesConfig { + pub iron: u32, + pub gold: u32, + pub diamond: u32, + pub emerald: u32, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct LocationsConfig { + pub lobby: Loc, + pub teams: HashMap, + pub generators: Vec, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct BedwarsConfig { + #[serde(rename = "world-name")] + pub world_name: String, + #[serde(rename = "lobby-server")] + pub lobby_server: String, + #[serde(rename = "min-players")] + pub min_players: u32, + #[serde(rename = "countdown-seconds")] + pub countdown_seconds: u32, + pub rates: RatesConfig, + pub locations: LocationsConfig, +} + +impl BedwarsConfig { + pub fn load_from_str(content: &str) -> Result { + serde_yaml::from_str(content) + } + + pub fn save_to_string(&self) -> Result { + serde_yaml::to_string(self) + } +} diff --git a/rust/src/events.rs b/rust/src/events.rs new file mode 100644 index 0000000..9e2dc38 --- /dev/null +++ b/rust/src/events.rs @@ -0,0 +1,155 @@ +use crate::config::Loc; +use crate::team::BedwarsTeam; +use crate::game::GameManager; +use crate::arena::{ArenaManager, BlockPos}; +use crate::state::GameState; + +pub struct BedwarsEvents; + +impl BedwarsEvents { + pub fn handle_block_place( + arena: &mut ArenaManager, + pos: BlockPos, + block_type: &str, + is_spectator: bool, + ) -> Result, &'static str> { + if is_spectator { + return Err("Spectators cannot place blocks"); + } + + if block_type == "minecraft:tnt" { + // Return command to spawn primed TNT and bypass block placement + Ok(Some("spawn_primed_tnt".to_string())) + } else { + arena.track_block(pos); + Ok(None) + } + } + + pub fn handle_block_break( + arena: &mut ArenaManager, + game_manager: &mut GameManager, + pos: BlockPos, + block_type: &str, + breaker_uuid: &str, + ) -> Result, &'static str> { + let breaker = game_manager.players.get(breaker_uuid).ok_or("Player not found")?; + if breaker.is_spectator { + return Err("Spectators cannot break blocks"); + } + + // 1. Bed Break logic + if block_type.ends_with("_bed") { + let breaker_team = breaker.team.ok_or("You are not on a team")?; + + // Search config locations for which team's bed is broken + let mut target_team = None; + for team_state in game_manager.teams.values() { + if team_state.enabled && team_state.has_bed { + // In host adapter, we would check block coordinate equality. + // For logic module, we match block team color to block type. + let bed_mat = team_state.team.get_bed_material(); + if block_type == format!("minecraft:{}", bed_mat) { + target_team = Some(team_state.team); + break; + } + } + } + + if let Some(broken_team) = target_team { + if breaker_team == broken_team { + return Err("You cannot break your own bed!"); + } + + game_manager.set_bed_broken(broken_team); + if let Some(p) = game_manager.players.get_mut(breaker_uuid) { + p.beds_broken += 1; + } + return Ok(Some(broken_team)); + } else { + return Err("This bed block is not registered to a team"); + } + } + + // 2. Standard Block Break logic + if arena.is_player_placed(&pos) { + arena.untrack_block(&pos); + Ok(None) + } else { + Err("You can only break blocks placed by players!") + } + } + + pub fn handle_void_check( + game_manager: &mut GameManager, + uuid: &str, + y_coord: f64, + ) -> bool { + if game_manager.state != GameState::Playing { + return false; + } + + if let Some(player) = game_manager.players.get(uuid) { + if player.is_spectator { + return false; + } + + if y_coord <= -20.0 { + game_manager.handle_death(uuid); + return true; + } + } + false + } + + pub fn handle_pickup_resource( + item_type: &str, + is_generator_spawned: bool, + picker_uuid: &str, + game_manager: &GameManager, + teammate_distances: Vec<(String, f64)>, // uuid -> distance squared + ) -> Vec<(String, String, u32)> { + // Vec of (teammate_uuid, item_type, amount) to replicate + + // Critical dupe check - only replicate resource drops spawned by a generator + if !is_generator_spawned { + return Vec::new(); + } + + if item_type != "minecraft:iron_ingot" && + item_type != "minecraft:gold_ingot" && + item_type != "minecraft:diamond" && + item_type != "minecraft:emerald" { + return Vec::new(); + } + + let picker = match game_manager.players.get(picker_uuid) { + Some(p) => p, + None => return Vec::new(), + }; + + let picker_team = match picker.team { + Some(t) => t, + None => return Vec::new(), + }; + + let mut replicate_spawns = Vec::new(); + + for (other_uuid, distance_sq) in teammate_distances { + if other_uuid == picker_uuid { + continue; + } + + if let Some(other) = game_manager.players.get(&other_uuid) { + if !other.is_spectator && other.team == Some(picker_team) { + // Check if teammate is within 3.5 blocks (12.25 blocks squared) + if distance_sq <= 12.25 { + replicate_spawns.push((other_uuid, item_type.to_string(), 1)); + } + } + } + } + + replicate_spawns + } +} diff --git a/rust/src/game.rs b/rust/src/game.rs new file mode 100644 index 0000000..21a929f --- /dev/null +++ b/rust/src/game.rs @@ -0,0 +1,201 @@ +use std::collections::HashMap; +use crate::state::GameState; +use crate::team::BedwarsTeam; +use crate::generator::Generator; +use crate::arena::ArenaManager; + +#[derive(Debug, Clone)] +pub struct PlayerState { + pub uuid: String, + pub name: String, + pub team: Option, + pub is_spectator: bool, + pub kills: u32, + pub deaths: u32, + pub beds_broken: u32, + pub respawn_timer: Option, // seconds remaining +} + +#[derive(Debug, Clone)] +pub struct TeamState { + pub team: BedwarsTeam, + pub has_bed: bool, + pub enabled: bool, +} + +pub struct GameManager { + pub state: GameState, + pub players: HashMap, + pub teams: HashMap, + pub countdown: Option, + pub generators: Vec, + pub arena: ArenaManager, + pub min_players: u32, +} + +impl GameManager { + pub fn new(min_players: u32, countdown_seconds: u32) -> Self { + let mut teams = HashMap::new(); + for team in &[BedwarsTeam::Red, BedwarsTeam::Blue, BedwarsTeam::Purple, BedwarsTeam::Yellow] { + teams.insert(*team, TeamState { + team: *team, + has_bed: true, + enabled: false, + }); + } + + Self { + state: GameState::Lobby, + players: HashMap::new(), + teams, + countdown: Some(countdown_seconds), + generators: Vec::new(), + arena: ArenaManager::new(), + min_players, + } + } + + pub fn join_player(&mut self, uuid: &str, name: &str) { + let state = PlayerState { + uuid: uuid.to_string(), + name: name.to_string(), + team: None, + is_spectator: false, + kills: 0, + deaths: 0, + beds_broken: 0, + respawn_timer: None, + }; + self.players.insert(uuid.to_string(), state); + } + + pub fn quit_player(&mut self, uuid: &str) { + self.players.remove(uuid); + if self.state == GameState::Playing { + self.check_winner(); + } + } + + pub fn enable_team(&mut self, team: BedwarsTeam) { + if let Some(state) = self.teams.get_mut(&team) { + state.enabled = true; + } + } + + pub fn start_game(&mut self) -> Result<(), &'static str> { + let enabled_teams: Vec = self.teams.values() + .filter(|t| t.enabled) + .map(|t| t.team) + .collect(); + + if enabled_teams.len() < 2 { + return Err("At least two teams must be enabled to start the game"); + } + + // Assign players to enabled teams evenly + let mut team_idx = 0; + for player in self.players.values_mut() { + if player.is_spectator { + continue; + } + let team = enabled_teams[team_idx]; + player.team = Some(team); + team_idx = (team_idx + 1) % enabled_teams.len(); + } + + self.state = GameState::Playing; + self.countdown = None; + Ok(()) + } + + pub fn handle_death(&mut self, uuid: &str) { + if let Some(player) = self.players.get_mut(uuid) { + player.deaths += 1; + if let Some(team) = player.team { + let has_bed = self.teams.get(&team).map(|t| t.has_bed).unwrap_or(false); + if has_bed { + player.respawn_timer = Some(5); // 5 seconds respawn queue + } else { + player.is_spectator = true; + player.respawn_timer = None; + } + } + } + self.check_winner(); + } + + pub fn tick_game(&mut self) -> Vec<(String, String)> { + let mut events = Vec::new(); + + // Handle Lobby countdown + if self.state == GameState::Lobby { + if let Some(ref mut time) = self.countdown { + if self.players.len() as u32 >= self.min_players { + if *time > 0 { + *time -= 1; + if *time == 0 { + if let Err(e) = self.start_game() { + tracing::error!("Failed to start game: {}", e); + self.countdown = Some(10); // retry in 10s + } + } + } + } else { + // Reset countdown if player count drops below min_players + *time = 30; + } + } + } + + // Handle Playing respawn timers + if self.state == GameState::Playing { + for player in self.players.values_mut() { + if let Some(ref mut time) = player.respawn_timer { + if *time > 0 { + *time -= 1; + if *time == 0 { + player.respawn_timer = None; + events.push((player.uuid.clone(), "respawn".to_string())); + } + } + } + } + } + + events + } + + pub fn set_bed_broken(&mut self, team: BedwarsTeam) { + if let Some(state) = self.teams.get_mut(&team) { + state.has_bed = false; + } + } + + pub fn check_winner(&mut self) -> Option { + if self.state != GameState::Playing { + return None; + } + + // Find teams that still have active, non-spectator players + let mut active_teams = HashMap::new(); + for player in self.players.values() { + if !player.is_spectator { + if let Some(team) = player.team { + *active_teams.entry(team).or_insert(0) += 1; + } + } + } + + if active_teams.len() == 1 { + let winning_team = *active_teams.keys().next().unwrap(); + self.state = GameState::Ending; + Some(winning_team) + } else if active_teams.is_empty() { + // Tie or no players left + self.state = GameState::Ending; + None + } else { + None + } + } +} diff --git a/rust/src/generator.rs b/rust/src/generator.rs new file mode 100644 index 0000000..f1f4d2d --- /dev/null +++ b/rust/src/generator.rs @@ -0,0 +1,79 @@ +use crate::config::Loc; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum GeneratorType { + IronGold, + Diamond, + Emerald, +} + +pub struct Generator { + pub location: Loc, + pub gen_type: GeneratorType, + pub ticks_until_spawn: i32, + pub max_spawn_ticks: i32, + pub upgrade_level: u32, +} + +impl Generator { + pub fn new(location: Loc, gen_type: GeneratorType, max_spawn_ticks: i32) -> Self { + Self { + location, + gen_type, + ticks_until_spawn: max_spawn_ticks, + max_spawn_ticks, + upgrade_level: 1, + } + } + + pub fn tick(&mut self, random_roll: f64) -> Option> { + self.ticks_until_spawn -= 1; + if self.ticks_until_spawn <= 0 { + let mut spawned = Vec::new(); + match self.gen_type { + GeneratorType::IronGold => { + spawned.push("minecraft:iron_ingot"); + // Gold at 1/4 rate, modified by upgrade + if random_roll < (0.25 * self.upgrade_level as f64) { + spawned.push("minecraft:gold_ingot"); + } + self.ticks_until_spawn = std::cmp::max(5, self.max_spawn_ticks - (self.upgrade_level as i32 - 1) * 3); + } + GeneratorType::Diamond => { + spawned.push("minecraft:diamond"); + self.ticks_until_spawn = self.max_spawn_ticks; + } + GeneratorType::Emerald => { + spawned.push("minecraft:emerald"); + self.ticks_until_spawn = self.max_spawn_ticks; + } + } + Some(spawned) + } else { + None + } + } +} + +pub struct GeneratorManager { + pub generators: Vec, +} + +impl GeneratorManager { + pub fn new() -> Self { + Self { + generators: Vec::new(), + } + } + + pub fn tick_all(&mut self) -> Vec<(Loc, Vec<&'static str>)> { + let mut spawns = Vec::new(); + for gen in &mut self.generators { + let roll = rand::random::(); // Bypassed in tests or run directly + if let Some(items) = gen.tick(roll) { + spawns.push((gen.location.clone(), items)); + } + } + spawns + } +} diff --git a/rust/src/lib.rs b/rust/src/lib.rs new file mode 100644 index 0000000..8182e10 --- /dev/null +++ b/rust/src/lib.rs @@ -0,0 +1,54 @@ +pub mod config; +pub mod team; +pub mod state; +pub mod generator; +pub mod arena; +pub mod shop; +pub mod game; +pub mod commands; +pub mod events; + +use pumpkin_plugin_api::{Context, Plugin, PluginMetadata}; +use tracing::info; + +pub struct BedwarsPlugin { + pub game_manager: Option, + pub config: Option, +} + +impl Plugin for BedwarsPlugin { + fn new() -> Self { + Self { + game_manager: None, + config: None, + } + } + + fn metadata(&self) -> PluginMetadata { + PluginMetadata { + name: "Bedwars Rust".into(), + version: env!("CARGO_PKG_VERSION").into(), + authors: vec!["Antigravity".into()], + description: "Rewrite of Bedwars plugin in Rust for Pumpkin MC".into(), + dependencies: vec![], + permissions: vec![], + } + } + + fn on_load(&mut self, _context: Context) -> pumpkin_plugin_api::Result<()> { + info!("Bedwars Rust Plugin has been loaded successfully!"); + + // Initialize default GameManager (requires min 2 players, 30 seconds lobby time) + let game_manager = game::GameManager::new(2, 30); + self.game_manager = Some(game_manager); + + Ok(()) + } + + fn on_unload(&mut self, _context: Context) -> pumpkin_plugin_api::Result<()> { + info!("Bedwars Rust Plugin has been unloaded. Goodbye!"); + Ok(()) + } +} + +pumpkin_plugin_api::register_plugin!(BedwarsPlugin); diff --git a/rust/src/shop.rs b/rust/src/shop.rs new file mode 100644 index 0000000..b8ce6b8 --- /dev/null +++ b/rust/src/shop.rs @@ -0,0 +1,118 @@ +use std::collections::HashMap; + +#[derive(Debug, Clone)] +pub struct ShopItem { + pub material: String, + pub cost_material: String, + pub cost_amount: u32, + pub display_name: String, + pub give_amount: u32, +} + +impl ShopItem { + pub fn parse(config_str: &str) -> Option { + // Example format: WOOL:IRON:4:&fWhite Wool:16 + let parts: Vec<&str> = config_str.split(':').collect(); + if parts.len() < 4 { + return None; + } + let raw_mat = parts[0].to_uppercase(); + let material = format!("minecraft:{}", raw_mat.to_lowercase()); + + let cost_type = parts[1].to_uppercase(); + let cost_material = match cost_type.as_str() { + "IRON" => "minecraft:iron_ingot", + "GOLD" => "minecraft:gold_ingot", + "DIAMOND" => "minecraft:diamond", + "EMERALD" => "minecraft:emerald", + _ => "minecraft:iron_ingot", + }.to_string(); + + let cost_amount = parts[2].parse::().ok()?; + let display_name = parts[3].to_string(); + + let give_amount = if parts.len() >= 5 { + parts[4].parse::().unwrap_or(1) + } else { + 1 + }; + + Some(Self { + material, + cost_material, + cost_amount, + display_name, + give_amount, + }) + } +} + +pub struct PlayerInventory { + pub items: HashMap, +} + +impl PlayerInventory { + pub fn new() -> Self { + Self { + items: HashMap::new(), + } + } + + pub fn get_item_count(&self, item_id: &str) -> u32 { + *self.items.get(item_id).unwrap_or(&0) + } + + pub fn take_items(&mut self, item_id: &str, amount: u32) -> bool { + let current = self.get_item_count(item_id); + if current >= amount { + if current == amount { + self.items.remove(item_id); + } else { + self.items.insert(item_id.to_string(), current - amount); + } + true + } else { + false + } + } + + pub fn give_items(&mut self, item_id: &str, amount: u32) { + let current = self.get_item_count(item_id); + self.items.insert(item_id.to_string(), current + amount); + } +} + +pub struct ShopManager { + pub categories: HashMap>, +} + +impl ShopManager { + pub fn new() -> Self { + Self { + categories: HashMap::new(), + } + } + + pub fn add_item(&mut self, category: &str, item_str: &str) { + if let Some(item) = ShopItem::parse(item_str) { + self.categories.entry(category.to_string()).or_default().push(item); + } + } + + pub fn attempt_purchase( + &self, + inventory: &mut PlayerInventory, + category: &str, + item_index: usize, + ) -> Result { + let items = self.categories.get(category).ok_or("Category not found")?; + let item = items.get(item_index).ok_or("Item not found")?; + + if inventory.take_items(&item.cost_material, item.cost_amount) { + inventory.give_items(&item.material, item.give_amount); + Ok(item.clone()) + } else { + Err("Insufficient funds") + } + } +} diff --git a/rust/src/state.rs b/rust/src/state.rs new file mode 100644 index 0000000..ba01f3c --- /dev/null +++ b/rust/src/state.rs @@ -0,0 +1,6 @@ +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum GameState { + Lobby, + Playing, + Ending, +} diff --git a/rust/src/team.rs b/rust/src/team.rs new file mode 100644 index 0000000..3de7d3d --- /dev/null +++ b/rust/src/team.rs @@ -0,0 +1,70 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum BedwarsTeam { + Red, + Blue, + Purple, + Yellow, +} + +impl BedwarsTeam { + pub fn from_str(s: &str) -> Option { + match s.to_lowercase().as_str() { + "red" => Some(Self::Red), + "blue" => Some(Self::Blue), + "purple" => Some(Self::Purple), + "yellow" => Some(Self::Yellow), + _ => None, + } + } + + pub fn get_name(&self) -> &'static str { + match self { + Self::Red => "Red", + Self::Blue => "Blue", + Self::Purple => "Purple", + Self::Yellow => "Yellow", + } + } + + pub fn get_color_code(&self) -> &'static str { + match self { + Self::Red => "§c", + Self::Blue => "§9", + Self::Purple => "§5", + Self::Yellow => "§e", + } + } + + pub fn get_prefix(&self) -> &'static str { + match self { + Self::Red => "§c[RED] ", + Self::Blue => "§9[BLUE] ", + Self::Purple => "§5[PURPLE] ", + Self::Yellow => "§e[YELLOW] ", + } + } + + pub fn get_wool_material(&self) -> &'static str { + match self { + Self::Red => "red_wool", + Self::Blue => "blue_wool", + Self::Purple => "purple_wool", + Self::Yellow => "yellow_wool", + } + } + + pub fn get_bed_material(&self) -> &'static str { + match self { + Self::Red => "red_bed", + Self::Blue => "blue_bed", + Self::Purple => "purple_bed", + Self::Yellow => "yellow_bed", + } + } + + pub fn get_colorized_name(&self) -> String { + format!("{}{}", self.get_color_code(), self.get_name()) + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..59bad24 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "bedwars" diff --git a/src/main/java/com/bedwars/ArenaManager.java b/src/main/java/com/bedwars/ArenaManager.java new file mode 100644 index 0000000..c9da9bb --- /dev/null +++ b/src/main/java/com/bedwars/ArenaManager.java @@ -0,0 +1,381 @@ +package com.bedwars; + +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.WorldCreator; +import org.bukkit.block.Block; +import org.bukkit.entity.Player; + +import java.io.*; +import java.nio.file.Files; +import java.util.HashSet; +import java.util.Set; + +public class ArenaManager { + + private final BedwarsPlugin plugin; + private final Set playerPlacedBlocks = new HashSet<>(); + private final java.util.Map savedBedStates = new java.util.HashMap<>(); + + public ArenaManager(BedwarsPlugin plugin) { + this.plugin = plugin; + } + + private File getTemplateFolder(String worldName) { + if (worldName.equalsIgnoreCase("world")) { + return new File(Bukkit.getWorldContainer(), "bedwars"); + } else { + File customTemplate = new File(Bukkit.getWorldContainer(), worldName + "_template"); + if (customTemplate.exists()) { + return customTemplate; + } + File bedwarsFolder = new File(Bukkit.getWorldContainer(), "bedwars"); + if (bedwarsFolder.exists()) { + return bedwarsFolder; + } + File fallbackTemplate = new File(Bukkit.getWorldContainer(), "bedwars_template"); + if (fallbackTemplate.exists()) { + return fallbackTemplate; + } + return new File(Bukkit.getWorldContainer(), "bedwars"); + } + } + + private File getWorldFolder(String worldName) { + return new File(Bukkit.getWorldContainer(), worldName); + } + + /** + * Startup file copying logic (called in plugin onLoad() before Bukkit loads worlds). + * Automatically copies bedwars template directory to active world folder. + */ + public void onLoadCopy() { + // Manually load config.yml to get world-name early on Bukkit onLoad() + File configFile = new File(plugin.getDataFolder(), "config.yml"); + String worldName = "world"; + if (configFile.exists()) { + org.bukkit.configuration.file.YamlConfiguration config = org.bukkit.configuration.file.YamlConfiguration.loadConfiguration(configFile); + worldName = config.getString("world-name", "world"); + } else { + worldName = plugin.getConfig().getString("world-name", "world"); + } + + File templateFolder = getTemplateFolder(worldName); + File worldFolder = getWorldFolder(worldName); + + if (templateFolder.equals(worldFolder)) return; + + if (templateFolder.exists()) { + Bukkit.getLogger().info("[Bedwars] onLoad: Overwriting world '" + worldName + "' from template '" + templateFolder.getName() + "'..."); + deleteDirectory(worldFolder); + try { + copyDirectory(templateFolder, worldFolder); + Bukkit.getLogger().info("[Bedwars] onLoad: Successfully copied '" + templateFolder.getName() + "' template to '" + worldName + "'!"); + } catch (IOException e) { + Bukkit.getLogger().severe("[Bedwars] onLoad: Failed to copy template: " + e.getMessage()); + } + } else { + // First time start: if world exists but template doesn't, initialize template! + if (worldFolder.exists()) { + Bukkit.getLogger().info("[Bedwars] onLoad: Template '" + templateFolder.getName() + "' not found. Creating backup of '" + worldName + "' as template '" + templateFolder.getName() + "'..."); + try { + copyDirectory(worldFolder, templateFolder); + Bukkit.getLogger().info("[Bedwars] onLoad: Successfully initialized template '" + templateFolder.getName() + "'!"); + } catch (IOException e) { + Bukkit.getLogger().severe("[Bedwars] onLoad: Failed to create template: " + e.getMessage()); + } + } + } + } + + /** + * Saves the active game world back to the bedwars template directory (called on /bw save). + */ + public void setupTemplate() { + String worldName = plugin.getConfig().getString("world-name", "world"); + File worldFolder = getWorldFolder(worldName); + File templateFolder = getTemplateFolder(worldName); + + plugin.getLogger().info("Saving active world '" + worldName + "' to template '" + templateFolder.getName() + "'..."); + + // Save the active world first to ensure blocks are saved to disk + World world = Bukkit.getWorld(worldName); + if (world != null) { + world.save(); + } + + try { + // Overwrite template folder + deleteDirectory(templateFolder); + copyDirectory(worldFolder, templateFolder); + plugin.getLogger().info("Successfully updated '" + templateFolder.getName() + "' template with setup changes!"); + } catch (IOException e) { + plugin.getLogger().severe("Failed to save changes to template: " + e.getMessage()); + } + } + + public void saveBedStates() { + savedBedStates.clear(); + String worldName = plugin.getConfig().getString("world-name", "world"); + World world = Bukkit.getWorld(worldName); + if (world == null) return; + + for (BedwarsTeam team : BedwarsTeam.values()) { + String path = "locations.teams." + team.getName().toLowerCase() + ".bed"; + if (plugin.getConfig().contains(path)) { + double x = plugin.getConfig().getDouble(path + ".x"); + double y = plugin.getConfig().getDouble(path + ".y"); + double z = plugin.getConfig().getDouble(path + ".z"); + Location loc = new Location(world, x, y, z); + + // Save this block and its surrounding blocks if they are beds + saveBedBlock(loc.getBlock()); + saveBedBlock(loc.clone().add(1, 0, 0).getBlock()); + saveBedBlock(loc.clone().add(-1, 0, 0).getBlock()); + saveBedBlock(loc.clone().add(0, 0, 1).getBlock()); + saveBedBlock(loc.clone().add(0, 0, -1).getBlock()); + } + } + } + + private void saveBedBlock(Block block) { + if (block.getType().name().contains("_BED")) { + savedBedStates.put(block.getLocation(), block.getBlockData().clone()); + } + } + + public void rollbackBlocks() { + String worldName = plugin.getConfig().getString("world-name", "world"); + World world = Bukkit.getWorld(worldName); + if (world == null) { + plugin.getLogger().warning("Could not execute rollbackBlocks: World '" + worldName + "' is not loaded."); + playerPlacedBlocks.clear(); + return; + } + + // 1. Clear player-placed blocks + plugin.getLogger().info("Clearing " + playerPlacedBlocks.size() + " player-placed blocks..."); + for (Location loc : playerPlacedBlocks) { + loc.setWorld(world); + loc.getBlock().setType(org.bukkit.Material.AIR); + } + playerPlacedBlocks.clear(); + + // 2. Restore bed states + plugin.getLogger().info("Restoring " + savedBedStates.size() + " bed blocks..."); + for (java.util.Map.Entry entry : savedBedStates.entrySet()) { + Location loc = entry.getKey(); + loc.setWorld(world); + Block block = loc.getBlock(); + block.setBlockData(entry.getValue()); + } + } + + /** + * Resets the game world back to the original bedwars template. + */ + public void resetWorld() { + String worldName = plugin.getConfig().getString("world-name", "world"); + World world = Bukkit.getWorld(worldName); + + plugin.getLogger().info("Resetting world '" + worldName + "' from template '" + getTemplateFolder(worldName).getName() + "'..."); + + // Always execute in-memory rollback as primary/reliable mechanism! + rollbackBlocks(); + + // 1. Kick players to lobby proxy server + boolean unloaded = false; + if (world != null) { + for (Player player : world.getPlayers()) { + plugin.getGameManager().sendToLobby(player); + } + // Unload the world (only works for non-default worlds once players leave) + unloaded = Bukkit.unloadWorld(world, false); + } + + if (unloaded) { + // 2. Overwrite the world folder from bedwars template + File worldFolder = getWorldFolder(worldName); + File templateFolder = getTemplateFolder(worldName); + + if (templateFolder.exists()) { + deleteDirectory(worldFolder); + try { + copyDirectory(templateFolder, worldFolder); + plugin.getLogger().info("Successfully restored world '" + worldName + "' from template '" + templateFolder.getName() + "'!"); + } catch (IOException e) { + plugin.getLogger().severe("Failed to copy bedwars template: " + e.getMessage()); + } + } else { + plugin.getLogger().warning("Template folder '" + templateFolder.getName() + "' not found! Cannot reset world."); + } + + // 3. Reload the world + World w = Bukkit.createWorld(new WorldCreator(worldName)); + if (w != null) { + w.setGameRule(org.bukkit.GameRule.ANNOUNCE_ADVANCEMENTS, false); + } + plugin.getLogger().info("World '" + worldName + "' loaded and ready!"); + } else { + plugin.getLogger().info("World '" + worldName + "' remained loaded. In-memory programmatic block rollback completed successfully!"); + } + + // 4. Regenerate waiting area lobby glass cage + createWaitingLobby(); + } + + public void clearAllMobs() { + String worldName = plugin.getConfig().getString("world-name", "world"); + World world = Bukkit.getWorld(worldName); + if (world != null) { + int count = 0; + for (org.bukkit.entity.Entity entity : world.getEntities()) { + if (entity instanceof org.bukkit.entity.Mob || entity instanceof org.bukkit.entity.Ambient) { + if (entity.getType() == org.bukkit.entity.EntityType.VILLAGER) { + continue; + } + entity.remove(); + count++; + } + } + plugin.getLogger().info("Purged " + count + " pre-existing mobs from world '" + worldName + "'!"); + } + } + + private final Set waitingLobbyBlocks = new HashSet<>(); + + /** + * Spawns a floating glass cage around the waiting lobby spawnpoint. + */ + public void createWaitingLobby() { + waitingLobbyBlocks.clear(); + clearAllMobs(); + + String worldName = plugin.getConfig().getString("locations.lobby.world", "world"); + World world = Bukkit.getWorld(worldName); + if (world == null) return; + + double lx = plugin.getConfig().getDouble("locations.lobby.x", 129.5); + double ly = plugin.getConfig().getDouble("locations.lobby.y", 90.0); + double lz = plugin.getConfig().getDouble("locations.lobby.z", 149.5); + Location center = new Location(world, lx, ly, lz); + + int cx = center.getBlockX(); + int cy = center.getBlockY(); + int cz = center.getBlockZ(); + + plugin.getLogger().info("Generating waiting lobby cage at: " + center); + + // Generate 7x7 platform at floor (cy - 1) and ceiling (cy + 3) + for (int x = cx - 3; x <= cx + 3; x++) { + for (int z = cz - 3; z <= cz + 3; z++) { + // Floor + Block floor = world.getBlockAt(x, cy - 1, z); + floor.setType(org.bukkit.Material.GLASS); + waitingLobbyBlocks.add(floor.getLocation()); + + // Ceiling + Block ceiling = world.getBlockAt(x, cy + 3, z); + ceiling.setType(org.bukkit.Material.GLASS); + waitingLobbyBlocks.add(ceiling.getLocation()); + } + } + + // Walls + for (int y = cy; y < cy + 3; y++) { + for (int x = cx - 3; x <= cx + 3; x++) { + Block b1 = world.getBlockAt(x, y, cz - 3); + Block b2 = world.getBlockAt(x, y, cz + 3); + b1.setType(org.bukkit.Material.GLASS); + b2.setType(org.bukkit.Material.GLASS); + waitingLobbyBlocks.add(b1.getLocation()); + waitingLobbyBlocks.add(b2.getLocation()); + } + for (int z = cz - 3; z <= cz + 3; z++) { + Block b1 = world.getBlockAt(cx - 3, y, z); + Block b2 = world.getBlockAt(cx + 3, y, z); + b1.setType(org.bukkit.Material.GLASS); + b2.setType(org.bukkit.Material.GLASS); + waitingLobbyBlocks.add(b1.getLocation()); + waitingLobbyBlocks.add(b2.getLocation()); + } + } + } + + /** + * Wipes the glass waiting lobby platform when the game starts. + */ + public void removeWaitingLobby() { + if (waitingLobbyBlocks.isEmpty()) return; + + plugin.getLogger().info("Removing waiting lobby cage block by block..."); + for (Location loc : waitingLobbyBlocks) { + loc.getBlock().setType(org.bukkit.Material.AIR); + } + waitingLobbyBlocks.clear(); + } + + public void trackBlockPlace(Block block) { + playerPlacedBlocks.add(block.getLocation()); + } + + public boolean isPlayerPlacedBlock(Block block) { + return playerPlacedBlocks.contains(block.getLocation()); + } + + public void removeTrackedBlock(Block block) { + playerPlacedBlocks.remove(block.getLocation()); + } + + // Helper method to delete a directory recursively + private void deleteDirectory(File path) { + if (path.exists()) { + File[] files = path.listFiles(); + if (files != null) { + for (File file : files) { + if (file.isDirectory()) { + deleteDirectory(file); + } else { + // Skip session.lock or uid.dat if JVM locks them + if (file.getName().equalsIgnoreCase("session.lock") || file.getName().equalsIgnoreCase("uid.dat")) { + continue; + } + file.delete(); + } + } + } + path.delete(); + } + } + + // Helper method to copy a directory recursively + private void copyDirectory(File source, File destination) throws IOException { + if (source.isDirectory()) { + if (!destination.exists()) { + destination.mkdirs(); + } + + String[] files = source.list(); + if (files != null) { + for (String file : files) { + if (file.equalsIgnoreCase("session.lock") || file.equalsIgnoreCase("uid.dat")) { + continue; // Skip active lock files + } + File srcFile = new File(source, file); + File destFile = new File(destination, file); + copyDirectory(srcFile, destFile); + } + } + } else { + try (InputStream in = new FileInputStream(source); + OutputStream out = new FileOutputStream(destination)) { + byte[] buffer = new byte[1024]; + int length; + while ((length = in.read(buffer)) > 0) { + out.write(buffer, 0, length); + } + } + } + } +} diff --git a/src/main/java/com/bedwars/BedwarsCommand.java b/src/main/java/com/bedwars/BedwarsCommand.java new file mode 100644 index 0000000..40e0b80 --- /dev/null +++ b/src/main/java/com/bedwars/BedwarsCommand.java @@ -0,0 +1,304 @@ +package com.bedwars; + +import org.bukkit.Location; +import org.bukkit.block.Block; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.command.TabCompleter; +import org.bukkit.entity.Player; + +import java.util.*; + +public class BedwarsCommand implements CommandExecutor, TabCompleter { + + private final BedwarsPlugin plugin; + + public BedwarsCommand(BedwarsPlugin plugin) { + this.plugin = plugin; + } + + @Override + public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { + if (!(sender instanceof Player player)) { + sender.sendMessage("§cOnly players can execute setup commands!"); + return true; + } + + if (!player.hasPermission("bedwars.admin")) { + player.sendMessage("§cYou do not have permission to configure Bedwars!"); + return true; + } + + if (args.length == 0 || args[0].equalsIgnoreCase("help")) { + sendHelp(player); + return true; + } + + String sub = args[0].toLowerCase(); + Location loc = player.getLocation(); + + switch (sub) { + case "setlobby" -> { + float yaw = loc.getYaw(); + float centeredYaw = Math.round(yaw / 90.0f) * 90.0f; + plugin.getConfig().set("locations.lobby.world", loc.getWorld().getName()); + plugin.getConfig().set("locations.lobby.x", loc.getBlockX() + 0.5); + plugin.getConfig().set("locations.lobby.y", (double) loc.getBlockY()); + plugin.getConfig().set("locations.lobby.z", loc.getBlockZ() + 0.5); + plugin.getConfig().set("locations.lobby.yaw", (double) centeredYaw); + plugin.getConfig().set("locations.lobby.pitch", 0.0); + plugin.saveConfig(); + player.sendMessage("§a[Bedwars] Waiting lobby spawn point set to your location and saved!"); + } + case "setspawn" -> { + if (args.length < 2) { + player.sendMessage("§cUsage: /bw setspawn "); + return true; + } + String team = args[1].toLowerCase(); + if (!isValidTeam(team)) { + player.sendMessage("§cInvalid team! Use red, blue, purple, or yellow."); + return true; + } + float yaw = loc.getYaw(); + float centeredYaw = Math.round(yaw / 90.0f) * 90.0f; + plugin.getConfig().set("locations.teams." + team + ".spawn.x", loc.getBlockX() + 0.5); + plugin.getConfig().set("locations.teams." + team + ".spawn.y", (double) loc.getBlockY()); + plugin.getConfig().set("locations.teams." + team + ".spawn.z", loc.getBlockZ() + 0.5); + plugin.getConfig().set("locations.teams." + team + ".spawn.yaw", (double) centeredYaw); + plugin.getConfig().set("locations.teams." + team + ".spawn.pitch", 0.0); + plugin.getConfig().set("locations.teams." + team + ".enabled", true); + player.sendMessage("§a[Bedwars] Spawn point for team " + team.toUpperCase() + " set and team ENABLED!"); + + // Scanning and auto-setting the matching color Bed block within a 10x5x10 boundary box + Location spawnLoc = loc.clone(); + org.bukkit.Material bedMat = BedwarsTeam.valueOf(team.toUpperCase()).getBedMaterial(); + Location foundBed = null; + + searchLoop: + for (int x = -10; x <= 10; x++) { + for (int y = -3; y <= 5; y++) { + for (int z = -10; z <= 10; z++) { + Block b = spawnLoc.clone().add(x, y, z).getBlock(); + if (b.getType() == bedMat) { + foundBed = b.getLocation(); + break searchLoop; + } + } + } + } + + if (foundBed != null) { + plugin.getConfig().set("locations.teams." + team + ".bed.x", foundBed.getX()); + plugin.getConfig().set("locations.teams." + team + ".bed.y", foundBed.getY()); + plugin.getConfig().set("locations.teams." + team + ".bed.z", foundBed.getZ()); + player.sendMessage("§a[Bedwars] Auto-detected matching " + team.toUpperCase() + " bed at " + foundBed.getBlockX() + ", " + foundBed.getBlockY() + ", " + foundBed.getBlockZ() + "!"); + } else { + player.sendMessage("§e[Bedwars] Warning: Matching " + team.toUpperCase() + " bed not found nearby spawn. Remember to set it manually if needed!"); + } + plugin.saveConfig(); + } + case "setbed" -> { + if (args.length < 2) { + player.sendMessage("§cUsage: /bw setbed "); + return true; + } + String team = args[1].toLowerCase(); + if (!isValidTeam(team)) { + player.sendMessage("§cInvalid team! Use red, blue, purple, or yellow."); + return true; + } + + Block target = player.getTargetBlockExact(5); + if (target == null || !target.getType().name().contains("_BED")) { + player.sendMessage("§cYou must be looking at a Bed block within 5 blocks!"); + return true; + } + + Location bedLoc = target.getLocation(); + plugin.getConfig().set("locations.teams." + team + ".bed.x", bedLoc.getX()); + plugin.getConfig().set("locations.teams." + team + ".bed.y", bedLoc.getY()); + plugin.getConfig().set("locations.teams." + team + ".bed.z", bedLoc.getZ()); + plugin.saveConfig(); + player.sendMessage("§a[Bedwars] Bed block for team " + team.toUpperCase() + " registered and saved!"); + } + case "setshop" -> { + if (args.length < 2) { + player.sendMessage("§cUsage: /bw setshop "); + return true; + } + String team = args[1].toLowerCase(); + if (!isValidTeam(team)) { + player.sendMessage("§cInvalid team! Use red, blue, purple, or yellow."); + return true; + } + float yaw = loc.getYaw(); + float centeredYaw = Math.round(yaw / 90.0f) * 90.0f; + plugin.getConfig().set("locations.teams." + team + ".shop.x", loc.getBlockX() + 0.5); + plugin.getConfig().set("locations.teams." + team + ".shop.y", (double) loc.getBlockY()); + plugin.getConfig().set("locations.teams." + team + ".shop.z", loc.getBlockZ() + 0.5); + plugin.getConfig().set("locations.teams." + team + ".shop.yaw", (double) centeredYaw); + plugin.getConfig().set("locations.teams." + team + ".shop.pitch", 0.0); + plugin.saveConfig(); + player.sendMessage("§a[Bedwars] Shop NPC location for team " + team.toUpperCase() + " set and saved!"); + } + case "setupgrades" -> { + if (args.length < 2) { + player.sendMessage("§cUsage: /bw setupgrades "); + return true; + } + String team = args[1].toLowerCase(); + if (!isValidTeam(team)) { + player.sendMessage("§cInvalid team! Use red, blue, purple, or yellow."); + return true; + } + float yaw = loc.getYaw(); + float centeredYaw = Math.round(yaw / 90.0f) * 90.0f; + plugin.getConfig().set("locations.teams." + team + ".upgrades.x", loc.getBlockX() + 0.5); + plugin.getConfig().set("locations.teams." + team + ".upgrades.y", (double) loc.getBlockY()); + plugin.getConfig().set("locations.teams." + team + ".upgrades.z", loc.getBlockZ() + 0.5); + plugin.getConfig().set("locations.teams." + team + ".upgrades.yaw", (double) centeredYaw); + plugin.getConfig().set("locations.teams." + team + ".upgrades.pitch", 0.0); + plugin.saveConfig(); + player.sendMessage("§a[Bedwars] Team Upgrades NPC location for team " + team.toUpperCase() + " set and saved!"); + } + case "setgenerator" -> { + if (args.length < 2) { + player.sendMessage("§cUsage: /bw setgenerator "); + return true; + } + String team = args[1].toLowerCase(); + if (!isValidTeam(team)) { + player.sendMessage("§cInvalid team! Use red, blue, purple, or yellow."); + return true; + } + plugin.getConfig().set("locations.teams." + team + ".generator.x", loc.getBlockX() + 0.5); + plugin.getConfig().set("locations.teams." + team + ".generator.y", (double) loc.getBlockY()); + plugin.getConfig().set("locations.teams." + team + ".generator.z", loc.getBlockZ() + 0.5); + plugin.saveConfig(); + player.sendMessage("§a[Bedwars] Base spawner (Iron/Gold) location centered and saved for team " + team.toUpperCase() + "!"); + } + case "setenderchest" -> { + if (args.length < 2) { + player.sendMessage("§cUsage: /bw setenderchest "); + return true; + } + String team = args[1].toLowerCase(); + if (!isValidTeam(team)) { + player.sendMessage("§cInvalid team! Use red, blue, purple, or yellow."); + return true; + } + Block b = loc.getBlock(); + b.setType(org.bukkit.Material.ENDER_CHEST); + + plugin.getConfig().set("locations.teams." + team + ".enderchest.x", (double) b.getX()); + plugin.getConfig().set("locations.teams." + team + ".enderchest.y", (double) b.getY()); + plugin.getConfig().set("locations.teams." + team + ".enderchest.z", (double) b.getZ()); + plugin.saveConfig(); + player.sendMessage("§a[Bedwars] Ender Chest for team " + team.toUpperCase() + " placed and saved!"); + } + case "addgenerator" -> { + if (args.length < 2) { + player.sendMessage("§cUsage: /bw addgenerator "); + return true; + } + String type = args[1].toLowerCase(); + if (!type.equals("diamond") && !type.equals("emerald")) { + player.sendMessage("§cInvalid spawner type! Use diamond or emerald."); + return true; + } + + List> genList = (List>) plugin.getConfig().get("locations.generators"); + if (genList == null) { + genList = new ArrayList<>(); + } + + Map newGen = new HashMap<>(); + newGen.put("type", type.toUpperCase()); + newGen.put("x", loc.getBlockX() + 0.5); + newGen.put("y", (double) loc.getBlockY()); + newGen.put("z", loc.getBlockZ() + 0.5); + + genList.add(newGen); + plugin.getConfig().set("locations.generators", genList); + plugin.saveConfig(); + player.sendMessage("§a[Bedwars] Added new " + type.toUpperCase() + " generator centered to block coordinates and saved!"); + } + case "save" -> { + plugin.saveConfig(); + player.sendMessage("§a[Bedwars] Configuration saved to config.yml!"); + player.sendMessage("§a[Bedwars] Preparing world backup copy as " + loc.getWorld().getName() + "_template..."); + plugin.getArenaManager().setupTemplate(); + } + case "start" -> { + if (plugin.getGameManager().getState() != GameState.LOBBY) { + player.sendMessage("§cThe game has already started!"); + return true; + } + plugin.getGameManager().startGame(); + player.sendMessage("§a[Bedwars] Force starting match!"); + } + default -> player.sendMessage("§cUnknown subcommand. Use /bw to see setup options."); + } + return true; + } + + private boolean isValidTeam(String team) { + return team.equals("red") || team.equals("blue") || team.equals("purple") || team.equals("yellow"); + } + + private void sendHelp(Player player) { + player.sendMessage("§d§m================ §b§lBEDWARS SETUP §d§m================"); + player.sendMessage("§d/bw setlobby §7- Set waiting lobby coordinate"); + player.sendMessage("§d/bw setspawn §7- Set team player spawn point"); + player.sendMessage("§d/bw setbed §7- Register team bed (look at bed block)"); + player.sendMessage("§d/bw setshop §7- Set shop villager position"); + player.sendMessage("§d/bw setupgrades §7- Set upgrade villager position"); + player.sendMessage("§d/bw setgenerator §7- Set base Iron/Gold spawner"); + player.sendMessage("§d/bw setenderchest §7- Place/Set team Ender Chest"); + player.sendMessage("§d/bw addgenerator §7- Add Diamond/Emerald generator"); + player.sendMessage("§d/bw save §7- Save config & back up current world as template"); + player.sendMessage("§d/bw start §7- Force start the game immediately"); + player.sendMessage("§d§m================================================"); + } + + @Override + public List onTabComplete(CommandSender sender, Command command, String alias, String[] args) { + if (args.length == 1) { + List subs = Arrays.asList("setlobby", "setspawn", "setbed", "setshop", "setupgrades", "setgenerator", "setenderchest", "addgenerator", "save", "start", "help"); + List list = new ArrayList<>(); + for (String s : subs) { + if (s.startsWith(args[0].toLowerCase())) { + list.add(s); + } + } + return list; + } + + if (args.length == 2) { + String sub = args[0].toLowerCase(); + if (sub.equals("setspawn") || sub.equals("setbed") || sub.equals("setshop") || sub.equals("setupgrades") || sub.equals("setgenerator") || sub.equals("setenderchest")) { + List teams = Arrays.asList("red", "blue", "purple", "yellow"); + List list = new ArrayList<>(); + for (String t : teams) { + if (t.startsWith(args[1].toLowerCase())) { + list.add(t); + } + } + return list; + } + if (sub.equals("addgenerator")) { + List types = Arrays.asList("diamond", "emerald"); + List list = new ArrayList<>(); + for (String t : types) { + if (t.startsWith(args[1].toLowerCase())) { + list.add(t); + } + } + return list; + } + } + return Collections.emptyList(); + } +} diff --git a/src/main/java/com/bedwars/BedwarsPlugin.java b/src/main/java/com/bedwars/BedwarsPlugin.java new file mode 100644 index 0000000..287a94c --- /dev/null +++ b/src/main/java/com/bedwars/BedwarsPlugin.java @@ -0,0 +1,132 @@ +package com.bedwars; + +import org.bukkit.Bukkit; +import org.bukkit.command.PluginCommand; +import org.bukkit.plugin.java.JavaPlugin; + +public class BedwarsPlugin extends JavaPlugin { + + private static BedwarsPlugin instance; + + // Managers + private GameManager gameManager; + private ArenaManager arenaManager; + private GeneratorManager generatorManager; + private ShopManager shopManager; + private UpgradesManager upgradesManager; + private ScoreboardManager scoreboardManager; + + @Override + public void onLoad() { + instance = this; + // Reset active world from pristine template BEFORE Bukkit loads the world + new ArenaManager(this).onLoadCopy(); + } + + @Override + public void onEnable() { + instance = this; + + // Save default config + saveDefaultConfig(); + + // Disable advancements gamerule for all loaded worlds at startup + for (org.bukkit.World world : Bukkit.getWorlds()) { + world.setGameRule(org.bukkit.GameRule.ANNOUNCE_ADVANCEMENTS, false); + } + + // Instantiate Managers + this.arenaManager = new ArenaManager(this); + this.gameManager = new GameManager(this); + this.generatorManager = new GeneratorManager(this); + this.shopManager = new ShopManager(this); + this.upgradesManager = new UpgradesManager(this); + this.scoreboardManager = new ScoreboardManager(this); + + // Register outgoing plugin messaging channels for Velocity Proxy redirection compatibility + getServer().getMessenger().registerOutgoingPluginChannel(this, "BungeeCord"); + getServer().getMessenger().registerOutgoingPluginChannel(this, "bungeecord:main"); + getServer().getMessenger().registerOutgoingPluginChannel(this, "nexoria:main"); + + // Load the active game world if it is different from the default world + String worldName = getConfig().getString("world-name", "world"); + if (Bukkit.getWorld(worldName) == null) { + getLogger().info("Loading Bedwars game world '" + worldName + "'..."); + org.bukkit.World w = Bukkit.createWorld(new org.bukkit.WorldCreator(worldName)); + if (w != null) { + w.setGameRule(org.bukkit.GameRule.ANNOUNCE_ADVANCEMENTS, false); + } + } else { + org.bukkit.World w = Bukkit.getWorld(worldName); + if (w != null) { + w.setGameRule(org.bukkit.GameRule.ANNOUNCE_ADVANCEMENTS, false); + } + } + + // Initialize game manager values + this.gameManager.init(); + + // Build waiting lobby glass platform cage + this.arenaManager.createWaitingLobby(); + + // Register Commands + PluginCommand bwCmd = getCommand("bw"); + if (bwCmd != null) { + BedwarsCommand executor = new BedwarsCommand(this); + bwCmd.setExecutor(executor); + bwCmd.setTabCompleter(executor); + } + + // Register Listeners + getServer().getPluginManager().registerEvents(new GameListener(this), this); + getServer().getPluginManager().registerEvents(new ShopListener(this), this); + + getLogger().info("================================================"); + getLogger().info(" Bedwars Plugin has been successfully enabled! "); + getLogger().info(" Compatible with 1.21.4+ and Nexoria Proxy "); + getLogger().info("================================================"); + } + + @Override + public void onDisable() { + // Cleanup all floating Text Displays (Holograms) to prevent visual dupes + if (generatorManager != null) { + generatorManager.cleanup(); + } + + // Unregister plugin channels cleanly + getServer().getMessenger().unregisterOutgoingPluginChannel(this, "BungeeCord"); + getServer().getMessenger().unregisterOutgoingPluginChannel(this, "bungeecord:main"); + getServer().getMessenger().unregisterOutgoingPluginChannel(this, "nexoria:main"); + + getLogger().info("Bedwars Plugin has been successfully disabled!"); + } + + public static BedwarsPlugin getInstance() { + return instance; + } + + public GameManager getGameManager() { + return gameManager; + } + + public ArenaManager getArenaManager() { + return arenaManager; + } + + public GeneratorManager getGeneratorManager() { + return generatorManager; + } + + public ShopManager getShopManager() { + return shopManager; + } + + public UpgradesManager getUpgradesManager() { + return upgradesManager; + } + + public ScoreboardManager getScoreboardManager() { + return scoreboardManager; + } +} diff --git a/src/main/java/com/bedwars/BedwarsTeam.java b/src/main/java/com/bedwars/BedwarsTeam.java new file mode 100644 index 0000000..5b57229 --- /dev/null +++ b/src/main/java/com/bedwars/BedwarsTeam.java @@ -0,0 +1,49 @@ +package com.bedwars; + +import org.bukkit.ChatColor; +import org.bukkit.Material; + +public enum BedwarsTeam { + RED("Red", ChatColor.RED, "§c[RED] ", Material.RED_WOOL, Material.RED_BED), + BLUE("Blue", ChatColor.BLUE, "§9[BLUE] ", Material.BLUE_WOOL, Material.BLUE_BED), + PURPLE("Purple", ChatColor.DARK_PURPLE, "§5[PURPLE] ", Material.PURPLE_WOOL, Material.PURPLE_BED), + YELLOW("Yellow", ChatColor.YELLOW, "§e[YELLOW] ", Material.YELLOW_WOOL, Material.YELLOW_BED); + + private final String name; + private final ChatColor color; + private final String prefix; + private final Material woolMaterial; + private final Material bedMaterial; + + BedwarsTeam(String name, ChatColor color, String prefix, Material woolMaterial, Material bedMaterial) { + this.name = name; + this.color = color; + this.prefix = prefix; + this.woolMaterial = woolMaterial; + this.bedMaterial = bedMaterial; + } + + public String getName() { + return name; + } + + public ChatColor getColor() { + return color; + } + + public String getPrefix() { + return prefix; + } + + public Material getWoolMaterial() { + return woolMaterial; + } + + public Material getBedMaterial() { + return bedMaterial; + } + + public String getColorizedName() { + return color + name; + } +} diff --git a/src/main/java/com/bedwars/GameListener.java b/src/main/java/com/bedwars/GameListener.java new file mode 100644 index 0000000..7caf41e --- /dev/null +++ b/src/main/java/com/bedwars/GameListener.java @@ -0,0 +1,823 @@ +package com.bedwars; + +import org.bukkit.*; +import org.bukkit.block.Block; +import org.bukkit.block.data.type.Bed; +import org.bukkit.entity.Player; +import org.bukkit.entity.Projectile; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.block.BlockBreakEvent; +import org.bukkit.event.block.BlockPlaceEvent; +import org.bukkit.event.entity.EntityDamageByEntityEvent; +import org.bukkit.event.entity.EntityDamageEvent; +import org.bukkit.event.entity.FoodLevelChangeEvent; +import org.bukkit.event.entity.EntityPickupItemEvent; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerMoveEvent; +import org.bukkit.event.player.PlayerQuitEvent; +import org.bukkit.inventory.ItemStack; + +public class GameListener implements Listener { + + private final BedwarsPlugin plugin; + private final java.util.Map fireballCooldowns = new java.util.HashMap<>(); + + public GameListener(BedwarsPlugin plugin) { + this.plugin = plugin; + } + + @EventHandler + public void onPlayerJoin(PlayerJoinEvent event) { + Player player = event.getPlayer(); + int online = Bukkit.getOnlinePlayers().size(); + int max = Bukkit.getMaxPlayers(); + event.setJoinMessage("§a[+] §7" + player.getName() + " §8[" + online + "/" + max + "]"); + + // Setup Scoreboard + plugin.getScoreboardManager().setupScoreboard(player); + + GameManager gm = plugin.getGameManager(); + gm.updateTabName(player); + if (gm.getState() == GameState.LOBBY) { + player.setGameMode(GameMode.SURVIVAL); + player.setHealth(20.0); + player.setFoodLevel(20); + player.getInventory().clear(); + player.getEnderChest().clear(); + + // Clear potion effects + for (org.bukkit.potion.PotionEffect effect : player.getActivePotionEffects()) { + player.removePotionEffect(effect.getType()); + } + + // Teleport to lobby spawn + teleportToLobby(player); + gm.checkStart(); + } else { + // Join as spectator if game is already running + gm.addSpectator(player); + player.sendMessage("§eMatch in progress! You are spectating."); + } + } + + @EventHandler + public void onPlayerQuit(PlayerQuitEvent event) { + Player player = event.getPlayer(); + int online = Bukkit.getOnlinePlayers().size() - 1; + int max = Bukkit.getMaxPlayers(); + event.setQuitMessage("§c[-] §7" + player.getName() + " §8[" + online + "/" + max + "]"); + + plugin.getScoreboardManager().removeScoreboard(player); + plugin.getShopManager().resetPlayerShopData(player); + + GameManager gm = plugin.getGameManager(); + if (online == 0 && gm.getState() == GameState.PLAYING) { + gm.resetGameImmediately(); + } else if (gm.getState() == GameState.PLAYING) { + gm.handlePlayerQuit(player); + } + } + + private void teleportToLobby(Player player) { + if (plugin.getConfig().contains("locations.lobby")) { + String worldName = plugin.getConfig().getString("locations.lobby.world", "world"); + World w = Bukkit.getWorld(worldName); + if (w != null) { + double x = plugin.getConfig().getDouble("locations.lobby.x"); + double y = plugin.getConfig().getDouble("locations.lobby.y"); + double z = plugin.getConfig().getDouble("locations.lobby.z"); + float yaw = (float) plugin.getConfig().getDouble("locations.lobby.yaw"); + float pitch = (float) plugin.getConfig().getDouble("locations.lobby.pitch"); + player.teleport(new Location(w, x, y, z, yaw, pitch)); + return; + } + } + // Fallback + player.teleport(player.getWorld().getSpawnLocation()); + } + + @EventHandler + public void onBlockPlace(BlockPlaceEvent event) { + GameManager gm = plugin.getGameManager(); + if (gm.getState() != GameState.PLAYING) { + if (!event.getPlayer().hasPermission("bedwars.admin")) { + event.setCancelled(true); + } + return; + } + + Player player = event.getPlayer(); + if (gm.isSpectator(player)) { + event.setCancelled(true); + return; + } + + Block block = event.getBlockPlaced(); + + // Custom TNT auto-explode logic + if (block.getType() == Material.TNT) { + event.setCancelled(true); + + // Deduct 1 TNT from hand + ItemStack handItem = event.getItemInHand(); + if (handItem != null) { + handItem.setAmount(handItem.getAmount() - 1); + } + + // Spawn primed TNT + Location spawnLoc = block.getLocation().add(0.5, 0.5, 0.5); + org.bukkit.entity.TNTPrimed tnt = spawnLoc.getWorld().spawn(spawnLoc, org.bukkit.entity.TNTPrimed.class); + tnt.setFuseTicks(40); // 2 seconds fuse + tnt.setSource(player); + + // Play ignite sound + player.getWorld().playSound(spawnLoc, Sound.ENTITY_TNT_PRIMED, 1.0f, 1.0f); + return; + } + + // Track player placed blocks so they can be broken + plugin.getArenaManager().trackBlockPlace(block); + } + + @EventHandler + public void onBlockBreak(BlockBreakEvent event) { + GameManager gm = plugin.getGameManager(); + Player player = event.getPlayer(); + + if (player.getGameMode() == GameMode.CREATIVE && player.hasPermission("bedwars.admin")) { + return; // Admins bypass + } + + if (gm.getState() != GameState.PLAYING) { + event.setCancelled(true); + return; + } + + if (gm.isSpectator(player)) { + event.setCancelled(true); + return; + } + + Block block = event.getBlock(); + Material mat = block.getType(); + + // 1. Check if Bed is broken + if (mat.name().contains("_BED")) { + event.setCancelled(true); // Don't drop item, handle custom deletion + + Location breakLoc = block.getLocation(); + BedwarsTeam brokenTeam = null; + + // Search for matching team bed coordinate + for (BedwarsTeam team : BedwarsTeam.values()) { + if (gm.isTeamEnabled(team)) { + String path = "locations.teams." + team.getName().toLowerCase() + ".bed"; + if (plugin.getConfig().contains(path)) { + double x = plugin.getConfig().getDouble(path + ".x"); + double y = plugin.getConfig().getDouble(path + ".y"); + double z = plugin.getConfig().getDouble(path + ".z"); + Location bedLoc = new Location(breakLoc.getWorld(), x, y, z); + + // Distance check <= 2.25 blocks squared to cover both halves of the bed + if (breakLoc.distanceSquared(bedLoc) <= 2.5) { + brokenTeam = team; + break; + } + } + } + } + + if (brokenTeam != null) { + BedwarsTeam breakerTeam = gm.getPlayerTeam(player); + if (breakerTeam == brokenTeam) { + player.sendMessage("§cYou cannot break your own bed!"); + return; + } + + // Bed is broken! + gm.setBedBroken(brokenTeam, true); + gm.addBedBroken(player); + + // Silently erase both halves of the bed block in world + if (block.getBlockData() instanceof Bed bedData) { + Block otherHalf = block.getRelative(bedData.getFacing()); + if (otherHalf.getType().name().contains("_BED")) { + otherHalf.setType(Material.AIR); + } + } + block.setType(Material.AIR); + + // Global Announce + Bukkit.broadcastMessage("§c§lBED DESTRUCTION!"); + Bukkit.broadcastMessage(" " + brokenTeam.getColorizedName() + " Bed §fwas destroyed by " + breakerTeam.getColor() + player.getName() + "!"); + + // Title and Sound + for (Player p : Bukkit.getOnlinePlayers()) { + p.playSound(p.getLocation(), Sound.ENTITY_ENDER_DRAGON_GROWL, 0.8f, 1.0f); + BedwarsTeam pTeam = gm.getPlayerTeam(p); + if (pTeam == brokenTeam) { + p.sendTitle("§c§lBED DESTROYED!", "§fYou will no longer respawn!", 10, 60, 10); + } + } + gm.checkWinner(); + } else { + player.sendMessage("§cThis is an un-registered bed block!"); + block.setType(Material.AIR); + } + return; + } + + // 2. Standard block break (Must be player placed block!) + if (plugin.getArenaManager().isPlayerPlacedBlock(block)) { + plugin.getArenaManager().removeTrackedBlock(block); + } else { + event.setCancelled(true); + player.sendMessage("§cYou can only break blocks placed by players!"); + } + } + + @EventHandler + public void onPlayerMove(PlayerMoveEvent event) { + GameManager gm = plugin.getGameManager(); + if (gm.getState() != GameState.PLAYING) return; + + Player player = event.getPlayer(); + if (gm.isSpectator(player)) return; + + // Void Fall Detector + if (player.getLocation().getY() <= -20.0) { + // Trigger instant death sequence + Player killer = gm.getLastAttacker(player); + gm.handlePlayerDeath(player, killer); + } + } + + @EventHandler + public void onEntityDamage(EntityDamageEvent event) { + if (!(event.getEntity() instanceof Player player)) return; + + GameManager gm = plugin.getGameManager(); + + // 1. Lobby and ending protection + if (gm.getState() != GameState.PLAYING) { + event.setCancelled(true); + return; + } + + // 2. Spectator protection + if (gm.isSpectator(player)) { + event.setCancelled(true); + return; + } + + // Intercept death and trigger custom Respawn sequences instead of Vanilla screen + if (player.getHealth() - event.getFinalDamage() <= 0) { + event.setCancelled(true); + + Player killer = null; + if (event instanceof EntityDamageByEntityEvent entityEvent) { + if (entityEvent.getDamager() instanceof Player p) { + killer = p; + } else if (entityEvent.getDamager() instanceof Projectile proj) { + if (proj.getShooter() instanceof Player p) { + killer = p; + } + } + } + + if (killer == null) { + killer = gm.getLastAttacker(player); + } + + gm.handlePlayerDeath(player, killer); + } + } + + @EventHandler + public void onEntityDamageByEntity(EntityDamageByEntityEvent event) { + if (!(event.getEntity() instanceof Player victim)) return; + + GameManager gm = plugin.getGameManager(); + if (gm.getState() != GameState.PLAYING) { + event.setCancelled(true); + return; + } + + // Prevent spectator damage + if (gm.isSpectator(victim)) { + event.setCancelled(true); + return; + } + + org.bukkit.entity.Entity damager = event.getDamager(); + Player attacker = null; + LastDamageType type = LastDamageType.OTHER; + + // 1. Handle Fireball Explosion / Direct Damage + if (damager instanceof org.bukkit.entity.Fireball fireball) { + Player shooter = fireball.getShooter() instanceof Player ? (Player) fireball.getShooter() : null; + if (shooter != null) { + attacker = shooter; + type = LastDamageType.FIREBALL; + + // Teammate and self-damage handling + if (shooter.equals(victim)) { + // Self fireball damage is scaled down to 1.0 (0.5 heart) + event.setDamage(1.0); + } else { + BedwarsTeam t1 = gm.getPlayerTeam(shooter); + BedwarsTeam t2 = gm.getPlayerTeam(victim); + if (t1 != null && t1 == t2) { + event.setCancelled(true); + return; + } + // Enemy fireball damage scaled down to max 3.0 (1.5 hearts) + event.setDamage(Math.min(event.getDamage(), 3.0)); + } + } + } + // 2. Handle TNT Explosion Damage + else if (damager instanceof org.bukkit.entity.TNTPrimed tnt) { + Player source = tnt.getSource() instanceof Player ? (Player) tnt.getSource() : null; + if (source != null) { + attacker = source; + type = LastDamageType.TNT; + + // Teammate and self-damage handling + if (source.equals(victim)) { + // Self TNT damage is scaled down to 2.0 (1 heart) + event.setDamage(2.0); + } else { + BedwarsTeam t1 = gm.getPlayerTeam(source); + BedwarsTeam t2 = gm.getPlayerTeam(victim); + if (t1 != null && t1 == t2) { + event.setCancelled(true); + return; + } + // Enemy TNT damage scaled down to max 5.0 (2.5 hearts) + event.setDamage(Math.min(event.getDamage(), 5.0)); + } + } + } + // 3. Handle physical hits (Melee) or Bow Projectiles + else { + if (damager instanceof Player p) { + attacker = p; + type = LastDamageType.MELEE; + } else if (damager instanceof Projectile proj) { + if (proj.getShooter() instanceof Player p) { + attacker = p; + type = LastDamageType.PROJECTILE; + } + } + + if (attacker != null) { + // Self damage from standard physical hits is impossible but check anyway + if (attacker.equals(victim)) { + return; + } + + // Friendly Fire block + BedwarsTeam t1 = gm.getPlayerTeam(attacker); + BedwarsTeam t2 = gm.getPlayerTeam(victim); + if (t1 != null && t1 == t2) { + event.setCancelled(true); + attacker.sendMessage("§cYou cannot attack your teammate!"); + return; + } + } + } + + // Apply general checks and tracking if there's a valid attacker + if (attacker != null) { + if (gm.isSpectator(attacker)) { + event.setCancelled(true); + return; + } + // Track last attacker for kill attribution + gm.setLastAttacker(victim, attacker, type); + } + } + + @EventHandler + public void onFoodLevelChange(FoodLevelChangeEvent event) { + // Disable hunger during lobby or match + event.setCancelled(true); + if (event.getEntity() instanceof Player player) { + player.setFoodLevel(20); + } + } + + @EventHandler + public void onPickupItem(EntityPickupItemEvent event) { + if (event.getEntity() instanceof Player player) { + GameManager gm = plugin.getGameManager(); + if (gm.isSpectator(player)) { + event.setCancelled(true); + return; + } + + // Apply sharpness automatically when sword is picked up + plugin.getUpgradesManager().applyTeamUpgrades(player); + + // Generator resource sharing (split) logic + Material type = event.getItem().getItemStack().getType(); + if (type == Material.IRON_INGOT || type == Material.GOLD_INGOT || + type == Material.DIAMOND || type == Material.EMERALD) { + + // Only split resources that were spawned by a generator, preventing duplication on player drops + org.bukkit.NamespacedKey key = new org.bukkit.NamespacedKey(plugin, "generator_spawned"); + if (!event.getItem().getPersistentDataContainer().has(key, org.bukkit.persistence.PersistentDataType.BYTE)) { + return; + } + + Location pickupLoc = event.getItem().getLocation(); + Generator nearbyGen = null; + for (Generator gen : plugin.getGeneratorManager().getActiveGenerators()) { + if (gen.getLocation().getWorld().equals(pickupLoc.getWorld()) && + gen.getLocation().distanceSquared(pickupLoc) <= 25.0) { // 5 blocks radius + nearbyGen = gen; + break; + } + } + + if (nearbyGen != null) { + int amount = event.getItem().getItemStack().getAmount(); + BedwarsTeam team = plugin.getGameManager().getPlayerTeam(player); + if (team != null) { + for (Player other : player.getWorld().getPlayers()) { + if (other.equals(player)) continue; + if (plugin.getGameManager().isSpectator(other)) continue; + + // Check if other player is teammate + if (plugin.getGameManager().getPlayerTeam(other) == team) { + // Check if other player is near the picking player (within 3.5 blocks) + if (other.getLocation().distanceSquared(player.getLocation()) <= 12.25) { + // Give them the same item! + ItemStack copy = new ItemStack(type, amount); + other.getInventory().addItem(copy); + other.playSound(other.getLocation(), Sound.ENTITY_ITEM_PICKUP, 0.5f, 1.5f); + } + } + } + } + } + } + } + } + + private String formatMaterialName(Material material) { + switch (material) { + case IRON_INGOT: + return "§fIron"; + case GOLD_INGOT: + return "§eGold"; + case DIAMOND: + return "§bDiamond"; + case EMERALD: + return "§aEmerald"; + default: + return material.name(); + } + } + + @EventHandler + public void onInventoryClick(InventoryClickEvent event) { + if (event.getWhoClicked() instanceof Player player) { + GameManager gm = plugin.getGameManager(); + + // Handle Teleporter inventory click + if (event.getView().getTitle().equals("§8Teleporter")) { + event.setCancelled(true); + ItemStack clicked = event.getCurrentItem(); + if (clicked != null && clicked.getType() == Material.PLAYER_HEAD) { + org.bukkit.inventory.meta.SkullMeta meta = (org.bukkit.inventory.meta.SkullMeta) clicked.getItemMeta(); + if (meta != null && meta.getOwningPlayer() != null) { + Player target = Bukkit.getPlayer(meta.getOwningPlayer().getUniqueId()); + if (target != null && target.isOnline()) { + player.teleport(target.getLocation()); + player.sendMessage("§aTeleported to " + clicked.getItemMeta().getDisplayName() + "§a!"); + player.closeInventory(); + } else { + player.sendMessage("§cThat player is no longer online!"); + } + } + } + return; + } + + // Block spectators from doing anything in inventories + if (gm.isSpectator(player)) { + event.setCancelled(true); + return; + } + + // Block taking off armor + if (event.getSlotType() == org.bukkit.event.inventory.InventoryType.SlotType.ARMOR) { + event.setCancelled(true); + player.sendMessage("§cYou cannot take off your team armor!"); + return; + } + + // Apply team upgrades when inventory changes or items are moved + Bukkit.getScheduler().runTaskLater(plugin, () -> { + plugin.getUpgradesManager().applyTeamUpgrades(player); + }, 1L); + } + } + + @EventHandler + public void onCreatureSpawn(org.bukkit.event.entity.CreatureSpawnEvent event) { + String worldName = plugin.getConfig().getString("world-name", "world"); + if (event.getEntity().getWorld().getName().equalsIgnoreCase(worldName)) { + // Cancel all spawns except command or custom (e.g. Villagers, utility mobs) + org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason reason = event.getSpawnReason(); + if (reason != org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.CUSTOM && + reason != org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.COMMAND) { + event.setCancelled(true); + } + } + } + + @EventHandler + public void onBlockPhysics(org.bukkit.event.block.BlockPhysicsEvent event) { + Material mat = event.getChangedType(); + String name = mat.name(); + if (name.contains("CONCRETE_POWDER") || name.contains("SAND") || name.equals("GRAVEL")) { + event.setCancelled(true); + } + } + + @EventHandler + public void onFallingBlockSpawn(org.bukkit.event.entity.EntitySpawnEvent event) { + if (event.getEntity() instanceof org.bukkit.entity.FallingBlock fallingBlock) { + Material mat = fallingBlock.getBlockData().getMaterial(); + String name = mat.name(); + if (name.contains("CONCRETE_POWDER") || name.contains("SAND") || name.equals("GRAVEL")) { + event.setCancelled(true); + } + } + } + + @EventHandler + public void onChunkLoad(org.bukkit.event.world.ChunkLoadEvent event) { + String worldName = plugin.getConfig().getString("world-name", "world"); + if (event.getChunk().getWorld().getName().equalsIgnoreCase(worldName)) { + for (org.bukkit.entity.Entity entity : event.getChunk().getEntities()) { + if (entity instanceof org.bukkit.entity.Mob || entity instanceof org.bukkit.entity.Ambient) { + if (entity.getType() == org.bukkit.entity.EntityType.VILLAGER) { + continue; + } + entity.remove(); + } + } + } + } + + @EventHandler + public void onPlayerAdvancementDone(org.bukkit.event.player.PlayerAdvancementDoneEvent event) { + Player player = event.getPlayer(); + org.bukkit.advancement.Advancement advancement = event.getAdvancement(); + org.bukkit.advancement.AdvancementProgress progress = player.getAdvancementProgress(advancement); + for (String criteria : progress.getAwardedCriteria()) { + progress.revokeCriteria(criteria); + } + } + + @EventHandler + public void onWorldLoad(org.bukkit.event.world.WorldLoadEvent event) { + event.getWorld().setGameRule(org.bukkit.GameRule.ANNOUNCE_ADVANCEMENTS, false); + } + + @EventHandler + public void onEntityExplode(org.bukkit.event.entity.EntityExplodeEvent event) { + GameManager gm = plugin.getGameManager(); + if (gm.getState() != GameState.PLAYING) { + event.setCancelled(true); + return; + } + + org.bukkit.entity.Entity exploder = event.getEntity(); + boolean isFireball = exploder instanceof org.bukkit.entity.Fireball; + boolean isTNT = exploder instanceof org.bukkit.entity.TNTPrimed; + + // Apply custom explosion propulsion to nearby players + if (isFireball || isTNT) { + Location explosionLoc = event.getLocation(); + double radius = isFireball ? 5.5 : 6.5; + double maxPower = isFireball ? 1.65 : 2.1; + double verticalBoost = isFireball ? 0.75 : 0.95; + + for (Player player : explosionLoc.getWorld().getPlayers()) { + if (gm.isSpectator(player)) continue; + + double distance = player.getLocation().distance(explosionLoc); + if (distance <= radius) { + // Calculate custom knockback vector + org.bukkit.util.Vector direction = player.getLocation().toVector().subtract(explosionLoc.toVector()); + if (distance < 0.1) { + direction = new org.bukkit.util.Vector(0, 1, 0); + distance = 0.1; + } else { + direction.normalize(); + } + + // Power calculation with distance falloff but strong baseline (Hypixel style) + double ratio = 1.0 - (distance / radius); + double power = maxPower * (0.45 + 0.55 * ratio); + + org.bukkit.util.Vector velocity = direction.multiply(power); + + // Upward launch enhancement + double yVal = direction.getY() * verticalBoost; + if (yVal < 0) yVal = 0; + double finalY = Math.max(0.42, yVal + 0.38 * ratio); + velocity.setY(finalY); + + // Smooth blending with player's current horizontal momentum + org.bukkit.util.Vector currentVel = player.getVelocity(); + double blendedX = currentVel.getX() * 0.45 + velocity.getX(); + double blendedZ = currentVel.getZ() * 0.45 + velocity.getZ(); + + org.bukkit.util.Vector finalVelocity = new org.bukkit.util.Vector(blendedX, finalY, blendedZ); + + // Clamp to prevent extreme/glitchy speeds + double maxSpeed = isFireball ? 3.0 : 3.8; + if (finalVelocity.length() > maxSpeed) { + finalVelocity.normalize().multiply(maxSpeed); + } + + // Schedule velocity change on next tick to cleanly overwrite vanilla explosion knockback + Bukkit.getScheduler().runTaskLater(plugin, () -> { + player.setVelocity(finalVelocity); + }, 1L); + } + } + } + + // Only allow breaking player placed blocks, and exclude blast-proof blocks (Glass and Obsidian) + java.util.Iterator iterator = event.blockList().iterator(); + while (iterator.hasNext()) { + Block block = iterator.next(); + Material type = block.getType(); + if (type == Material.GLASS || type == Material.OBSIDIAN) { + iterator.remove(); + continue; + } + if (!plugin.getArenaManager().isPlayerPlacedBlock(block)) { + iterator.remove(); + } else { + // Remove from tracking since it will be destroyed + plugin.getArenaManager().removeTrackedBlock(block); + } + } + } + + @EventHandler + public void onPlayerInteract(org.bukkit.event.player.PlayerInteractEvent event) { + Player player = event.getPlayer(); + GameManager gm = plugin.getGameManager(); + if (gm.isSpectator(player)) { + event.setCancelled(true); + ItemStack item = event.getItem(); + if (item != null) { + if (item.getType() == Material.COMPASS) { + openTeleporterGui(player); + } else if (item.getType() == Material.RED_BED) { + gm.sendToLobby(player); + } + } + return; + } + + if (event.getAction() == org.bukkit.event.block.Action.RIGHT_CLICK_AIR || + event.getAction() == org.bukkit.event.block.Action.RIGHT_CLICK_BLOCK) { + + ItemStack item = event.getItem(); + + if (item != null && item.getType() == Material.FIRE_CHARGE) { + // Cancel setting fire to blocks + event.setCancelled(true); + + if (gm.getState() != GameState.PLAYING || gm.isSpectator(player)) { + return; + } + + // Cooldown check (500ms) + long now = System.currentTimeMillis(); + if (fireballCooldowns.containsKey(player.getUniqueId())) { + long lastShoot = fireballCooldowns.get(player.getUniqueId()); + if (now - lastShoot < 500) { + player.playSound(player.getLocation(), Sound.ENTITY_ENDER_DRAGON_FLAP, 0.5f, 1.5f); + return; // Silent cancel to prevent spam + } + } + fireballCooldowns.put(player.getUniqueId(), now); + + // Shoot fireball + org.bukkit.entity.Fireball fireball = player.launchProjectile(org.bukkit.entity.Fireball.class); + fireball.setYield(2.5f); // Safe explosion size + fireball.setIsIncendiary(false); // Don't set fires + fireball.setShooter(player); + + // Apply straight and fast velocity like Hypixel + org.bukkit.util.Vector direction = player.getLocation().getDirection(); + fireball.setDirection(direction); + fireball.setVelocity(direction.multiply(1.6)); + + // Play shoot sound + player.getWorld().playSound(player.getLocation(), Sound.ENTITY_GHAST_SHOOT, 1.0f, 1.0f); + + // Deduct 1 fireball + int amount = item.getAmount(); + if (amount > 1) { + item.setAmount(amount - 1); + } else { + player.getInventory().setItemInMainHand(null); + } + } + } + } + + @EventHandler + public void onCraftItem(org.bukkit.event.inventory.CraftItemEvent event) { + // Disable all crafting + event.setCancelled(true); + } + + @EventHandler + public void onPrepareCraft(org.bukkit.event.inventory.PrepareItemCraftEvent event) { + // Clear crafting result to disable personal 2x2 crafting in player inventory + event.getInventory().setResult(new org.bukkit.inventory.ItemStack(org.bukkit.Material.AIR)); + } + + @EventHandler + public void onAdvancementDone(org.bukkit.event.player.PlayerAdvancementDoneEvent event) { + // Silently revoke all advancements immediately so players never earn them or get spammed + final Player player = event.getPlayer(); + final org.bukkit.advancement.Advancement advancement = event.getAdvancement(); + final org.bukkit.advancement.AdvancementProgress progress = player.getAdvancementProgress(advancement); + for (String criteria : progress.getAwardedCriteria()) { + Bukkit.getScheduler().runTask(plugin, () -> progress.revokeCriteria(criteria)); + } + } + + @EventHandler + public void onPlayerDropItem(org.bukkit.event.player.PlayerDropItemEvent event) { + Player player = event.getPlayer(); + GameManager gm = plugin.getGameManager(); + if (gm.isSpectator(player)) { + event.setCancelled(true); + } + } + + private void openTeleporterGui(Player player) { + java.util.List alivePlayers = new java.util.ArrayList<>(); + GameManager gm = plugin.getGameManager(); + for (Player p : Bukkit.getOnlinePlayers()) { + if (!gm.isSpectator(p)) { + alivePlayers.add(p); + } + } + + if (alivePlayers.isEmpty()) { + player.sendMessage("§cNo active players to spectate!"); + return; + } + + int size = ((alivePlayers.size() / 9) + 1) * 9; + if (size > 54) size = 54; + org.bukkit.inventory.Inventory inv = Bukkit.createInventory(null, size, "§8Teleporter"); + + for (int i = 0; i < Math.min(alivePlayers.size(), size); i++) { + Player target = alivePlayers.get(i); + ItemStack head = new ItemStack(Material.PLAYER_HEAD); + org.bukkit.inventory.meta.SkullMeta meta = (org.bukkit.inventory.meta.SkullMeta) head.getItemMeta(); + if (meta != null) { + meta.setOwningPlayer(target); + meta.setDisplayName(gm.getFormattedName(target)); + java.util.List lore = new java.util.ArrayList<>(); + lore.add("§7Click to teleport to player"); + meta.setLore(lore); + head.setItemMeta(meta); + } + inv.setItem(i, head); + } + + player.openInventory(inv); + } + + @EventHandler + public void onEntityRegainHealth(org.bukkit.event.entity.EntityRegainHealthEvent event) { + if (event.getEntity() instanceof Player player) { + GameManager gm = plugin.getGameManager(); + if (gm.getState() == GameState.PLAYING && !gm.isSpectator(player)) { + if (event.getRegainReason() == org.bukkit.event.entity.EntityRegainHealthEvent.RegainReason.SATIATED) { + event.setCancelled(true); + } + } + } + } +} diff --git a/src/main/java/com/bedwars/GameManager.java b/src/main/java/com/bedwars/GameManager.java new file mode 100644 index 0000000..7ba4395 --- /dev/null +++ b/src/main/java/com/bedwars/GameManager.java @@ -0,0 +1,1040 @@ +package com.bedwars; + +import org.bukkit.*; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.entity.Firework; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.FireworkMeta; +import org.bukkit.potion.PotionEffect; +import org.bukkit.potion.PotionEffectType; +import org.bukkit.scheduler.BukkitRunnable; +import org.bukkit.scheduler.BukkitTask; +import org.bukkit.event.entity.EntityDamageEvent; + +import java.util.*; + +public class GameManager { + + private final BedwarsPlugin plugin; + private GameState state = GameState.LOBBY; + + private final Map playerTeams = new HashMap<>(); + private final Map teamBeds = new HashMap<>(); + private final Set spectators = new HashSet<>(); + private final Set eliminatedTeams = new HashSet<>(); + private final Set eliminatedPlayers = new HashSet<>(); + + // Stats + private final Map kills = new HashMap<>(); + private final Map bedsBroken = new HashMap<>(); + + // Combat Tracking + private final Map lastAttacker = new HashMap<>(); + private final Map lastAttackTime = new HashMap<>(); + private final Map lastDamageType = new HashMap<>(); + + // Countdowns + private boolean countingDown = false; + private int countdownTime = 30; + private BukkitTask countdownTask; + private BukkitTask gameLoopTask; + private final List respawnTasks = new ArrayList<>(); + private int gameTimeTicks = 0; + + public GameManager(BedwarsPlugin plugin) { + this.plugin = plugin; + resetStats(); + } + + public void init() { + // Initialize bed status + for (BedwarsTeam team : BedwarsTeam.values()) { + teamBeds.put(team, true); + } + // Save pristine bed states from world + plugin.getArenaManager().saveBedStates(); + } + + public GameState getState() { + return state; + } + + public void setState(GameState state) { + this.state = state; + plugin.getScoreboardManager().updateAll(); + } + + public boolean isCountingDown() { + return countingDown; + } + + public int getCountdownTime() { + return countdownTime; + } + + public BedwarsTeam getPlayerTeam(Player player) { + return playerTeams.get(player.getUniqueId()); + } + + public void setPlayerTeam(Player player, BedwarsTeam team) { + playerTeams.put(player.getUniqueId(), team); + updateTabName(player); + plugin.getScoreboardManager().updateAll(); + } + + public void updateTabName(Player player) { + player.setPlayerListHeaderFooter("", ""); + BedwarsTeam team = getPlayerTeam(player); + if (team != null) { + if (isSpectator(player)) { + player.setPlayerListName("§7" + player.getName()); + player.setDisplayName("§7" + player.getName() + "§r"); + } else { + player.setPlayerListName(team.getColor() + player.getName()); + player.setDisplayName(team.getColor() + player.getName() + "§r"); + } + } else { + player.setPlayerListName("§7" + player.getName()); + player.setDisplayName("§7" + player.getName() + "§r"); + } + } + + public boolean hasBed(BedwarsTeam team) { + return teamBeds.getOrDefault(team, false); + } + + public void setBedBroken(BedwarsTeam team, boolean broken) { + teamBeds.put(team, !broken); + plugin.getScoreboardManager().updateAll(); + } + + public boolean isSpectator(Player player) { + return spectators.contains(player.getUniqueId()); + } + + public int getKills(Player player) { + return kills.getOrDefault(player.getUniqueId(), 0); + } + + public void addKill(Player player) { + kills.put(player.getUniqueId(), getKills(player) + 1); + plugin.getScoreboardManager().updateScoreboard(player); + } + + public int getBedsBroken(Player player) { + return bedsBroken.getOrDefault(player.getUniqueId(), 0); + } + + public void addBedBroken(Player player) { + bedsBroken.put(player.getUniqueId(), getBedsBroken(player) + 1); + plugin.getScoreboardManager().updateScoreboard(player); + } + + public void resetStats() { + playerTeams.clear(); + spectators.clear(); + eliminatedTeams.clear(); + eliminatedPlayers.clear(); + kills.clear(); + bedsBroken.clear(); + lastAttacker.clear(); + lastAttackTime.clear(); + lastDamageType.clear(); + for (BedwarsTeam team : BedwarsTeam.values()) { + teamBeds.put(team, false); // Disabled until game starts + } + if (plugin.getUpgradesManager() != null) { + plugin.getUpgradesManager().resetTeamUpgrades(); + } + if (plugin.getShopManager() != null) { + plugin.getShopManager().resetAllShopData(); + } + gameTimeTicks = 0; + + // Show all players to all players again + for (Player p1 : Bukkit.getOnlinePlayers()) { + for (Player p2 : Bukkit.getOnlinePlayers()) { + p1.showPlayer(plugin, p2); + } + } + } + + /** + * Attempts to start the lobby countdown. + */ + public void checkStart() { + if (state != GameState.LOBBY || countingDown) return; + + int minPlayers = plugin.getConfig().getInt("min-players", 2); + if (Bukkit.getOnlinePlayers().size() >= minPlayers) { + startCountdown(); + } + } + + private void startCountdown() { + countingDown = true; + countdownTime = plugin.getConfig().getInt("countdown-seconds", 30); + + countdownTask = new BukkitRunnable() { + @Override + public void run() { + if (Bukkit.getOnlinePlayers().size() < plugin.getConfig().getInt("min-players", 2)) { + cancelCountdown(); + Bukkit.broadcastMessage("§cNot enough players! Countdown cancelled."); + return; + } + + if (countdownTime <= 0) { + startGame(); + cancel(); + return; + } + + if (countdownTime == 30 || countdownTime == 15 || countdownTime == 10 || countdownTime <= 5) { + Bukkit.broadcastMessage("§eThe match will begin in §a" + countdownTime + " §eseconds!"); + for (Player p : Bukkit.getOnlinePlayers()) { + p.playSound(p.getLocation(), Sound.BLOCK_NOTE_BLOCK_PLING, 1.0f, 1.0f); + } + } + + countdownTime--; + plugin.getScoreboardManager().updateAll(); + } + }.runTaskTimer(plugin, 0L, 20L); + } + + public void cancelCountdown() { + countingDown = false; + if (countdownTask != null) { + countdownTask.cancel(); + } + plugin.getScoreboardManager().updateAll(); + } + + /** + * Core Game Start logic. + */ + public void startGame() { + setState(GameState.PLAYING); + countingDown = false; + + // Remove waiting area glass cage + plugin.getArenaManager().removeWaitingLobby(); + + // Purge residual mobs from world region files + plugin.getArenaManager().clearAllMobs(); + + // 1. Setup active teams in the game + List activeTeams = new ArrayList<>(); + ConfigurationSection teamsSec = plugin.getConfig().getConfigurationSection("locations.teams"); + if (teamsSec != null) { + for (String key : teamsSec.getKeys(false)) { + if (teamsSec.getBoolean(key + ".enabled", false)) { + try { + BedwarsTeam t = BedwarsTeam.valueOf(key.toUpperCase()); + activeTeams.add(t); + teamBeds.put(t, true); // Bed active! + } catch (Exception ignored) {} + } + } + } + + if (activeTeams.isEmpty()) { + Bukkit.broadcastMessage("§cError: No teams have been configured in /bw setup! Force ending game."); + endGame(null); + return; + } + + // 2. Distribute players into teams + List players = new ArrayList<>(Bukkit.getOnlinePlayers()); + Collections.shuffle(players); + + int teamIndex = 0; + for (Player player : players) { + BedwarsTeam team = activeTeams.get(teamIndex); + setPlayerTeam(player, team); + teamIndex = (teamIndex + 1) % activeTeams.size(); + + // Set up player + player.setGameMode(GameMode.SURVIVAL); + player.setHealth(20.0); + player.setFoodLevel(20); + player.setSaturation(20.0f); + player.getInventory().clear(); + player.getEnderChest().clear(); + + // Clear potion effects + for (org.bukkit.potion.PotionEffect effect : player.getActivePotionEffects()) { + player.removePotionEffect(effect.getType()); + } + + // Hide spectators from this player + hideAllSpectatorsFrom(player); + + // Apply starting gear + giveStartingGear(player); + + // Teleport to Team Spawn + teleportToTeamSpawn(player, team); + + // Setup tab and display name colors + updateTabName(player); + + player.sendMessage("§eMatch started! You are in team: " + team.getColorizedName()); + player.playSound(player.getLocation(), Sound.ENTITY_ENDER_DRAGON_GROWL, 0.8f, 1.0f); + } + + // Mark empty teams as eliminated immediately so they don't block victory + for (BedwarsTeam team : activeTeams) { + int teamSize = 0; + for (Player p : Bukkit.getOnlinePlayers()) { + if (getPlayerTeam(p) == team) { + teamSize++; + } + } + if (teamSize == 0) { + teamBeds.put(team, false); + eliminatedTeams.add(team); + } + } + + // 3. Spawn Shop Keepers & Upgrades Shop Keepers + spawnNPCs(activeTeams); + + // Place enderchests for active teams asynchronously to prevent synchronous chunk loading locks + String worldName = plugin.getConfig().getString("world-name", "bedwars"); + World world = Bukkit.getWorld(worldName); + if (world != null) { + for (BedwarsTeam team : activeTeams) { + String teamName = team.getName().toLowerCase(); + String path = "locations.teams." + teamName + ".enderchest"; + if (plugin.getConfig().contains(path)) { + final double x = plugin.getConfig().getDouble(path + ".x"); + final double y = plugin.getConfig().getDouble(path + ".y"); + final double z = plugin.getConfig().getDouble(path + ".z"); + world.getChunkAtAsync((int) x >> 4, (int) z >> 4).thenAccept(chunk -> { + chunk.getWorld().getBlockAt((int) x, (int) y, (int) z).setType(Material.ENDER_CHEST); + }); + } + } + } + + // 4. Load generators and start game loop task + plugin.getGeneratorManager().loadGenerators(); + startGameLoop(); + + Bukkit.broadcastMessage("§a§lThe game has begun! Protect your bed!"); + checkWinner(); + } + + private void giveStartingGear(Player player) { + player.getInventory().setItem(0, new ItemStack(Material.WOODEN_SWORD)); + // Give leather armor colored with team dye + BedwarsTeam team = getPlayerTeam(player); + if (team != null) { + player.getInventory().setHelmet(createColoredArmor(Material.LEATHER_HELMET, team.getColor())); + player.getInventory().setChestplate(createColoredArmor(Material.LEATHER_CHESTPLATE, team.getColor())); + + // Retrieve and equip permanent bought armor tier, falling back to team colored leather + int tier = plugin.getShopManager().getArmorTier(player); + if (tier > 0) { + plugin.getShopManager().equipArmorTier(player, tier); + } else { + player.getInventory().setLeggings(createColoredArmor(Material.LEATHER_LEGGINGS, team.getColor())); + player.getInventory().setBoots(createColoredArmor(Material.LEATHER_BOOTS, team.getColor())); + } + + // Retrieve and give permanent tools (Shears, Pickaxe, Axe) + plugin.getShopManager().giveTools(player); + } + } + + private ItemStack createColoredArmor(Material material, ChatColor chatColor) { + ItemStack item = new ItemStack(material); + org.bukkit.inventory.meta.LeatherArmorMeta meta = (org.bukkit.inventory.meta.LeatherArmorMeta) item.getItemMeta(); + if (meta != null) { + Color color = Color.WHITE; + if (chatColor == ChatColor.RED) color = Color.RED; + else if (chatColor == ChatColor.BLUE) color = Color.BLUE; + else if (chatColor == ChatColor.DARK_PURPLE) color = Color.PURPLE; + else if (chatColor == ChatColor.YELLOW) color = Color.YELLOW; + + meta.setColor(color); + meta.setUnbreakable(true); + item.setItemMeta(meta); + } + return item; + } + + private void teleportToTeamSpawn(Player player, BedwarsTeam team) { + String colorName = team.getName().toLowerCase(); + String worldName = plugin.getConfig().getString("world-name", "bedwars"); + World w = Bukkit.getWorld(worldName); + if (w != null && plugin.getConfig().contains("locations.teams." + colorName + ".spawn")) { + double x = plugin.getConfig().getDouble("locations.teams." + colorName + ".spawn.x"); + double y = plugin.getConfig().getDouble("locations.teams." + colorName + ".spawn.y"); + double z = plugin.getConfig().getDouble("locations.teams." + colorName + ".spawn.z"); + float yaw = (float) plugin.getConfig().getDouble("locations.teams." + colorName + ".spawn.yaw"); + float pitch = (float) plugin.getConfig().getDouble("locations.teams." + colorName + ".spawn.pitch"); + player.teleport(new Location(w, x, y, z, yaw, pitch)); + } else { + // Fallback + player.teleport(w != null ? w.getSpawnLocation() : player.getWorld().getSpawnLocation()); + } + } + + private void spawnNPCs(List activeTeams) { + String worldName = plugin.getConfig().getString("world-name", "bedwars"); + World world = Bukkit.getWorld(worldName); + if (world == null) return; + + // Clear existing villager entities in the bedwars world + world.getEntitiesByClass(org.bukkit.entity.Villager.class).forEach(org.bukkit.entity.Entity::remove); + + for (BedwarsTeam team : activeTeams) { + String path = "locations.teams." + team.getName().toLowerCase(); + + // 1. Spawn Item Shop Villager + if (plugin.getConfig().contains(path + ".shop")) { + double x = plugin.getConfig().getDouble(path + ".shop.x"); + double y = plugin.getConfig().getDouble(path + ".shop.y"); + double z = plugin.getConfig().getDouble(path + ".shop.z"); + float yaw = (float) plugin.getConfig().getDouble(path + ".shop.yaw"); + float pitch = (float) plugin.getConfig().getDouble(path + ".shop.pitch"); + + org.bukkit.entity.Villager villager = world.spawn(new Location(world, x, y, z, yaw, pitch), org.bukkit.entity.Villager.class); + villager.setCustomName("§a§lITEM SHOP\n§7Right Click"); + villager.setCustomNameVisible(true); + villager.setProfession(org.bukkit.entity.Villager.Profession.ARMORER); + villager.setAI(false); + villager.setInvulnerable(true); + villager.setSilent(true); + } + + // 2. Spawn Upgrades Shop Villager + if (plugin.getConfig().contains(path + ".upgrades")) { + double x = plugin.getConfig().getDouble(path + ".upgrades.x"); + double y = plugin.getConfig().getDouble(path + ".upgrades.y"); + double z = plugin.getConfig().getDouble(path + ".upgrades.z"); + float yaw = (float) plugin.getConfig().getDouble(path + ".upgrades.yaw"); + float pitch = (float) plugin.getConfig().getDouble(path + ".upgrades.pitch"); + + org.bukkit.entity.Villager villager = world.spawn(new Location(world, x, y, z, yaw, pitch), org.bukkit.entity.Villager.class); + villager.setCustomName("§b§lTEAM UPGRADES\n§7Right Click"); + villager.setCustomNameVisible(true); + villager.setProfession(org.bukkit.entity.Villager.Profession.WEAPONSMITH); + villager.setAI(false); + villager.setInvulnerable(true); + villager.setSilent(true); + } + } + } + + private void startGameLoop() { + gameTimeTicks = 0; + gameLoopTask = new BukkitRunnable() { + @Override + public void run() { + if (state != GameState.PLAYING) { + cancel(); + return; + } + + // Tick generators + plugin.getGeneratorManager().tick(); + + // Increment ticks and check game duration (20 minutes = 24000 ticks) + gameTimeTicks++; + + // Custom regeneration every 5 seconds (100 ticks) + if (gameTimeTicks % 100 == 0) { + for (Player player : Bukkit.getOnlinePlayers()) { + if (!isSpectator(player) && !eliminatedPlayers.contains(player.getUniqueId())) { + if (!player.isDead() && player.getHealth() > 0) { + double maxHealth = player.getMaxHealth(); + double currentHealth = player.getHealth(); + if (currentHealth < maxHealth) { + player.setHealth(Math.min(maxHealth, currentHealth + 1.0)); + } + } + } + } + } + + int secondsLeft = (24000 - gameTimeTicks) / 20; + if (gameTimeTicks < 24000 && gameTimeTicks % 20 == 0) { + // This runs once every second + if (secondsLeft == 300) { // 5 minutes + Bukkit.broadcastMessage("§c§lWARNING! §eThe game will end in §a5 minutes§e!"); + } else if (secondsLeft == 60) { // 1 minute + Bukkit.broadcastMessage("§c§lWARNING! §eThe game will end in §a1 minute§e!"); + } else if (secondsLeft == 30) { + Bukkit.broadcastMessage("§c§lWARNING! §eThe game will end in §a30 seconds§e!"); + } else if (secondsLeft == 10) { + Bukkit.broadcastMessage("§c§lWARNING! §eThe game will end in §a10 seconds§e!"); + } else if (secondsLeft > 0 && secondsLeft <= 5) { + Bukkit.broadcastMessage("§c§lWARNING! §eThe game will end in §c" + secondsLeft + " §eseconds!"); + for (Player p : Bukkit.getOnlinePlayers()) { + p.playSound(p.getLocation(), Sound.BLOCK_NOTE_BLOCK_PLING, 1.0f, 1.0f); + } + } + } + + if (gameTimeTicks >= 24000) { + Bukkit.broadcastMessage("§c§lTIME UP! §eThe game has ended in a draw."); + endGame(null); + cancel(); + } + } + }.runTaskTimer(plugin, 0L, 1L); + } + + /** + * Handles a player's death, respawn loop, or final elimination. + */ + public void handlePlayerDeath(Player player, Player killer) { + BedwarsTeam team = getPlayerTeam(player); + if (team == null) return; + + // Check if player died in the void + boolean inVoid = player.getLocation().getY() <= -20.0 || (player.getLastDamageCause() != null && player.getLastDamageCause().getCause() == EntityDamageEvent.DamageCause.VOID); + + // Handle tool levels and shears on death + plugin.getShopManager().handlePlayerDeath(player); + + // Drop player resources or transfer to killer if in void + if (inVoid && killer != null) { + giveResourcesToKiller(player, killer); + } else { + dropPlayerResources(player); + } + + // Determine death message + String deathMsg = getDeathMessage(player, killer, inVoid); + boolean isFinal = !hasBed(team); + String finalSuffix = isFinal ? " §c§lFINAL KILL!" : ""; + Bukkit.broadcastMessage("§7" + deathMsg + finalSuffix); + + // Clear combat tracking + clearCombatTag(player); + + // Record stats + if (killer != null) { + addKill(killer); + killer.sendMessage("§eYou killed " + team.getColor() + player.getName() + "!"); + killer.playSound(killer.getLocation(), Sound.ENTITY_EXPERIENCE_ORB_PICKUP, 0.8f, 1.4f); + } + + // Check if player has bed + if (hasBed(team)) { + // Can respawn! + startRespawnSequence(player, team); + } else { + // Eliminated! + eliminatePlayer(player); + checkWinner(); + } + } + + public void handlePlayerQuit(Player player) { + if (state != GameState.PLAYING) return; + if (isSpectator(player) || eliminatedPlayers.contains(player.getUniqueId())) return; + + BedwarsTeam team = getPlayerTeam(player); + if (team == null) return; + + // Eliminate player + eliminatedPlayers.add(player.getUniqueId()); + spectators.add(player.getUniqueId()); + + // Broadcast quit final kill/elimination + Bukkit.broadcastMessage(team.getColor() + player.getName() + " §c§lquit and was eliminated!"); + + // Drop player resources at their last location + dropPlayerResources(player); + + // Check if team is eliminated and check winner + checkWinner(); + } + + public String getFormattedName(Player player) { + BedwarsTeam team = getPlayerTeam(player); + if (team != null) { + return team.getColor() + player.getName() + "§7"; + } + return "§7" + player.getName(); + } + + private String getDeathMessage(Player player, Player killer, boolean inVoid) { + String victimName = getFormattedName(player); + + if (killer != null) { + String killerName = getFormattedName(killer); + LastDamageType type = lastDamageType.getOrDefault(player.getUniqueId(), LastDamageType.OTHER); + + if (inVoid) { + switch (type) { + case FIREBALL: + return victimName + " was fireballed into the void by " + killerName + "."; + case TNT: + return victimName + " was blown into the void by " + killerName + "."; + case PROJECTILE: + return victimName + " was shot into the void by " + killerName + "."; + case MELEE: + default: + return victimName + " was knocked into the void by " + killerName + "."; + } + } else { + switch (type) { + case FIREBALL: + return victimName + " was fireballed by " + killerName + "."; + case TNT: + return victimName + " was blown up by " + killerName + "."; + case PROJECTILE: + return victimName + " was shot by " + killerName + "."; + case MELEE: + default: + return victimName + " was slain by " + killerName + "."; + } + } + } else { + if (inVoid) { + return victimName + " fell into the void."; + } else { + EntityDamageEvent lastDamage = player.getLastDamageCause(); + if (lastDamage != null) { + EntityDamageEvent.DamageCause cause = lastDamage.getCause(); + if (cause == EntityDamageEvent.DamageCause.FALL) { + return victimName + " fell to their death."; + } else if (cause == EntityDamageEvent.DamageCause.BLOCK_EXPLOSION || cause == EntityDamageEvent.DamageCause.ENTITY_EXPLOSION) { + return victimName + " was blown up."; + } + } + return victimName + " died."; + } + } + } + + private void dropPlayerResources(Player player) { + Location loc = player.getLocation(); + for (ItemStack item : player.getInventory().getContents()) { + if (item != null) { + Material type = item.getType(); + if (type == Material.IRON_INGOT || type == Material.GOLD_INGOT || + type == Material.DIAMOND || type == Material.EMERALD) { + loc.getWorld().dropItemNaturally(loc, item.clone()); + } + } + } + player.getInventory().clear(); + } + + private void giveResourcesToKiller(Player player, Player killer) { + for (ItemStack item : player.getInventory().getContents()) { + if (item != null) { + Material type = item.getType(); + if (type == Material.IRON_INGOT || type == Material.GOLD_INGOT || + type == Material.DIAMOND || type == Material.EMERALD) { + // Give directly to killer's inventory + HashMap remaining = killer.getInventory().addItem(item.clone()); + if (!remaining.isEmpty()) { + // Drop any items that don't fit at killer's feet + for (ItemStack overflow : remaining.values()) { + killer.getWorld().dropItemNaturally(killer.getLocation(), overflow); + } + } + // Send informative message to killer + killer.sendMessage("§aReceived " + item.getAmount() + "x " + formatMaterialName(type) + " §afrom " + player.getName() + "'s void death!"); + } + } + } + player.getInventory().clear(); + } + + private String formatMaterialName(Material material) { + switch (material) { + case IRON_INGOT: + return "§fIron"; + case GOLD_INGOT: + return "§eGold"; + case DIAMOND: + return "§bDiamond"; + case EMERALD: + return "§aEmerald"; + default: + return material.name(); + } + } + + public void setLastAttacker(Player victim, Player attacker, LastDamageType type) { + lastAttacker.put(victim.getUniqueId(), attacker.getUniqueId()); + lastAttackTime.put(victim.getUniqueId(), System.currentTimeMillis()); + lastDamageType.put(victim.getUniqueId(), type); + } + + public Player getLastAttacker(Player player) { + UUID victimId = player.getUniqueId(); + if (lastAttacker.containsKey(victimId)) { + long time = lastAttackTime.getOrDefault(victimId, 0L); + if (System.currentTimeMillis() - time <= 15000) { // 15 seconds combat window + UUID attackerId = lastAttacker.get(victimId); + return Bukkit.getPlayer(attackerId); + } + } + return null; + } + + public void clearCombatTag(Player player) { + lastAttacker.remove(player.getUniqueId()); + lastAttackTime.remove(player.getUniqueId()); + lastDamageType.remove(player.getUniqueId()); + } + + private void startRespawnSequence(Player player, BedwarsTeam team) { + spectators.add(player.getUniqueId()); + player.setGameMode(GameMode.SPECTATOR); + player.teleport(player.getLocation().clone().add(0, 5, 0)); + updateTabName(player); + + BukkitTask task = new BukkitRunnable() { + int time = 5; + + @Override + public void run() { + if (state != GameState.PLAYING || !player.isOnline()) { + cancel(); + return; + } + + if (time <= 0) { + respawnPlayer(player, team); + cancel(); + return; + } + + player.sendTitle("§cYOU DIED!", "§eRespawning in §a" + time + "s", 0, 22, 0); + player.playSound(player.getLocation(), Sound.UI_BUTTON_CLICK, 0.8f, 1.2f); + time--; + } + }.runTaskTimer(plugin, 0L, 20L); + + respawnTasks.add(task); + } + + private void respawnPlayer(Player player, BedwarsTeam team) { + spectators.remove(player.getUniqueId()); + player.setGameMode(GameMode.SURVIVAL); + player.setHealth(20.0); + player.setFoodLevel(20); + player.setSaturation(20.0f); + player.getInventory().clear(); + + // Clear potion effects + for (org.bukkit.potion.PotionEffect effect : player.getActivePotionEffects()) { + player.removePotionEffect(effect.getType()); + } + + // Hide all active spectators from this player + hideAllSpectatorsFrom(player); + + // Gear + giveStartingGear(player); + + // Apply passive team upgrades (Haste, Sharpness, Protection) + plugin.getUpgradesManager().applyTeamUpgrades(player); + + teleportToTeamSpawn(player, team); + updateTabName(player); + player.sendTitle("§a§lRESPAWNED!", "§fGo get them!", 5, 20, 5); + player.playSound(player.getLocation(), Sound.ENTITY_PLAYER_LEVELUP, 0.8f, 1.5f); + } + + private void eliminatePlayer(Player player) { + spectators.add(player.getUniqueId()); + eliminatedPlayers.add(player.getUniqueId()); + player.setGameMode(GameMode.CREATIVE); + player.getInventory().clear(); + giveSpectatorItems(player); + hideSpectator(player); + updateTabName(player); + player.sendTitle("§c§lELIMINATED!", "§7You have no respawns left!", 10, 40, 10); + player.playSound(player.getLocation(), Sound.ENTITY_LIGHTNING_BOLT_THUNDER, 0.8f, 0.8f); + } + + public void addSpectator(Player player) { + spectators.add(player.getUniqueId()); + player.setGameMode(GameMode.CREATIVE); + player.getInventory().clear(); + giveSpectatorItems(player); + hideSpectator(player); + updateTabName(player); + plugin.getScoreboardManager().updateAll(); + } + + public void giveSpectatorItems(Player player) { + player.getInventory().clear(); + + ItemStack compass = new ItemStack(Material.COMPASS); + org.bukkit.inventory.meta.ItemMeta m1 = compass.getItemMeta(); + if (m1 != null) { + m1.setDisplayName("§a§lTeleporter §7(Right Click)"); + compass.setItemMeta(m1); + } + player.getInventory().setItem(0, compass); + + ItemStack bed = new ItemStack(Material.RED_BED); + org.bukkit.inventory.meta.ItemMeta m2 = bed.getItemMeta(); + if (m2 != null) { + m2.setDisplayName("§c§lReturn to Lobby §7(Right Click)"); + bed.setItemMeta(m2); + } + player.getInventory().setItem(8, bed); + } + + public void hideSpectator(Player player) { + for (Player p : Bukkit.getOnlinePlayers()) { + if (!isSpectator(p)) { + p.hidePlayer(plugin, player); + } + } + } + + public void hideAllSpectatorsFrom(Player alivePlayer) { + for (UUID specId : spectators) { + Player spec = Bukkit.getPlayer(specId); + if (spec != null && spec.isOnline()) { + alivePlayer.hidePlayer(plugin, spec); + } + } + } + + public boolean isTeamEnabled(BedwarsTeam team) { + return plugin.getConfig().getBoolean("locations.teams." + team.getName().toLowerCase() + ".enabled", false); + } + + public int getAliveTeamCount(BedwarsTeam team) { + int alive = 0; + for (Player p : Bukkit.getOnlinePlayers()) { + if (getPlayerTeam(p) == team && !eliminatedPlayers.contains(p.getUniqueId())) { + alive++; + } + } + return alive; + } + + /** + * Checks if only one team is left alive. + */ + public void checkWinner() { + if (state != GameState.PLAYING) return; + + List activeTeams = new ArrayList<>(); + ConfigurationSection teamsSec = plugin.getConfig().getConfigurationSection("locations.teams"); + if (teamsSec != null) { + for (String key : teamsSec.getKeys(false)) { + if (teamsSec.getBoolean(key + ".enabled", false)) { + activeTeams.add(BedwarsTeam.valueOf(key.toUpperCase())); + } + } + } + + // Check for team eliminations (if getAliveTeamCount(team) == 0, even if there is a bed, the team is eliminated!) + for (BedwarsTeam team : activeTeams) { + if (!eliminatedTeams.contains(team)) { + if (getAliveTeamCount(team) == 0) { + eliminatedTeams.add(team); + teamBeds.put(team, false); // No bed active if team has no alive players! + Bukkit.broadcastMessage("§r"); + Bukkit.broadcastMessage("§d§lTEAM ELIMINATED! §fTeam " + team.getColor() + team.name() + " §fhas been §c§lELIMINATED§f!"); + Bukkit.broadcastMessage("§r"); + } + } + } + + BedwarsTeam winningTeam = null; + int activeTeamCount = 0; + + for (BedwarsTeam team : activeTeams) { + // A team is active if they have at least one alive player + if (getAliveTeamCount(team) > 0) { + activeTeamCount++; + winningTeam = team; + } + } + + // If there is only one player online, do not end the game so they can test features alone! + if (Bukkit.getOnlinePlayers().size() > 1) { + if (activeTeamCount <= 1) { + endGame(winningTeam); + } + } + } + + private BedwarsTeam winnerTeam = null; + + public BedwarsTeam getWinnerTeam() { + return winnerTeam; + } + + /** + * Game Over, show winner, celebrate, redirect to proxy lobby server, reset world. + */ + public void endGame(BedwarsTeam winner) { + setState(GameState.ENDING); + this.winnerTeam = winner; + + if (winner != null) { + Bukkit.broadcastMessage(" §b§lVICTORY!"); + Bukkit.broadcastMessage(" " + winner.getColorizedName() + " §fhas won the match!"); + + for (Player p : Bukkit.getOnlinePlayers()) { + p.sendTitle("§6§lVICTORY!", winner.getColorizedName() + " §fhas won!", 10, 80, 10); + p.playSound(p.getLocation(), Sound.UI_TOAST_CHALLENGE_COMPLETE, 0.8f, 1.0f); + } + + // Spawn fireworks + String colorName = winner.getName().toLowerCase(); + String worldName = plugin.getConfig().getString("world-name", "bedwars"); + World w = Bukkit.getWorld(worldName); + if (w != null && plugin.getConfig().contains("locations.teams." + colorName + ".spawn")) { + double x = plugin.getConfig().getDouble("locations.teams." + colorName + ".spawn.x"); + double y = plugin.getConfig().getDouble("locations.teams." + colorName + ".spawn.y"); + double z = plugin.getConfig().getDouble("locations.teams." + colorName + ".spawn.z"); + Location spawnLoc = new Location(w, x, y + 2, z); + + new BukkitRunnable() { + int fireworkCount = 10; + @Override + public void run() { + if (fireworkCount <= 0 || state != GameState.ENDING) { + cancel(); + return; + } + + Firework fw = spawnLoc.getWorld().spawn(spawnLoc.clone().add(Math.random()*4-2, 0, Math.random()*4-2), Firework.class); + FireworkMeta meta = fw.getFireworkMeta(); + meta.addEffect(FireworkEffect.builder() + .withColor(winner.getColor() == ChatColor.RED ? Color.RED : winner.getColor() == ChatColor.BLUE ? Color.BLUE : winner.getColor() == ChatColor.DARK_PURPLE ? Color.PURPLE : Color.YELLOW) + .with(FireworkEffect.Type.BALL_LARGE) + .flicker(true) + .trail(true) + .build()); + meta.setPower(1); + fw.setFireworkMeta(meta); + fireworkCount--; + } + }.runTaskTimer(plugin, 0L, 10L); + } + } else { + Bukkit.broadcastMessage("§cThe game ended in a draw!"); + } + + // Start 10 seconds ending countdown before unloading and resetting world + new BukkitRunnable() { + int timeLeft = 10; + + @Override + public void run() { + if (timeLeft <= 0) { + // Kick all players to trigger proxy redirection + for (Player p : Bukkit.getOnlinePlayers()) { + p.kickPlayer("§a§lGAME OVER!\n\n§fThe game has ended.\n§eWinner: " + (winner != null ? winner.getColorizedName() : "§cDraw") + "\n\n§bThank you for playing!"); + } + + // Schedule world reset shortly after kicking so connection is fully released + Bukkit.getScheduler().runTaskLater(plugin, () -> { + // Cleanup generators and NPCs + plugin.getGeneratorManager().cleanup(); + + // Reset the world map + plugin.getArenaManager().resetWorld(); + + // Cancel all tasks + for (BukkitTask task : respawnTasks) { + task.cancel(); + } + respawnTasks.clear(); + + if (gameLoopTask != null) { + gameLoopTask.cancel(); + } + + // Reinitialize variables + resetStats(); + winnerTeam = null; + eliminatedTeams.clear(); + setState(GameState.LOBBY); + + // Check if players are already inside waiting lobby (in case they joined during reset) + checkStart(); + }, 40L); + + cancel(); + return; + } + + if (timeLeft <= 5) { + Bukkit.broadcastMessage("§eGame closing and resetting in §a" + timeLeft + " §eseconds..."); + for (Player p : Bukkit.getOnlinePlayers()) { + p.playSound(p.getLocation(), Sound.BLOCK_NOTE_BLOCK_PLING, 1.0f, 1.2f); + } + } + timeLeft--; + } + }.runTaskTimer(plugin, 0L, 20L); + } + + /** + * Sends players back to the configured lobby proxy server. + */ + public void sendToLobby(Player player) { + String serverName = plugin.getConfig().getString("lobby-server", "lobby"); + player.sendMessage("§b§lNEXORIA §fsending you back to the lobby..."); + try { + // 1. Send standard BungeeCord packets + com.google.common.io.ByteArrayDataOutput bungeeOut = com.google.common.io.ByteStreams.newDataOutput(); + bungeeOut.writeUTF("Connect"); + bungeeOut.writeUTF(serverName); + player.sendPluginMessage(plugin, "BungeeCord", bungeeOut.toByteArray()); + player.sendPluginMessage(plugin, "bungeecord:main", bungeeOut.toByteArray()); + + // 2. Send custom nexoria direct channel packet + com.google.common.io.ByteArrayDataOutput nexoriaOut = com.google.common.io.ByteStreams.newDataOutput(); + nexoriaOut.writeUTF("Connect"); + nexoriaOut.writeUTF(player.getName()); + nexoriaOut.writeUTF(serverName); + player.sendPluginMessage(plugin, "nexoria:main", nexoriaOut.toByteArray()); + } catch (Exception e) { + player.sendMessage("§cFailed to redirect to proxy lobby: " + e.getMessage()); + } + } + + public void resetGameImmediately() { + // Cleanup generators and NPCs + plugin.getGeneratorManager().cleanup(); + + // Reset the world map + plugin.getArenaManager().resetWorld(); + + // Cancel all tasks + for (BukkitTask task : respawnTasks) { + task.cancel(); + } + respawnTasks.clear(); + + if (gameLoopTask != null) { + gameLoopTask.cancel(); + } + + // Reinitialize variables + resetStats(); + winnerTeam = null; + eliminatedTeams.clear(); + setState(GameState.LOBBY); + countingDown = false; + if (countdownTask != null) { + countdownTask.cancel(); + } + } +} diff --git a/src/main/java/com/bedwars/GameState.java b/src/main/java/com/bedwars/GameState.java new file mode 100644 index 0000000..e84b983 --- /dev/null +++ b/src/main/java/com/bedwars/GameState.java @@ -0,0 +1,7 @@ +package com.bedwars; + +public enum GameState { + LOBBY, + PLAYING, + ENDING +} diff --git a/src/main/java/com/bedwars/Generator.java b/src/main/java/com/bedwars/Generator.java new file mode 100644 index 0000000..ab8d784 --- /dev/null +++ b/src/main/java/com/bedwars/Generator.java @@ -0,0 +1,191 @@ +package com.bedwars; + +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.Particle; +import org.bukkit.Sound; +import org.bukkit.entity.Item; +import org.bukkit.entity.TextDisplay; +import org.bukkit.inventory.ItemStack; +import org.bukkit.util.Vector; + +public class Generator { + + private final Location location; + private final GeneratorType type; + private int ticksUntilSpawn; + private final int maxSpawnTicks; + private int upgradeLevel = 1; // Used for base generator upgrades + private TextDisplay textDisplay; + + public Generator(Location location, GeneratorType type, int maxSpawnTicks) { + this.location = location; + this.type = type; + this.maxSpawnTicks = maxSpawnTicks; + this.ticksUntilSpawn = maxSpawnTicks; + spawnHologram(); + } + + private void spawnHologram() { + if (type == GeneratorType.IRON_GOLD) { + return; // No hologram needed for base spawners + } + + // Spawn a TextDisplay entity 2 blocks above the generator, perfectly centered + double cx = Math.floor(location.getX()) + 0.5; + double cy = location.getY() + 2.0; + double cz = Math.floor(location.getZ()) + 0.5; + Location displayLoc = new Location(location.getWorld(), cx, cy, cz); + if (displayLoc.getWorld() != null) { + // Delete any existing text display entities nearby first to avoid duplicates + displayLoc.getWorld().getNearbyEntities(displayLoc, 1, 2, 1).forEach(entity -> { + if (entity instanceof TextDisplay) { + entity.remove(); + } + }); + + textDisplay = displayLoc.getWorld().spawn(displayLoc, TextDisplay.class); + textDisplay.setBillboard(TextDisplay.Billboard.CENTER); + textDisplay.setGravity(false); + textDisplay.setInvulnerable(true); + textDisplay.setPersistent(false); + + // Set transparent background + textDisplay.setBackgroundColor(org.bukkit.Color.fromARGB(0, 0, 0, 0)); + updateHologramText(); + } + } + + public void updateHologramText() { + if (textDisplay == null || !textDisplay.isValid()) return; + + double secondsLeft = ticksUntilSpawn / 20.0; + String color = type == GeneratorType.DIAMOND ? "§b§l" : "§a§l"; + String name = type.getDisplayName().toUpperCase(); + + String text = color + name + " GENERATOR\n" + + "§eSpawning in §c" + String.format("%.1f", secondsLeft) + "s\n" + + "§fLevel " + upgradeLevel; + + textDisplay.setText(text); + } + + public void tick() { + // Handle Base Spawner (Iron and Gold) + if (type == GeneratorType.IRON_GOLD) { + ticksUntilSpawn--; + if (ticksUntilSpawn <= 0) { + spawnItem(Material.IRON_INGOT); + + // Spawn Gold at 1/4 the rate (upgrade increases rate) + if (Math.random() < (0.25 * upgradeLevel)) { + spawnItem(Material.GOLD_INGOT); + } + + // Reset with upgrade level modifier (faster spawns) + ticksUntilSpawn = Math.max(5, maxSpawnTicks - (upgradeLevel - 1) * 3); + } + return; + } + + // Handle Diamond / Emerald Spawners + ticksUntilSpawn--; + if (ticksUntilSpawn % 5 == 0) { + updateHologramText(); + } + + if (ticksUntilSpawn <= 0) { + // Spawn resource item + spawnItem(type.getMaterial()); + + // Sparkle effect + if (location.getWorld() != null) { + location.getWorld().spawnParticle( + type == GeneratorType.DIAMOND ? Particle.GLOW : Particle.HAPPY_VILLAGER, + location.clone().add(0.5, 0.5, 0.5), 15, 0.2, 0.2, 0.2, 0.05 + ); + + // Premium chime sound + location.getWorld().playSound( + location, Sound.ENTITY_EXPERIENCE_ORB_PICKUP, 0.5f, 1.2f + ); + } + + ticksUntilSpawn = maxSpawnTicks; + updateHologramText(); + } + } + + private int getExistingItemCount(Material material) { + if (location.getWorld() == null) return 0; + int count = 0; + double radius = 1.5; + for (org.bukkit.entity.Entity entity : location.getWorld().getNearbyEntities(location.clone().add(0.5, 0.5, 0.5), radius, radius, radius)) { + if (entity instanceof Item itemEntity) { + ItemStack stack = itemEntity.getItemStack(); + if (stack.getType() == material) { + count += stack.getAmount(); + } + } + } + return count; + } + + private int getMaxLimit(Material material) { + switch (material) { + case EMERALD: return 4; + case DIAMOND: return 8; + case GOLD_INGOT: return 12; + case IRON_INGOT: return 48; + default: return Integer.MAX_VALUE; + } + } + + private void spawnItem(Material material) { + if (location.getWorld() == null) return; + + int limit = getMaxLimit(material); + int currentCount = getExistingItemCount(material); + if (currentCount >= limit) { + return; // Cap reached, do not drop item + } + + double cx = Math.floor(location.getX()) + 0.5; + double cy = location.getY() + 0.5; + double cz = Math.floor(location.getZ()) + 0.5; + Location spawnLoc = new Location(location.getWorld(), cx, cy, cz); + Item item = location.getWorld().dropItem(spawnLoc, new ItemStack(material, 1)); + + // Zero out initial velocity so items drop cleanly on the spawner + item.setVelocity(new Vector(0, 0, 0)); + + // Mark this item as spawned by a generator to prevent duplication when dropped by a player + BedwarsPlugin plugin = BedwarsPlugin.getInstance(); + if (plugin != null) { + org.bukkit.NamespacedKey key = new org.bukkit.NamespacedKey(plugin, "generator_spawned"); + item.getPersistentDataContainer().set(key, org.bukkit.persistence.PersistentDataType.BYTE, (byte) 1); + } + } + + public void remove() { + if (textDisplay != null && textDisplay.isValid()) { + textDisplay.remove(); + } + } + + public Location getLocation() { + return location; + } + + public GeneratorType getType() { + return type; + } + + public int getUpgradeLevel() { + return upgradeLevel; + } + + public void setUpgradeLevel(int upgradeLevel) { + this.upgradeLevel = upgradeLevel; + } +} diff --git a/src/main/java/com/bedwars/GeneratorManager.java b/src/main/java/com/bedwars/GeneratorManager.java new file mode 100644 index 0000000..85a7d13 --- /dev/null +++ b/src/main/java/com/bedwars/GeneratorManager.java @@ -0,0 +1,131 @@ +package com.bedwars; + +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.configuration.ConfigurationSection; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class GeneratorManager { + + private final BedwarsPlugin plugin; + private final List activeGenerators = new ArrayList<>(); + + public GeneratorManager(BedwarsPlugin plugin) { + this.plugin = plugin; + } + + /** + * Loads generators from the plugin config and activates them in the world. + */ + public void loadGenerators() { + cleanup(); // Clean up existing first + + String worldName = plugin.getConfig().getString("world-name", "bedwars"); + World world = Bukkit.getWorld(worldName); + if (world == null) { + plugin.getLogger().warning("Cannot load generators: World '" + worldName + "' is not loaded!"); + return; + } + + int ironRate = plugin.getConfig().getInt("rates.iron", 20) * 2; + int goldRate = plugin.getConfig().getInt("rates.gold", 80) * 2; + int diamondRate = plugin.getConfig().getInt("rates.diamond", 600) * 2; + int emeraldRate = plugin.getConfig().getInt("rates.emerald", 1200) * 2; + + // Load map-wide generators (Diamond & Emerald) from the list in config + List> generatorList = plugin.getConfig().getMapList("locations.generators"); + for (Map genMap : generatorList) { + try { + String typeStr = (String) genMap.get("type"); + double x = ((Number) genMap.get("x")).doubleValue(); + double y = ((Number) genMap.get("y")).doubleValue(); + double z = ((Number) genMap.get("z")).doubleValue(); + + GeneratorType type = GeneratorType.valueOf(typeStr.toUpperCase()); + Location loc = new Location(world, x, y, z); + + int ticks = type == GeneratorType.DIAMOND ? diamondRate : emeraldRate; + activeGenerators.add(new Generator(loc, type, ticks)); + } catch (Exception e) { + plugin.getLogger().severe("Failed to parse generator: " + e.getMessage()); + } + } + + // Load base generators for active teams + ConfigurationSection teamsSec = plugin.getConfig().getConfigurationSection("locations.teams"); + if (teamsSec != null) { + for (String key : teamsSec.getKeys(false)) { + ConfigurationSection teamSec = teamsSec.getConfigurationSection(key); + if (teamSec != null && teamSec.getBoolean("enabled", false)) { + if (teamSec.contains("generator")) { + try { + double x = teamSec.getDouble("generator.x"); + double y = teamSec.getDouble("generator.y"); + double z = teamSec.getDouble("generator.z"); + Location loc = new Location(world, x, y, z); + + // Base generator produces Iron and Gold + activeGenerators.add(new Generator(loc, GeneratorType.IRON_GOLD, ironRate)); + } catch (Exception e) { + plugin.getLogger().severe("Failed to load base generator for team " + key + ": " + e.getMessage()); + } + } + } + } + } + + plugin.getLogger().info("Loaded " + activeGenerators.size() + " Bedwars resource generators!"); + } + + /** + * Ticks all active generators. Should be run every tick. + */ + public void tick() { + for (Generator gen : activeGenerators) { + gen.tick(); + } + } + + /** + * Upgrades base generator rates for a specific team. + */ + public void upgradeTeamGenerator(BedwarsTeam team, int level) { + String worldName = plugin.getConfig().getString("world-name", "bedwars"); + World world = Bukkit.getWorld(worldName); + if (world == null) return; + + double targetX = plugin.getConfig().getDouble("locations.teams." + team.getName().toLowerCase() + ".generator.x"); + double targetY = plugin.getConfig().getDouble("locations.teams." + team.getName().toLowerCase() + ".generator.y"); + double targetZ = plugin.getConfig().getDouble("locations.teams." + team.getName().toLowerCase() + ".generator.z"); + + for (Generator gen : activeGenerators) { + if (gen.getType() == GeneratorType.IRON_GOLD) { + Location loc = gen.getLocation(); + if (loc.getWorld() != null && loc.getWorld().equals(world) && + Math.abs(loc.getX() - targetX) < 1.0 && + Math.abs(loc.getY() - targetY) < 1.0 && + Math.abs(loc.getZ() - targetZ) < 1.0) { + gen.setUpgradeLevel(level); + } + } + } + } + + /** + * Removes all active generators and deletes their hologram entities. + */ + public void cleanup() { + for (Generator gen : activeGenerators) { + gen.remove(); + } + activeGenerators.clear(); + } + + public List getActiveGenerators() { + return activeGenerators; + } +} diff --git a/src/main/java/com/bedwars/GeneratorType.java b/src/main/java/com/bedwars/GeneratorType.java new file mode 100644 index 0000000..880edc0 --- /dev/null +++ b/src/main/java/com/bedwars/GeneratorType.java @@ -0,0 +1,32 @@ +package com.bedwars; + +import org.bukkit.ChatColor; +import org.bukkit.Material; + +public enum GeneratorType { + IRON_GOLD("Iron & Gold", ChatColor.GRAY, Material.IRON_INGOT), + DIAMOND("Diamond", ChatColor.AQUA, Material.DIAMOND), + EMERALD("Emerald", ChatColor.GREEN, Material.EMERALD); + + private final String displayName; + private final ChatColor color; + private final Material material; + + GeneratorType(String displayName, ChatColor color, Material material) { + this.displayName = displayName; + this.color = color; + this.material = material; + } + + public String getDisplayName() { + return displayName; + } + + public ChatColor getColor() { + return color; + } + + public Material getMaterial() { + return material; + } +} diff --git a/src/main/java/com/bedwars/LastDamageType.java b/src/main/java/com/bedwars/LastDamageType.java new file mode 100644 index 0000000..64dfe43 --- /dev/null +++ b/src/main/java/com/bedwars/LastDamageType.java @@ -0,0 +1,9 @@ +package com.bedwars; + +public enum LastDamageType { + MELEE, + PROJECTILE, + FIREBALL, + TNT, + OTHER +} diff --git a/src/main/java/com/bedwars/ScoreboardManager.java b/src/main/java/com/bedwars/ScoreboardManager.java new file mode 100644 index 0000000..d2fea76 --- /dev/null +++ b/src/main/java/com/bedwars/ScoreboardManager.java @@ -0,0 +1,159 @@ +package com.bedwars; + +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.entity.Player; +import org.bukkit.scoreboard.*; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +public class ScoreboardManager { + + private final BedwarsPlugin plugin; + private final Map playerScoreboards = new HashMap<>(); + + public ScoreboardManager(BedwarsPlugin plugin) { + this.plugin = plugin; + } + + public void setupScoreboard(Player player) { + org.bukkit.scoreboard.ScoreboardManager scoreboardManager = Bukkit.getScoreboardManager(); + if (scoreboardManager == null) return; + + Scoreboard board = scoreboardManager.getNewScoreboard(); + Objective objective = board.registerNewObjective("bedwars", "dummy", "§e§lBEDWARS"); + objective.setDisplaySlot(DisplaySlot.SIDEBAR); + + player.setScoreboard(board); + playerScoreboards.put(player.getUniqueId(), board); + updateScoreboard(player); + } + + public void updateScoreboard(Player player) { + Scoreboard board = playerScoreboards.get(player.getUniqueId()); + if (board == null) return; + + GameManager gm = plugin.getGameManager(); + + // Register Bedwars teams and spectator team on this scoreboard for name tag and tab list colors + for (BedwarsTeam t : BedwarsTeam.values()) { + Team scoreTeam = board.getTeam(t.getName()); + if (scoreTeam == null) { + scoreTeam = board.registerNewTeam(t.getName()); + } + scoreTeam.setColor(t.getColor()); + scoreTeam.setPrefix(t.getColor().toString()); + scoreTeam.setOption(Team.Option.COLLISION_RULE, Team.OptionStatus.NEVER); + + // Clear existing entries to prevent stale data + for (String entry : new java.util.ArrayList<>(scoreTeam.getEntries())) { + scoreTeam.removeEntry(entry); + } + } + + Team specTeam = board.getTeam("Spectators"); + if (specTeam == null) { + specTeam = board.registerNewTeam("Spectators"); + } + specTeam.setColor(ChatColor.GRAY); + specTeam.setPrefix("§7"); + specTeam.setOption(Team.Option.COLLISION_RULE, Team.OptionStatus.NEVER); + for (String entry : new java.util.ArrayList<>(specTeam.getEntries())) { + specTeam.removeEntry(entry); + } + + // Add all online players to their respective teams on this scoreboard + for (Player p : Bukkit.getOnlinePlayers()) { + if (gm.isSpectator(p)) { + specTeam.addEntry(p.getName()); + } else { + BedwarsTeam pTeam = gm.getPlayerTeam(p); + if (pTeam != null) { + Team scoreTeam = board.getTeam(pTeam.getName()); + if (scoreTeam != null) { + scoreTeam.addEntry(p.getName()); + } + } + } + } + + Objective objective = board.getObjective("bedwars"); + if (objective == null) return; + + // Clear existing scores by unregistering and re-registering to ensure clean slate + objective.unregister(); + objective = board.registerNewObjective("bedwars", "dummy", "§e§lBEDWARS"); + objective.setDisplaySlot(DisplaySlot.SIDEBAR); + + int line = 15; + setLine(objective, "§7" + java.time.LocalDate.now().format(java.time.format.DateTimeFormatter.ofPattern("dd/MM/yy")), line--); + setLine(objective, " ", line--); + + GameState state = gm.getState(); + + if (state == GameState.LOBBY) { + setLine(objective, "§fPlayers: §a" + Bukkit.getOnlinePlayers().size() + "§7/§a" + Bukkit.getMaxPlayers(), line--); + setLine(objective, " ", line--); + if (gm.isCountingDown()) { + setLine(objective, "§fStarting in: §a" + gm.getCountdownTime() + "s", line--); + } else { + setLine(objective, "§fWaiting for players...", line--); + } + } else if (state == GameState.PLAYING) { + setLine(objective, "§fMap: §a" + plugin.getConfig().getString("world-name", "bedwars"), line--); + setLine(objective, " ", line--); + + // Display team statuses + for (BedwarsTeam team : BedwarsTeam.values()) { + if (gm.isTeamEnabled(team)) { + String status; + if (gm.hasBed(team)) { + status = "§a✔"; + } else { + int alive = gm.getAliveTeamCount(team); + if (alive > 0) { + status = "§e" + alive; + } else { + status = "§c✘"; + } + } + setLine(objective, team.getColor() + team.getName().substring(0, 1) + " §f" + team.getName() + ": " + status, line--); + } + } + + setLine(objective, " ", line--); + setLine(objective, "§fKills: §a" + gm.getKills(player), line--); + setLine(objective, "§fBeds Broken: §a" + gm.getBedsBroken(player), line--); + } else if (state == GameState.ENDING) { + setLine(objective, "§a§lGAME OVER!", line--); + setLine(objective, " ", line--); + BedwarsTeam winner = gm.getWinnerTeam(); + if (winner != null) { + setLine(objective, "§fWinner: " + winner.getColorizedName(), line--); + } else { + setLine(objective, "§fWinner: §7None", line--); + } + } + + setLine(objective, " ", line--); + setLine(objective, "§dnexoria", line--); + } + + public void removeScoreboard(Player player) { + playerScoreboards.remove(player.getUniqueId()); + player.setScoreboard(Bukkit.getScoreboardManager().getMainScoreboard()); + } + + public void updateAll() { + for (Player p : Bukkit.getOnlinePlayers()) { + updateScoreboard(p); + } + } + + private void setLine(Objective obj, String text, int scoreValue) { + Score score = obj.getScore(text); + score.setScore(scoreValue); + } +} diff --git a/src/main/java/com/bedwars/ShopListener.java b/src/main/java/com/bedwars/ShopListener.java new file mode 100644 index 0000000..4142771 --- /dev/null +++ b/src/main/java/com/bedwars/ShopListener.java @@ -0,0 +1,107 @@ +package com.bedwars; + +import org.bukkit.Sound; +import org.bukkit.entity.Player; +import org.bukkit.entity.Villager; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.event.inventory.InventoryDragEvent; +import org.bukkit.event.player.PlayerInteractEntityEvent; + +public class ShopListener implements Listener { + + private final BedwarsPlugin plugin; + + public ShopListener(BedwarsPlugin plugin) { + this.plugin = plugin; + } + + @EventHandler + public void onPlayerInteractEntity(PlayerInteractEntityEvent event) { + if (!(event.getRightClicked() instanceof Villager villager)) return; + + String name = villager.getCustomName(); + if (name == null) return; + + Player player = event.getPlayer(); + GameManager gm = plugin.getGameManager(); + + if (gm.getState() != GameState.PLAYING || gm.isSpectator(player)) { + event.setCancelled(true); + return; + } + + if (name.contains("ITEM SHOP")) { + event.setCancelled(true); + plugin.getShopManager().openShop(player, "blocks"); + player.playSound(player.getLocation(), Sound.ENTITY_VILLAGER_YES, 0.8f, 1.2f); + } else if (name.contains("TEAM UPGRADES")) { + event.setCancelled(true); + plugin.getUpgradesManager().openUpgradesGui(player); + player.playSound(player.getLocation(), Sound.ENTITY_VILLAGER_YES, 0.8f, 1.2f); + } + } + + @EventHandler + public void onInventoryClick(InventoryClickEvent event) { + if (!(event.getWhoClicked() instanceof Player player)) return; + + String title = event.getView().getTitle(); + + // 1. Handle Item Shop clicks + if (title.startsWith("§8Item Shop - ")) { + event.setCancelled(true); + + // Make sure the click is inside the top inventory + if (event.getClickedInventory() == null || event.getClickedInventory().equals(player.getInventory())) { + return; + } + + int slot = event.getSlot(); + String currentCategory = title.replace("§8Item Shop - ", "").toLowerCase(); + + // Clicked Category Switcher (slots 1 to 7) + if (slot >= 1 && slot <= 7) { + String[] categories = {"blocks", "weapons", "armor", "tools", "bows", "potions", "utility"}; + String targetCat = categories[slot - 1]; + if (!targetCat.equalsIgnoreCase(currentCategory)) { + plugin.getShopManager().openShop(player, targetCat); + player.playSound(player.getLocation(), Sound.UI_BUTTON_CLICK, 0.8f, 1.4f); + } + return; + } + + // Clicked item purchase area (slots 18 to 44) + if (slot >= 18 && slot <= 44) { + // Ensure they didn't click border pane slots + if (slot == 26 || slot == 27 || slot == 35 || slot == 36) return; + + plugin.getShopManager().purchaseItem(player, currentCategory, slot); + } + return; + } + + // 2. Handle Upgrades GUI clicks + if (title.equals("§8Team Upgrades")) { + event.setCancelled(true); + + if (event.getClickedInventory() == null || event.getClickedInventory().equals(player.getInventory())) { + return; + } + + int slot = event.getSlot(); + if (slot >= 10 && slot <= 13) { + plugin.getUpgradesManager().purchaseUpgrade(player, slot); + } + } + } + + @EventHandler + public void onInventoryDrag(InventoryDragEvent event) { + String title = event.getView().getTitle(); + if (title.startsWith("§8Item Shop - ") || title.equals("§8Team Upgrades")) { + event.setCancelled(true); + } + } +} diff --git a/src/main/java/com/bedwars/ShopManager.java b/src/main/java/com/bedwars/ShopManager.java new file mode 100644 index 0000000..3e7308d --- /dev/null +++ b/src/main/java/com/bedwars/ShopManager.java @@ -0,0 +1,790 @@ +package com.bedwars; + +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.Material; +import org.bukkit.Sound; +import org.bukkit.enchantments.Enchantment; +import org.bukkit.entity.Player; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.inventory.meta.PotionMeta; +import org.bukkit.potion.PotionEffect; +import org.bukkit.potion.PotionEffectType; + +import java.util.*; + +public class ShopManager { + + private final BedwarsPlugin plugin; + + // Track tool and armor levels for players + // 0 = None, 1 = Wood, 2 = Stone, 3 = Iron, 4 = Diamond + private final Map pickaxeLevels = new HashMap<>(); + private final Map axeLevels = new HashMap<>(); + private final Set hasShears = new HashSet<>(); + private final Map armorLevels = new HashMap<>(); + + public ShopManager(BedwarsPlugin plugin) { + this.plugin = plugin; + } + + public void resetPlayerShopData(Player player) { + UUID uuid = player.getUniqueId(); + pickaxeLevels.remove(uuid); + axeLevels.remove(uuid); + hasShears.remove(uuid); + armorLevels.remove(uuid); + } + + public void resetAllShopData() { + pickaxeLevels.clear(); + axeLevels.clear(); + hasShears.clear(); + armorLevels.clear(); + } + + public int getArmorTier(Player player) { + return armorLevels.getOrDefault(player.getUniqueId(), 0); + } + + public void equipArmorTier(Player player, int tier) { + Material legMat = Material.LEATHER_LEGGINGS; + Material bootMat = Material.LEATHER_BOOTS; + + if (tier == 1) { + legMat = Material.CHAINMAIL_LEGGINGS; + bootMat = Material.CHAINMAIL_BOOTS; + } else if (tier == 2) { + legMat = Material.IRON_LEGGINGS; + bootMat = Material.IRON_BOOTS; + } else if (tier == 3) { + legMat = Material.DIAMOND_LEGGINGS; + bootMat = Material.DIAMOND_BOOTS; + } + + ItemStack legs = new ItemStack(legMat); + ItemStack boots = new ItemStack(bootMat); + + ItemMeta m1 = legs.getItemMeta(); + ItemMeta m2 = boots.getItemMeta(); + if (m1 != null) { m1.setUnbreakable(true); legs.setItemMeta(m1); } + if (m2 != null) { m2.setUnbreakable(true); boots.setItemMeta(m2); } + + player.getInventory().setLeggings(legs); + player.getInventory().setBoots(boots); + + // Reapply team upgrades if present + plugin.getUpgradesManager().applyTeamUpgrades(player); + } + + public void handlePlayerDeath(Player player) { + UUID uuid = player.getUniqueId(); + // Bedwars rule: keep shears (do not remove), and degrade pickaxe/axe by one tier (keep wooden tier 1) + + if (pickaxeLevels.containsKey(uuid)) { + int current = pickaxeLevels.get(uuid); + if (current > 1) { + pickaxeLevels.put(uuid, current - 1); + } + } + + if (axeLevels.containsKey(uuid)) { + int current = axeLevels.get(uuid); + if (current > 1) { + axeLevels.put(uuid, current - 1); + } + } + } + + public boolean hasShears(Player player) { + return hasShears.contains(player.getUniqueId()); + } + + public int getPickaxeLevel(Player player) { + return pickaxeLevels.getOrDefault(player.getUniqueId(), 0); + } + + public int getAxeLevel(Player player) { + return axeLevels.getOrDefault(player.getUniqueId(), 0); + } + + public Material getPickaxeMaterial(int level) { + switch (level) { + case 1: return Material.WOODEN_PICKAXE; + case 2: return Material.STONE_PICKAXE; + case 3: return Material.IRON_PICKAXE; + case 4: return Material.DIAMOND_PICKAXE; + default: return null; + } + } + + public Material getAxeMaterial(int level) { + switch (level) { + case 1: return Material.WOODEN_AXE; + case 2: return Material.STONE_AXE; + case 3: return Material.IRON_AXE; + case 4: return Material.DIAMOND_AXE; + default: return null; + } + } + + public void giveTools(Player player) { + UUID uuid = player.getUniqueId(); + + // 1. Give shears if they have them + if (hasShears.contains(uuid)) { + ItemStack shears = new ItemStack(Material.SHEARS); + ItemMeta m = shears.getItemMeta(); + if (m != null) { + m.setUnbreakable(true); + shears.setItemMeta(m); + } + player.getInventory().addItem(shears); + } + + // 2. Give pickaxe if they have it + int pickLevel = pickaxeLevels.getOrDefault(uuid, 0); + if (pickLevel > 0) { + Material pickMat = getPickaxeMaterial(pickLevel); + if (pickMat != null) { + ItemStack pick = new ItemStack(pickMat); + ItemMeta m = pick.getItemMeta(); + if (m != null) { + m.setUnbreakable(true); + pick.setItemMeta(m); + } + player.getInventory().addItem(pick); + } + } + + // 3. Give axe if they have it + int axeLevel = axeLevels.getOrDefault(uuid, 0); + if (axeLevel > 0) { + Material axeMat = getAxeMaterial(axeLevel); + if (axeMat != null) { + ItemStack axe = new ItemStack(axeMat); + ItemMeta m = axe.getItemMeta(); + if (m != null) { + m.setUnbreakable(true); + axe.setItemMeta(m); + } + player.getInventory().addItem(axe); + } + } + } + + /** + * Opens the Bedwars Shop GUI for a player. + */ + public void openShop(Player player, String category) { + Inventory inv = Bukkit.createInventory(null, 54, "§8Item Shop - " + capitalize(category)); + + // 1. Fill categories row (Row 0) + setupCategories(inv, category); + + // 2. Fill separator row (Row 1) + for (int i = 9; i < 18; i++) { + inv.setItem(i, createGuiItem(Material.BLACK_STAINED_GLASS_PANE, "§r", null)); + } + + // 3. Load items from config for selected category + List itemsStr = plugin.getConfig().getStringList("shop." + category); + int slot = 19; // Start listing in middle rows + + for (String itemStr : itemsStr) { + // Check if slot falls out of boundaries or enters right border + if (slot == 26 || slot == 35 || slot == 44) slot += 2; // skip borders + + try { + ParsedShopItem shopItem = parseShopItem(itemStr, player); + if (shopItem != null) { + ItemStack displayStack = buildShopDisplayItem(shopItem, player); + inv.setItem(slot++, displayStack); + } + } catch (Exception e) { + plugin.getLogger().warning("Failed to parse shop item line: " + itemStr + " - Error: " + e.getMessage()); + } + } + + player.openInventory(inv); + } + + private void setupCategories(Inventory inv, String activeCategory) { + String[] categories = {"blocks", "weapons", "armor", "tools", "bows", "potions", "utility"}; + Material[] icons = { + Material.RED_WOOL, Material.GOLDEN_SWORD, Material.CHAINMAIL_BOOTS, + Material.STONE_PICKAXE, Material.BOW, Material.BREWING_STAND, Material.TNT + }; + String[] names = {"§aBlocks", "§aWeapons", "§aArmor", "§aTools", "§aBows", "§aPotions", "§aUtility"}; + + for (int i = 0; i < categories.length; i++) { + int slot = i + 1; // slots 1 to 7 + + ItemStack item = new ItemStack(icons[i]); + ItemMeta meta = item.getItemMeta(); + if (meta != null) { + meta.setDisplayName(names[i]); + List lore = new ArrayList<>(); + lore.add("§7Click to open category"); + meta.setLore(lore); + item.setItemMeta(meta); + } + inv.setItem(slot, item); + } + + // Left and right border padding for Row 0 + inv.setItem(0, createGuiItem(Material.GRAY_STAINED_GLASS_PANE, "§r", null)); + inv.setItem(8, createGuiItem(Material.GRAY_STAINED_GLASS_PANE, "§r", null)); + } + + private ParsedShopItem parseShopItem(String raw, Player player) { + String[] parts = raw.split(":"); + if (parts.length < 3) return null; + + String matStr = parts[0]; + String costMatStr = parts[1]; + int costAmount = Integer.parseInt(parts[2]); + String displayName = ChatColor.translateAlternateColorCodes('&', parts[3]); + int amount = parts.length >= 5 ? Integer.parseInt(parts[4]) : 1; + + // Resolve Wool color dynamically based on player's team + Material material; + if (matStr.equalsIgnoreCase("WOOL")) { + BedwarsTeam team = plugin.getGameManager().getPlayerTeam(player); + material = team != null ? team.getWoolMaterial() : Material.WHITE_WOOL; + } else if (matStr.equalsIgnoreCase("BOW_POWER")) { + material = Material.BOW; + } else if (matStr.equalsIgnoreCase("KNOCKBACK_STICK")) { + material = Material.STICK; + } else if (matStr.equalsIgnoreCase("SPEED_POTION") || matStr.equalsIgnoreCase("INVISIBILITY_POTION") || matStr.equalsIgnoreCase("JUMP_POTION")) { + material = Material.POTION; + } else { + material = Material.valueOf(matStr.toUpperCase()); + } + + // Resolve Cost Material + Material costMaterial; + if (costMatStr.equalsIgnoreCase("IRON")) costMaterial = Material.IRON_INGOT; + else if (costMatStr.equalsIgnoreCase("GOLD")) costMaterial = Material.GOLD_INGOT; + else if (costMatStr.equalsIgnoreCase("DIAMOND")) costMaterial = Material.DIAMOND; + else if (costMatStr.equalsIgnoreCase("EMERALD")) costMaterial = Material.EMERALD; + else costMaterial = Material.valueOf(costMatStr.toUpperCase()); + + return new ParsedShopItem(raw, material, costMaterial, costAmount, displayName, amount); + } + + private ItemStack buildShopDisplayItem(ParsedShopItem item, Player player) { + // Prevent buying weaker/equal armor visually + if (item.material == Material.CHAINMAIL_BOOTS || item.material == Material.IRON_BOOTS || item.material == Material.DIAMOND_BOOTS) { + int itemTier = 1; + if (item.material == Material.IRON_BOOTS) itemTier = 2; + else if (item.material == Material.DIAMOND_BOOTS) itemTier = 3; + + int currentTier = getArmorTier(player); + if (currentTier >= itemTier) { + ItemStack stack = new ItemStack(item.material, item.amount); + ItemMeta meta = stack.getItemMeta(); + if (meta != null) { + meta.setDisplayName(item.displayName); + List lore = new ArrayList<>(); + lore.add("§7Amount: §b" + item.amount); + lore.add(""); + lore.add("§a§lALREADY OWNED"); + meta.setLore(lore); + stack.setItemMeta(meta); + } + return stack; + } + } + + // Prevent buying weaker/equal swords visually + if (item.material.name().contains("SWORD")) { + int itemTier = getSwordTier(item.material); + int currentBestTier = getBestSwordTier(player); + if (currentBestTier >= itemTier) { + ItemStack stack = new ItemStack(item.material, item.amount); + ItemMeta meta = stack.getItemMeta(); + if (meta != null) { + meta.setDisplayName(item.displayName); + List lore = new ArrayList<>(); + lore.add("§7Amount: §b" + item.amount); + lore.add(""); + lore.add("§a§lALREADY OWNED"); + meta.setLore(lore); + stack.setItemMeta(meta); + } + return stack; + } + } + + // Prevent buying shears visually if already owned + if (item.material == Material.SHEARS && hasShears(player)) { + ItemStack stack = new ItemStack(item.material, item.amount); + ItemMeta meta = stack.getItemMeta(); + if (meta != null) { + meta.setDisplayName(item.displayName); + List lore = new ArrayList<>(); + lore.add("§7Amount: §b" + item.amount); + lore.add(""); + lore.add("§a§lALREADY OWNED"); + meta.setLore(lore); + stack.setItemMeta(meta); + } + return stack; + } + + // Prevent buying pickaxe visually if already maxed + if (item.material == Material.WOODEN_PICKAXE && getPickaxeLevel(player) >= 4) { + ItemStack stack = new ItemStack(Material.DIAMOND_PICKAXE, item.amount); + ItemMeta meta = stack.getItemMeta(); + if (meta != null) { + meta.setDisplayName(item.displayName); + List lore = new ArrayList<>(); + lore.add("§7Amount: §b" + item.amount); + lore.add(""); + lore.add("§a§lMAXED OUT"); + meta.setLore(lore); + stack.setItemMeta(meta); + } + return stack; + } + + // Prevent buying axe visually if already maxed + if (item.material == Material.WOODEN_AXE && getAxeLevel(player) >= 4) { + ItemStack stack = new ItemStack(Material.DIAMOND_AXE, item.amount); + ItemMeta meta = stack.getItemMeta(); + if (meta != null) { + meta.setDisplayName(item.displayName); + List lore = new ArrayList<>(); + lore.add("§7Amount: §b" + item.amount); + lore.add(""); + lore.add("§a§lMAXED OUT"); + meta.setLore(lore); + stack.setItemMeta(meta); + } + return stack; + } + + ItemStack stack = new ItemStack(item.material, item.amount); + + // Handle special modifications for tools, armor, potions, weapons + applySpecialAttributes(stack, item, player); + + ItemMeta meta = stack.getItemMeta(); + if (meta != null) { + meta.setDisplayName(item.displayName); + List lore = new ArrayList<>(); + lore.add("§7Amount: §b" + item.amount); + lore.add("§7Cost: " + getCurrencyColor(item.costMaterial) + item.costAmount + " " + getCurrencyName(item.costMaterial)); + lore.add(""); + + boolean canAfford = hasEnoughResource(player, item.costMaterial, item.costAmount); + if (canAfford) { + lore.add("§e§lCLICK TO PURCHASE"); + } else { + lore.add("§c§lYOU CANNOT AFFORD THIS"); + } + meta.setLore(lore); + stack.setItemMeta(meta); + } + return stack; + } + + private void applySpecialAttributes(ItemStack stack, ParsedShopItem item, Player player) { + ItemMeta meta = stack.getItemMeta(); + if (meta == null) return; + + // 1. Knockback Stick + if (item.rawString.contains("KNOCKBACK_STICK")) { + stack.setType(Material.STICK); + meta.addEnchant(Enchantment.KNOCKBACK, 1, true); + stack.setItemMeta(meta); + } + + // 2. Bow with Power I + if (item.rawString.contains("BOW_POWER")) { + stack.setType(Material.BOW); + meta.addEnchant(Enchantment.POWER, 1, true); + stack.setItemMeta(meta); + } + + // 3. Potions + if (item.material == Material.POTION || item.material == Material.SPLASH_POTION) { + PotionMeta potMeta = (PotionMeta) meta; + if (item.rawString.contains("SPEED_POTION")) { + potMeta.addCustomEffect(new PotionEffect(PotionEffectType.SPEED, 900, 1), true); // Speed II for 45s + } else if (item.rawString.contains("INVISIBILITY_POTION")) { + potMeta.addCustomEffect(new PotionEffect(PotionEffectType.INVISIBILITY, 600, 0), true); // Invisibility for 30s + } else if (item.rawString.contains("JUMP_POTION")) { + potMeta.addCustomEffect(new PotionEffect(PotionEffectType.JUMP_BOOST, 900, 4), true); // Jump V for 45s + } + stack.setItemMeta(potMeta); + } + + // 4. Tools Progression + if (item.material == Material.WOODEN_PICKAXE) { + int currentLevel = pickaxeLevels.getOrDefault(player.getUniqueId(), 0); + int nextLevel = Math.min(4, currentLevel + 1); + + Material nextMat = Material.WOODEN_PICKAXE; + String toolName = "§7Wooden Pickaxe"; + if (nextLevel == 2) { nextMat = Material.STONE_PICKAXE; toolName = "§7Stone Pickaxe"; } + else if (nextLevel == 3) { nextMat = Material.IRON_PICKAXE; toolName = "§fIron Pickaxe"; } + else if (nextLevel == 4) { nextMat = Material.DIAMOND_PICKAXE; toolName = "§bDiamond Pickaxe"; } + + stack.setType(nextMat); + item.material = nextMat; + item.displayName = toolName + " §e(Tier " + nextLevel + ")"; + } + + if (item.material == Material.WOODEN_AXE) { + int currentLevel = axeLevels.getOrDefault(player.getUniqueId(), 0); + int nextLevel = Math.min(4, currentLevel + 1); + + Material nextMat = Material.WOODEN_AXE; + String toolName = "§7Wooden Axe"; + if (nextLevel == 2) { nextMat = Material.STONE_AXE; toolName = "§7Stone Axe"; } + else if (nextLevel == 3) { nextMat = Material.IRON_AXE; toolName = "§fIron Axe"; } + else if (nextLevel == 4) { nextMat = Material.DIAMOND_AXE; toolName = "§bDiamond Axe"; } + + stack.setType(nextMat); + item.material = nextMat; + item.displayName = toolName + " §e(Tier " + nextLevel + ")"; + } + } + + /** + * Executes the purchase of a shop item. + */ + public void purchaseItem(Player player, String category, int slot) { + List itemsStr = plugin.getConfig().getStringList("shop." + category); + + // Match slot back to config indices + int expectedIndex = 0; + int currentSlot = 19; + boolean found = false; + + for (String ignored : itemsStr) { + if (currentSlot == 26 || currentSlot == 35 || currentSlot == 44) currentSlot += 2; + if (currentSlot == slot) { + found = true; + break; + } + currentSlot++; + expectedIndex++; + } + + if (!found || expectedIndex >= itemsStr.size()) return; + + String itemStr = itemsStr.get(expectedIndex); + ParsedShopItem shopItem = parseShopItem(itemStr, player); + if (shopItem == null) return; + + // Prevent buying weaker/equal armor + if (shopItem.material == Material.CHAINMAIL_BOOTS || shopItem.material == Material.IRON_BOOTS || shopItem.material == Material.DIAMOND_BOOTS) { + int itemTier = 1; + if (shopItem.material == Material.IRON_BOOTS) itemTier = 2; + else if (shopItem.material == Material.DIAMOND_BOOTS) itemTier = 3; + + int currentTier = getArmorTier(player); + if (currentTier >= itemTier) { + player.sendMessage("§cYou already own equal or better armor!"); + player.playSound(player.getLocation(), Sound.ENTITY_VILLAGER_NO, 1.0f, 1.0f); + return; + } + } + + // Prevent buying weaker/equal swords + if (shopItem.material.name().contains("SWORD")) { + int itemTier = getSwordTier(shopItem.material); + int currentBestTier = getBestSwordTier(player); + if (currentBestTier >= itemTier) { + player.sendMessage("§cYou already own an equal or better sword!"); + player.playSound(player.getLocation(), Sound.ENTITY_VILLAGER_NO, 1.0f, 1.0f); + return; + } + } + + // Prevent buying shears if already owned + if (shopItem.material == Material.SHEARS && hasShears(player)) { + player.sendMessage("§cYou already own shears!"); + player.playSound(player.getLocation(), Sound.ENTITY_VILLAGER_NO, 1.0f, 1.0f); + return; + } + + // Prevent buying pickaxe if already maxed + if (shopItem.material == Material.WOODEN_PICKAXE && getPickaxeLevel(player) >= 4) { + player.sendMessage("§cYour Pickaxe is already maxed!"); + player.playSound(player.getLocation(), Sound.ENTITY_VILLAGER_NO, 1.0f, 1.0f); + return; + } + + // Prevent buying axe if already maxed + if (shopItem.material == Material.WOODEN_AXE && getAxeLevel(player) >= 4) { + player.sendMessage("§cYour Axe is already maxed!"); + player.playSound(player.getLocation(), Sound.ENTITY_VILLAGER_NO, 1.0f, 1.0f); + return; + } + + // Apply special modifications for display/upgrade levels + ItemStack dummyStack = new ItemStack(shopItem.material); + applySpecialAttributes(dummyStack, shopItem, player); + + // Check if player has enough resource + if (!hasEnoughResource(player, shopItem.costMaterial, shopItem.costAmount)) { + player.sendMessage("§cYou cannot afford this item!"); + player.playSound(player.getLocation(), Sound.ENTITY_VILLAGER_NO, 1.0f, 1.0f); + return; + } + + // Deduct cost + deductResource(player, shopItem.costMaterial, shopItem.costAmount); + + // Deliver item + deliverPurchase(player, shopItem); + + // Refresh Shop + openShop(player, category); + player.playSound(player.getLocation(), Sound.ENTITY_ITEM_PICKUP, 0.8f, 1.2f); + } + + private void deliverPurchase(Player player, ParsedShopItem shopItem) { + UUID uuid = player.getUniqueId(); + Material mat = shopItem.material; + + // 1. Shears + if (mat == Material.SHEARS) { + if (hasShears.contains(uuid)) { + player.sendMessage("§cYou already have shears!"); + return; + } + hasShears.add(uuid); + player.getInventory().addItem(new ItemStack(Material.SHEARS)); + player.sendMessage("§aPurchased Shears!"); + return; + } + + // 2. Pickaxe Progression + if (mat == Material.WOODEN_PICKAXE || mat == Material.STONE_PICKAXE || + mat == Material.IRON_PICKAXE || mat == Material.DIAMOND_PICKAXE) { + int currentLevel = pickaxeLevels.getOrDefault(uuid, 0); + int nextLevel = Math.min(4, currentLevel + 1); + pickaxeLevels.put(uuid, nextLevel); + + // Remove old pickaxes + removeMaterials(player, Material.WOODEN_PICKAXE, Material.STONE_PICKAXE, Material.IRON_PICKAXE, Material.DIAMOND_PICKAXE); + + ItemStack pick = new ItemStack(mat); + ItemMeta m = pick.getItemMeta(); + if (m != null) { + m.setUnbreakable(true); + pick.setItemMeta(m); + } + + player.getInventory().addItem(pick); + player.sendMessage("§aUpgraded Pickaxe to Tier " + nextLevel + "!"); + return; + } + + // 3. Axe Progression + if (mat == Material.WOODEN_AXE || mat == Material.STONE_AXE || + mat == Material.IRON_AXE || mat == Material.DIAMOND_AXE) { + int currentLevel = axeLevels.getOrDefault(uuid, 0); + int nextLevel = Math.min(4, currentLevel + 1); + axeLevels.put(uuid, nextLevel); + + // Remove old axes + removeMaterials(player, Material.WOODEN_AXE, Material.STONE_AXE, Material.IRON_AXE, Material.DIAMOND_AXE); + + ItemStack axe = new ItemStack(mat); + ItemMeta m = axe.getItemMeta(); + if (m != null) { + m.setUnbreakable(true); + axe.setItemMeta(m); + } + + player.getInventory().addItem(axe); + player.sendMessage("§aUpgraded Axe to Tier " + nextLevel + "!"); + return; + } + + // 4. Armor Upgrades (Chainmail, Iron, Diamond) + if (mat == Material.CHAINMAIL_BOOTS || mat == Material.IRON_BOOTS || mat == Material.DIAMOND_BOOTS) { + int targetTier = 1; + if (mat == Material.IRON_BOOTS) targetTier = 2; + else if (mat == Material.DIAMOND_BOOTS) targetTier = 3; + + armorLevels.put(uuid, targetTier); + equipArmorTier(player, targetTier); + player.sendMessage("§aArmor upgraded successfully!"); + return; + } + + // 5. Swords replacing logic + if (mat.name().contains("SWORD")) { + handleSwordPurchase(player, mat, shopItem.displayName); + return; + } + + // 5. Standard Item + ItemStack purchasedStack = new ItemStack(mat, shopItem.amount); + + // Reapply custom attributes (Knockback stick, bow, potions, etc.) + applySpecialAttributes(purchasedStack, shopItem, player); + + player.getInventory().addItem(purchasedStack); + player.sendMessage("§aPurchased " + shopItem.displayName + "!"); + } + + private void removeMaterials(Player player, Material... materials) { + List list = Arrays.asList(materials); + for (int i = 0; i < player.getInventory().getSize(); i++) { + ItemStack item = player.getInventory().getItem(i); + if (item != null && list.contains(item.getType())) { + player.getInventory().setItem(i, null); + } + } + } + + // Checking currency counts + public boolean hasEnoughResource(Player player, Material currency, int amount) { + int count = 0; + for (ItemStack item : player.getInventory().getContents()) { + if (item != null && item.getType() == currency) { + count += item.getAmount(); + } + } + return count >= amount; + } + + private void deductResource(Player player, Material currency, int amount) { + int leftToDeduct = amount; + ItemStack[] contents = player.getInventory().getContents(); + for (int i = 0; i < contents.length; i++) { + ItemStack item = contents[i]; + if (item != null && item.getType() == currency) { + if (item.getAmount() > leftToDeduct) { + item.setAmount(item.getAmount() - leftToDeduct); + break; + } else { + leftToDeduct -= item.getAmount(); + player.getInventory().setItem(i, null); + } + } + if (leftToDeduct <= 0) break; + } + } + + private String getCurrencyColor(Material material) { + if (material == Material.IRON_INGOT) return "§f"; + if (material == Material.GOLD_INGOT) return "§6"; + if (material == Material.DIAMOND) return "§b"; + if (material == Material.EMERALD) return "§a"; + return "§7"; + } + + private String getCurrencyName(Material material) { + if (material == Material.IRON_INGOT) return "Iron"; + if (material == Material.GOLD_INGOT) return "Gold"; + if (material == Material.DIAMOND) return "Diamond"; + if (material == Material.EMERALD) return "Emerald"; + return material.name(); + } + + private String capitalize(String text) { + if (text == null || text.isEmpty()) return ""; + return text.substring(0, 1).toUpperCase() + text.substring(1).toLowerCase(); + } + + private ItemStack createGuiItem(Material mat, String name, List lore) { + ItemStack item = new ItemStack(mat); + ItemMeta meta = item.getItemMeta(); + if (meta != null) { + meta.setDisplayName(name); + meta.setLore(lore); + item.setItemMeta(meta); + } + return item; + } + + private int getBestSwordTier(Player player) { + int bestTier = -1; // -1 means no sword + for (ItemStack item : player.getInventory().getContents()) { + if (item != null) { + int tier = getSwordTier(item.getType()); + if (tier > bestTier) { + bestTier = tier; + } + } + } + return bestTier; + } + + private int getSwordTier(Material material) { + if (material == null) return -1; + switch (material) { + case WOODEN_SWORD: return 0; + case STONE_SWORD: return 1; + case IRON_SWORD: return 2; + case DIAMOND_SWORD: return 3; + default: return -1; + } + } + + private void handleSwordPurchase(Player player, Material newSwordMaterial, String displayName) { + ItemStack[] contents = player.getInventory().getContents(); + int bestSwordSlot = -1; + int bestSwordTier = -1; + + for (int i = 0; i < contents.length; i++) { + ItemStack item = contents[i]; + if (item != null) { + int tier = getSwordTier(item.getType()); + if (tier > bestSwordTier) { + bestSwordTier = tier; + bestSwordSlot = i; + } + } + } + + ItemStack newSword = new ItemStack(newSwordMaterial); + ItemMeta m = newSword.getItemMeta(); + if (m != null) { + m.setUnbreakable(true); + newSword.setItemMeta(m); + } + + if (bestSwordSlot != -1) { + // Replace the old sword at its slot! + player.getInventory().setItem(bestSwordSlot, newSword); + player.sendMessage("§aUpgraded sword to " + displayName + "!"); + } else { + // Otherwise, just add it to the inventory + player.getInventory().addItem(newSword); + player.sendMessage("§aPurchased " + displayName + "!"); + } + + // Apply team upgrades (like sharpness!) to the new sword + plugin.getUpgradesManager().applyTeamUpgrades(player); + } + + private static class ParsedShopItem { + final String rawString; + Material material; + final Material costMaterial; + final int costAmount; + String displayName; + final int amount; + + ParsedShopItem(String rawString, Material material, Material costMaterial, int costAmount, String displayName, int amount) { + this.rawString = rawString; + this.material = material; + this.costMaterial = costMaterial; + this.costAmount = costAmount; + this.displayName = displayName; + this.amount = amount; + } + } +} diff --git a/src/main/java/com/bedwars/UpgradesManager.java b/src/main/java/com/bedwars/UpgradesManager.java new file mode 100644 index 0000000..82aae56 --- /dev/null +++ b/src/main/java/com/bedwars/UpgradesManager.java @@ -0,0 +1,254 @@ +package com.bedwars; + +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.Sound; +import org.bukkit.enchantments.Enchantment; +import org.bukkit.entity.Player; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.potion.PotionEffect; +import org.bukkit.potion.PotionEffectType; + +import java.util.*; + +public class UpgradesManager { + + private final BedwarsPlugin plugin; + + // Upgrades maps per team + private final Map sharpnessLevels = new HashMap<>(); + private final Map protectionLevels = new HashMap<>(); + private final Map hasteLevels = new HashMap<>(); + private final Map forgeLevels = new HashMap<>(); + + public UpgradesManager(BedwarsPlugin plugin) { + this.plugin = plugin; + } + + public void resetTeamUpgrades() { + sharpnessLevels.clear(); + protectionLevels.clear(); + hasteLevels.clear(); + forgeLevels.clear(); + } + + public int getSharpnessLevel(BedwarsTeam team) { + return sharpnessLevels.getOrDefault(team, 0); + } + + public int getProtectionLevel(BedwarsTeam team) { + return protectionLevels.getOrDefault(team, 0); + } + + public int getHasteLevel(BedwarsTeam team) { + return hasteLevels.getOrDefault(team, 0); + } + + public int getForgeLevel(BedwarsTeam team) { + return forgeLevels.getOrDefault(team, 0); + } + + /** + * Opens the Team Upgrades GUI for a player. + */ + public void openUpgradesGui(Player player) { + BedwarsTeam team = plugin.getGameManager().getPlayerTeam(player); + if (team == null) return; + + Inventory inv = Bukkit.createInventory(null, 27, "§8Team Upgrades"); + + // Padding + for (int i = 0; i < 27; i++) { + inv.setItem(i, createGuiItem(Material.BLACK_STAINED_GLASS_PANE, "§r", null)); + } + + // 1. Sharpness (Slot 10) + setupUpgradeIcon(inv, team, 10, Material.IRON_SWORD, "Sharpness", "§fPermanent Sharpness I on swords", "sharpness", getSharpnessLevel(team), 1); + + // 2. Protection (Slot 11) + setupUpgradeIcon(inv, team, 11, Material.IRON_CHESTPLATE, "Protection", "§fPermanent Protection on armor", "protection", getProtectionLevel(team), 4); + + // 3. Haste (Slot 12) + setupUpgradeIcon(inv, team, 12, Material.GOLDEN_PICKAXE, "Haste", "§fPermanent Haste effect", "haste", getHasteLevel(team), 2); + + // 4. Forge (Slot 13) + setupUpgradeIcon(inv, team, 13, Material.FURNACE, "Forge", "§fSpawns base resources faster", "forge", getForgeLevel(team), 4); + + player.openInventory(inv); + } + + private void setupUpgradeIcon(Inventory inv, BedwarsTeam team, int slot, Material icon, String name, String benefit, String key, int currentLvl, int maxLvl) { + ItemStack item = new ItemStack(icon); + ItemMeta meta = item.getItemMeta(); + if (meta != null) { + meta.setDisplayName("§b§l" + name); + List lore = new ArrayList<>(); + lore.add("§7Benefit: " + benefit); + lore.add("§7Current Level: §f" + (currentLvl == 0 ? "None" : "Tier " + currentLvl)); + lore.add(""); + + if (currentLvl >= maxLvl) { + lore.add("§a§lMAX LEVEL REACHED"); + } else { + int nextLvl = currentLvl + 1; + int cost = plugin.getConfig().getIntegerList("upgrades." + key + ".cost").get(nextLvl - 1); + String costMatName = plugin.getConfig().getString("upgrades." + key + ".material", "DIAMOND"); + + lore.add("§7Next Level: §aTier " + nextLvl); + lore.add("§7Cost: §b" + cost + " " + costMatName); + lore.add(""); + lore.add("§eClick to buy upgrade"); + } + meta.setLore(lore); + item.setItemMeta(meta); + } + inv.setItem(slot, item); + } + + /** + * Handles clicking an upgrade icon in the GUI. + */ + public void purchaseUpgrade(Player player, int slot) { + BedwarsTeam team = plugin.getGameManager().getPlayerTeam(player); + if (team == null) return; + + String key = null; + int maxLvl = 1; + int currentLvl = 0; + + if (slot == 10) { key = "sharpness"; maxLvl = 1; currentLvl = getSharpnessLevel(team); } + else if (slot == 11) { key = "protection"; maxLvl = 4; currentLvl = getProtectionLevel(team); } + else if (slot == 12) { key = "haste"; maxLvl = 2; currentLvl = getHasteLevel(team); } + else if (slot == 13) { key = "forge"; maxLvl = 4; currentLvl = getForgeLevel(team); } + + if (key == null) return; + + if (currentLvl >= maxLvl) { + player.sendMessage("§cYou have already maxed out this upgrade!"); + player.playSound(player.getLocation(), Sound.ENTITY_VILLAGER_NO, 1.0f, 1.0f); + return; + } + + int nextLvl = currentLvl + 1; + int cost = plugin.getConfig().getIntegerList("upgrades." + key + ".cost").get(nextLvl - 1); + String costMatStr = plugin.getConfig().getString("upgrades." + key + ".material", "DIAMOND"); + Material costMat = Material.valueOf(costMatStr.toUpperCase()); + + if (!plugin.getShopManager().hasEnoughResource(player, costMat, cost)) { + player.sendMessage("§cYour team doesn't have enough Diamonds for this upgrade!"); + player.playSound(player.getLocation(), Sound.ENTITY_VILLAGER_NO, 1.0f, 1.0f); + return; + } + + // Deduct diamonds + deductResource(player, costMat, cost); + + // Apply levels + if (key.equals("sharpness")) sharpnessLevels.put(team, nextLvl); + else if (key.equals("protection")) protectionLevels.put(team, nextLvl); + else if (key.equals("haste")) hasteLevels.put(team, nextLvl); + else if (key.equals("forge")) { + forgeLevels.put(team, nextLvl); + // Upgrade spawner ticks + plugin.getGeneratorManager().upgradeTeamGenerator(team, nextLvl); + } + + // Apply perks to team members instantly + applyTeamUpgradesToAll(team); + + // Notify team + for (Player p : Bukkit.getOnlinePlayers()) { + if (plugin.getGameManager().getPlayerTeam(p) == team) { + p.sendMessage("§a§lUPGRADE BOUGHT! §f" + player.getName() + " bought " + key.toUpperCase() + " Tier " + nextLvl + "!"); + p.playSound(p.getLocation(), Sound.ENTITY_PLAYER_LEVELUP, 0.8f, 1.2f); + } + } + + // Reopen GUI + openUpgradesGui(player); + } + + /** + * Applies upgrades to a specific team player (usually on spawn/respawn or when bought). + */ + public void applyTeamUpgrades(Player player) { + BedwarsTeam team = plugin.getGameManager().getPlayerTeam(player); + if (team == null) return; + + // 1. Sharpness + int sharpLvl = getSharpnessLevel(team); + if (sharpLvl > 0) { + for (ItemStack item : player.getInventory().getContents()) { + if (item != null && item.getType().name().contains("SWORD")) { + ItemMeta meta = item.getItemMeta(); + if (meta != null && !meta.hasEnchant(Enchantment.SHARPNESS)) { + meta.addEnchant(Enchantment.SHARPNESS, sharpLvl, true); + item.setItemMeta(meta); + } + } + } + } + + // 2. Protection + int protLvl = getProtectionLevel(team); + if (protLvl > 0) { + for (ItemStack armor : player.getInventory().getArmorContents()) { + if (armor != null && armor.getType() != Material.AIR) { + ItemMeta meta = armor.getItemMeta(); + if (meta != null) { + meta.addEnchant(Enchantment.PROTECTION, protLvl, true); + armor.setItemMeta(meta); + } + } + } + } + + // 3. Haste + int hasteLvl = getHasteLevel(team); + if (hasteLvl > 0) { + // Remove old haste first + player.removePotionEffect(PotionEffectType.HASTE); + // Apply infinite haste effect + player.addPotionEffect(new PotionEffect(PotionEffectType.HASTE, Integer.MAX_VALUE, hasteLvl - 1, true, false)); + } + } + + public void applyTeamUpgradesToAll(BedwarsTeam team) { + for (Player p : Bukkit.getOnlinePlayers()) { + if (plugin.getGameManager().getPlayerTeam(p) == team) { + applyTeamUpgrades(p); + } + } + } + + private void deductResource(Player player, Material currency, int amount) { + int leftToDeduct = amount; + ItemStack[] contents = player.getInventory().getContents(); + for (int i = 0; i < contents.length; i++) { + ItemStack item = contents[i]; + if (item != null && item.getType() == currency) { + if (item.getAmount() > leftToDeduct) { + item.setAmount(item.getAmount() - leftToDeduct); + break; + } else { + leftToDeduct -= item.getAmount(); + player.getInventory().setItem(i, null); + } + } + if (leftToDeduct <= 0) break; + } + } + + private ItemStack createGuiItem(Material mat, String name, List lore) { + ItemStack item = new ItemStack(mat); + ItemMeta meta = item.getItemMeta(); + if (meta != null) { + meta.setDisplayName(name); + meta.setLore(lore); + item.setItemMeta(meta); + } + return item; + } +} diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml new file mode 100644 index 0000000..122b1e0 --- /dev/null +++ b/src/main/resources/config.yml @@ -0,0 +1,247 @@ +world-name: map +lobby-server: lobby +min-players: 2 +countdown-seconds: 30 + +rates: + iron: 20 + gold: 80 + diamond: 600 + emerald: 1200 + +# Format: Material:CostMaterial:CostAmount:Name:Amount (Optional) +shop: + blocks: + - WOOL:IRON:4:&fWhite Wool:16 + - TERRACOTTA:IRON:12:&6Hardened Clay:16 + - GLASS:IRON:16:&eBlast-proof Glass:4 + - END_STONE:IRON:24:&eEnd Stone:12 + - OAK_PLANKS:GOLD:4:&Wood:12 + - OBSIDIAN:EMERALD:4:&5Obsidian:4 + weapons: + - STONE_SWORD:IRON:10:&7Stone Sword:1 + - IRON_SWORD:GOLD:7:&fIron Sword:1 + - DIAMOND_SWORD:EMERALD:4:&bDiamond Sword:1 + armor: + - CHAINMAIL_BOOTS:IRON:40:&7Chainmail Armor:1 + - IRON_BOOTS:GOLD:12:&fIron Armor:1 + - DIAMOND_BOOTS:EMERALD:6:&bDiamond Armor:1 + tools: + - SHEARS:IRON:20:&aShears:1 + - WOODEN_PICKAXE:IRON:10:&7Pickaxe (Progression):1 + - WOODEN_AXE:IRON:10:&7Axe (Progression):1 + bows: + - BOW:GOLD:12:&aBow:1 + - BOW_POWER:GOLD:24:&aBow (Power I):1 + - ARROW:GOLD:2:&aArrow:8 + potions: + - SPEED_POTION:EMERALD:1:&bSpeed II Potion:1 + - INVISIBILITY_POTION:EMERALD:2:&7Invisibility Potion:1 + - JUMP_POTION:EMERALD:1:&aJump V Potion:1 + utility: + - GOLDEN_APPLE:GOLD:3:&6Golden Apple:1 + - FIRE_CHARGE:IRON:40:&cFireball:1 + - ENDER_PEARL:EMERALD:4:&5Ender Pearl:1 + - SPONGE:GOLD:6:&eSponge:4 + - TNT:GOLD:4:&cTNT:1 + +upgrades: + sharpness: + cost: + - 4 + - 8 + material: DIAMOND + protection: + cost: + - 2 + - 4 + - 8 + - 16 + material: DIAMOND + haste: + cost: + - 2 + - 4 + material: DIAMOND + forge: + cost: + - 2 + - 4 + - 6 + - 8 + material: DIAMOND + +locations: + lobby: + world: world + x: 129.5 + y: 90.0 + z: 149.5 + yaw: 0.0 + pitch: 0.0 + teams: + red: + enabled: true + spawn: + x: 128.5 + y: 5.0 + z: 56.5 + yaw: 0.0 + pitch: 0.0 + bed: + x: 128.0 + y: 5.0 + z: 63.0 + generator: + x: 128.5 + y: 5.0 + z: 45.5 + shop: + x: 135.5 + y: 5.0 + z: 54.5 + yaw: 90.0 + pitch: 0.0 + upgrades: + x: 120.5 + y: 5.0 + z: 54.5 + yaw: -90.0 + pitch: 0.0 + enderchest: + x: 121.0 + y: 5.0 + z: 61.0 + blue: + enabled: true + spawn: + x: 222.5 + y: 5.0 + z: 150.5 + yaw: 90.0 + pitch: 0.0 + bed: + x: 214.0 + y: 5.0 + z: 150.0 + shop: + x: 224.5 + y: 5.0 + z: 157.5 + yaw: 180.0 + pitch: 0.0 + upgrades: + x: 224.5 + y: 5.0 + z: 142.5 + yaw: 0.0 + pitch: 0.0 + generator: + x: 233.5 + y: 5.0 + z: 150.5 + enderchest: + x: 217.0 + y: 5.0 + z: 143.0 + purple: + enabled: true + spawn: + x: 128.5 + y: 5.0 + z: 244.5 + yaw: -180.0 + pitch: 0.0 + bed: + x: 128.0 + y: 5.0 + z: 236.0 + generator: + x: 128.5 + y: 5.0 + z: 255.5 + shop: + x: 121.5 + y: 5.0 + z: 246.5 + yaw: -90.0 + pitch: 0.0 + upgrades: + x: 136.5 + y: 5.0 + z: 246.5 + yaw: 90.0 + pitch: 0.0 + enderchest: + x: 135.0 + y: 5.0 + z: 239.0 + yellow: + enabled: true + spawn: + x: 34.5 + y: 5.0 + z: 150.5 + yaw: -90.0 + pitch: 0.0 + bed: + x: 41.0 + y: 5.0 + z: 150.0 + generator: + x: 23.5 + y: 5.0 + z: 150.5 + shop: + x: 32.5 + y: 5.0 + z: 143.5 + yaw: 0.0 + pitch: 0.0 + upgrades: + x: 32.5 + y: 5.0 + z: 158.5 + yaw: -180.0 + pitch: 0.0 + enderchest: + x: 39.0 + y: 5.0 + z: 157.0 + generators: + - x: 129.5 + y: 8.0 + z: 149.5 + type: EMERALD + - x: 144.5 + y: 8.0 + z: 134.5 + type: EMERALD + - x: 114.5 + y: 8.0 + z: 134.5 + type: EMERALD + - x: 114.5 + y: 8.0 + z: 164.5 + type: EMERALD + - x: 144.5 + y: 8.0 + z: 164.5 + type: EMERALD + - x: 182.5 + y: 7.0 + z: 201.5 + type: DIAMOND + - x: 77.5 + y: 7.0 + z: 202.5 + type: DIAMOND + - x: 76.5 + y: 7.0 + z: 97.5 + type: DIAMOND + - x: 181.5 + y: 7.0 + z: 96.5 + type: DIAMOND diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml new file mode 100644 index 0000000..2364aae --- /dev/null +++ b/src/main/resources/plugin.yml @@ -0,0 +1,11 @@ +name: Bedwars +version: 1.0.0 +main: com.bedwars.BedwarsPlugin +api-version: 1.21 +description: A premium Bedwars plugin for 1.21.4+ with world resets, shops, upgrades, and Velocity proxy integration. +author: kyouki +commands: + bw: + description: Bedwars administration and setup command + permission: bedwars.admin + aliases: [bedwars]