commit a2935ad53d4a1125de4760e8405b5b2fad98f26c Author: fucksophie Date: Sun Feb 15 20:48:20 2026 +0200 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7577384 --- /dev/null +++ b/.gitignore @@ -0,0 +1,170 @@ +# Created by https://www.toptal.com/developers/gitignore/api/intellij,java,gradle +# Edit at https://www.toptal.com/developers/gitignore?templates=intellij,java,gradle + +### Intellij ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### Intellij Patch ### +# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 + +# *.iml +# modules.xml +# .idea/misc.xml +# *.ipr + +# Sonarlint plugin +# https://plugins.jetbrains.com/plugin/7973-sonarlint +.idea/**/sonarlint/ + +# SonarQube Plugin +# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin +.idea/**/sonarIssues.xml + +# Markdown Navigator plugin +# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced +.idea/**/markdown-navigator.xml +.idea/**/markdown-navigator-enh.xml +.idea/**/markdown-navigator/ + +# Cache file creation bug +# See https://youtrack.jetbrains.com/issue/JBR-2257 +.idea/$CACHE_FILE$ + +# CodeStream plugin +# https://plugins.jetbrains.com/plugin/12206-codestream +.idea/codestream.xml + +# Azure Toolkit for IntelliJ plugin +# https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij +.idea/**/azureSettings.xml + +### Java ### +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* +replay_pid* + +### Gradle ### +.gradle +**/build/ +!src/**/build/ + +# Ignore Gradle GUI config +gradle-app.setting + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar + +# Avoid ignore Gradle wrappper properties +!gradle-wrapper.properties + +# Cache of project +.gradletasknamecache + +# Eclipse Gradle plugin generated files +# Eclipse Core +.project +# JDT-specific (Eclipse Java Development Tools) +.classpath + +### Gradle Patch ### +# Java heap dump +*.hprof + +# End of https://www.toptal.com/developers/gitignore/api/intellij,java,gradle diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..b86273d --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..6adc085 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..b92124a --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules/SnowCore.main.iml b/.idea/modules/SnowCore.main.iml new file mode 100644 index 0000000..a376b96 --- /dev/null +++ b/.idea/modules/SnowCore.main.iml @@ -0,0 +1,13 @@ + + + + + + + ADVENTURE + + 1 + + + + \ No newline at end of file diff --git a/.idea/modules/SnowCore.test.iml b/.idea/modules/SnowCore.test.iml new file mode 100644 index 0000000..a376b96 --- /dev/null +++ b/.idea/modules/SnowCore.test.iml @@ -0,0 +1,13 @@ + + + + + + + ADVENTURE + + 1 + + + + \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..74c17b6 --- /dev/null +++ b/build.gradle @@ -0,0 +1,73 @@ +plugins { + id 'java' + id("xyz.jpenilla.run-paper") version "2.3.1" + id "io.github.goooler.shadow" version "8.1.8" +} + +group = 'ovh.sad' +version = '1.0-SNAPSHOT' + +repositories { + mavenCentral() + maven { + name = "papermc-repo" + url = "https://repo.papermc.io/repository/maven-public/" + } + maven { url 'https://jitpack.io' } + maven { + url = 'https://repo.extendedclip.com/releases/' + } +} +tasks.build { + dependsOn shadowJar +} +tasks.shadowJar { + archiveClassifier.set("shaded") // removes "-all" +} + +shadowJar { + relocate("dev.triumphteam.gui", "ovh.sad.snowcore.gui") +} + +dependencies { + compileOnly("io.papermc.paper:paper-api:1.21.11-R0.1-SNAPSHOT") + implementation "dev.triumphteam:triumph-gui:3.1.13" + compileOnly("com.github.MilkBowl:VaultAPI:1.7") { exclude( group: 'org.bukkit', module: 'bukkit' ) } + compileOnly 'me.clip:placeholderapi:2.12.2' +} + +tasks { + runServer { + // Configure the Minecraft version for our task. + // This is the only required configuration besides applying the plugin. + // Your plugin's jar (or shadowJar if present) will be used automatically. + minecraftVersion("1.21") + } +} + +def targetJavaVersion = 21 +java { + def javaVersion = JavaVersion.toVersion(targetJavaVersion) + sourceCompatibility = javaVersion + targetCompatibility = javaVersion + if (JavaVersion.current() < javaVersion) { + toolchain.languageVersion = JavaLanguageVersion.of(targetJavaVersion) + } +} + +tasks.withType(JavaCompile).configureEach { + options.encoding = 'UTF-8' + + if (targetJavaVersion >= 10 || JavaVersion.current().isJava10Compatible()) { + options.release.set(targetJavaVersion) + } +} + +processResources { + def props = [version: version] + inputs.properties props + filteringCharset 'UTF-8' + filesMatching('plugin.yml') { + expand props + } +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..e69de29 diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..e644113 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..a441313 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..b740cf1 --- /dev/null +++ b/gradlew @@ -0,0 +1,249 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..7101f8e --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..756a2c3 --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'SnowCore' diff --git a/src/main/java/ovh/sad/snowcore/Area.java b/src/main/java/ovh/sad/snowcore/Area.java new file mode 100644 index 0000000..fef3152 --- /dev/null +++ b/src/main/java/ovh/sad/snowcore/Area.java @@ -0,0 +1,179 @@ +package ovh.sad.snowcore; + + +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.World; +import org.bukkit.block.Block; +import org.bukkit.block.data.Ageable; +import org.bukkit.block.data.Bisected; +import org.bukkit.block.data.BlockData; +import org.bukkit.block.data.Waterlogged; + +import java.util.concurrent.ThreadLocalRandom; + +public class Area { + static class MaterialWithChance { + public Material material; + public float chance; // in precentage 1 to 100, 33.33 works aswell + + public MaterialWithChance(Material material, float chance) { + this.material = material; + this.chance = chance; + } + } + + private final Location positionOne; + private final Location positionTwo; + + public final String name; + + private final MaterialWithChance[] materials; + public final float airPercentage; + + public Area(String name, MaterialWithChance[] materials, + Location positionOne, + Location positionTwo, + float airPercentage + ) { + this.name = name; + this.materials = materials; + this.positionOne = positionOne; + this.positionTwo = positionTwo; + this.airPercentage = airPercentage; + } + + public void regen(World world) { + + int minX = Math.min(positionOne.getBlockX(), positionTwo.getBlockX()); + int minY = Math.min(positionOne.getBlockY(), positionTwo.getBlockY()); + int minZ = Math.min(positionOne.getBlockZ(), positionTwo.getBlockZ()); + + int maxX = Math.max(positionOne.getBlockX(), positionTwo.getBlockX()); + int maxZ = Math.max(positionOne.getBlockZ(), positionTwo.getBlockZ()); + ThreadLocalRandom random = ThreadLocalRandom.current(); + + for (int x = minX; x <= maxX; x++) { + for (int z = minZ; z <= maxZ; z++) { + + Block base = world.getBlockAt(x, minY, z); + if (!isReplaceable(base.getType())) continue; + + if (airPercentage > 0 && random.nextInt(100) < airPercentage) { + + for (int y = minY; y <= minY + 3; y++) { + Block b = world.getBlockAt(x, y, z); + if (isReplaceable(b.getType())) { + b.setType(Material.AIR, false); + } + } + + continue; + } + + int totalWeight = 0; + for (MaterialWithChance m : materials) { + totalWeight += m.chance; + } + + int roll = random.nextInt(totalWeight); + int current = 0; + MaterialWithChance chosen = null; + + for (MaterialWithChance m : materials) { + current += m.chance; + if (roll < current) { + chosen = m; + break; + } + } + + if (chosen == null) continue; + + for (int y = minY; y <= minY + 3; y++) { + Block b = world.getBlockAt(x, y, z); + if (isReplaceable(b.getType())) { + b.setType(Material.AIR, false); + } + } + + if (isDoubleHeight(chosen.material)) { + placeBisected(world, x, minY, z, chosen.material); + } else { + base.setType(chosen.material, false); + + BlockData data = base.getBlockData(); + + if(data instanceof Ageable ageable) { + ageable.setAge(1+random.nextInt(ageable.getMaximumAge())); + base.setBlockData(ageable, false); + } + + if (data instanceof Waterlogged waterlogged) { + waterlogged.setWaterlogged(false); + base.setBlockData(waterlogged, false); + } + + if (data instanceof org.bukkit.block.data.type.FlowerBed petals) { + int max = petals.getMaximumFlowerAmount(); + petals.setFlowerAmount(random.nextInt(1, max + 1)); + base.setBlockData(petals, false); + } + } + } + } + + } + private boolean isReplaceable(Material m) { + if (m == Material.AIR) return true; + + for (MaterialWithChance allowed : materials) { + if (allowed.material == m) return true; + } + return false; + } + private static void placeBisected(World world, int x, int y, int z, Material mat) { + Block bottom = world.getBlockAt(x, y, z); + Block top = world.getBlockAt(x, y + 1, z); + + bottom.setType(mat, false); + top.setType(mat, false); + + Bisected lower = (Bisected) bottom.getBlockData(); + Bisected upper = (Bisected) top.getBlockData(); + + lower.setHalf(Bisected.Half.BOTTOM); + upper.setHalf(Bisected.Half.TOP); + + bottom.setBlockData(lower, false); + top.setBlockData(upper, false); + } + + public float getAirPercentage(World world) { + int minX = Math.min(positionOne.getBlockX(), positionTwo.getBlockX()); + int maxX = Math.max(positionOne.getBlockX(), positionTwo.getBlockX()); + + int minY = Math.min(positionOne.getBlockY(), positionTwo.getBlockY()); + + int minZ = Math.min(positionOne.getBlockZ(), positionTwo.getBlockZ()); + int maxZ = Math.max(positionOne.getBlockZ(), positionTwo.getBlockZ()); + + int total = 0; + int airCount = 0; + + for (int x = minX; x <= maxX; x++) { + for (int z = minZ; z <= maxZ; z++) { + total++; + Block block = world.getBlockAt(x, minY, z); + if (block.getType() == Material.AIR) airCount++; + } + } + + if (total == 0) return 0f; + + return (airCount / (float) total) * 100f; + } + private static boolean isDoubleHeight(Material m) { + return m.createBlockData() instanceof Bisected; + } +} diff --git a/src/main/java/ovh/sad/snowcore/Prices.java b/src/main/java/ovh/sad/snowcore/Prices.java new file mode 100644 index 0000000..ac4bd5c --- /dev/null +++ b/src/main/java/ovh/sad/snowcore/Prices.java @@ -0,0 +1,115 @@ +package ovh.sad.snowcore; + + +import org.bukkit.Material; +import org.jspecify.annotations.Nullable; +import ovh.sad.snowcore.items.Hoe; +import ovh.sad.snowcore.items.Shovel; +import ovh.sad.snowcore.items.Tool; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class Prices { + + public static final Map SELL_PRICES = new HashMap<>(); + private static final Map toolPrices = new HashMap<>(); + + static { + + /* ========================= + Snow (very common / farmable) + ========================= */ + SELL_PRICES.put(Material.SNOWBALL, 0.5); // safe value + + + /* ========================= + Cactus Area + ========================= */ + SELL_PRICES.put(Material.CACTUS_FLOWER, 4.0); + SELL_PRICES.put(Material.PEONY, 4.0); + SELL_PRICES.put(Material.PINK_TULIP, 4.0); + SELL_PRICES.put(Material.CACTUS, 7.0); + + + /* ========================= + Spruce Area + ========================= */ + SELL_PRICES.put(Material.WILDFLOWERS, 1.0); + SELL_PRICES.put(Material.SPRUCE_SAPLING, 10.0); + + + /* ========================= + Pink Area + ========================= */ + SELL_PRICES.put(Material.PINK_PETALS, 1.0); + SELL_PRICES.put(Material.ALLIUM, 7.0); + SELL_PRICES.put(Material.CHERRY_SAPLING, 20.0); + + + /* ========================= + White Area + ========================= */ + SELL_PRICES.put(Material.OXEYE_DAISY, 2.0); + SELL_PRICES.put(Material.LILY_OF_THE_VALLEY, 2.0); + SELL_PRICES.put(Material.CLOSED_EYEBLOSSOM, 10.0); + + + /* ========================= + Gray Area + ========================= */ + SELL_PRICES.put(Material.DEAD_FIRE_CORAL, 4.0); + SELL_PRICES.put(Material.DEAD_TUBE_CORAL, 4.0); + SELL_PRICES.put(Material.DEAD_HORN_CORAL, 4.0); + SELL_PRICES.put(Material.DEAD_BRAIN_CORAL_FAN, 4.0); + SELL_PRICES.put(Material.OPEN_EYEBLOSSOM, 20.0); + + + /* ========================= + Red Area + ========================= */ + SELL_PRICES.put(Material.ROSE_BUSH, 3.0); + SELL_PRICES.put(Material.POPPY, 3.0); + SELL_PRICES.put(Material.RED_MUSHROOM, 3.0); + SELL_PRICES.put(Material.CRIMSON_FUNGUS, 10.0); + + + /* Farming areas */ + + SELL_PRICES.put(Material.BEETROOT_SEEDS, 0.5); + SELL_PRICES.put(Material.POTATO, 1.0); + SELL_PRICES.put(Material.CARROT, 1.0); + SELL_PRICES.put(Material.WHEAT_SEEDS, 0.5); + + SELL_PRICES.put(Material.POISONOUS_POTATO, 10.0 ); + SELL_PRICES.put(Material.WHEAT, 4.0); + SELL_PRICES.put(Material.BEETROOT, 2.0); + + + // Load hoe prices + List> hoes = Hoe.HoeDefinitions.ORDERED; + int[] hoePrices = {0, 1000, 5000, 10000, 40000, 200000}; // adjust if needed + for (int i = 0; i < hoes.size() && i < hoePrices.length; i++) { + toolPrices.put(hoes.get(i).material, hoePrices[i]); + } + + // Load shovel prices + List> shovels = Shovel.ShovelDefinitions.ORDERED; + int[] shovelPrices = {0, 1000, 5000, 10000, 40000, 200000}; // adjust if needed + for (int i = 0; i < shovels.size() && i < shovelPrices.length; i++) { + toolPrices.put(shovels.get(i).material, shovelPrices[i]); + } + } + + public static Integer getToolBuyPrice(Tool tool) { + return toolPrices.get(tool.material); + } + public static boolean hasSellPrice(Material mat) { + return SELL_PRICES.containsKey(mat); + } + + public static double getSellPrice(Material mat) { + return SELL_PRICES.get(mat); + } +} diff --git a/src/main/java/ovh/sad/snowcore/Snowcore.java b/src/main/java/ovh/sad/snowcore/Snowcore.java new file mode 100644 index 0000000..6aa91b9 --- /dev/null +++ b/src/main/java/ovh/sad/snowcore/Snowcore.java @@ -0,0 +1,318 @@ +package ovh.sad.snowcore; + +import io.papermc.paper.command.brigadier.BasicCommand; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.Style; +import net.kyori.adventure.text.format.TextColor; +import net.kyori.adventure.text.logger.slf4j.ComponentLogger; +import net.kyori.adventure.text.minimessage.MiniMessage; +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; +import net.milkbowl.vault.economy.Economy; +import org.bukkit.*; +import org.bukkit.block.Block; +import org.bukkit.block.data.type.Snow; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.bukkit.plugin.RegisteredServiceProvider; +import org.bukkit.plugin.java.JavaPlugin; +import org.bukkit.scheduler.BukkitRunnable; +import ovh.sad.snowcore.commands.RegenCommand; +import ovh.sad.snowcore.commands.SellCommand; +import ovh.sad.snowcore.commands.SnowifyCommand; +import ovh.sad.snowcore.commands.ViewItemsCommand; +import ovh.sad.snowcore.listeners.BreakListener; +import ovh.sad.snowcore.listeners.InteractListener; +import ovh.sad.snowcore.listeners.PlaceListener; + +import java.util.*; +import java.util.concurrent.ThreadLocalRandom; + + +public final class Snowcore extends JavaPlugin { + public static Economy econ = null; + + public static Area[] areas = new Area[]{ + new Area( + "Cactus Area", + + new Area.MaterialWithChance[]{ + new Area.MaterialWithChance(Material.CACTUS_FLOWER, 28.57f), + new Area.MaterialWithChance(Material.PEONY, 28.57f), + new Area.MaterialWithChance(Material.PINK_TULIP, 28.57f), + new Area.MaterialWithChance(Material.CACTUS, 14.29f) + }, + new Location(null, 80, 129, 161), + new Location(null, 73, 131, 184), + 15 + ), + new Area( + "Spruce Area", + new Area.MaterialWithChance[]{ + new Area.MaterialWithChance(Material.SPRUCE_SAPLING, 10), + new Area.MaterialWithChance(Material.WILDFLOWERS, 80) + }, + new Location(null, 64, 129, 161), + new Location(null, 69, 129, 170), + 5 + ), + new Area( + "Pink Area", + new Area.MaterialWithChance[]{ + new Area.MaterialWithChance(Material.PINK_PETALS, 80), + new Area.MaterialWithChance(Material.ALLIUM, 15), + new Area.MaterialWithChance(Material.CHERRY_SAPLING, 5) + }, + new Location(null, 64, 130, 174), + new Location(null, 69, 129, 184), + 0 + ), + new Area( + "White Area", + new Area.MaterialWithChance[]{ + new Area.MaterialWithChance(Material.OXEYE_DAISY, 45), + new Area.MaterialWithChance(Material.LILY_OF_THE_VALLEY, 45), + new Area.MaterialWithChance(Material.CLOSED_EYEBLOSSOM, 10) + + }, + new Location(null, 69, 129, 189), + new Location(null, 64, 129, 198), + 10 + ), + new Area( + "Gray Area", + new Area.MaterialWithChance[]{ + new Area.MaterialWithChance(Material.DEAD_FIRE_CORAL, 23.75f), + new Area.MaterialWithChance(Material.DEAD_TUBE_CORAL, 23.75f), + new Area.MaterialWithChance(Material.DEAD_HORN_CORAL, 23.75f), + new Area.MaterialWithChance(Material.DEAD_BRAIN_CORAL_FAN, 23.75f), + new Area.MaterialWithChance(Material.OPEN_EYEBLOSSOM, 5f) + }, + new Location(null, 69, 129, 202), + new Location(null, 64, 129, 211), + 20 + ), + new Area( + "Red Area", + new Area.MaterialWithChance[]{ + new Area.MaterialWithChance(Material.ROSE_BUSH, 30), + new Area.MaterialWithChance(Material.POPPY, 30), + new Area.MaterialWithChance(Material.RED_MUSHROOM, 30), + new Area.MaterialWithChance(Material.CRIMSON_FUNGUS, 10) + + }, + new Location(null, 73, 129, 189), + new Location(null, 80, 131, 211), + 30 + ), + new Area( + "Wheat & Beetroot Area", + new Area.MaterialWithChance[]{ + new Area.MaterialWithChance(Material.WHEAT, 50), + new Area.MaterialWithChance(Material.BEETROOTS, 50) + }, + new Location(null, 43, 129, 184), + new Location(null, 59, 129, 161), + 35 + ), + new Area( + "Carrot & Potato Area", + new Area.MaterialWithChance[]{ + new Area.MaterialWithChance(Material.CARROTS, 50), + new Area.MaterialWithChance(Material.POTATOES, 50) + }, + new Location(null, 43, 129, 189), + new Location(null, 59, 129, 211), + 35 + ) + }; + + public static void snowify(World world, Location pos1, Location pos2) { + int minX = Math.min(pos1.getBlockX(), pos2.getBlockX()); + int maxX = Math.max(pos1.getBlockX(), pos2.getBlockX()); + int minY = Math.min(pos1.getBlockY(), pos2.getBlockY()); + int maxY = Math.max(pos1.getBlockY(), pos2.getBlockY()); + int minZ = Math.min(pos1.getBlockZ(), pos2.getBlockZ()); + int maxZ = Math.max(pos1.getBlockZ(), pos2.getBlockZ()); + ThreadLocalRandom random = ThreadLocalRandom.current(); + + for (int x = minX; x <= maxX; x++) { + for (int z = minZ; z <= maxZ; z++) { + for (int y = maxY; y >= minY; y--) { + Block block = world.getBlockAt(x, y, z); + + // Only care about full snow blocks as the base + if (block.getType() != Material.SNOW_BLOCK) continue; + + Block above = world.getBlockAt(x, y + 1, z); + Material aboveType = above.getType(); + + if (aboveType == Material.AIR) { + if (random.nextInt(100) < 35) { + above.setType(Material.SNOW, false); + Snow snow = (Snow) above.getBlockData(); + + int layers = random.nextInt(1, 5); // 1–4 + snow.setLayers(Math.min(layers, snow.getMaximumLayers())); + above.setBlockData(snow, false); + } + } else if (aboveType == Material.SNOW) { + if (random.nextInt(100) < 35) { + Snow snow = (Snow) above.getBlockData(); + + int add = random.nextInt(1, 5); // 1–4 + int max = snow.getMaximumLayers(); + + snow.setLayers(Math.min(snow.getLayers() + add, max)); + above.setBlockData(snow, false); + } + } + } + } + } + } + + public static ComponentLogger LOGGER; + public static JavaPlugin PLUGIN; + + @Override + public void onEnable() { + LOGGER = this.getComponentLogger(); + PLUGIN = this; + + BasicCommand regen = new RegenCommand(); + BasicCommand snowify = new SnowifyCommand(); + BasicCommand sell = new SellCommand(); + + BasicCommand viewitems = new ViewItemsCommand(); + RegisteredServiceProvider rsp = + getServer().getServicesManager().getRegistration(Economy.class); + + if (rsp == null) { + getLogger().severe("No Vault economy provider found!"); + getServer().getPluginManager().disablePlugin(this); + return; + } + if (Bukkit.getPluginManager().isPluginEnabled("PlaceholderAPI")) { + new SnowcoreExpansion(this).register(); + } + + if (Bukkit.getPluginManager().isPluginEnabled("Vault")) { + econ = rsp.getProvider(); + + } + + try { + registerCommand("regen", regen); + registerCommand("sell", sell); + registerCommand("snowify", snowify); + registerCommand("viewitems", viewitems); + + getServer().getPluginManager().registerEvents(new BreakListener(), this); + getServer().getPluginManager().registerEvents(new PlaceListener(), this); + getServer().getPluginManager().registerEvents(new InteractListener(), this); + } catch(Exception e) { + LOGGER.error(Component.text("You are most likely restarting the plugin via /plm restart snowcore. Please don't do this, commands will not be registered.")); + } + + LOGGER.info(Component.text("SNOWCORE - good morning!", Style.style(TextColor.color(0, 255, 0)))); + + Random random = new Random(); + + new BukkitRunnable() { + @Override + public void run() { + World world = Bukkit.getWorld("world"); + + for(Area area : areas) { + if (area.getAirPercentage(world) > 65) { + area.regen(world); + + final Component areaReplenish = MiniMessage.miniMessage().deserialize( + "‧₊˚❀༉‧₊˚. Seems like the area has been replenished.. ‧₊˚❀༉‧₊˚.", + Placeholder.unparsed("area", area.name) + ); + + Bukkit.getOnlinePlayers().forEach(player -> { + player.sendActionBar(areaReplenish); + }); + } + } + } + }.runTaskTimer(this, 0L, 10L); // 0L delay, 200L = 10s + new BukkitRunnable() { + @Override + public void run() { + if (random.nextInt(100) < 15) { + final Component snowing = MiniMessage.miniMessage().deserialize( + "❄❄❄ It seems to be snowing.." + ); + + World world = Bukkit.getWorld("world"); + Snowcore.snowify( + world, + new Location(world, 39, 127, 157), + new Location(world, 8, 139, 215) + ); + + Bukkit.broadcast(snowing); + } + } + }.runTaskTimer(this, 0L, 100L); // 0L delay, 200L = 10s + } + + public static int fortuneToDrops(int fortune, int drops) { + if (fortune <= 0) return drops; + + // Clamp fortune to 1-20 + fortune = Math.min(Math.max(fortune, 1), 20); + + // Base chance multiplier + double multiplier = 1.0 + Math.pow(fortune, 1.3) / 10.0; + + // Apply multiplier to original drops + int newDrops = (int) Math.round(drops * multiplier); + + return Math.max(newDrops, drops); // never return less than original + } + public static int getLevelFromExp(int exp) { + // EXP = 5 * L^2 -> L = sqrt(exp / 5) + return (int) Math.floor(Math.sqrt(exp / 5.0)); + } + + public static void sendActionBar(Player player, Component message) { + Component component = MiniMessage.miniMessage().deserialize( + " " + ).append(message); + player.sendActionBar(component); + } + + public static void sellInventory(Player player) { + var inv = player.getInventory(); + + double finalPrice = 0d; + + for (int slot = 0; slot < 35; slot++) { + ItemStack item = inv.getItem(slot); + + if (item == null || item.getType().isAir()) continue; + + boolean prices = Prices.hasSellPrice(item.getType()); + + if(prices) { + double price = Prices.getSellPrice(item.getType()) * item.getAmount(); + + finalPrice += price; + + inv.setItem(slot, null); + } + } + + Snowcore.econ.depositPlayer(player, finalPrice); + player.sendMessage(Component.text("Sold items for §a" + finalPrice + "§r!")); + } + + @Override + public void onDisable() { + LOGGER.info(Component.text("SNOWCORE - good night.", Style.style(TextColor.color(255, 0, 0)))); + } +} diff --git a/src/main/java/ovh/sad/snowcore/SnowcoreExpansion.java b/src/main/java/ovh/sad/snowcore/SnowcoreExpansion.java new file mode 100644 index 0000000..960abaa --- /dev/null +++ b/src/main/java/ovh/sad/snowcore/SnowcoreExpansion.java @@ -0,0 +1,79 @@ +package ovh.sad.snowcore; + +import me.clip.placeholderapi.expansion.PlaceholderExpansion; +import org.bukkit.Material; +import org.bukkit.OfflinePlayer; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.NotNull; +import ovh.sad.snowcore.items.Hoe; +import ovh.sad.snowcore.items.ToolUtils; +import ovh.sad.snowcore.items.Shovel; + +public class SnowcoreExpansion extends PlaceholderExpansion { + private final Snowcore plugin; // + + public SnowcoreExpansion(Snowcore plugin) { + this.plugin = plugin; + } + + @Override + @NotNull + public String getAuthor() { + return String.join(", ", plugin.getPluginMeta().getAuthors()); + } + + @Override + @NotNull + public String getIdentifier() { + return "snowcore"; + } + + @Override + @NotNull + public String getVersion() { + return plugin.getPluginMeta().getVersion(); + } + + @Override + public boolean persist() { + return true; + + + } + @Override + public String onRequest(OfflinePlayer oplayer, @NotNull String params) { + if (params.equalsIgnoreCase("tool_level")) { + Player player = oplayer.getPlayer(); + if (player != null) { + ItemStack item = player.getInventory().getItemInMainHand(); + Material type = item.getType(); + + if (Hoe.HoeDefinitions.ALL_HOES.containsKey(type) + || Shovel.ShovelDefinitions.ALL_SHOVELS.containsKey(type)) { + + int level = ToolUtils.getLevel(item); + + // Clamp between 1 and 80 + int clampedLevel = Math.max(1, Math.min(80, level)); + + // Interpolate hue (0° red → 270° violet) + float hue = (clampedLevel - 1) / 79.0f * 270f; + + java.awt.Color color = java.awt.Color.getHSBColor(hue / 360f, 1f, 1f); + + String hex = String.format("#%02x%02x%02x", + color.getRed(), + color.getGreen(), + color.getBlue()); + + return net.md_5.bungee.api.ChatColor.of(hex) + "❁ " + level; + } else { + return "0"; + } + } + } + return null; + } + +} \ No newline at end of file diff --git a/src/main/java/ovh/sad/snowcore/commands/RegenCommand.java b/src/main/java/ovh/sad/snowcore/commands/RegenCommand.java new file mode 100644 index 0000000..68f8366 --- /dev/null +++ b/src/main/java/ovh/sad/snowcore/commands/RegenCommand.java @@ -0,0 +1,25 @@ +package ovh.sad.snowcore.commands; + +import io.papermc.paper.command.brigadier.BasicCommand; +import io.papermc.paper.command.brigadier.CommandSourceStack; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; +import ovh.sad.snowcore.Area; +import ovh.sad.snowcore.Snowcore; + +import java.util.Objects; + +@NullMarked +public class RegenCommand implements BasicCommand { + @Override + public void execute(CommandSourceStack source, String[] args) { + for(Area area: Snowcore.areas) { + area.regen(Objects.requireNonNull(source.getExecutor()).getWorld()); + } + } + + @Override + public @Nullable String permission() { + return "snowcore.regen_areas"; + } +} diff --git a/src/main/java/ovh/sad/snowcore/commands/SellCommand.java b/src/main/java/ovh/sad/snowcore/commands/SellCommand.java new file mode 100644 index 0000000..684f117 --- /dev/null +++ b/src/main/java/ovh/sad/snowcore/commands/SellCommand.java @@ -0,0 +1,123 @@ +package ovh.sad.snowcore.commands; + +import dev.triumphteam.gui.builder.item.ItemBuilder; +import dev.triumphteam.gui.guis.Gui; +import dev.triumphteam.gui.guis.GuiItem; +import io.papermc.paper.command.brigadier.BasicCommand; +import io.papermc.paper.command.brigadier.CommandSourceStack; +import net.kyori.adventure.text.Component; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.InventoryCloseEvent; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.NotNull; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; +import ovh.sad.snowcore.Prices; +import ovh.sad.snowcore.Snowcore; + +import java.util.Objects; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicBoolean; + +@NullMarked +public class SellCommand implements BasicCommand { + + @Override + public void execute(CommandSourceStack source, String[] args) { + AtomicBoolean sold = new AtomicBoolean(false); + Gui gui = Gui.gui() + .title(Component.text("Sell Items")) + .rows(6) + .create(); + + gui.setDefaultClickAction(event -> { + if (event.getSlot() >= 45) event.setCancelled(true); // only block bottom row + }); + + + GuiItem pane = ItemBuilder.from(Material.BLACK_STAINED_GLASS_PANE) + .name(Component.text(" ")) + .asGuiItem(); + + // Fill entire bottom row + for (int col = 1; col <= 9; col++) { + gui.setItem(6, col, pane); + } + + GuiItem confirm = ItemBuilder.from(Material.GREEN_WOOL) + .name(Component.text("§aConfirm Sell")) + .asGuiItem(event -> { + event.setCancelled(true); + + Player player = (Player) event.getWhoClicked(); + + sellItemsFromInv(sold, player, event.getInventory()); + }); + + // Cancel (red wool) + GuiItem cancel = ItemBuilder.from(Material.RED_WOOL) + .name(Component.text("§cCancel")) + .asGuiItem(event -> { + Player player = (Player)event.getWhoClicked(); + + event.setCancelled(true); + player.closeInventory(); + }); + + gui.setItem(6, 4, confirm); + + gui.setItem(6, 6, cancel); + + gui.setCloseGuiAction(event -> { + Player player = (Player) event.getPlayer(); + UUID id = player.getUniqueId(); + + // If they sold, DON'T return items + if (!sold.get()) { + sellItemsFromInv(sold, player, event.getInventory()); + } + }); + + + gui.open((Player) Objects.requireNonNull(source.getExecutor())); + + } + + private void sellItemsFromInv(AtomicBoolean sold, Player player, Inventory inventory) { + sold.set(true); // mark as sold + + var inv = inventory; + + double finalPrice = 0d; + + for (int slot = 0; slot < 45; slot++) { + ItemStack item = inv.getItem(slot); + + if (item == null || item.getType().isAir()) continue; + + boolean prices = Prices.hasSellPrice(item.getType()); + + if(!prices) { + player.getInventory().addItem(item); + inv.setItem(slot, null); + } else { + double price = Prices.getSellPrice(item.getType()) * item.getAmount(); + + finalPrice += price; + + inv.setItem(slot, null); + } + } + + Snowcore.econ.depositPlayer(player, finalPrice); + player.sendMessage(Component.text("Sold items for §a" + finalPrice + "§r!")); + player.closeInventory(); + } + + @Override + public @Nullable String permission() { + return "snowcore.sell"; + } +} diff --git a/src/main/java/ovh/sad/snowcore/commands/SnowifyCommand.java b/src/main/java/ovh/sad/snowcore/commands/SnowifyCommand.java new file mode 100644 index 0000000..e3f962d --- /dev/null +++ b/src/main/java/ovh/sad/snowcore/commands/SnowifyCommand.java @@ -0,0 +1,30 @@ +package ovh.sad.snowcore.commands; + +import io.papermc.paper.command.brigadier.BasicCommand; +import io.papermc.paper.command.brigadier.CommandSourceStack; +import org.bukkit.Location; +import org.bukkit.World; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; +import ovh.sad.snowcore.Snowcore; + +import java.util.Objects; + +@NullMarked +public class SnowifyCommand implements BasicCommand { + + @Override + public void execute(CommandSourceStack source, String[] args) { + World world = Objects.requireNonNull(source.getExecutor()).getWorld(); + Snowcore.snowify( + world, + new Location(world, 39, 127, 157), + new Location(world, 8, 139, 215) + ); + } + + @Override + public @Nullable String permission() { + return "snowcore.snowify"; + } +} diff --git a/src/main/java/ovh/sad/snowcore/commands/ViewItemsCommand.java b/src/main/java/ovh/sad/snowcore/commands/ViewItemsCommand.java new file mode 100644 index 0000000..50a3813 --- /dev/null +++ b/src/main/java/ovh/sad/snowcore/commands/ViewItemsCommand.java @@ -0,0 +1,39 @@ +package ovh.sad.snowcore.commands; + +import dev.triumphteam.gui.guis.Gui; +import dev.triumphteam.gui.guis.GuiItem; +import io.papermc.paper.command.brigadier.BasicCommand; +import io.papermc.paper.command.brigadier.CommandSourceStack; +import net.kyori.adventure.text.Component; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; +import org.jspecify.annotations.Nullable; +import ovh.sad.snowcore.items.Hoe; +import ovh.sad.snowcore.items.Shovel; + +import java.util.Objects; + +public class ViewItemsCommand implements BasicCommand { + @Override + public void execute(@NotNull CommandSourceStack source, String @NotNull [] args) { + Gui gui = Gui.gui() + .title(Component.text("All obtainable items")) + .rows(6) + .create(); + + Hoe.HoeDefinitions.ALL_HOES.forEach((material, hoe) -> { + gui.addItem(new GuiItem(hoe.renderItem(null))); + }); + + Shovel.ShovelDefinitions.ALL_SHOVELS.forEach((material, shovel) -> { + gui.addItem(new GuiItem(shovel.renderItem(null))); + }); + + gui.open((Player) Objects.requireNonNull(source.getExecutor())); + } + + @Override + public @Nullable String permission() { + return "snowcore.view_items"; + } +} diff --git a/src/main/java/ovh/sad/snowcore/guis/UpgradeYourToolsGui.java b/src/main/java/ovh/sad/snowcore/guis/UpgradeYourToolsGui.java new file mode 100644 index 0000000..7fe4650 --- /dev/null +++ b/src/main/java/ovh/sad/snowcore/guis/UpgradeYourToolsGui.java @@ -0,0 +1,200 @@ +package ovh.sad.snowcore.guis; + +import dev.triumphteam.gui.builder.item.ItemBuilder; +import dev.triumphteam.gui.guis.Gui; +import dev.triumphteam.gui.guis.GuiItem; +import dev.triumphteam.gui.guis.PaginatedGui; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.Style; +import net.kyori.adventure.text.minimessage.MiniMessage; +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.event.player.PlayerInteractEntityEvent; +import org.bukkit.inventory.ItemStack; +import ovh.sad.snowcore.Prices; +import ovh.sad.snowcore.Snowcore; +import ovh.sad.snowcore.items.Hoe; +import ovh.sad.snowcore.items.Shovel; +import ovh.sad.snowcore.items.Tool; +import ovh.sad.snowcore.items.ToolUtils; + +import java.util.concurrent.atomic.AtomicBoolean; + +public class UpgradeYourToolsGui { + + public static void open(PlayerInteractEntityEvent event) { + openHoes(event.getPlayer()); + } + + // ========================================================= + // HOES + // ========================================================= + private static void openHoes(Player player) { + + PaginatedGui gui = Gui.paginated() + .title(Component.text("Upgrade Your Tools - Hoes")) + .rows(3) + .pageSize(7) // only columns 2-8 + .create(); + + gui.setDefaultClickAction(e -> e.setCancelled(true)); + + fillLayout(gui); + + // Add ordered hoes + for (Hoe hoe : Hoe.HoeDefinitions.ORDERED) { + gui.addItem(createToolGuiItem(hoe, player, gui)); + } + + // Navigation (bottom right) + gui.setItem(3, 9, + ItemBuilder.from(Material.ARROW) + .name(Component.text("Next → Shovels", NamedTextColor.YELLOW)) + .asGuiItem(e -> { + e.setCancelled(true); + openShovels(player); + }) + ); + + gui.open(player); + } + + // ========================================================= + // SHOVELS + // ========================================================= + private static void openShovels(Player player) { + + PaginatedGui gui = Gui.paginated() + .title(Component.text("Upgrade Your Tools - Shovels")) + .rows(3) + .pageSize(7) + .create(); + + gui.setDefaultClickAction(e -> e.setCancelled(true)); + + fillLayout(gui); + + for (Shovel shovel : Shovel.ShovelDefinitions.ORDERED) { + gui.addItem(createToolGuiItem(shovel, player, gui)); + } + + gui.setItem(3, 9, + ItemBuilder.from(Material.ARROW) + .name(Component.text("← Previous Hoes", NamedTextColor.YELLOW)) + .asGuiItem(e -> { + e.setCancelled(true); + openHoes(player); + }) + ); + + gui.open(player); + } + + private static > T getPreviousTool(T tool) { + int index; + if (tool instanceof Shovel) { + index = Shovel.ShovelDefinitions.ORDERED.indexOf(tool); + if (index <= 0) return null; + return (T) Shovel.ShovelDefinitions.ORDERED.get(index - 1); + } else if (tool instanceof Hoe) { + index = Hoe.HoeDefinitions.ORDERED.indexOf(tool); + if (index <= 0) return null; + return (T) Hoe.HoeDefinitions.ORDERED.get(index - 1); + } + return null; + } + + private static > GuiItem createToolGuiItem(T tool, Player player, PaginatedGui gui) { + return new GuiItem( + tool.renderItem(null), + e -> { + e.setCancelled(true); + + if (tool.material == Material.WOODEN_SHOVEL || tool.material == Material.WOODEN_HOE) { + // First tool is free + player.sendMessage( + MiniMessage.miniMessage().deserialize( + "✔ You've been given a ! Enjoy Snowing.", + Placeholder.component("name", tool.name()) + ) + ); + + player.getInventory().addItem(tool.renderItem(null)); + gui.close(player); + return; + } + + // Get previous tool + Tool previousTool = getPreviousTool(tool); + if (previousTool == null) return; // safety + + // Check requirement + boolean hasPrevious = false; + for (ItemStack item : player.getInventory()) { + if (item != null && item.getType() == previousTool.material) { + hasPrevious = true; + break; + } + } + + if (!hasPrevious) { + player.sendMessage(MiniMessage.miniMessage().deserialize( + "❌ Missing a requirement: ", + Placeholder.component("previousname", previousTool.name()) + )); + return; + } + + // Check economy + double balance = Snowcore.econ.getBalance(player); + Integer cost = Prices.getToolBuyPrice(tool); + + if (balance >= cost) { + Snowcore.econ.withdrawPlayer(player, cost); + player.getInventory().remove(previousTool.material); + player.getInventory().addItem(tool.renderItem(null)); + + player.sendMessage( + MiniMessage.miniMessage().deserialize( + "✔ Unlocked for $ and ", + Placeholder.component("previousname", previousTool.name()), + Placeholder.component("name", tool.name()), + Placeholder.unparsed("cost", cost.toString()) + ) + ); + gui.close(player); + } else { + player.sendMessage( + MiniMessage.miniMessage().deserialize( + "✘ Not enough money! Need $, you have $", + Placeholder.unparsed("balance", String.valueOf(balance)), + Placeholder.unparsed("cost", cost.toString()) + ) + ); + } + } + ); + } + + + private static void fillLayout(PaginatedGui gui) { + GuiItem pane = ItemBuilder.from(Material.GRAY_STAINED_GLASS_PANE) + .name(Component.empty()) + .asGuiItem(e -> e.setCancelled(true)); + + for (int col = 1; col <= 9; col++) { + gui.setItem(1, col, pane); + } + + // Row 2 (side panes only) + gui.setItem(2, 1, pane); + gui.setItem(2, 9, pane); + + // Row 3 (all panes) + for (int col = 1; col <= 9; col++) { + gui.setItem(3, col, pane); + } + } +} diff --git a/src/main/java/ovh/sad/snowcore/items/Hoe.java b/src/main/java/ovh/sad/snowcore/items/Hoe.java new file mode 100644 index 0000000..00c57e3 --- /dev/null +++ b/src/main/java/ovh/sad/snowcore/items/Hoe.java @@ -0,0 +1,140 @@ +package ovh.sad.snowcore.items; + +import org.bukkit.Material; +import org.jspecify.annotations.Nullable; + +import java.util.*; + +public class Hoe extends Tool { + private final List canBreak = new ArrayList<>(); + private final List dependsOn = new ArrayList<>(); + + public Hoe(Material material, String name) { + super(material, name); + } + + public Hoe addCanBreaks(Collection m) { + canBreak.addAll(m); + return this; + } + + public Hoe addDepend(Hoe h) { + dependsOn.add(h); + return this; + } + + public static class HoeDefinitions { + public static final Hoe WOODEN_HOE = new Hoe(Material.WOODEN_HOE, "<#DCB190>Wooden Hoe") + .addEnchantment(ToolUtils.EnchantmentType.WIND, 5) + .addCanBreaks(List.of( Material.SPRUCE_SAPLING, Material.WILDFLOWERS )); + public static final Hoe STONE_HOE = new Hoe(Material.STONE_HOE, "Stone Hoe") + .addEnchantment(ToolUtils.EnchantmentType.WIND, 15) + .addEnchantment(ToolUtils.EnchantmentType.DOUBLE_DROP, 5) + .addCanBreaks(List.of( Material.PINK_PETALS, Material.ALLIUM, Material.CHERRY_SAPLING )) + .addDepend(WOODEN_HOE); + + public static final Hoe COPPER_HOE = new Hoe(Material.COPPER_HOE, "Copper Hoe") + .addEnchantment(ToolUtils.EnchantmentType.WIND, 25) + .addEnchantment(ToolUtils.EnchantmentType.DOUBLE_DROP, 15) + .addEnchantment(ToolUtils.EnchantmentType.FORTUNE, 1) + .limit(ToolUtils.EnchantmentType.FORTUNE, 5) + .limit(ToolUtils.EnchantmentType.WIND, 40) + .addCanBreaks(List.of( + Material.OXEYE_DAISY, Material.LILY_OF_THE_VALLEY, Material.CLOSED_EYEBLOSSOM, + Material.CARROTS, Material.POTATOES + )) + .addDepend(STONE_HOE); + + public static final Hoe IRON_HOE = new Hoe(Material.IRON_HOE, "Iron Hoe") + .addEnchantment(ToolUtils.EnchantmentType.WIND, 30) + .addEnchantment(ToolUtils.EnchantmentType.DOUBLE_DROP, 20) + .addEnchantment(ToolUtils.EnchantmentType.FORTUNE, 2) + .limit(ToolUtils.EnchantmentType.FORTUNE, 10) + .limit(ToolUtils.EnchantmentType.WIND, 50) + .addAbility(ToolUtils.AbilityType.JACKPOT, 3, 0, 0, true) + .addAbility(ToolUtils.AbilityType.MULTIPLIER, 1.5, 5, 50_000, false) + .addCanBreaks( + List.of( + Material.WHEAT, + Material.BEETROOTS, + Material.DEAD_FIRE_CORAL, Material.DEAD_TUBE_CORAL, Material.DEAD_HORN_CORAL, Material.DEAD_BRAIN_CORAL_FAN, Material.OPEN_EYEBLOSSOM + )) + .addDepend(COPPER_HOE); + + public static final Hoe DIAMOND_HOE = new Hoe(Material.DIAMOND_HOE, "Diamond Hoe") + .addEnchantment(ToolUtils.EnchantmentType.WIND, 30) + .addEnchantment(ToolUtils.EnchantmentType.DOUBLE_DROP, 20) + .addEnchantment(ToolUtils.EnchantmentType.FORTUNE, 3) + .limit(ToolUtils.EnchantmentType.FORTUNE, 20) + .limit(ToolUtils.EnchantmentType.WIND, 60) + .addAbility(ToolUtils.AbilityType.MULTIPLIER, 2.5, 5, 50_000, false) + .addAbility(ToolUtils.AbilityType.JACKPOT, 5, 0, 0, true) + .addAbility(ToolUtils.AbilityType.THREE_BY_THREE, 0, 0, 0, true) + .addAbility(ToolUtils.AbilityType.AUTOSELL, 1, 15, 100_000, false) + .addCanBreaks( List.of( Material.ROSE_BUSH, Material.POPPY, Material.RED_MUSHROOM, Material.CRIMSON_FUNGUS ) ) + .addDepend(IRON_HOE); + + public static final Hoe NETHERITE_HOE = new Hoe(Material.NETHERITE_HOE, "Netherite Hoe") + .addEnchantment(ToolUtils.EnchantmentType.WIND, 40) + .addEnchantment(ToolUtils.EnchantmentType.DOUBLE_DROP, 30) + .addEnchantment(ToolUtils.EnchantmentType.FORTUNE, 4) + .limit(ToolUtils.EnchantmentType.FORTUNE, 30) + .limit(ToolUtils.EnchantmentType.WIND, 70) + .limit(ToolUtils.EnchantmentType.DOUBLE_DROP, 60) + .addAbility(ToolUtils.AbilityType.MULTIPLIER, 5, 5, 75_000, false) + .addAbility(ToolUtils.AbilityType.AUTOSELL, 1, 25, 150_000, false) + .addAbility(ToolUtils.AbilityType.THREE_BY_THREE, 0, 0, 0, true) + .addAbility(ToolUtils.AbilityType.JACKPOT, 15, 0, 15_000, true) + .addCanBreaks( List.of( Material.CACTUS_FLOWER, Material.PEONY, Material.PINK_TULIP, Material.CACTUS ) ) + .addDepend(DIAMOND_HOE); + + public static final HashMap ALL_HOES = new HashMap<>( + Map.of(WOODEN_HOE.material, WOODEN_HOE, + STONE_HOE.material, STONE_HOE, + COPPER_HOE.material, COPPER_HOE, + IRON_HOE.material, IRON_HOE, + DIAMOND_HOE.material, DIAMOND_HOE, + NETHERITE_HOE.material, NETHERITE_HOE + ) + ); + + public static final List ORDERED = List.of( + WOODEN_HOE, + STONE_HOE, + COPPER_HOE, + IRON_HOE, + DIAMOND_HOE, + NETHERITE_HOE + ); + + public static @Nullable Hoe getRequiredHoe(Material material) { + for (Hoe hoe : ORDERED) { + if (getAllBlocks(hoe).contains(material)) { + return hoe; + } + } + return null; + } + + public static List getAllBlocks(Hoe hoe) { + Set result = new HashSet<>(); + Set visited = new HashSet<>(); + + collectBlocks(hoe, result, visited); + + return new ArrayList<>(result); + } + + private static void collectBlocks(Hoe hoe, Set result, Set visited) { + if (!visited.add(hoe)) return; // already processed + + result.addAll(hoe.canBreak); + + for (Hoe parent : hoe.dependsOn) { + collectBlocks(parent, result, visited); + } + } + } + + +} diff --git a/src/main/java/ovh/sad/snowcore/items/Shovel.java b/src/main/java/ovh/sad/snowcore/items/Shovel.java new file mode 100644 index 0000000..062cd26 --- /dev/null +++ b/src/main/java/ovh/sad/snowcore/items/Shovel.java @@ -0,0 +1,85 @@ +package ovh.sad.snowcore.items; + +import org.bukkit.Material; +import java.util.*; + +public class Shovel extends Tool { + public Shovel(Material material, String name) { + super(material, name); + } + + public static class ShovelDefinitions { + public static final Shovel WOODEN_SHOVEL = new Shovel(Material.WOODEN_SHOVEL, "<#90bbdc>Wooden Shovel") + .addEnchantment(ToolUtils.EnchantmentType.SWEEPER, 10) + .limit(ToolUtils.EnchantmentType.SWEEPER, 15); + public static final Shovel STONE_SHOVEL = new Shovel(Material.STONE_SHOVEL, "Stone Shovel") + .addEnchantment(ToolUtils.EnchantmentType.SWEEPER, 20) + .addEnchantment(ToolUtils.EnchantmentType.LAYER_REMOVER, 10) + .limit(ToolUtils.EnchantmentType.SWEEPER, 25) + .limit(ToolUtils.EnchantmentType.LAYER_REMOVER, 12); + + public static final Shovel COPPER_SHOVEL = new Shovel(Material.COPPER_SHOVEL, "<#6699ff>Copper Shovel<#6699ff>") + .addEnchantment(ToolUtils.EnchantmentType.SWEEPER, 30) + .addEnchantment(ToolUtils.EnchantmentType.LAYER_REMOVER, 15) + .addEnchantment(ToolUtils.EnchantmentType.FORTUNE, 1) + .limit(ToolUtils.EnchantmentType.FORTUNE, 5) + .limit(ToolUtils.EnchantmentType.SWEEPER, 40) + .limit(ToolUtils.EnchantmentType.LAYER_REMOVER, 20); + + public static final Shovel IRON_SHOVEL = new Shovel(Material.IRON_SHOVEL, "Iron Shovel") + .addEnchantment(ToolUtils.EnchantmentType.SWEEPER, 40) + .addEnchantment(ToolUtils.EnchantmentType.LAYER_REMOVER, 25) + .addEnchantment(ToolUtils.EnchantmentType.FORTUNE, 3) + .limit(ToolUtils.EnchantmentType.FORTUNE, 10) + .limit(ToolUtils.EnchantmentType.SWEEPER, 50) + .limit(ToolUtils.EnchantmentType.LAYER_REMOVER, 30) + .addAbility(ToolUtils.AbilityType.NUKE, 0, 0, 0, true) + .addAbility(ToolUtils.AbilityType.MULTIPLIER, 2, 5, 40_000, false) + .addAbility(ToolUtils.AbilityType.AUTOSELL, 1, 20, 15_000, false); + + public static final Shovel DIAMOND_SHOVEL = new Shovel(Material.DIAMOND_SHOVEL, "<#ff5555>Diamond Shovel<#ff5555>") + .addEnchantment(ToolUtils.EnchantmentType.SWEEPER, 50) + .addEnchantment(ToolUtils.EnchantmentType.LAYER_REMOVER, 35) + .addEnchantment(ToolUtils.EnchantmentType.FORTUNE, 4) + .limit(ToolUtils.EnchantmentType.FORTUNE, 10) + .limit(ToolUtils.EnchantmentType.SWEEPER, 60) + .limit(ToolUtils.EnchantmentType.LAYER_REMOVER, 40) + .addAbility(ToolUtils.AbilityType.NUKE, 0, 0, 0, true) + .addAbility(ToolUtils.AbilityType.WIFI, 2, 0, 0, true) + .addAbility(ToolUtils.AbilityType.MULTIPLIER, 3, 5, 50_000, false) + .addAbility(ToolUtils.AbilityType.AUTOSELL, 1, 25, 40_000, false); + + public static final Shovel NETHERITE_SHOVEL = new Shovel(Material.NETHERITE_SHOVEL, "<#4dc44d>Netherite Shovel<#4dc44d>") + .addEnchantment(ToolUtils.EnchantmentType.SWEEPER, 50) + .addEnchantment(ToolUtils.EnchantmentType.LAYER_REMOVER, 40) + .addEnchantment(ToolUtils.EnchantmentType.FORTUNE, 6) + .limit(ToolUtils.EnchantmentType.FORTUNE, 30) + .limit(ToolUtils.EnchantmentType.SWEEPER, 100) + .limit(ToolUtils.EnchantmentType.LAYER_REMOVER, 100) + .addAbility(ToolUtils.AbilityType.MULTIPLIER, 5, 5, 75_000, false) + .addAbility(ToolUtils.AbilityType.AUTOSELL, 1, 30, 60_000, false) + .addAbility(ToolUtils.AbilityType.NUKE, 0, 0, 0, true) + .addAbility(ToolUtils.AbilityType.WIFI, 6, 0, 0 , true); + + public static final HashMap ALL_SHOVELS = new HashMap<>( + Map.of(WOODEN_SHOVEL.material, WOODEN_SHOVEL, + STONE_SHOVEL.material, STONE_SHOVEL, + COPPER_SHOVEL.material, COPPER_SHOVEL, + IRON_SHOVEL.material, IRON_SHOVEL, + DIAMOND_SHOVEL.material, DIAMOND_SHOVEL, + NETHERITE_SHOVEL.material, NETHERITE_SHOVEL + ) + ); + + public static final List ORDERED = List.of( + WOODEN_SHOVEL, + STONE_SHOVEL, + COPPER_SHOVEL, + IRON_SHOVEL, + DIAMOND_SHOVEL, + NETHERITE_SHOVEL + ); + } + + +} diff --git a/src/main/java/ovh/sad/snowcore/items/Tool.java b/src/main/java/ovh/sad/snowcore/items/Tool.java new file mode 100644 index 0000000..707ab9d --- /dev/null +++ b/src/main/java/ovh/sad/snowcore/items/Tool.java @@ -0,0 +1,184 @@ +package ovh.sad.snowcore.items; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.MiniMessage; +import org.bukkit.Material; +import org.bukkit.inventory.ItemFlag; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.persistence.PersistentDataContainer; +import org.bukkit.persistence.PersistentDataType; +import org.jspecify.annotations.Nullable; + +import java.util.*; + +public abstract class Tool> { + + public final Material material; + protected final String name; + + public final List definedEnchantments = new ArrayList<>(); + public final Map definedEnchantLimits = new HashMap<>(); + public final List definedAbilities = new ArrayList<>(); + + protected Tool(Material material, String name) { + this.material = material; + this.name = name; + } + + + public ItemStack renderItem(@Nullable ItemStack itemToRender) { + if(itemToRender == null) { + ItemStack item = new ItemStack(material); + + ItemMeta meta = item.getItemMeta(); + + if(meta == null) throw new Error("RenderItem errored, meta could not be found!"); + + Component name = MiniMessage.miniMessage().deserialize( + "[✩ 1, ❁ 1] " + this.name + ); + + meta.setUnbreakable(true); + meta.addItemFlags(ItemFlag.HIDE_UNBREAKABLE); + meta.displayName(name); + + List lore = new ArrayList<>(); + List abilities = definedAbilities.stream().filter(a -> a.byDefault).toList(); + + lore.add(MiniMessage.miniMessage().deserialize( + "(Shift rightclick to upgrade!)" + )); + + lore.add(Component.text("")); + + definedEnchantments.forEach(z -> { + if(z.type.usesLevel) { + lore.add(MiniMessage.miniMessage().deserialize( + " • " + z.type.name + " " + z.amount + )); + } else { + lore.add(MiniMessage.miniMessage().deserialize( + " • " + z.type.name + " (" + z.amount + "%)" + )); + } + }); + + abilities.forEach(z -> { + lore.add(MiniMessage.miniMessage().deserialize( + "" + z.type.name + ", " + z.type.description.replaceAll("%maxValue%", String.valueOf(z.maxValue)) + )); + }); + + meta.lore(lore); + + + PersistentDataContainer pdc = meta.getPersistentDataContainer(); + + pdc.set(ToolUtils.EXPERIENCE, PersistentDataType.INTEGER, 1); + pdc.set(ToolUtils.TOTAL_EXPERIENCE, PersistentDataType.INTEGER, 1); + + pdc.set(ToolUtils.ENCHANTMENTS, + PersistentDataType.LIST.listTypeFrom(ToolUtils.Enchantment.EMPTY_INSTANCE), + definedEnchantments); + + pdc.set(ToolUtils.ABILITIES, + PersistentDataType.LIST.listTypeFrom(ToolUtils.Ability.EMPTY_INSTANCE), + definedAbilities.stream().filter(a -> a.byDefault).toList()); + + meta.setEnchantmentGlintOverride(true); + + item.setItemMeta(meta); + return item; + } else { + ItemStack item = itemToRender.clone(); + ItemMeta meta = item.getItemMeta(); + + if(meta == null) throw new Error("RenderItem errored, meta could not be found!"); + + Integer exp = ToolUtils.getExp(item); + Integer level = ToolUtils.getLevel(item); + + Component name = MiniMessage.miniMessage().deserialize( + "[✩ " + exp + ", ❁ " + level + "] " + this.name + ); + meta.displayName(name); + + List lore = new ArrayList<>(); + List abilities = ToolUtils.getAbilities(item); + + lore.add(MiniMessage.miniMessage().deserialize( + "(Shift rightclick to upgrade!)" + )); + + lore.add(Component.text("")); + ToolUtils.getEnchantments(item).forEach(z -> { + if(z.type.usesLevel) { + lore.add(MiniMessage.miniMessage().deserialize( + " • " + z.type.name + " " + z.amount + )); + } else { + lore.add(MiniMessage.miniMessage().deserialize( + " • " + z.type.name + " (" + z.amount + "%)" + )); + } + }); + + abilities.forEach(z -> { + lore.add(MiniMessage.miniMessage().deserialize( + "" + z.type.name + ", " + z.type.description.replaceAll("%maxValue%", String.valueOf(z.maxValue)) + )); + }); + + meta.lore(lore); + + meta.setEnchantmentGlintOverride(true); + + item.setItemMeta(meta); + return item; + } + }; + + + public ItemStack increaseEXP(ItemStack item, int amount) { + ItemMeta meta = item.getItemMeta(); + if (meta == null) return item; + + PersistentDataContainer pdc = meta.getPersistentDataContainer(); + + int exp = pdc.get(ToolUtils.EXPERIENCE, PersistentDataType.INTEGER) + amount; + int total = pdc.get(ToolUtils.TOTAL_EXPERIENCE, PersistentDataType.INTEGER) + amount; + + pdc.set(ToolUtils.EXPERIENCE, PersistentDataType.INTEGER, exp); + pdc.set(ToolUtils.TOTAL_EXPERIENCE, PersistentDataType.INTEGER, total); + + item.setItemMeta(meta); + return this.renderItem(item); + } + + public Component name() { + return MiniMessage.miniMessage().deserialize(name); + } + + /* ------------------------------------------------ */ + /* Builder API (shared) */ + /* ------------------------------------------------ */ + + @SuppressWarnings("unchecked") + public T addEnchantment(ToolUtils.EnchantmentType type, int amount) { + definedEnchantments.add(new ToolUtils.Enchantment(type, amount)); + return (T) this; + } + + @SuppressWarnings("unchecked") + public T limit(ToolUtils.EnchantmentType type, int max) { + definedEnchantLimits.put(type, max); + return (T) this; + } + + @SuppressWarnings("unchecked") + public T addAbility(ToolUtils.AbilityType type, double max, int unlock, double cost, boolean byDefault) { + definedAbilities.add(new ToolUtils.Ability(type, max, unlock, cost, byDefault)); + return (T) this; + } +} diff --git a/src/main/java/ovh/sad/snowcore/items/ToolUtils.java b/src/main/java/ovh/sad/snowcore/items/ToolUtils.java new file mode 100644 index 0000000..227f0de --- /dev/null +++ b/src/main/java/ovh/sad/snowcore/items/ToolUtils.java @@ -0,0 +1,267 @@ +package ovh.sad.snowcore.items; + + +import org.bukkit.NamespacedKey; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.persistence.PersistentDataAdapterContext; +import org.bukkit.persistence.PersistentDataContainer; +import org.bukkit.persistence.PersistentDataType; +import org.jetbrains.annotations.NotNull; +import org.jspecify.annotations.Nullable; +import ovh.sad.snowcore.Snowcore; + +import java.nio.ByteBuffer; +import java.util.List; + +public class ToolUtils { + static final NamespacedKey ABILITIES = new NamespacedKey(Snowcore.PLUGIN, "snowcore_abilities"); + static final NamespacedKey ENCHANTMENTS = new NamespacedKey(Snowcore.PLUGIN, "snowcore_enchants"); + static final NamespacedKey EXPERIENCE = new NamespacedKey(Snowcore.PLUGIN, "snowcore_exp"); + static final NamespacedKey TOTAL_EXPERIENCE = new NamespacedKey(Snowcore.PLUGIN, "snowcore_exp_total"); + + public enum EnchantmentType { + + FORTUNE("Fortune", 200, true), + WIND("Wind", 120, false), + DOUBLE_DROP("Double Drop", 350, false), + NONE("418 Teapot", 0, false), + + SWEEPER("Sweeper", 100, false), + LAYER_REMOVER("Layer Remover", 150, false); + + public final String name; + public final int baseExpCost; + public final boolean usesLevel; // false = chance % + + EnchantmentType(String name, int baseExpCost, boolean usesLevel) { + this.baseExpCost = baseExpCost; + this.usesLevel = usesLevel; + this.name = name; + } + } + + public static class Enchantment implements PersistentDataType { + public static final Enchantment EMPTY_INSTANCE = new Enchantment(EnchantmentType.NONE, 0); + + public final EnchantmentType type; + public int amount; + + public Enchantment(EnchantmentType type, int amount) { + this.type = type; + this.amount = amount; + } + + public int getExpRequired(int level) { + if(type.usesLevel) { + return type.baseExpCost * level * level; + } else { + double l = level / 10.0; + return (int)Math.ceil(type.baseExpCost * Math.pow(l, 1.7)); + } + } + + public Enchantment copy() { + return new Enchantment(type, amount); + } + + + @Override + public @NotNull Class getPrimitiveType() { + return byte[].class; + } + + @Override + public @NotNull Class getComplexType() { + return Enchantment.class; + } + + @Override + public byte @NotNull [] toPrimitive(Enchantment complex, @NotNull PersistentDataAdapterContext context) { + ByteBuffer bb = ByteBuffer.allocate( + Integer.BYTES + // enum ordinal + Integer.BYTES // amount + ); + + bb.putInt(complex.type.ordinal()); + bb.putInt(complex.amount); + + return bb.array(); + } + + @Override + public @NotNull Enchantment fromPrimitive(byte @NotNull [] primitive, @NotNull PersistentDataAdapterContext context) { + ByteBuffer bb = ByteBuffer.wrap(primitive); + + EnchantmentType type = EnchantmentType.values()[bb.getInt()]; + int amount = bb.getInt(); + + return new Enchantment(type, amount); + } + } + + + public enum AbilityType { + // Hoe only + JACKPOT("Jackpot", "you have a %maxValue%% chance to drop 5x items!"), + THREE_BY_THREE("3x3", "you mine blocks in a 3x3 grid!"), + + // Shovel only + NUKE("Nuke", "you have a 10% chance to remove 108 layers of snow in a 6x6 area!"), + WIFI("Free wifi anywhere you go", "you have a %maxValue%% chance of getting a inventory full of snowballs!"), + + // Mutual + MULTIPLIER("Multiplier", "your drops are multiplied by %maxValue%x!"), + AUTOSELL("Auto-sell", "you automatically sell all of your items when your inventory is full"), + + NONE("HTTP", "418 Teapot"); + + public final String name; + public final String description; + + AbilityType(String name, String description) { + this.name = name; + this.description = description; + + } + } + + public static class Ability implements PersistentDataType { + public static final Ability EMPTY_INSTANCE = new Ability(AbilityType.NONE, 0, 0, 0, false); + + public AbilityType type; + + public double maxValue; + public int unlockLevel; + public double baseCost; + + public boolean byDefault; + + public Ability(AbilityType type, double maxValue, int unlockLevel, double baseCost, boolean byDefault) { + this.type = type; + this.maxValue = maxValue; + this.unlockLevel = unlockLevel; + this.baseCost = baseCost; + this.byDefault = byDefault; + } + + + @Override + public @NotNull Class getPrimitiveType() { + return byte[].class; + } + + @Override + public @NotNull Class getComplexType() { + return Ability.class; + } + @Override + public byte @NotNull [] toPrimitive(Ability complex, @NotNull PersistentDataAdapterContext context) { + ByteBuffer bb = ByteBuffer.allocate( + Integer.BYTES + // enum ordinal + Double.BYTES + // maxValue + Integer.BYTES + // unlockLevel + Double.BYTES + // baseCost + Short.BYTES // byDefault + ); + + bb.putInt(complex.type.ordinal()); + bb.putDouble(complex.maxValue); + bb.putInt(complex.unlockLevel); + bb.putDouble(complex.baseCost); + + if(complex.byDefault) { + bb.putShort((short)1); + } else { + bb.putShort((short)0); + } + return bb.array(); + } + + @Override + public @NotNull Ability fromPrimitive(byte @NotNull [] primitive, @NotNull PersistentDataAdapterContext context) { + ByteBuffer bb = ByteBuffer.wrap(primitive); + + AbilityType type = AbilityType.values()[bb.getInt()]; + double maxValue = bb.getDouble(); + int unlockLevel = bb.getInt(); + double baseCost = bb.getDouble(); + boolean byDefault = bb.getShort() == 1; + + return new Ability(type, maxValue, unlockLevel, baseCost, byDefault); + } + } + + public static @Nullable Integer getLevel(@NotNull ItemStack item) { + ItemMeta meta = item.getItemMeta(); + if (meta == null) return null; + + PersistentDataContainer pdc = meta.getPersistentDataContainer(); + + return Snowcore.getLevelFromExp(pdc.get(ToolUtils.TOTAL_EXPERIENCE, PersistentDataType.INTEGER)); + } + + public static @Nullable Integer getExp(@NotNull ItemStack item) { + ItemMeta meta = item.getItemMeta(); + if (meta == null) return null; + + PersistentDataContainer pdc = meta.getPersistentDataContainer(); + + return pdc.get(ToolUtils.EXPERIENCE, PersistentDataType.INTEGER); + } + + public static @Nullable ItemStack setExp(Tool tool, @NotNull ItemStack item, Integer exp) { + ItemMeta meta = item.getItemMeta(); + if (meta == null) return null; + + PersistentDataContainer pdc = meta.getPersistentDataContainer(); + pdc.set(ToolUtils.EXPERIENCE, PersistentDataType.INTEGER, exp); + item.setItemMeta(meta); + + return tool.renderItem(item); + } + public static @Nullable List getEnchantments(@NotNull ItemStack item) { + ItemMeta meta = item.getItemMeta(); + if (meta == null) return null; + + PersistentDataContainer pdc = meta.getPersistentDataContainer(); + + return pdc.get(ToolUtils.ENCHANTMENTS, PersistentDataType.LIST.listTypeFrom(ToolUtils.Enchantment.EMPTY_INSTANCE)); + } + + public static @NotNull ItemStack setEnchantments(Tool tool, @NotNull ItemStack item, @NotNull List enchantments) { + ItemMeta meta = item.getItemMeta(); + if (meta == null) return item; + + PersistentDataContainer pdc = meta.getPersistentDataContainer(); + pdc.set(ToolUtils.ENCHANTMENTS, + PersistentDataType.LIST.listTypeFrom(ToolUtils.Enchantment.EMPTY_INSTANCE), + enchantments); + + item.setItemMeta(meta); + return tool.renderItem(item); + } + + public static @Nullable List getAbilities(@NotNull ItemStack item) { + ItemMeta meta = item.getItemMeta(); + if (meta == null) return null; + + PersistentDataContainer pdc = meta.getPersistentDataContainer(); + + return pdc.get(ToolUtils.ABILITIES, PersistentDataType.LIST.listTypeFrom(ToolUtils.Ability.EMPTY_INSTANCE)); + } + + public static @NotNull ItemStack setAbilities(Tool tool, @NotNull ItemStack item, @NotNull List abilities) { + ItemMeta meta = item.getItemMeta(); + if (meta == null) return item; + + PersistentDataContainer pdc = meta.getPersistentDataContainer(); + pdc.set(ToolUtils.ABILITIES, + PersistentDataType.LIST.listTypeFrom(ToolUtils.Ability.EMPTY_INSTANCE), + abilities); + + item.setItemMeta(meta); + return tool.renderItem(item); + } + +} diff --git a/src/main/java/ovh/sad/snowcore/listeners/BreakListener.java b/src/main/java/ovh/sad/snowcore/listeners/BreakListener.java new file mode 100644 index 0000000..e97afea --- /dev/null +++ b/src/main/java/ovh/sad/snowcore/listeners/BreakListener.java @@ -0,0 +1,400 @@ +package ovh.sad.snowcore.listeners; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.Style; +import net.kyori.adventure.text.format.TextColor; +import net.kyori.adventure.text.format.TextDecoration; +import net.kyori.adventure.text.minimessage.MiniMessage; +import net.kyori.adventure.title.Title; +import org.apache.commons.lang3.tuple.Pair; +import org.bukkit.*; +import org.bukkit.block.Block; +import org.bukkit.block.data.type.Snow; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.block.BlockBreakEvent; +import org.bukkit.inventory.ItemStack; + +import java.util.*; + +import org.bukkit.inventory.PlayerInventory; +import ovh.sad.snowcore.items.Hoe; +import ovh.sad.snowcore.items.ToolUtils; +import ovh.sad.snowcore.Snowcore; +import ovh.sad.snowcore.items.Shovel; + + +public class BreakListener implements Listener { + @EventHandler + public void onBlockBreak(BlockBreakEvent event) { + Player player = event.getPlayer(); + + // Creative mode can break anything + if (player.getGameMode() == GameMode.CREATIVE) return; + + PlayerInventory inv = player.getInventory(); + ItemStack hand = inv.getItemInMainHand(); + Material tool = hand.getType(); + Material blockType = event.getBlock().getType(); + + Hoe hoe = Hoe.HoeDefinitions.ALL_HOES.get(tool); + + // If block has a required hoe and player doesn't meet it + if (hoe != null && !Hoe.HoeDefinitions.getAllBlocks(hoe).contains(blockType)) { + event.setCancelled(true); + Snowcore.sendActionBar(player, + Component.text("You need at least a ") + .append(Objects.requireNonNull(Hoe.HoeDefinitions.getRequiredHoe(blockType)).name()) + .append(Component.text(" to break this block!")) + ); + return; + } + + if(hoe != null) { + event.setDropItems(false); + List abilities = ToolUtils.getAbilities(hand); + List enchants = ToolUtils.getEnchantments(hand); + + int multiplier = 1; + + Optional hasMultiplier + = abilities.stream().filter(z -> z.type == ToolUtils.AbilityType.MULTIPLIER).findFirst(); + Optional hasJackpot + = abilities.stream().filter(z -> z.type == ToolUtils.AbilityType.JACKPOT).findFirst(); + Optional windEnchantment + = enchants.stream().filter(z -> z.type == ToolUtils.EnchantmentType.WIND).findFirst(); + Optional doubleDropEnchantment + = enchants.stream().filter(z -> z.type == ToolUtils.EnchantmentType.DOUBLE_DROP).findFirst(); + + if(hasMultiplier.isPresent()) { + multiplier = (int)Math.ceil(hasMultiplier.get().maxValue); + } + + + boolean jackpotHit = false; + double random = Math.random(); // 0.0 - 1.0 + + if (hasJackpot.isPresent()) { + if (random <= (hasJackpot.get().maxValue / 100)) { // 10% chance + multiplier *= 5; + jackpotHit = true; + } + } + + if (jackpotHit) { + player.sendActionBar(MiniMessage.miniMessage().deserialize(" ࿏࿏࿏࿏ Jackpot! You got 5x drops!")); + } + + ArrayList drops = new ArrayList<>(); + List breakableBlocks = Hoe.HoeDefinitions.getAllBlocks(hoe); + List blocksToBreak = new ArrayList<>(); + blocksToBreak.add(event.getBlock()); + + if (abilities.stream().anyMatch(z -> z.type == ToolUtils.AbilityType.THREE_BY_THREE)) { + for (int dx = -1; dx <= 1; dx++) { + for (int dz = -1; dz <= 1; dz++) { + if (dx == 0 && dz == 0) continue; // skip center (already added) + Block b = event.getBlock().getWorld().getBlockAt( + event.getBlock().getX() + dx, + event.getBlock().getY(), + event.getBlock().getZ() + dz + ); + if (breakableBlocks.contains(b.getType())) { + blocksToBreak.add(b); + } + } + } + } + + if(windEnchantment.isPresent() && Math.random() <= (double) windEnchantment.get().amount / 100.0) { + org.bukkit.util.Vector randomDir = new org.bukkit.util.Vector( + (Math.random() * 2.0) - 1.0, + 0, + (Math.random() * 2.0) - 1.0 + ).normalize(); + + World world = event.getBlock().getWorld(); + Block origin = event.getBlock(); + + for (int i = 1; i <= 3; i++) { + org.bukkit.util.Vector offset = randomDir.clone().multiply(i); + + Block b = world.getBlockAt( + origin.getX() + offset.getBlockX(), + origin.getY(), + origin.getZ() + offset.getBlockZ() + ); + + if (breakableBlocks.contains(b.getType())) { + blocksToBreak.add(b); + } + } + + //org.bukkit.util.Vector velocityBoost = randomDir.clone().multiply(0.2); + //velocityBoost.setY(0.1); + //player.setVelocity(player.getVelocity().add(velocityBoost)); + } + + for (Block b : blocksToBreak) { + Collection dropsForThisBlock = b.getDrops(hand); + + if(dropsForThisBlock.isEmpty()) { + drops.add(new ItemStack(b.getType(), 1)); + } + + drops.addAll(dropsForThisBlock); + + b.setType(Material.AIR); + } + + Optional hasAutosell + = abilities.stream().filter(z -> z.type == ToolUtils.AbilityType.AUTOSELL).findFirst(); + Optional fortuneLevel = enchants.stream().filter(z -> z.type == ToolUtils.EnchantmentType.FORTUNE).findFirst(); + + int exp = 0; + + for (ItemStack drop : drops) { + int amount = drop.getAmount(); + + if(fortuneLevel.isPresent()) { + amount = Snowcore.fortuneToDrops(fortuneLevel.get().amount, drop.getAmount()); + } + + if(doubleDropEnchantment.isPresent() && Math.random() <= (double) doubleDropEnchantment.get().amount / 100.0) { + drop.setAmount((int) Math.ceil(Math.pow(amount * multiplier, 2))); + + } else { + drop.setAmount(amount * multiplier); + } + + exp += amount * multiplier; + + HashMap leftover = inv.addItem(drop); + if(!leftover.isEmpty()) { + if(hasAutosell.isPresent()) { + Snowcore.sellInventory(player); + } else { + fullInventory(player); + + event.setCancelled(true); + return; + } + } + } + + + ItemStack stack = hoe.increaseEXP(hand, exp); + + inv.setItemInMainHand(stack); + + player.incrementStatistic(Statistic.MINE_BLOCK, Material.BEDROCK, breakableBlocks.size()); // bedrock because i'd like to group all flowers up into a single statistic. this works well enough! + event.getBlock().setType(Material.AIR); + } else { + if(Tag.ITEMS_SHOVELS.isTagged(tool) && blockType.equals(Material.SNOW)) { + event.setDropItems(false); + Snow snowData = (Snow) event.getBlock().getBlockData(); + Shovel shovel = Shovel.ShovelDefinitions.ALL_SHOVELS.get(tool); + + List abilities = ToolUtils.getAbilities(hand); + List enchants = ToolUtils.getEnchantments(hand); + + int amountOfSnow = snowData.getLayers(); + + Block origin = event.getBlock(); + var world = origin.getWorld(); + + ToolUtils.Ability nukeAbility = abilities.stream() + .filter(a -> a.type == ToolUtils.AbilityType.NUKE) + .findFirst() + .orElse(null); + + ToolUtils.Enchantment sweepEnchantment = enchants.stream() + .filter(a -> a.type == ToolUtils.EnchantmentType.SWEEPER) + .findFirst() + .orElse(null); + + ToolUtils.Enchantment layerRemoverEnchantment = enchants.stream() + .filter(a -> a.type == ToolUtils.EnchantmentType.LAYER_REMOVER) + .findFirst() + .orElse(null); + ToolUtils.Ability multiplierAbility = abilities.stream() + .filter(a -> a.type == ToolUtils.AbilityType.MULTIPLIER) + .findFirst() + .orElse(null); + + boolean hasNuke = nukeAbility != null; + double chance = hasNuke ? nukeAbility.maxValue : 0.0; + + boolean triggerNuke = hasNuke && Math.random() <= chance / 100.0; + + List> targets = new ArrayList<>(); + + targets.add(Pair.of(event.getBlock(), amountOfSnow)); + + if (sweepEnchantment != null && + Math.random() <= (double) sweepEnchantment.amount / 100.0) { + + // player horizontal facing + org.bukkit.util.Vector forward = player.getLocation().getDirection().setY(0).normalize(); + + // perpendicular sideways vector + org.bukkit.util.Vector side = new org.bukkit.util.Vector(-forward.getZ(), 0, forward.getX()); + + for (int depth = 1; depth <= 2; depth++) { + for (int width = -1; width <= 1; width++) { + + org.bukkit.util.Vector offset = forward.clone().multiply(depth) + .add(side.clone().multiply(width)); + + Block b = origin.getRelative( + offset.getBlockX(), + 0, + offset.getBlockZ() + ); + + if (b.getType() == Material.SNOW) { + Snow snow = (Snow) b.getBlockData(); + int snowToRemove = Math.min(2, snow.getLayers()); + amountOfSnow += snowToRemove; + targets.add(Pair.of(b, snowToRemove)); + } + } + } + } + + if(layerRemoverEnchantment != null && Math.random() <= (double) layerRemoverEnchantment.amount / 100.0) { + org.bukkit.util.Vector forward = player.getLocation().getDirection().setY(0).normalize(); + org.bukkit.util.Vector side = new org.bukkit.util.Vector(-forward.getZ(), 0, forward.getX()); + + for (int depth = 1; depth <= 6; depth++) { + org.bukkit.util.Vector forwardOffset = forward.clone().multiply(depth); + + Block center = origin.getRelative( + forwardOffset.getBlockX(), + 0, + forwardOffset.getBlockZ() + ); + + if (center.getType() == Material.SNOW) { + Snow snow = (Snow) center.getBlockData(); + int layers = snow.getLayers(); + amountOfSnow += layers; + targets.add(Pair.of(center, layers)); + } + + for (int dir = -1; dir <= 1; dir += 2) { + org.bukkit.util.Vector sideOffset = forwardOffset.clone() + .add(side.clone().multiply(dir)); + + Block sideBlock = origin.getRelative( + sideOffset.getBlockX(), + 0, + sideOffset.getBlockZ() + ); + + if (sideBlock.getType() == Material.SNOW) { + Snow snow = (Snow) sideBlock.getBlockData(); + int snowToRemove = Math.min( + 1 + (int)(Math.random() * 2), + snow.getLayers() + ); + + amountOfSnow += snowToRemove; + targets.add(Pair.of(sideBlock, snowToRemove)); + } + } + } + } + + if (triggerNuke) { + for (int dx = -6; dx <= 6; dx++) { + for (int dz = -6; dz <= 6; dz++) { + + if (dx == 0 && dz == 0) continue; + + Block b = world.getBlockAt( + origin.getX() + dx, + origin.getY(), + origin.getZ() + dz + ); + + if (b.getType() != Material.SNOW) continue; + + Snow snow = (Snow) b.getBlockData(); + int snowToRemove = Math.min(3, snow.getLayers()); + + amountOfSnow += snowToRemove; + targets.add(Pair.of(b, snowToRemove)); // store for later destruction + } + } + } + + Optional fortune = enchants.stream().filter(z-> z.type == ToolUtils.EnchantmentType.FORTUNE).findFirst(); + + if(fortune.isPresent()) { + amountOfSnow = Snowcore.fortuneToDrops(fortune.get().amount, amountOfSnow); + } + + if(multiplierAbility != null) { + amountOfSnow += (int) Math.ceil(multiplierAbility.maxValue); + } + HashMap leftover = + inv.addItem(new ItemStack(Material.SNOWBALL, amountOfSnow)); + + if (!leftover.isEmpty()) { + boolean hasAutosell = abilities.stream() + .anyMatch(a -> a.type == ToolUtils.AbilityType.AUTOSELL); + + if (hasAutosell) { + Snowcore.sellInventory(player); + event.setCancelled(true); + return; // nothing destroyed + } else { + fullInventory(player); + event.setCancelled(true); + return; // nothing destroyed + } + } + + for (Pair p : targets) { + Snow snow = (Snow) p.getLeft().getBlockData(); + int layers = snow.getLayers(); + + if (layers <= p.getRight()) { + p.getLeft().setType(Material.AIR, false); + } else { + snow.setLayers(layers - p.getRight()); + p.getLeft().setBlockData(snow); + } + } + + ItemStack stack = shovel.increaseEXP(hand, amountOfSnow); + + player.getInventory().setItemInMainHand(stack); + + player.incrementStatistic(Statistic.MINE_BLOCK, Material.SNOW, amountOfSnow); + event.getBlock().setType(Material.AIR); + event.setCancelled(true); + } else { + if(blockType.equals(Material.SNOW)) { + Snowcore.sendActionBar(player, Component.text("You need a shovel to break snow with!")); + } else { + Snowcore.sendActionBar(player, Component.text("You do not have permission to break this block.")); + } + + event.setCancelled(true); + } + } + } + + private void fullInventory(Player player) { + player.showTitle(Title.title( + Component.text("⚠ ⚠ WARNING ⚠ ⚠") + .style(Style.style(TextColor.color(255, 0, 0), TextDecoration.BOLD)), + Component.text("Your inventory is full! Use /sell") + .style(Style.style(TextColor.color(255, 255, 0))) + )); + } + +} diff --git a/src/main/java/ovh/sad/snowcore/listeners/InteractListener.java b/src/main/java/ovh/sad/snowcore/listeners/InteractListener.java new file mode 100644 index 0000000..05cf66d --- /dev/null +++ b/src/main/java/ovh/sad/snowcore/listeners/InteractListener.java @@ -0,0 +1,266 @@ +package ovh.sad.snowcore.listeners; + +import dev.triumphteam.gui.guis.Gui; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.minimessage.MiniMessage; +import org.bukkit.GameMode; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerArmorStandManipulateEvent; +import org.bukkit.event.player.PlayerInteractEntityEvent; +import org.bukkit.event.player.PlayerInteractEvent; +import org.bukkit.inventory.EquipmentSlot; +import org.bukkit.inventory.ItemStack; +import ovh.sad.snowcore.Snowcore; +import ovh.sad.snowcore.guis.UpgradeYourToolsGui; +import ovh.sad.snowcore.items.Hoe; +import dev.triumphteam.gui.guis.GuiItem; +import dev.triumphteam.gui.builder.item.ItemBuilder; +import org.bukkit.Material; +import ovh.sad.snowcore.items.ToolUtils; +import ovh.sad.snowcore.items.Shovel; +import ovh.sad.snowcore.items.Tool; + +import java.util.*; +import java.util.stream.Collectors; + +public class InteractListener implements Listener { + @EventHandler + public void glass(PlayerInteractEntityEvent event) { + UUID entityUuid = event.getRightClicked().getUniqueId(); + + if(entityUuid.equals(UUID.fromString("96a1d63d-f9ba-490c-bee7-f5edb73a08e8"))) { + event.setCancelled(true); + UpgradeYourToolsGui.open(event); + } else if(entityUuid.equals(UUID.fromString("237d77c8-a25f-4cd8-882e-c9da2d62ea79"))) { + event.setCancelled(true); + event.getPlayer().sendMessage(MiniMessage.miniMessage().deserialize(" \uD83D\uDEA7\uD83D\uDEA7 Under construction!")); + } + } + + @EventHandler + public void armorStand(PlayerArmorStandManipulateEvent pasme) { + if(pasme.getPlayer().getGameMode() != GameMode.CREATIVE) { + pasme.setCancelled(true); + } + } + + @EventHandler + public void onInteract(PlayerInteractEvent piEvent) { + + Player player = piEvent.getPlayer(); + ItemStack item = player.getInventory().getItemInMainHand(); + + if (piEvent.getClickedBlock() != null) return; + if (!player.isSneaking()) return; + + Tool tool = Hoe.HoeDefinitions.ALL_HOES.get(item.getType()); + if(tool == null) { + tool = Shovel.ShovelDefinitions.ALL_SHOVELS.get(item.getType()); + } + + if(tool == null) return; + + // Create GUI + Gui gui = Gui.gui() + .title(Component.text("Upgrade your Tool")) + .rows(6) + .create(); + + GuiItem filler = ItemBuilder.from(Material.BLACK_STAINED_GLASS_PANE) + .name(Component.text(" ")) + .asGuiItem(); + + for (int row = 1; row <= 6; row++) { + for (int col = 1; col <= 9; col++) { + if (row == 1 && col == 5) + continue; + + if ((row == 2 || row == 3) && (col >= 2 && col <= 8)) continue; + + if ((row == 4 || row == 5) && (col >= 2 && col <= 8)) continue; + + gui.setItem(row, col, filler); + } + } + + Map ownedEnchants = + Objects.requireNonNull(ToolUtils.getEnchantments(item)) + .stream() + .collect(Collectors.toMap(e -> e.type, e -> e)); + + Map ownedAbilities = + Objects.requireNonNull(ToolUtils.getAbilities(item)) + .stream() + .collect(Collectors.toMap(a -> a.type, a -> a)); + + int exp = ToolUtils.getExp(item); + int hoeLevel = ToolUtils.getLevel(item); + Iterator definedEnchants = tool.definedEnchantments.stream().map(ToolUtils.Enchantment::copy).iterator(); + for (int row = 2; row <= 3 && definedEnchants.hasNext(); row++) { + for (int col = 2; col <= 8 && definedEnchants.hasNext(); col++) { + + ToolUtils.Enchantment def = definedEnchants.next(); + ToolUtils.Enchantment owned = ownedEnchants.get(def.type); + + boolean has = owned != null; + int level = has ? owned.amount : 0; + int nextLevel = level + 1; + int cost = def.getExpRequired(nextLevel); + + Component name; + List lore = new ArrayList<>(); + Integer hasEnchantmentLimit = tool.definedEnchantLimits.get(def.type); + + if(hasEnchantmentLimit != null && level >= hasEnchantmentLimit) { + name = Component.text("❌ " + def.type.name + " is at max level!", NamedTextColor.RED); + } else if (has) { + name = Component.text("✔ " + def.type.name + " Lv." + level, NamedTextColor.GREEN); + + lore.add(Component.text("Click to upgrade → Lv." + nextLevel, NamedTextColor.YELLOW)); + lore.add(Component.text("Cost: " + cost + " EXP", NamedTextColor.GOLD)); + } else { + name = Component.text(def.type.name, NamedTextColor.YELLOW); + + lore.add(Component.text("Unlock for " + cost + " EXP", NamedTextColor.GOLD)); + } + + Tool finalTool = tool; + gui.setItem(row, col, + ItemBuilder.from(Material.ENCHANTED_BOOK) + .name(name) + .lore(lore) + .asGuiItem(ev -> { + ev.setCancelled(true); + Player whoClicked = (Player) ev.getWhoClicked(); + + if(level >= finalTool.definedEnchantLimits.get(def.type)) { + return; + } + + if (exp >= cost) { + if(has) { + owned.amount = owned.amount + 1; + ownedEnchants.put(owned.type, owned); + } else { + ToolUtils.Enchantment copy = def.copy(); + + copy.amount = copy.amount + 1; + ownedEnchants.put(copy.type, copy); + } + + ItemStack realItem = ToolUtils.setExp( + finalTool, + ToolUtils.setEnchantments(finalTool, item, ownedEnchants.values().stream().toList()), + exp - cost + ); + + whoClicked.getInventory().setItemInMainHand(realItem); + whoClicked.sendMessage( + MiniMessage.miniMessage().deserialize( + "✔ Unlocked " + def.type.name() + + " for " + (int) cost + " EXP" + ) + ); + gui.close(whoClicked); + } else { + whoClicked.sendMessage( + MiniMessage.miniMessage().deserialize( + "✘ Not enough EXP! Need " + (int) cost + + " EXP , you have " + (int) exp + " EXP" + ) + ); + } + + }) + ); + } + } + + Iterator definedAbilities = tool.definedAbilities.stream().iterator(); + + for (int row = 4; row <= 5 && definedAbilities.hasNext(); row++) { + for (int col = 2; col <= 8 && definedAbilities.hasNext(); col++) { + + ToolUtils.Ability def = definedAbilities.next(); + ToolUtils.Ability owned = ownedAbilities.get(def.type); + + boolean has = owned != null || def.byDefault; + boolean unlocked = hoeLevel >= def.unlockLevel; + + Component name; + List lore = new ArrayList<>(); + + if (has) { + name = Component.text("✔ " + def.type.name, NamedTextColor.GREEN); + lore.add(Component.text("Already unlocked", NamedTextColor.GRAY)); + + } else if (!unlocked) { + name = Component.text("🔒 " + def.type.name, NamedTextColor.RED); + lore.add(Component.text("Unlocks at level " + def.unlockLevel, NamedTextColor.GRAY)); + + } else { + name = Component.text(def.type.name, NamedTextColor.YELLOW); + lore.add(Component.text("Cost: " + (int) def.baseCost + "$", NamedTextColor.GOLD)); + lore.add(Component.text("Click to unlock", NamedTextColor.GRAY)); + } + + Tool finalTool1 = tool; + gui.setItem(row, col, + ItemBuilder.from(Material.NETHER_STAR) + .name(name) + .lore(lore) + .asGuiItem(ev -> { + ev.setCancelled(true); + Player whoClicked = (Player) ev.getWhoClicked(); + + if(def.unlockLevel > hoeLevel) { + whoClicked.sendMessage( + MiniMessage.miniMessage().deserialize( + "✘ Your level is not high enough. Required level is " + def.unlockLevel + ) + ); + return; + + } + double cost = def.baseCost; + double balance = Snowcore.econ.getBalance(whoClicked); + + if (balance >= cost) { + Snowcore.econ.withdrawPlayer(whoClicked, cost); + + ownedAbilities.put(def.type, def); + ItemStack realItem = + ToolUtils.setAbilities(finalTool1, item, ownedAbilities.values().stream().toList()); + whoClicked.getInventory().setItemInMainHand(realItem); + whoClicked.sendMessage( + MiniMessage.miniMessage().deserialize( + "✔ Unlocked " + def.type.name() + + " for $" + (int) cost + "" + ) + ); + gui.close(whoClicked); + } else { + whoClicked.sendMessage( + MiniMessage.miniMessage().deserialize( + "✘ Not enough money! Need $" + (int) cost + + ", you have $" + (int) balance + "" + ) + ); + } + }) + ); + + } + } + + + gui.setItem(1, 5, new GuiItem(item)); + + gui.setDefaultClickAction(ev -> ev.setCancelled(true)); + + gui.open(player); + } +} diff --git a/src/main/java/ovh/sad/snowcore/listeners/PlaceListener.java b/src/main/java/ovh/sad/snowcore/listeners/PlaceListener.java new file mode 100644 index 0000000..3315941 --- /dev/null +++ b/src/main/java/ovh/sad/snowcore/listeners/PlaceListener.java @@ -0,0 +1,18 @@ +package ovh.sad.snowcore.listeners; + +import net.kyori.adventure.text.Component; +import org.bukkit.GameMode; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.block.BlockPlaceEvent; +import ovh.sad.snowcore.Snowcore; + +public class PlaceListener implements Listener { + @EventHandler + public void onPlace(BlockPlaceEvent event) { + if(event.getPlayer().getGameMode() != GameMode.CREATIVE) { + Snowcore.sendActionBar(event.getPlayer(), Component.text("You cannot place blocks here!")); + event.setCancelled(true); + } + } +} diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml new file mode 100644 index 0000000..6ab0105 --- /dev/null +++ b/src/main/resources/plugin.yml @@ -0,0 +1,8 @@ +name: snowcore +version: '1.0-SNAPSHOT' +main: ovh.sad.snowcore.Snowcore +api-version: '1.21' +depend: + - Vault + - EconomySystem + - PlaceholderAPI \ No newline at end of file