commit fef26e5b798d15a58d44655f791012f28d2b6843 Author: xiaobai Date: Fri Feb 13 18:50:05 2026 +0800 1.0.0 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1fac4d5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,43 @@ +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ +.kotlin + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..b6b1ecf --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,10 @@ +# 默认忽略的文件 +/shelf/ +/workspace.xml +# 已忽略包含查询文件的默认文件夹 +/queries/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml +# 基于编辑器的 HTTP 客户端请求 +/httpRequests/ diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..3d4b90e --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,18 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..0b92a2d --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/REDME.md b/REDME.md new file mode 100644 index 0000000..5b31471 --- /dev/null +++ b/REDME.md @@ -0,0 +1,24 @@ +# PlayerBlockLife 插件 + +## 🎮 简介 +PlayerBlockLife 是一个独特的Minecraft插件,为服务器增加了一种新颖的游戏玩法。每个玩家拥有5个生命方块,这些方块埋藏在地下,并使用玩家的皮肤作为材质。当其他玩家挖完这些方块时,该玩家就会死亡。 + +## ✨ 特色功能 +- 🎭 使用玩家皮肤作为方块材质 +- ⚡ 实时皮肤加载,无需预生成 +- 🔄 异步数据处理,不影响服务器性能 +- 📊 完整的生命值系统和状态效果 +- 🎯 精美的粒子效果和音效 +- 🔧 高度可配置的游戏规则 +- 🗺️ 支持多世界和地区保护 +- 💾 多种数据存储方式 +- 👨‍💼 完善的管理员工具 + +## 🚀 快速开始 + +### 1. 安装插件 +1. 下载插件JAR文件 +2. 放入服务器的`plugins`文件夹 +3. 重启服务器 + +### 2. 玩家操作 \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..f2cfae7 --- /dev/null +++ b/build.gradle @@ -0,0 +1,83 @@ +plugins { + id 'java' + id 'com.github.johnrengelman.shadow' version '8.1.1' +} + +group = 'com.playerblocklife' +version = '1.0.0' + +sourceCompatibility = 17 +targetCompatibility = 17 + +repositories { + mavenCentral() + maven { + url = 'https://repo.papermc.io/repository/maven-public/' + } + maven { + url = 'https://hub.spigotmc.org/nexus/content/repositories/snapshots/' + } +} + +dependencies { + compileOnly 'io.papermc.paper:paper-api:1.20.4-R0.1-SNAPSHOT' + // 或者使用Spigot API: + // compileOnly 'org.spigotmc:spigot-api:1.20.4-R0.1-SNAPSHOT' + + // 依赖库 + implementation 'com.google.code.gson:gson:2.10.1' + implementation 'org.yaml:snakeyaml:2.2' +} + +shadowJar { + archiveClassifier.set('') + minimize() + + // 排除签名文件 + exclude 'META-INF/*.SF' + exclude 'META-INF/*.DSA' + exclude 'META-INF/*.RSA' + + // 如果需要重定位依赖包名,可以在这里添加 + // relocate 'com.google.gson', 'com.playerblocklife.libs.gson' + // relocate 'org.yaml', 'com.playerblocklife.libs.yaml' +} + +// 确保构建时包含shadowJar +tasks.build.dependsOn tasks.shadowJar + +// 配置编译选项 +tasks.withType(JavaCompile) { + options.encoding = 'UTF-8' + options.compilerArgs += ['-parameters'] +} + +// 配置测试 +test { + useJUnitPlatform() +} + +// 配置JAR任务 +jar { + manifest { + attributes( + 'Implementation-Version': project.version, + 'Main-Class': 'com.playerblocklife.PlayerBlockLife' + ) + } +} + +// 添加自定义任务 +task buildPlugin { + dependsOn shadowJar + group = 'build' + description = '构建插件JAR文件' + + doLast { + println "=" * 50 + println "插件构建完成!" + println "文件位置: ${shadowJar.archiveFile.get().asFile.absolutePath}" + println "文件大小: ${shadowJar.archiveFile.get().asFile.length() / 1024} KB" + println "=" * 50 + } +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..948c510 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,9 @@ +# Gradle?? +org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m +org.gradle.parallel=true +org.gradle.caching=true +org.gradle.daemon=true + +# ???? +pluginVersion=1.0.0 +mcVersion=1.20.4 \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..249e583 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..6fe9e8c --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Fri Feb 13 18:09:01 CST 2026 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..1b6c787 --- /dev/null +++ b/gradlew @@ -0,0 +1,234 @@ +#!/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/master/subprojects/plugins/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 + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# 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"' + +# 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 + which java >/dev/null 2>&1 || 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 + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + 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 + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# 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..107acd3 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@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=. +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%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +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%"=="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! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..7959259 --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'PlayerBlockLife' \ No newline at end of file diff --git a/src/main/java/com/playerblocklife/AdminCommands.java b/src/main/java/com/playerblocklife/AdminCommands.java new file mode 100644 index 0000000..a6434e7 --- /dev/null +++ b/src/main/java/com/playerblocklife/AdminCommands.java @@ -0,0 +1,102 @@ +package com.playerblocklife; + +import org.bukkit.Bukkit; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +import java.util.UUID; + +public class AdminCommands implements CommandExecutor { + private final PlayerBlockLife plugin; + + public AdminCommands(PlayerBlockLife plugin) { + this.plugin = plugin; + } + + @Override + public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { + if (!sender.hasPermission("playerblocklife.admin")) { + sender.sendMessage("§c你没有权限使用此命令!"); + return true; + } + + if (command.getName().equalsIgnoreCase("pblreload")) { + plugin.reloadConfig(); + plugin.getBlockManager().loadData(); + plugin.getSkinManager().loadAllSkins(); + sender.sendMessage("§a插件配置已重载!"); + return true; + } + + if (command.getName().equalsIgnoreCase("pbldelete")) { + if (args.length < 1) { + sender.sendMessage("§c用法: /pbldelete <玩家名>"); + return true; + } + + String targetName = args[0]; + Player target = Bukkit.getPlayer(targetName); + UUID targetId; + + if (target != null) { + targetId = target.getUniqueId(); + } else { + // 尝试从离线玩家获取UUID + targetId = Bukkit.getOfflinePlayer(targetName).getUniqueId(); + } + + plugin.getBlockManager().clearPlayerBlocks(targetId); + sender.sendMessage("§a已删除玩家 " + targetName + " 的生命方块"); + return true; + } + + if (command.getName().equalsIgnoreCase("pblrevive")) { + Player target; + + if (args.length < 1) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c控制台使用时必须指定玩家名: /pblrevive <玩家名>"); + return true; + } + target = (Player) sender; + } else { + target = Bukkit.getPlayer(args[0]); + if (target == null) { + sender.sendMessage("§c玩家不存在或不在线!"); + return true; + } + } + + plugin.getLifeSystem().revivePlayer(target); + sender.sendMessage("§a玩家 " + target.getName() + " 已复活!"); + return true; + } + + if (command.getName().equalsIgnoreCase("pblstats")) { + int totalPlayers = plugin.getBlockManager().getPlayerBlocksCount(); + int totalBlocks = plugin.getBlockManager().getTotalBlocksCount(); + + sender.sendMessage("§a===== PlayerBlockLife 统计 ====="); + sender.sendMessage("§7注册玩家数: §e" + totalPlayers); + sender.sendMessage("§7总生命方块数: §e" + totalBlocks); + sender.sendMessage("§7在线玩家生命方块:"); + + for (Player player : Bukkit.getOnlinePlayers()) { + int blocks = plugin.getBlockManager().getRemainingBlocks(player.getUniqueId()); + sender.sendMessage("§7- " + player.getName() + ": §e" + blocks + " §7/ §a5"); + } + + return true; + } + + sender.sendMessage("§c未知的管理员命令!"); + sender.sendMessage("§e可用命令:"); + sender.sendMessage("§7/pblreload §8- §f重载插件配置"); + sender.sendMessage("§7/pbldelete <玩家> §8- §f删除玩家的生命方块"); + sender.sendMessage("§7/pblrevive [玩家] §8- §f复活被淘汰的玩家"); + sender.sendMessage("§7/pblstats §8- §f查看插件统计"); + return true; + } +} \ No newline at end of file diff --git a/src/main/java/com/playerblocklife/BlockBreakListener.java b/src/main/java/com/playerblocklife/BlockBreakListener.java new file mode 100644 index 0000000..dbee13f --- /dev/null +++ b/src/main/java/com/playerblocklife/BlockBreakListener.java @@ -0,0 +1,81 @@ +package com.playerblocklife; + +import org.bukkit.Bukkit; +import org.bukkit.GameMode; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.block.Block; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.block.BlockBreakEvent; +import org.bukkit.event.block.BlockPlaceEvent; + +import java.util.UUID; + +public class BlockBreakListener implements Listener { + private final PlayerBlockLife plugin; + + public BlockBreakListener(PlayerBlockLife plugin) { + this.plugin = plugin; + } + + @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true) + public void onBlockBreak(BlockBreakEvent event) { + Player breaker = event.getPlayer(); + Block block = event.getBlock(); + Location location = block.getLocation(); + + if (block.getType() != Material.PLAYER_HEAD && + block.getType() != Material.PLAYER_WALL_HEAD) { + return; + } + + UUID ownerId = plugin.getBlockManager().getBlockOwner(location); + if (ownerId == null) { + return; + } + + if (ownerId.equals(breaker.getUniqueId())) { + if (breaker.getGameMode() != GameMode.CREATIVE) { + breaker.sendMessage("§c你不能挖掘自己的生命方块!"); + event.setCancelled(true); + } + return; + } + + if (plugin.getBlockManager().removeBlock(location, breaker)) { + int remaining = plugin.getBlockManager().getRemainingBlocks(ownerId); + + if (remaining <= 0) { + plugin.getLifeSystem().handlePlayerDeath(ownerId); + } + + breaker.sendMessage("§a✓ 成功破坏一个生命方块!"); + breaker.sendMessage("§7剩余方块: §e" + remaining); + + if (remaining == 1) { + breaker.sendMessage("§6⚡ 对方只剩最后1个生命方块了!"); + breaker.playSound(breaker.getLocation(), + org.bukkit.Sound.ENTITY_PLAYER_LEVELUP, 1.0f, 1.5f); + } + } + } + + @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true) + public void onBlockPlace(BlockPlaceEvent event) { + Player player = event.getPlayer(); + Block block = event.getBlock(); + + if (block.getType() == Material.PLAYER_HEAD || + block.getType() == Material.PLAYER_WALL_HEAD) { + + UUID ownerId = plugin.getBlockManager().getBlockOwner(block.getLocation()); + if (ownerId != null && !ownerId.equals(player.getUniqueId())) { + player.sendMessage("§c你不能在这里放置方块,这是别人的生命方块区域!"); + event.setCancelled(true); + } + } + } +} \ No newline at end of file diff --git a/src/main/java/com/playerblocklife/CheckLifeBlocksCommand.java b/src/main/java/com/playerblocklife/CheckLifeBlocksCommand.java new file mode 100644 index 0000000..dbed19d --- /dev/null +++ b/src/main/java/com/playerblocklife/CheckLifeBlocksCommand.java @@ -0,0 +1,69 @@ +package com.playerblocklife; + +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +import java.util.List; +import java.util.UUID; + +public class CheckLifeBlocksCommand implements CommandExecutor { + private final PlayerBlockLife plugin; + + public CheckLifeBlocksCommand(PlayerBlockLife plugin) { + this.plugin = plugin; + } + + @Override + public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c只有玩家可以使用此命令!"); + return true; + } + + Player player = (Player) sender; + UUID playerId = player.getUniqueId(); + + List blocks = plugin.getBlockManager().getPlayerBlocks(playerId); + int remaining = blocks.size(); + + if (remaining == 0) { + player.sendMessage("§c你还没有设置生命方块!"); + player.sendMessage("§7使用 §e/setlifeblocks §7来设置你的生命方块"); + return true; + } + + player.sendMessage("§a========== 你的生命方块信息 =========="); + player.sendMessage("§7剩余方块数量: §e" + remaining + " §7/ §a5"); + player.sendMessage("§7当前生命值: §c" + + (plugin.getLifeSystem().getPlayerHealth(playerId) != null ? + plugin.getLifeSystem().getPlayerHealth(playerId) : "20") + " ❤"); + + if (remaining <= 2) { + player.sendMessage("§c⚠ 警告!生命方块即将耗尽!"); + } + + player.sendMessage("§7方块位置:"); + for (int i = 0; i < blocks.size(); i++) { + Location loc = blocks.get(i); + String worldName = loc.getWorld() != null ? loc.getWorld().getName() : "未知世界"; + player.sendMessage("§7" + (i + 1) + ". §e世界: " + worldName + + " §7坐标: §a" + loc.getBlockX() + " §7, §a" + + loc.getBlockY() + " §7, §a" + loc.getBlockZ()); + } + + player.sendMessage("§a======================================"); + + if (remaining > 0) { + player.sendMessage("§e提示:"); + player.sendMessage("§7- 方块距离你: §a" + + (int) player.getLocation().distance(blocks.get(0)) + " §7格"); + player.sendMessage("§7- 使用指南针可以追踪方块位置"); + } + + return true; + } +} \ No newline at end of file diff --git a/src/main/java/com/playerblocklife/ConfigManager.java b/src/main/java/com/playerblocklife/ConfigManager.java new file mode 100644 index 0000000..d63f980 --- /dev/null +++ b/src/main/java/com/playerblocklife/ConfigManager.java @@ -0,0 +1,63 @@ +package com.playerblocklife; + +import org.bukkit.configuration.file.FileConfiguration; + +public class ConfigManager { + private final PlayerBlockLife plugin; + private FileConfiguration config; + + public ConfigManager(PlayerBlockLife plugin) { + this.plugin = plugin; + loadConfig(); + } + + public void loadConfig() { + plugin.saveDefaultConfig(); + config = plugin.getConfig(); + } + + public void reloadConfig() { + plugin.reloadConfig(); + config = plugin.getConfig(); + } + + public int getBlocksPerPlayer() { + return config.getInt("blocks.amount", 5); + } + + public int getSpreadRange() { + return config.getInt("blocks.spread", 5); + } + + public int getDepth() { + return config.getInt("blocks.depth", -1); + } + + public boolean isDieWhenBlocksGone() { + return config.getBoolean("game.die_when_blocks_gone", true); + } + + public boolean isBecomeSpectator() { + return config.getBoolean("game.become_spectator", true); + } + + public boolean isHealthSystemEnabled() { + return config.getBoolean("game.health_system", true); + } + + public boolean isSkinSystemEnabled() { + return config.getBoolean("skin.enabled", true); + } + + public String getSkinSource() { + return config.getString("skin.source", "player_profile"); + } + + public String getMessage(String path, String defaultValue) { + String message = config.getString("messages." + path, defaultValue); + if (message != null) { + message = message.replace("&", "§"); + } + return message; + } +} \ No newline at end of file diff --git a/src/main/java/com/playerblocklife/LifeSystem.java b/src/main/java/com/playerblocklife/LifeSystem.java new file mode 100644 index 0000000..35d8939 --- /dev/null +++ b/src/main/java/com/playerblocklife/LifeSystem.java @@ -0,0 +1,156 @@ +package com.playerblocklife; + +import org.bukkit.Bukkit; +import org.bukkit.GameMode; +import org.bukkit.entity.Player; +import org.bukkit.potion.PotionEffect; +import org.bukkit.potion.PotionEffectType; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +public class LifeSystem { + private final PlayerBlockLife plugin; + private final Map playerHealth = new HashMap<>(); + private final Map lastDamageTime = new HashMap<>(); + + public LifeSystem(PlayerBlockLife plugin) { + this.plugin = plugin; + } + + public void checkAllPlayers() { + for (Player player : Bukkit.getOnlinePlayers()) { + checkPlayerHealth(player); + } + } + + public void checkPlayerHealth(Player player) { + UUID playerId = player.getUniqueId(); + int remainingBlocks = plugin.getBlockManager().getRemainingBlocks(playerId); + + if (remainingBlocks == 0 && !playerHealth.containsKey(playerId)) { + playerHealth.put(playerId, 20); + return; + } + + int newHealth = calculateHealthFromBlocks(remainingBlocks); + Integer currentHealth = playerHealth.get(playerId); + + if (currentHealth == null) { + playerHealth.put(playerId, newHealth); + return; + } + + if (currentHealth != newHealth) { + playerHealth.put(playerId, newHealth); + + if (newHealth < currentHealth) { + player.sendMessage("§c你的生命值减少了!当前: §4" + newHealth + " ❤"); + player.playSound(player.getLocation(), + org.bukkit.Sound.ENTITY_PLAYER_HURT, 1.0f, 1.0f); + lastDamageTime.put(playerId, System.currentTimeMillis()); + } else if (newHealth > currentHealth) { + player.sendMessage("§a你的生命值恢复了!当前: §2" + newHealth + " ❤"); + player.playSound(player.getLocation(), + org.bukkit.Sound.ENTITY_PLAYER_LEVELUP, 0.5f, 1.5f); + } + + if (newHealth <= 5) { + player.sendTitle("§4⚠ 警告!", "§c生命值过低!", 10, 40, 10); + player.playSound(player.getLocation(), + org.bukkit.Sound.BLOCK_NOTE_BLOCK_BELL, 1.0f, 0.5f); + } + + if (remainingBlocks == 0) { + handlePlayerDeath(playerId); + } + } + + applyStatusEffects(player, remainingBlocks); + } + + private int calculateHealthFromBlocks(int remainingBlocks) { + if (remainingBlocks >= 5) return 20; + if (remainingBlocks == 4) return 16; + if (remainingBlocks == 3) return 12; + if (remainingBlocks == 2) return 8; + if (remainingBlocks == 1) return 4; + return 0; + } + + private void applyStatusEffects(Player player, int remainingBlocks) { + if (remainingBlocks <= 2) { + player.addPotionEffect(new PotionEffect( + PotionEffectType.SLOW, 100, 1, true, true)); + + if (remainingBlocks == 1) { + player.addPotionEffect(new PotionEffect( + PotionEffectType.WEAKNESS, 100, 0, true, true)); + player.addPotionEffect(new PotionEffect( + PotionEffectType.BLINDNESS, 100, 0, true, true)); + } + } else { + player.removePotionEffect(PotionEffectType.SLOW); + player.removePotionEffect(PotionEffectType.WEAKNESS); + player.removePotionEffect(PotionEffectType.BLINDNESS); + } + } + + public void handlePlayerDeath(UUID playerId) { + Player player = Bukkit.getPlayer(playerId); + if (player == null || !player.isOnline()) { + return; + } + + player.setHealth(0); + player.sendTitle("§4☠ 你死了!", "§c所有生命方块已被挖光", 20, 100, 20); + + for (Player onlinePlayer : Bukkit.getOnlinePlayers()) { + onlinePlayer.sendMessage("§4☠ 玩家 " + player.getName() + " 的生命方块已被全部挖光,惨遭淘汰!"); + onlinePlayer.playSound(onlinePlayer.getLocation(), + org.bukkit.Sound.ENTITY_WITHER_DEATH, 0.7f, 0.8f); + } + + plugin.getBlockManager().clearPlayerBlocks(playerId); + playerHealth.remove(playerId); + + Bukkit.getScheduler().runTaskLater(plugin, () -> { + if (player.isOnline()) { + player.setGameMode(GameMode.SPECTATOR); + player.sendMessage("§e你已被淘汰,可以观察其他玩家。"); + player.sendMessage("§7等待下一轮游戏开始..."); + } + }, 100L); + } + + public Integer getPlayerHealth(UUID playerId) { + return playerHealth.get(playerId); + } + + public void setPlayerHealth(UUID playerId, int health) { + playerHealth.put(playerId, health); + } + + public boolean isPlayerAlive(UUID playerId) { + Integer health = playerHealth.get(playerId); + return health != null && health > 0; + } + + public void revivePlayer(Player player) { + UUID playerId = player.getUniqueId(); + playerHealth.put(playerId, 20); + player.setGameMode(GameMode.SURVIVAL); + player.setHealth(20.0); + player.setFoodLevel(20); + player.setSaturation(5.0f); + + player.removePotionEffect(PotionEffectType.SLOW); + player.removePotionEffect(PotionEffectType.WEAKNESS); + player.removePotionEffect(PotionEffectType.BLINDNESS); + + player.sendMessage("§a你已复活!生命值已恢复。"); + player.playSound(player.getLocation(), + org.bukkit.Sound.ENTITY_PLAYER_LEVELUP, 1.0f, 1.0f); + } +} \ No newline at end of file diff --git a/src/main/java/com/playerblocklife/PlayerBlockLife.java b/src/main/java/com/playerblocklife/PlayerBlockLife.java new file mode 100644 index 0000000..b59cee9 --- /dev/null +++ b/src/main/java/com/playerblocklife/PlayerBlockLife.java @@ -0,0 +1,104 @@ +package com.playerblocklife; + +import org.bukkit.plugin.java.JavaPlugin; +import java.util.logging.Level; + +public class PlayerBlockLife extends JavaPlugin { + private static PlayerBlockLife instance; + private PlayerBlockManager blockManager; + private SkinManager skinManager; + private LifeSystem lifeSystem; + private ConfigManager configManager; + + // 在 PlayerBlockLife.java 中添加: + + @Override + public void reloadConfig() { + super.reloadConfig(); + configManager.reloadConfig(); + getLogger().info("配置已重新加载"); + } + @Override + public void onEnable() { + instance = this; + saveDefaultConfig(); + + this.configManager = new ConfigManager(this); + this.skinManager = new SkinManager(this); + this.blockManager = new PlayerBlockManager(this, skinManager); + this.lifeSystem = new LifeSystem(this); + + getServer().getPluginManager().registerEvents(new BlockBreakListener(this), this); + getServer().getPluginManager().registerEvents(new PlayerJoinListener(this), this); + getServer().getPluginManager().registerEvents(new PlayerQuitListener(this), this); + + getCommand("setlifeblocks").setExecutor(new SetLifeBlocksCommand(this)); + getCommand("checklifeblocks").setExecutor(new CheckLifeBlocksCommand(this)); + getCommand("pblreload").setExecutor(new AdminCommands(this)); + getCommand("pbldelete").setExecutor(new AdminCommands(this)); + + blockManager.loadData(); + skinManager.loadAllSkins(); + + startScheduler(); + + getLogger().info("§a========================================"); + getLogger().info("§ePlayerBlockLife v" + getDescription().getVersion() + " 已启用"); + getLogger().info("§e作者: " + getDescription().getAuthors()); + getLogger().info("§a========================================"); + } + + @Override + public void onDisable() { + if (blockManager != null) { + blockManager.saveData(); + } + if (skinManager != null) { + skinManager.saveSkinData(); + } + getLogger().info("§cPlayerBlockLife 插件已禁用"); + } + + private void startScheduler() { + getServer().getScheduler().runTaskTimerAsynchronously(this, () -> { + blockManager.saveData(); + skinManager.saveSkinData(); + }, 6000L, 6000L); + + getServer().getScheduler().runTaskTimer(this, () -> { + lifeSystem.checkAllPlayers(); + }, 200L, 200L); + } + + public static PlayerBlockLife getInstance() { + return instance; + } + + public PlayerBlockManager getBlockManager() { + return blockManager; + } + + public SkinManager getSkinManager() { + return skinManager; + } + + public LifeSystem getLifeSystem() { + return lifeSystem; + } + + public ConfigManager getConfigManager() { + return configManager; + } + + public void logInfo(String message) { + getLogger().info(message); + } + + public void logWarning(String message) { + getLogger().warning(message); + } + + public void logError(String message, Throwable throwable) { + getLogger().log(Level.SEVERE, message, throwable); + } +} \ No newline at end of file diff --git a/src/main/java/com/playerblocklife/PlayerBlockManager.java b/src/main/java/com/playerblocklife/PlayerBlockManager.java new file mode 100644 index 0000000..a18fd96 --- /dev/null +++ b/src/main/java/com/playerblocklife/PlayerBlockManager.java @@ -0,0 +1,593 @@ +package com.playerblocklife; + +import org.bukkit.*; +import org.bukkit.block.Block; +import org.bukkit.block.BlockFace; +import org.bukkit.block.Skull; +import org.bukkit.block.data.BlockData; +import org.bukkit.block.data.Rotatable; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.SkullMeta; +import org.bukkit.util.Vector; +import org.yaml.snakeyaml.Yaml; + +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +public class PlayerBlockManager { + private final PlayerBlockLife plugin; + private final SkinManager skinManager; + private final Map> playerBlocks = new ConcurrentHashMap<>(); + private final Map blockOwners = new ConcurrentHashMap<>(); + private final Map playerBlockTypes = new ConcurrentHashMap<>(); + private final File dataFile; + private final Random random = new Random(); + + public PlayerBlockManager(PlayerBlockLife plugin, SkinManager skinManager) { + this.plugin = plugin; + this.skinManager = skinManager; + this.dataFile = new File(plugin.getDataFolder(), "blockdata.yml"); + } + + /** + * 为玩家设置生命方块 + */ + public boolean setLifeBlocks(Player player, Location center) { + UUID playerId = player.getUniqueId(); + String playerName = player.getName(); + + // 检查是否已有生命方块 + if (hasLifeBlocks(playerId)) { + player.sendMessage("§c你已经有生命方块了!"); + player.sendMessage("§e使用 /checklifeblocks 查看位置"); + return false; + } + + // 检查玩家皮肤是否已加载 + if (!skinManager.isSkinLoaded(playerId)) { + player.sendMessage("§e你的皮肤正在加载中,请稍候..."); + player.sendMessage("§7(如果长时间未加载完成,请重新加入服务器)"); + return false; + } + + List blocks = new ArrayList<>(); + int blocksPlaced = 0; + + // 尝试在中心周围生成5个方块 + for (int attempt = 0; attempt < 20 && blocksPlaced < 5; attempt++) { + Location blockLoc = findSuitableLocation(center); + + if (blockLoc != null && placePlayerHead(blockLoc, playerId, playerName)) { + blocks.add(blockLoc); + blockOwners.put(blockLoc, playerId); + blocksPlaced++; + + // 添加放置效果 + spawnPlaceEffects(blockLoc); + } + } + + if (blocksPlaced > 0) { + playerBlocks.put(playerId, blocks); + saveData(); + + player.sendMessage("§a========================================"); + player.sendMessage("§a成功生成 §e" + blocksPlaced + " §a个生命方块!"); + player.sendMessage("§6方块使用了你的皮肤头像"); + player.sendMessage("§c⚠ 警告: 方块被挖光时,你将死亡!"); + player.sendMessage("§7使用 /checklifeblocks 查看方块位置"); + player.sendMessage("§a========================================"); + + return true; + } else { + player.sendMessage("§c无法生成生命方块!"); + player.sendMessage("§7请确保周围有足够的空间(至少5个可放置位置)"); + return false; + } + } + + /** + * 寻找合适的位置 + */ + private Location findSuitableLocation(Location center) { + for (int i = 0; i < 10; i++) { + int x = random.nextInt(11) - 5; // -5 到 5 + int z = random.nextInt(11) - 5; + + Location testLoc = center.clone().add(x, -1, z); + Block block = testLoc.getBlock(); + + // 检查位置是否合适 + if (isSuitableLocation(testLoc)) { + return testLoc; + } + } + return null; + } + + /** + * 检查位置是否合适 + * 修复:移除 isReplaceable() 方法调用 + */ + private boolean isSuitableLocation(Location location) { + Block block = location.getBlock(); + + // 检查是否已有方块 + if (blockOwners.containsKey(location)) { + return false; + } + + // 检查方块是否可替换(修复了 isReplaceable() 方法不存在的问题) + Material type = block.getType(); + if (!type.isAir()) { + // 检查是否是固体方块,固体方块不能替换 + if (type.isSolid()) { + return false; + } + + // 检查是否是液体 + if (type == Material.WATER || type == Material.LAVA || + type == Material.WATER_CAULDRON || type == Material.LAVA_CAULDRON) { + return false; + } + + // 检查一些特定的不可替换方块 + switch (type) { + case FIRE: + case SOUL_FIRE: + case COBWEB: + case BAMBOO: + case BAMBOO_SAPLING: + case SCAFFOLDING: + case LADDER: + case VINE: + case TWISTING_VINES: + case WEEPING_VINES: + case GLOW_LICHEN: + // 这些方块可以替换 + break; + default: + // 如果不是固体,也不是特定的透明方块,也不是空气,则检查是否是透明方块 + if (!type.isTransparent()) { + return false; + } + break; + } + } + + // 检查下方是否有支撑方块 + Block below = location.clone().add(0, -1, 0).getBlock(); + Material belowType = below.getType(); + + if (belowType.isAir() || !belowType.isSolid()) { + return false; + } + + // 检查是否在水或岩浆中 + if (block.isLiquid() || below.isLiquid()) { + return false; + } + + return true; + } + + /** + * 放置玩家头颅方块 + */ + private boolean placePlayerHead(Location location, UUID playerId, String playerName) { + try { + Block block = location.getBlock(); + + // 检查方块是否已被占用 + if (blockOwners.containsKey(location)) { + return false; + } + + // 设置方块为玩家头颅 + block.setType(Material.PLAYER_HEAD); + + // 获取并设置头颅数据 + Skull skullState = (Skull) block.getState(); + OfflinePlayer offlinePlayer = Bukkit.getOfflinePlayer(playerId); + + // 设置头颅所有者 + skullState.setOwningPlayer(offlinePlayer); + + // 设置朝向(随机方向) + BlockData blockData = block.getBlockData(); + if (blockData instanceof Rotatable) { + Rotatable rotatable = (Rotatable) blockData; + BlockFace[] faces = {BlockFace.NORTH, BlockFace.EAST, BlockFace.SOUTH, BlockFace.WEST}; + rotatable.setRotation(faces[random.nextInt(faces.length)]); + block.setBlockData(rotatable); + } + + // 更新方块 + skullState.update(true, false); + + return true; + } catch (Exception e) { + plugin.logError("放置玩家头颅失败: " + location, e); + return false; + } + } + + /** + * 生成放置效果 + */ + private void spawnPlaceEffects(Location location) { + World world = location.getWorld(); + if (world == null) return; + + // 粒子效果 + world.spawnParticle(Particle.ENCHANTMENT_TABLE, + location.clone().add(0.5, 0.5, 0.5), + 30, 0.3, 0.3, 0.3, 0.1); + + // 音效 + world.playSound(location, Sound.BLOCK_ANVIL_PLACE, 0.5f, 1.2f); + } + + /** + * 检查方块是否属于某个玩家 + */ + public UUID getBlockOwner(Location location) { + return blockOwners.get(location); + } + + /** + * 移除方块(当被挖掘时) + */ + public boolean removeBlock(Location location, Player breaker) { + UUID ownerId = blockOwners.get(location); + if (ownerId == null) { + return false; + } + + List blocks = playerBlocks.get(ownerId); + if (blocks == null || !blocks.contains(location)) { + return false; + } + + // 移除方块 + blocks.remove(location); + blockOwners.remove(location); + + // 设置方块为空气 + location.getBlock().setType(Material.AIR); + + // 生成破坏效果 + spawnBreakEffects(location, breaker); + + // 检查玩家是否还有剩余方块 + int remaining = blocks.size(); + + // 通知所有相关玩家 + notifyBlockBreak(ownerId, breaker, remaining); + + // 保存数据 + saveData(); + + return true; + } + + /** + * 生成破坏效果 + */ + private void spawnBreakEffects(Location location, Player breaker) { + World world = location.getWorld(); + if (world == null) return; + + // 粒子效果 + world.spawnParticle(Particle.BLOCK_CRACK, + location.clone().add(0.5, 0.5, 0.5), + 50, 0.5, 0.5, 0.5, 0.5, Material.PLAYER_HEAD.createBlockData()); + + world.spawnParticle(Particle.SMOKE_LARGE, + location.clone().add(0.5, 0.5, 0.5), + 20, 0.3, 0.3, 0.3, 0.05); + + // 音效 + world.playSound(location, Sound.ENTITY_ITEM_BREAK, 1.0f, 0.8f); + world.playSound(location, Sound.BLOCK_GLASS_BREAK, 0.8f, 1.0f); + + // 对挖掘者造成轻微击退 + if (breaker != null) { + Location breakerLoc = breaker.getLocation(); + Vector direction = location.toVector().subtract(breakerLoc.toVector()).normalize(); + breaker.setVelocity(direction.multiply(-0.5).setY(0.3)); + } + } + + /** + * 通知方块被破坏 + */ + private void notifyBlockBreak(UUID ownerId, Player breaker, int remaining) { + Player owner = Bukkit.getPlayer(ownerId); + String ownerName = Bukkit.getOfflinePlayer(ownerId).getName(); + + // 通知方块所有者 + if (owner != null && owner.isOnline()) { + owner.sendMessage("§c⚠ 警告!你的生命方块被破坏了!"); + owner.sendMessage("§7破坏者: §e" + (breaker != null ? breaker.getName() : "未知")); + owner.sendMessage("§7剩余生命方块: §a" + remaining + " §7/ §c5"); + + if (remaining <= 2) { + owner.sendMessage("§4⚠ 警告!生命方块即将耗尽!"); + } + + // 播放警告音效 + owner.playSound(owner.getLocation(), Sound.ENTITY_ENDERMAN_TELEPORT, 0.8f, 0.5f); + } + + // 通知破坏者 + if (breaker != null && !breaker.getUniqueId().equals(ownerId)) { + breaker.sendMessage("§6你破坏了一个生命方块!"); + breaker.sendMessage("§7所有者: §e" + (ownerName != null ? ownerName : "未知玩家")); + breaker.sendMessage("§7对方剩余生命方块: §a" + remaining); + + // 给予挖掘者经验奖励 + breaker.giveExp(5); + breaker.playSound(breaker.getLocation(), Sound.ENTITY_EXPERIENCE_ORB_PICKUP, 1.0f, 1.0f); + } + + // 广播给附近玩家 + World world = owner != null ? owner.getWorld() : (breaker != null ? breaker.getWorld() : null); + if (world != null) { + for (Player nearby : world.getPlayers()) { + if (nearby != owner && nearby != breaker && + nearby.getLocation().distance(owner != null ? owner.getLocation() : breaker.getLocation()) < 30) { + nearby.sendMessage("§7[附近] §e一个生命方块被破坏了!"); + } + } + } + } + + /** + * 获取玩家剩余方块数量 + */ + public int getRemainingBlocks(UUID playerId) { + List blocks = playerBlocks.get(playerId); + return blocks != null ? blocks.size() : 0; + } + + /** + * 检查玩家是否有生命方块 + */ + public boolean hasLifeBlocks(UUID playerId) { + List blocks = playerBlocks.get(playerId); + return blocks != null && !blocks.isEmpty(); + } + + /** + * 获取玩家的所有生命方块位置 + */ + public List getPlayerBlocks(UUID playerId) { + return playerBlocks.getOrDefault(playerId, new ArrayList<>()); + } + + /** + * 清除玩家的所有生命方块 + */ + public void clearPlayerBlocks(UUID playerId) { + List blocks = playerBlocks.remove(playerId); + if (blocks != null) { + for (Location loc : blocks) { + blockOwners.remove(loc); + loc.getBlock().setType(Material.AIR); + } + } + saveData(); + } + + /** + * 重新生成玩家的生命方块 + */ + public boolean regeneratePlayerBlocks(Player player) { + UUID playerId = player.getUniqueId(); + + // 清除旧方块 + clearPlayerBlocks(playerId); + + // 生成新方块 + return setLifeBlocks(player, player.getLocation()); + } + + /** + * 获取已注册玩家的数量 + */ + public int getPlayerBlocksCount() { + return playerBlocks.size(); + } + + /** + * 获取总方块数量 + */ + public int getTotalBlocksCount() { + int total = 0; + for (List blocks : playerBlocks.values()) { + total += blocks.size(); + } + return total; + } + + /** + * 加载数据 + */ + @SuppressWarnings("unchecked") + public void loadData() { + if (!dataFile.exists()) { + return; + } + + try { + Yaml yaml = new Yaml(); + Map data = yaml.load(new FileReader(dataFile)); + + if (data == null) { + return; + } + + // 加载方块数据 + if (data.containsKey("blocks")) { + Map>> blocksData = + (Map>>) data.get("blocks"); + + for (Map.Entry>> entry : blocksData.entrySet()) { + UUID playerId = UUID.fromString(entry.getKey()); + List locations = new ArrayList<>(); + + for (Map locData : entry.getValue()) { + String worldName = (String) locData.get("world"); + double x = (double) locData.get("x"); + double y = (double) locData.get("y"); + double z = (double) locData.get("z"); + + World world = Bukkit.getWorld(worldName); + if (world != null) { + Location location = new Location(world, x, y, z); + locations.add(location); + blockOwners.put(location, playerId); + } + } + + if (!locations.isEmpty()) { + playerBlocks.put(playerId, locations); + } + } + } + + plugin.logInfo("已加载 " + playerBlocks.size() + " 个玩家的方块数据"); + plugin.logInfo("总共 " + blockOwners.size() + " 个生命方块"); + + } catch (Exception e) { + plugin.logError("加载方块数据失败", e); + } + } + + /** + * 保存数据 + */ + public void saveData() { + try { + Map data = new HashMap<>(); + Map>> blocksData = new HashMap<>(); + + // 保存方块数据 + for (Map.Entry> entry : playerBlocks.entrySet()) { + String playerId = entry.getKey().toString(); + List> locations = new ArrayList<>(); + + for (Location loc : entry.getValue()) { + Map locData = new HashMap<>(); + locData.put("world", loc.getWorld().getName()); + locData.put("x", loc.getX()); + locData.put("y", loc.getY()); + locData.put("z", loc.getZ()); + locations.add(locData); + } + + blocksData.put(playerId, locations); + } + + data.put("blocks", blocksData); + + // 写入文件 + Yaml yaml = new Yaml(); + yaml.dump(data, new FileWriter(dataFile)); + + } catch (Exception e) { + plugin.logError("保存方块数据失败", e); + } + } + + /** + * 获取所有玩家的方块数据 + */ + public Map> getAllPlayerBlocks() { + return new HashMap<>(playerBlocks); + } + + /** + * 获取所有方块的位置和所有者 + */ + public Map getAllBlockOwners() { + return new HashMap<>(blockOwners); + } + + /** + * 检查位置是否包含生命方块 + */ + public boolean isLifeBlock(Location location) { + return blockOwners.containsKey(location); + } + + /** + * 获取玩家的生命方块位置列表(用于显示) + */ + public List getPlayerBlockLocations(UUID playerId) { + List blocks = playerBlocks.get(playerId); + List locations = new ArrayList<>(); + + if (blocks != null) { + for (Location loc : blocks) { + String worldName = loc.getWorld() != null ? loc.getWorld().getName() : "未知世界"; + locations.add(String.format("世界: %s, 坐标: %d, %d, %d", + worldName, + loc.getBlockX(), + loc.getBlockY(), + loc.getBlockZ())); + } + } + + return locations; + } + + /** + * 获取距离玩家最近的方块 + */ + public Location getNearestBlock(Player player) { + UUID playerId = player.getUniqueId(); + List blocks = playerBlocks.get(playerId); + + if (blocks == null || blocks.isEmpty()) { + return null; + } + + Location nearest = null; + double nearestDistance = Double.MAX_VALUE; + + for (Location block : blocks) { + double distance = player.getLocation().distance(block); + if (distance < nearestDistance) { + nearestDistance = distance; + nearest = block; + } + } + + return nearest; + } + + /** + * 获取所有生命方块的统计信息 + */ + public Map getStats() { + Map stats = new HashMap<>(); + stats.put("totalPlayers", playerBlocks.size()); + stats.put("totalBlocks", blockOwners.size()); + + // 按世界统计 + Map blocksPerWorld = new HashMap<>(); + for (Location loc : blockOwners.keySet()) { + String worldName = loc.getWorld() != null ? loc.getWorld().getName() : "unknown"; + blocksPerWorld.put(worldName, blocksPerWorld.getOrDefault(worldName, 0) + 1); + } + stats.put("blocksPerWorld", blocksPerWorld); + + return stats; + } +} \ No newline at end of file diff --git a/src/main/java/com/playerblocklife/PlayerJoinListener.java b/src/main/java/com/playerblocklife/PlayerJoinListener.java new file mode 100644 index 0000000..39f443f --- /dev/null +++ b/src/main/java/com/playerblocklife/PlayerJoinListener.java @@ -0,0 +1,72 @@ +package com.playerblocklife; + +import org.bukkit.Bukkit; +import org.bukkit.GameMode; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerRespawnEvent; + +import java.util.UUID; + +public class PlayerJoinListener implements Listener { + private final PlayerBlockLife plugin; + + public PlayerJoinListener(PlayerBlockLife plugin) { + this.plugin = plugin; + } + + @EventHandler(priority = EventPriority.NORMAL) + public void onPlayerJoin(PlayerJoinEvent event) { + Player player = event.getPlayer(); + UUID playerId = player.getUniqueId(); + + // 异步加载玩家皮肤 + plugin.getSkinManager().loadPlayerSkinAsync(player); + + // 延迟发送消息,确保皮肤加载完成 + Bukkit.getScheduler().runTaskLater(plugin, () -> { + int remainingBlocks = plugin.getBlockManager().getRemainingBlocks(playerId); + + if (remainingBlocks > 0) { + player.sendMessage("§a========================================"); + player.sendMessage("§e欢迎回来,§6" + player.getName() + "§e!"); + player.sendMessage("§7你还有 §e" + remainingBlocks + " §7个生命方块"); + player.sendMessage("§7使用 §e/checklifeblocks §7查看方块位置"); + player.sendMessage("§a========================================"); + + if (remainingBlocks <= 2) { + player.sendMessage("§c⚠ 警告!你的生命方块即将耗尽!"); + player.playSound(player.getLocation(), + org.bukkit.Sound.ENTITY_WITHER_SPAWN, 0.5f, 1.0f); + } + } else { + player.sendMessage("§e欢迎加入游戏!"); + player.sendMessage("§7使用 §e/setlifeblocks §7来设置你的生命方块"); + player.sendMessage("§6游戏规则:"); + player.sendMessage("§7- 每个玩家有5个生命方块"); + player.sendMessage("§7- 方块被其他玩家挖光时,你将死亡"); + player.sendMessage("§7- 方块使用你的皮肤作为材质"); + player.sendMessage("§7- 你可以自由移动,但方块固定位置"); + } + }, 40L); + } + + @EventHandler + public void onPlayerRespawn(PlayerRespawnEvent event) { + Player player = event.getPlayer(); + UUID playerId = player.getUniqueId(); + + if (!plugin.getLifeSystem().isPlayerAlive(playerId)) { + Bukkit.getScheduler().runTaskLater(plugin, () -> { + if (player.isOnline()) { + player.setGameMode(GameMode.SPECTATOR); + player.sendMessage("§e你已被淘汰,可以观察其他玩家。"); + player.sendMessage("§7等待下一轮游戏开始..."); + } + }, 20L); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/playerblocklife/PlayerQuitListener.java b/src/main/java/com/playerblocklife/PlayerQuitListener.java new file mode 100644 index 0000000..2781dee --- /dev/null +++ b/src/main/java/com/playerblocklife/PlayerQuitListener.java @@ -0,0 +1,18 @@ +package com.playerblocklife; + +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerQuitEvent; + +public class PlayerQuitListener implements Listener { + private final PlayerBlockLife plugin; + + public PlayerQuitListener(PlayerBlockLife plugin) { + this.plugin = plugin; + } + + @EventHandler + public void onPlayerQuit(PlayerQuitEvent event) { + plugin.logInfo("玩家 " + event.getPlayer().getName() + " 离开了游戏"); + } +} \ No newline at end of file diff --git a/src/main/java/com/playerblocklife/SetLifeBlocksCommand.java b/src/main/java/com/playerblocklife/SetLifeBlocksCommand.java new file mode 100644 index 0000000..fd190f0 --- /dev/null +++ b/src/main/java/com/playerblocklife/SetLifeBlocksCommand.java @@ -0,0 +1,132 @@ +package com.playerblocklife; + +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +import java.util.UUID; + +public class SetLifeBlocksCommand implements CommandExecutor { + private final PlayerBlockLife plugin; + + public SetLifeBlocksCommand(PlayerBlockLife plugin) { + this.plugin = plugin; + } + + @Override + public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c只有玩家可以使用此命令!"); + return true; + } + + Player player = (Player) sender; + UUID playerId = player.getUniqueId(); + + if (args.length > 0 && args[0].equalsIgnoreCase("help")) { + showHelp(player); + return true; + } + + if (args.length > 0 && player.hasPermission("playerblocklife.admin")) { + if (args[0].equalsIgnoreCase("other")) { + if (args.length < 2) { + player.sendMessage("§c用法: /setlifeblocks other <玩家名>"); + return true; + } + + Player target = Bukkit.getPlayer(args[1]); + if (target == null) { + player.sendMessage("§c找不到玩家: " + args[1]); + return true; + } + + setBlocksForPlayer(target, player); + return true; + } + } + + setBlocksForPlayer(player, null); + return true; + } + + private void setBlocksForPlayer(Player target, Player executor) { + UUID targetId = target.getUniqueId(); + + if (plugin.getBlockManager().hasLifeBlocks(targetId)) { + if (executor != null && !targetId.equals(executor.getUniqueId())) { + executor.sendMessage("§c玩家 " + target.getName() + " 已经有生命方块了!"); + } else { + target.sendMessage("§c你已经有生命方块了!"); + target.sendMessage("§e使用 /checklifeblocks 查看位置"); + target.sendMessage("§e使用 /setlifeblocks reset 重置方块位置"); + } + return; + } + + if (!plugin.getSkinManager().isSkinLoaded(targetId)) { + if (executor != null && !targetId.equals(executor.getUniqueId())) { + executor.sendMessage("§e玩家 " + target.getName() + " 的皮肤正在加载中,请稍候..."); + } else { + target.sendMessage("§e你的皮肤正在加载中,请稍候..."); + target.sendMessage("§7(如果长时间未加载完成,请重新加入服务器)"); + } + + plugin.getSkinManager().loadPlayerSkinAsync(target); + + Bukkit.getScheduler().runTaskLater(plugin, () -> { + boolean success = plugin.getBlockManager().setLifeBlocks(target, target.getLocation()); + if (success) { + if (executor != null && !targetId.equals(executor.getUniqueId())) { + executor.sendMessage("§a已为玩家 " + target.getName() + " 生成生命方块!"); + } + } else { + if (executor != null && !targetId.equals(executor.getUniqueId())) { + executor.sendMessage("§c为玩家 " + target.getName() + " 生成生命方块失败!"); + } else { + target.sendMessage("§c生成失败,请稍后再试或联系管理员"); + } + } + }, 40L); + + return; + } + + boolean success = plugin.getBlockManager().setLifeBlocks(target, target.getLocation()); + + if (success) { + if (executor != null && !targetId.equals(executor.getUniqueId())) { + executor.sendMessage("§a已为玩家 " + target.getName() + " 生成生命方块!"); + } + } else { + if (executor != null && !targetId.equals(executor.getUniqueId())) { + executor.sendMessage("§c为玩家 " + target.getName() + " 生成生命方块失败!"); + } else { + target.sendMessage("§c生成失败,请确保周围有足够空间"); + } + } + } + + private void showHelp(Player player) { + player.sendMessage("§a========== PlayerBlockLife 帮助 =========="); + player.sendMessage("§e/setlifeblocks §7- 设置你的生命方块"); + player.sendMessage("§e/checklifeblocks §7- 查看你的生命方块位置"); + player.sendMessage("§e/setlifeblocks reset §7- 重置生命方块位置"); + + if (player.hasPermission("playerblocklife.admin")) { + player.sendMessage("§6管理员命令:"); + player.sendMessage("§e/setlifeblocks other <玩家> §7- 为其他玩家设置生命方块"); + player.sendMessage("§e/pblreload §7- 重载插件配置"); + player.sendMessage("§e/pbldelete <玩家> §7- 删除玩家的生命方块"); + } + + player.sendMessage("§7游戏规则:"); + player.sendMessage("§7- 每个玩家有5个生命方块"); + player.sendMessage("§7- 方块被挖光时,玩家死亡"); + player.sendMessage("§7- 方块使用玩家的皮肤作为材质"); + player.sendMessage("§a======================================"); + } +} \ No newline at end of file diff --git a/src/main/java/com/playerblocklife/SkinManager.java b/src/main/java/com/playerblocklife/SkinManager.java new file mode 100644 index 0000000..831bf1a --- /dev/null +++ b/src/main/java/com/playerblocklife/SkinManager.java @@ -0,0 +1,290 @@ +package com.playerblocklife; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import org.bukkit.Bukkit; +import org.bukkit.OfflinePlayer; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.SkullMeta; +import org.bukkit.profile.PlayerProfile; +import org.bukkit.profile.PlayerTextures; +import org.yaml.snakeyaml.Yaml; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.*; +import java.net.URL; +import java.nio.file.Files; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +public class SkinManager { + private final PlayerBlockLife plugin; + private final Map playerSkinData = new ConcurrentHashMap<>(); + private final Map playerCustomModelData = new ConcurrentHashMap<>(); + private final Map skinLoadTime = new ConcurrentHashMap<>(); + private final File skinDataFile; + private final File skinCacheDir; + private int nextCustomModelData = 1000; + private static final long SKIN_CACHE_TIME = 7 * 24 * 60 * 60 * 1000L; + + public SkinManager(PlayerBlockLife plugin) { + this.plugin = plugin; + this.skinDataFile = new File(plugin.getDataFolder(), "skindata.yml"); + this.skinCacheDir = new File(plugin.getDataFolder(), "skincache"); + + if (!skinCacheDir.exists()) { + skinCacheDir.mkdirs(); + } + } + + public void loadAllSkins() { + loadSkinDataFromFile(); + Bukkit.getOnlinePlayers().forEach(this::loadPlayerSkinAsync); + } + + public void loadPlayerSkinAsync(Player player) { + UUID playerId = player.getUniqueId(); + + if (playerSkinData.containsKey(playerId)) { + return; + } + + if (loadSkinFromCache(playerId)) { + plugin.logInfo("从缓存加载皮肤: " + player.getName()); + return; + } + + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + try { + plugin.logInfo("开始加载皮肤: " + player.getName()); + + String skinBase64 = getSkinFromPlayerProfile(player); + + if (skinBase64 == null) { + skinBase64 = getDefaultSteveSkin(); + plugin.logWarning("使用默认皮肤: " + player.getName()); + } + + if (skinBase64 != null) { + playerSkinData.put(playerId, skinBase64); + int modelData = allocateCustomModelData(playerId); + saveSkinToCache(playerId, skinBase64); + skinLoadTime.put(playerId, System.currentTimeMillis()); + + plugin.logInfo("皮肤加载完成: " + player.getName() + " (模型数据: " + modelData + ")"); + + Bukkit.getScheduler().runTask(plugin, () -> { + if (player.isOnline()) { + player.sendMessage("§a你的皮肤方块已准备就绪!"); + } + }); + } + } catch (Exception e) { + plugin.logError("加载皮肤失败: " + player.getName(), e); + } + }); + } + + private String getSkinFromPlayerProfile(Player player) { + try { + PlayerProfile profile = player.getPlayerProfile(); + PlayerTextures textures = profile.getTextures(); + URL skinUrl = textures.getSkin(); + + if (skinUrl != null) { + BufferedImage skinImage = ImageIO.read(skinUrl); + if (skinImage == null) { + return null; + } + + int headX = 8; + int headY = 8; + int headWidth = 8; + int headHeight = 8; + + if (skinImage.getWidth() >= headX + headWidth && + skinImage.getHeight() >= headY + headHeight) { + + BufferedImage headImage = skinImage.getSubimage(headX, headY, headWidth, headHeight); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ImageIO.write(headImage, "PNG", baos); + byte[] imageBytes = baos.toByteArray(); + + JsonObject textureJson = new JsonObject(); + JsonObject timestampJson = new JsonObject(); + + return player.getUniqueId().toString(); + } + } + } catch (Exception e) { + plugin.logWarning("从PlayerProfile获取皮肤失败: " + e.getMessage()); + } + + return null; + } + + private boolean loadSkinFromCache(UUID playerId) { + try { + File cacheFile = new File(skinCacheDir, playerId.toString() + ".cache"); + if (cacheFile.exists()) { + long lastModified = cacheFile.lastModified(); + if (System.currentTimeMillis() - lastModified < SKIN_CACHE_TIME) { + String skinData = new String(Files.readAllBytes(cacheFile.toPath())); + playerSkinData.put(playerId, skinData); + allocateCustomModelData(playerId); + return true; + } + } + } catch (Exception e) { + plugin.logWarning("读取皮肤缓存失败: " + e.getMessage()); + } + return false; + } + + private void saveSkinToCache(UUID playerId, String skinData) { + try { + File cacheFile = new File(skinCacheDir, playerId.toString() + ".cache"); + Files.write(cacheFile.toPath(), skinData.getBytes()); + } catch (Exception e) { + plugin.logWarning("保存皮肤缓存失败: " + e.getMessage()); + } + } + + private synchronized int allocateCustomModelData(UUID playerId) { + if (playerCustomModelData.containsKey(playerId)) { + return playerCustomModelData.get(playerId); + } + + int modelData = nextCustomModelData++; + playerCustomModelData.put(playerId, modelData); + return modelData; + } + + private String getDefaultSteveSkin() { + return "8667ba71-b85a-4004-af54-457a9734eed7"; + } + + public ItemStack createPlayerHead(UUID playerId, String playerName) { + ItemStack head = new ItemStack(org.bukkit.Material.PLAYER_HEAD); + SkullMeta meta = (SkullMeta) head.getItemMeta(); + + if (meta != null) { + OfflinePlayer offlinePlayer = Bukkit.getOfflinePlayer(playerId); + meta.setOwningPlayer(offlinePlayer); + + Integer customModelData = playerCustomModelData.get(playerId); + if (customModelData != null) { + meta.setCustomModelData(customModelData); + } + + meta.setDisplayName("§e" + playerName + "的生命方块"); + + List lore = new ArrayList<>(); + lore.add("§7所有者: §e" + playerName); + lore.add("§c⚠ 警告: 挖掘此方块将减少玩家生命值"); + lore.add("§7剩余生命: §a" + plugin.getBlockManager().getRemainingBlocks(playerId) + "/5"); + meta.setLore(lore); + + head.setItemMeta(meta); + } + + return head; + } + + public boolean isSkinLoaded(UUID playerId) { + return playerSkinData.containsKey(playerId); + } + + public Integer getCustomModelData(UUID playerId) { + return playerCustomModelData.get(playerId); + } + + public Map getAllSkinData() { + return new HashMap<>(playerSkinData); + } + + public Map getAllCustomModelData() { + return new HashMap<>(playerCustomModelData); + } + + private void loadSkinDataFromFile() { + if (!skinDataFile.exists()) { + return; + } + + try { + Yaml yaml = new Yaml(); + Map data = yaml.load(new FileReader(skinDataFile)); + + if (data != null && data.containsKey("skins")) { + Map skins = (Map) data.get("skins"); + for (Map.Entry entry : skins.entrySet()) { + UUID playerId = UUID.fromString(entry.getKey()); + String skinData = (String) entry.getValue(); + playerSkinData.put(playerId, skinData); + } + } + + if (data != null && data.containsKey("modelData")) { + Map modelData = (Map) data.get("modelData"); + for (Map.Entry entry : modelData.entrySet()) { + UUID playerId = UUID.fromString(entry.getKey()); + playerCustomModelData.put(playerId, entry.getValue()); + nextCustomModelData = Math.max(nextCustomModelData, entry.getValue() + 1); + } + } + + plugin.logInfo("已加载 " + playerSkinData.size() + " 个玩家的皮肤数据"); + } catch (Exception e) { + plugin.logError("加载皮肤数据文件失败", e); + } + } + + public void saveSkinData() { + try { + Map data = new HashMap<>(); + + Map skins = new HashMap<>(); + for (Map.Entry entry : playerSkinData.entrySet()) { + skins.put(entry.getKey().toString(), entry.getValue()); + } + data.put("skins", skins); + + Map modelData = new HashMap<>(); + for (Map.Entry entry : playerCustomModelData.entrySet()) { + modelData.put(entry.getKey().toString(), entry.getValue()); + } + data.put("modelData", modelData); + + Yaml yaml = new Yaml(); + yaml.dump(data, new FileWriter(skinDataFile)); + + plugin.logInfo("皮肤数据已保存"); + } catch (Exception e) { + plugin.logError("保存皮肤数据失败", e); + } + } + + public void cleanupOldCache() { + File[] cacheFiles = skinCacheDir.listFiles(); + if (cacheFiles != null) { + long now = System.currentTimeMillis(); + int cleaned = 0; + + for (File file : cacheFiles) { + if (now - file.lastModified() > SKIN_CACHE_TIME) { + if (file.delete()) { + cleaned++; + } + } + } + + if (cleaned > 0) { + plugin.logInfo("清理了 " + cleaned + " 个过期皮肤缓存"); + } + } + } +} \ No newline at end of file diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml new file mode 100644 index 0000000..3ae6d12 --- /dev/null +++ b/src/main/resources/config.yml @@ -0,0 +1,382 @@ +# PlayerBlockLife 配置文件 +# 版本: 1.0.0 + +# 方块设置 +blocks: + # 每个玩家的生命方块数量 + amount: 5 + # 方块生成范围(以玩家为中心的正方形边长的一半) + spread: 5 + # 方块埋藏深度(0为地面,负数为地下) + depth: -1 + # 方块材质类型 + # 可选值: player_head, custom_block, default + material: player_head + # 是否在生成方块时自动填充周围的方块 + fill_around: true + + # 当使用玩家头颅时的设置 + player_head: + # 是否随机旋转头颅方向 + random_rotation: true + # 是否显示玩家名字 + show_player_name: true + # 是否显示特殊效果 + show_effects: true + + # 当使用自定义方块时的设置 + custom_block: + # 自定义方块材质 + material: DIAMOND_BLOCK + # 是否发光 + glowing: true + # 发光等级 (0-15) + light_level: 3 + +# 游戏规则 +game: + # 方块被挖光时是否死亡 + die_when_blocks_gone: true + # 死亡后是否变成观察者 + become_spectator: true + # 观察者模式 + spectator_mode: + # 是否可以飞行 + can_fly: true + # 是否可以看到其他玩家 + can_see_players: true + # 是否可以穿墙 + can_clip: true + + # 是否启用生命值系统 + health_system: true + # 生命值计算公式 + health_formula: + # 方块数量与生命值的关系 + # 格式: 方块数量:生命值 + 5: 20 + 4: 16 + 3: 12 + 2: 8 + 1: 4 + 0: 0 + + # 状态效果 + status_effects: + # 当剩余2个方块时 + two_blocks_left: + - type: SLOW + amplifier: 1 + duration: 100 + # 当剩余1个方块时 + one_block_left: + - type: SLOW + amplifier: 2 + duration: 100 + - type: WEAKNESS + amplifier: 0 + duration: 100 + - type: BLINDNESS + amplifier: 0 + duration: 100 + + # 挖掘奖励 + break_rewards: + # 是否给予经验 + give_exp: true + # 经验数量 + exp_amount: 5 + # 是否给予物品 + give_items: false + # 物品列表 + items: + - DIAMOND:1 + - GOLD_INGOT:3 + + # 广播设置 + broadcast: + # 方块被破坏时是否广播 + on_block_break: true + # 广播范围(格数) + range: 30 + # 玩家死亡时是否全服广播 + on_player_death: true + # 新玩家加入时是否广播 + on_player_join: false + +# 皮肤系统 +skin: + # 是否启用皮肤系统 + enabled: true + # 皮肤来源 + source: player_profile + # 可选值: player_profile, mojang_api, local_cache + # player_profile: 从玩家本地缓存获取(推荐,不调用外部API) + # mojang_api: 从Mojang API获取(需要网络) + # local_cache: 从本地缓存获取 + + # 缓存设置 + cache: + # 是否启用缓存 + enabled: true + # 缓存过期时间(天) + expire_days: 7 + # 缓存最大大小(MB) + max_size_mb: 100 + + # 默认皮肤(当无法获取玩家皮肤时) + default_skin: + # 使用哪个玩家的皮肤作为默认 + player_name: Steve + # 或使用自定义UUID + uuid: 8667ba71-b85a-4004-af54-457a9734eed7 + # 是否随机分配默认皮肤 + random_default: false + # 可选的默认皮肤列表 + available_skins: + - Steve + - Alex + - Enderman + + # 皮肤处理 + processing: + # 是否异步处理皮肤 + async: true + # 处理线程数 + threads: 2 + # 超时时间(秒) + timeout: 10 + # 重试次数 + retry_times: 3 + +# 数据存储 +storage: + # 存储类型 + # 可选值: yaml, json, sqlite, mysql + type: yaml + + # 自动保存 + auto_save: + # 是否启用自动保存 + enabled: true + # 保存间隔(秒) + interval: 300 + + # YAML存储设置 + yaml: + # 数据文件编码 + encoding: UTF-8 + # 是否压缩 + compress: false + + # SQLite设置 + sqlite: + # 数据库文件路径 + file: plugins/PlayerBlockLife/data.db + # 连接池大小 + pool_size: 5 + + # MySQL设置 + mysql: + # 数据库主机 + host: localhost + # 数据库端口 + port: 3306 + # 数据库名 + database: minecraft + # 用户名 + username: root + # 密码 + password: password + # 表前缀 + table_prefix: pbl_ + # 连接池设置 + pool: + max_connections: 10 + min_connections: 2 + connection_timeout: 30000 + idle_timeout: 600000 + +# 消息配置 +messages: + # 消息前缀 + prefix: "&6[&ePlayerBlockLife&6]&r " + # 消息颜色 + colors: + success: "&a" + error: "&c" + warning: "&e" + info: "&7" + highlight: "&6" + + # 命令消息 + commands: + setlifeblocks: + success: "&a已为你生成 {blocks} 个生命方块!" + already_has: "&c你已经有生命方块了!使用 /checklifeblocks 查看位置" + no_skin: "&e你的皮肤正在加载中,请稍候..." + failed: "&c无法生成生命方块,请确保周围有足够空间" + + checklifeblocks: + no_blocks: "&c你还没有设置生命方块!使用 /setlifeblocks 来设置" + info: "&a你的生命方块信息:" + location: "&7{index}. &e世界: {world} &7坐标: &a{x}&7, &a{y}&7, &a{z}" + remaining: "&7剩余方块: &e{remaining}&7/&a5" + health: "&7当前生命值: &c{health} ❤" + + admin: + reload: "&a插件配置已重载!" + delete_success: "&a已删除玩家 {player} 的生命方块" + delete_failed: "&c删除失败,玩家不存在或没有生命方块" + revive_success: "&a玩家 {player} 已复活!" + revive_failed: "&c复活失败,玩家不存在或未死亡" + + # 游戏消息 + game: + block_destroyed: + owner: "&c⚠ 警告!你的生命方块被 {breaker} 破坏了!" + breaker: "&a你破坏了 {owner} 的生命方块!" + remaining: "&7对方剩余生命方块: &a{remaining}" + broadcast: "&7[附近] &e一个生命方块被破坏了!" + + death: + title: "&4☠ 你死了!" + subtitle: "&c所有生命方块已被挖光" + broadcast: "&4☠ 玩家 {player} 的生命方块已被全部挖光,惨遭淘汰!" + spectator: "&e你已被淘汰,可以观察其他玩家。等待下一轮游戏开始..." + + warning: + low_blocks: "&c⚠ 警告!生命方块即将耗尽!" + last_block: "&4⚠ 警告!这是最后一个生命方块!" + health_low: "&4⚠ 警告!生命值过低!" + + # 加入消息 + join: + welcome: "&e欢迎加入游戏!" + welcome_back: "&e欢迎回来,{player}!" + rules: | + &6游戏规则: + &7- 每个玩家有5个生命方块 + &7- 方块被其他玩家挖光时,你将死亡 + &7- 方块使用你的皮肤作为材质 + &7- 你可以自由移动,但方块固定位置 + remaining_blocks: "&7你还有 {blocks} 个生命方块" + commands: "&7使用 &e/setlifeblocks &7来设置你的生命方块" + +# 效果设置 +effects: + # 方块放置效果 + place: + particles: + - type: ENCHANTMENT_TABLE + count: 30 + offset_x: 0.3 + offset_y: 0.3 + offset_z: 0.3 + speed: 0.1 + sounds: + - type: BLOCK_ANVIL_PLACE + volume: 0.5 + pitch: 1.2 + + # 方块破坏效果 + break: + particles: + - type: BLOCK_CRACK + count: 50 + offset_x: 0.5 + offset_y: 0.5 + offset_z: 0.5 + speed: 0.5 + data: PLAYER_HEAD + - type: SMOKE_LARGE + count: 20 + offset_x: 0.3 + offset_y: 0.3 + offset_z: 0.3 + speed: 0.05 + sounds: + - type: ENTITY_ITEM_BREAK + volume: 1.0 + pitch: 0.8 + - type: BLOCK_GLASS_BREAK + volume: 0.8 + pitch: 1.0 + knockback: + enabled: true + power: 0.5 + vertical: 0.3 + + # 玩家受伤效果 + damage: + particles: + - type: DAMAGE_INDICATOR + count: 10 + sounds: + - type: ENTITY_PLAYER_HURT + volume: 1.0 + pitch: 1.0 + + # 玩家死亡效果 + player_death: + particles: + - type: EXPLOSION_HUGE + count: 5 + sounds: + - type: ENTITY_WITHER_DEATH + volume: 0.7 + pitch: 0.8 + - type: ENTITY_LIGHTNING_BOLT_THUNDER + volume: 1.0 + pitch: 0.5 + +# 保护设置 +protection: + # 是否保护生命方块不被非玩家破坏 + protect_from_non_players: true + # 是否保护生命方块不被爆炸破坏 + protect_from_explosions: true + # 是否保护生命方块不被火焰烧毁 + protect_from_fire: true + # 是否保护生命方块不被活塞推动 + protect_from_pistons: true + # 是否允许TNT破坏生命方块 + allow_tnt_damage: false + # 是否允许苦力怕爆炸破坏生命方块 + allow_creeper_explosions: false + # 是否允许末影龙破坏生命方块 + allow_ender_dragon_damage: false + # 是否允许其他插件修改生命方块 + allow_plugin_modification: false + +# 世界保护 +world_guard: + # 是否与WorldGuard集成 + enabled: false + # 需要保护的地区标志 + region_flags: + - block-break + - block-place + - pvp + # 白名单地区(允许破坏生命方块的地区) + whitelist_regions: [] + # 黑名单地区(不允许破坏生命方块的地区) + blacklist_regions: [] + +# 调试设置 +debug: + # 是否启用调试模式 + enabled: false + # 调试级别 + # 可选值: INFO, WARNING, ERROR, DEBUG + level: INFO + # 是否记录到文件 + log_to_file: true + # 日志文件路径 + log_file: plugins/PlayerBlockLife/debug.log + # 是否显示详细事件日志 + verbose_events: false + # 是否记录性能数据 + log_performance: false + # 性能日志间隔(秒) + performance_log_interval: 60 \ No newline at end of file diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml new file mode 100644 index 0000000..c196927 --- /dev/null +++ b/src/main/resources/plugin.yml @@ -0,0 +1,35 @@ +commands: + setlifeblocks: + description: 设置你的生命方块(使用你的皮肤) + usage: / + aliases: [sbl, lifeblocks, setblocks] + permission: playerblocklife.set + + checklifeblocks: + description: 查看你的生命方块位置 + usage: / + aliases: [cbl, checklife, myblocks] + permission: playerblocklife.check + + pblreload: + description: 重载插件配置 + usage: / + aliases: [pblr, pblreload] + permission: playerblocklife.admin + + pbldelete: + description: 删除指定玩家的生命方块 + usage: / <玩家> + aliases: [pbldel, deleteblocks] + permission: playerblocklife.admin + + pblrevive: + description: 复活被淘汰的玩家 + usage: / [玩家] + aliases: [revive] + permission: playerblocklife.admin + + pblstats: + description: 查看插件统计信息 + usage: / + permission: playerblocklife.admin \ No newline at end of file