fix all
This commit is contained in:
parent
5088e3ad1c
commit
d1d2b93742
82 changed files with 4008 additions and 3032 deletions
|
|
@ -8,113 +8,173 @@ import net.kyori.adventure.text.Component;
|
|||
import net.kyori.adventure.text.event.ClickEvent;
|
||||
import net.kyori.adventure.text.event.HoverEvent;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import net.kyori.adventure.text.format.TextColor;
|
||||
import net.kyori.adventure.text.format.TextDecoration;
|
||||
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Рендерер чата с поддержкой глобального/локального режимов
|
||||
* Рендерер чата — глобальный и локальный режимы.
|
||||
* Цвета PREFIX и текста сообщения полностью настраиваются через chat-appearance.yml.
|
||||
*/
|
||||
public class EnhancedChatRenderer implements ChatRenderer {
|
||||
|
||||
private static final TextColor FALLBACK_COLOR = NamedTextColor.WHITE;
|
||||
|
||||
private final JavaPlugin plugin;
|
||||
private final Player sender;
|
||||
private final Component originalMessage;
|
||||
private final boolean isGlobal;
|
||||
private final MentionHandler mentionHandler;
|
||||
private final com.loki.lochat.config.AppearanceConfig cfg;
|
||||
private final com.loki.lochat.config.HardcodedMessages hm;
|
||||
|
||||
public EnhancedChatRenderer(JavaPlugin plugin, Player sender, Component originalMessage, boolean isGlobal) {
|
||||
this.plugin = plugin;
|
||||
this.sender = sender;
|
||||
this.originalMessage = originalMessage;
|
||||
this.isGlobal = isGlobal;
|
||||
this.mentionHandler = new MentionHandler(plugin);
|
||||
|
||||
com.loki.lochat.LoChat loChat = (com.loki.lochat.LoChat) plugin;
|
||||
this.cfg = loChat.getConfigManager().getAppearanceConfig();
|
||||
this.hm = loChat.getConfigManager().getHardcodedMessages();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Component render(Player source, Component sourceDisplayName, Component message, Audience viewer) {
|
||||
// Красивые префиксы с градиентами и эмодзи
|
||||
Component prefixComponent;
|
||||
if (isGlobal) {
|
||||
// 🌍 GLOBAL с градиентом от золотого к оранжевому
|
||||
prefixComponent = Component.text("🌍 ")
|
||||
.append(Component.text("G").color(net.kyori.adventure.text.format.TextColor.fromHexString("#FFD700")))
|
||||
.append(Component.text("L").color(net.kyori.adventure.text.format.TextColor.fromHexString("#FFA500")))
|
||||
.append(Component.text("O").color(net.kyori.adventure.text.format.TextColor.fromHexString("#FF8C00")))
|
||||
.append(Component.text("B").color(net.kyori.adventure.text.format.TextColor.fromHexString("#FF7F50")))
|
||||
.append(Component.text("A").color(net.kyori.adventure.text.format.TextColor.fromHexString("#FF6347")))
|
||||
.append(Component.text("L").color(net.kyori.adventure.text.format.TextColor.fromHexString("#FF4500")));
|
||||
} else {
|
||||
// LOCAL с градиентом от голубого к синему (без эмодзи)
|
||||
prefixComponent = Component.text("L").color(net.kyori.adventure.text.format.TextColor.fromHexString("#87CEEB"))
|
||||
.append(Component.text("O").color(net.kyori.adventure.text.format.TextColor.fromHexString("#87CEFA")))
|
||||
.append(Component.text("C").color(net.kyori.adventure.text.format.TextColor.fromHexString("#00BFFF")))
|
||||
.append(Component.text("A").color(net.kyori.adventure.text.format.TextColor.fromHexString("#1E90FF")))
|
||||
.append(Component.text("L").color(net.kyori.adventure.text.format.TextColor.fromHexString("#4169E1")));
|
||||
}
|
||||
|
||||
// Используем displayName игрока напрямую (он уже Component с градиентом)
|
||||
Component playerComponent = createPlayerComponent(source, source.displayName());
|
||||
Component processedMessage = message;
|
||||
Component prefix = buildPrefix();
|
||||
Component separator = buildSeparator();
|
||||
Component player = buildPlayerComponent(source);
|
||||
|
||||
processedMessage = TextFormatter.formatMarkdown(processedMessage);
|
||||
processedMessage = processUrls(processedMessage);
|
||||
processedMessage = processEmojis(processedMessage);
|
||||
Component processed = message;
|
||||
processed = TextFormatter.formatMarkdown(processed);
|
||||
processed = processUrls(processed);
|
||||
processed = processEmojis(processed);
|
||||
|
||||
if (viewer instanceof Player viewerPlayer) {
|
||||
processedMessage = mentionHandler.processMentions(processedMessage, source, viewerPlayer);
|
||||
if (viewer instanceof Player vp) {
|
||||
processed = mentionHandler.processMentions(processed, source, vp);
|
||||
}
|
||||
|
||||
// Красивое разделение с градиентными стрелками
|
||||
Component separator = isGlobal ?
|
||||
Component.text(" ▶ ", net.kyori.adventure.text.format.TextColor.fromHexString("#FFD700")) :
|
||||
Component.text(" ▶ ", net.kyori.adventure.text.format.TextColor.fromHexString("#FFFFFF"));
|
||||
|
||||
return prefixComponent
|
||||
return prefix
|
||||
.append(separator)
|
||||
.append(playerComponent)
|
||||
.append(Component.text(": ", net.kyori.adventure.text.format.NamedTextColor.WHITE))
|
||||
.append(processedMessage);
|
||||
.append(player)
|
||||
.append(Component.text(": ", NamedTextColor.WHITE))
|
||||
.append(processed);
|
||||
}
|
||||
|
||||
private Component createPlayerComponent(Player player, Component displayName) {
|
||||
if (!plugin.getConfig().getBoolean("chat.hover.enabled", true)) {
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
// Префикс
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
|
||||
private Component buildPrefix() {
|
||||
String emoji = isGlobal ? cfg.getGlobalEmoji() : cfg.getLocalEmoji();
|
||||
String text = isGlobal ? cfg.getGlobalText() : cfg.getLocalText();
|
||||
List<String> colors = isGlobal ? cfg.getGlobalColors() : cfg.getLocalColors();
|
||||
|
||||
Component prefix = Component.empty();
|
||||
|
||||
if (!emoji.isEmpty()) {
|
||||
prefix = prefix.append(Component.text(emoji + " "));
|
||||
}
|
||||
|
||||
prefix = prefix.append(buildGradientText(text, colors));
|
||||
return prefix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Строит компонент с плавным градиентом.
|
||||
* colors — список hex-цветов (минимум 1).
|
||||
* Если цветов меньше чем букв — интерполируем плавно между соседними цветами.
|
||||
* Если цветов больше чем букв — берём по одному на букву.
|
||||
*/
|
||||
private Component buildGradientText(String text, List<String> colors) {
|
||||
if (text.isEmpty()) return Component.empty();
|
||||
|
||||
if (colors.isEmpty()) {
|
||||
return Component.text(text, FALLBACK_COLOR);
|
||||
}
|
||||
|
||||
// Один цвет — просто красим весь текст
|
||||
if (colors.size() == 1) {
|
||||
return Component.text(text, parseColor(colors.get(0)));
|
||||
}
|
||||
|
||||
char[] chars = text.toCharArray();
|
||||
int len = chars.length;
|
||||
Component result = Component.empty();
|
||||
|
||||
for (int i = 0; i < len; i++) {
|
||||
// Нормализованная позиция 0..1
|
||||
float t = len == 1 ? 0f : (float) i / (len - 1);
|
||||
|
||||
// Какой сегмент градиента
|
||||
float scaled = t * (colors.size() - 1);
|
||||
int seg = (int) scaled;
|
||||
float frac = scaled - seg;
|
||||
|
||||
// Зажимаем в пределах списка
|
||||
if (seg >= colors.size() - 1) {
|
||||
seg = colors.size() - 2;
|
||||
frac = 1f;
|
||||
}
|
||||
|
||||
TextColor from = parseColor(colors.get(seg));
|
||||
TextColor to = parseColor(colors.get(seg + 1));
|
||||
TextColor blended = blend(from, to, frac);
|
||||
|
||||
result = result.append(Component.text(String.valueOf(chars[i]), blended));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Линейная интерполяция между двумя TextColor */
|
||||
private static TextColor blend(TextColor from, TextColor to, float t) {
|
||||
int r = Math.round(from.red() + t * (to.red() - from.red()));
|
||||
int g = Math.round(from.green() + t * (to.green() - from.green()));
|
||||
int b = Math.round(from.blue() + t * (to.blue() - from.blue()));
|
||||
return TextColor.color(clamp(r), clamp(g), clamp(b));
|
||||
}
|
||||
|
||||
private static int clamp(int v) { return Math.max(0, Math.min(255, v)); }
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
// Разделитель
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
|
||||
private Component buildSeparator() {
|
||||
String text = isGlobal ? cfg.getGlobalSeparatorText() : cfg.getLocalSeparatorText();
|
||||
String color = isGlobal ? cfg.getGlobalSeparatorColor() : cfg.getLocalSeparatorColor();
|
||||
return Component.text(text, parseColor(color));
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
// Ник игрока с hover
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
|
||||
private Component buildPlayerComponent(Player player) {
|
||||
Component displayName = player.displayName();
|
||||
|
||||
if (!cfg.isHoverEnabled()) {
|
||||
return displayName.clickEvent(ClickEvent.suggestCommand("/msg " + player.getName() + " "));
|
||||
}
|
||||
|
||||
java.util.List<String> hoverLines = plugin.getConfig().getStringList("chat.hover.format");
|
||||
if (hoverLines.isEmpty()) {
|
||||
hoverLines = java.util.Arrays.asList(
|
||||
"<gradient:#FFD700:#FFA500><bold>▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬</bold></gradient>",
|
||||
"<gradient:#87CEEB:#4169E1><bold>👤 Информация об игроке</bold></gradient>",
|
||||
"",
|
||||
"<gray>🏷️ Имя: <white><bold>{player}</bold>",
|
||||
"<gray>📶 Пинг: <yellow><bold>{ping}ms</bold>",
|
||||
"<gray>❤️ Здоровье: <red><bold>{health}/20</bold>",
|
||||
"<gray>🍖 Голод: <gold><bold>{food}/20</bold>",
|
||||
"",
|
||||
"<gradient:#FFD700:#FFA500><bold>▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬</bold></gradient>",
|
||||
"<yellow>💬 <bold>Клик:</bold> <gray>написать личное сообщение"
|
||||
);
|
||||
}
|
||||
|
||||
List<String> hoverLines = cfg.getHoverFormat();
|
||||
net.kyori.adventure.text.TextComponent.Builder hoverBuilder = Component.text();
|
||||
|
||||
for (int i = 0; i < hoverLines.size(); i++) {
|
||||
String line = hoverLines.get(i);
|
||||
String line = hoverLines.get(i)
|
||||
.replace("{player}", player.getName())
|
||||
.replace("{world}", player.getWorld().getName())
|
||||
.replace("{ping}", String.valueOf(player.getPing()))
|
||||
.replace("{gamemode}", hm.getGamemodeName(player.getGameMode().name().toLowerCase()))
|
||||
.replace("{health}", String.valueOf(Math.round(player.getHealth())))
|
||||
.replace("{food}", String.valueOf(player.getFoodLevel()));
|
||||
|
||||
line = line.replace("{player}", player.getName())
|
||||
.replace("{world}", player.getWorld().getName())
|
||||
.replace("{ping}", String.valueOf(player.getPing()))
|
||||
.replace("{gamemode}", getGameModeDisplay(player.getGameMode()))
|
||||
.replace("{health}", String.valueOf(Math.round(player.getHealth())))
|
||||
.replace("{food}", String.valueOf(player.getFoodLevel()));
|
||||
|
||||
Component lineComponent = com.loki.lochat.utils.ChatFormatter.parse(line);
|
||||
hoverBuilder.append(lineComponent);
|
||||
|
||||
if (i < hoverLines.size() - 1) {
|
||||
hoverBuilder.append(Component.newline());
|
||||
}
|
||||
hoverBuilder.append(com.loki.lochat.utils.ChatFormatter.parse(line));
|
||||
if (i < hoverLines.size() - 1) hoverBuilder.append(Component.newline());
|
||||
}
|
||||
|
||||
return displayName
|
||||
|
|
@ -122,86 +182,43 @@ public class EnhancedChatRenderer implements ChatRenderer {
|
|||
.clickEvent(ClickEvent.suggestCommand("/msg " + player.getName() + " "));
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить красивое отображение игрового режима
|
||||
*/
|
||||
private String getGameModeDisplay(org.bukkit.GameMode gameMode) {
|
||||
return switch (gameMode) {
|
||||
case SURVIVAL -> "Выживание";
|
||||
case CREATIVE -> "Творчество";
|
||||
case ADVENTURE -> "Приключение";
|
||||
case SPECTATOR -> "Наблюдатель";
|
||||
};
|
||||
}
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
// URL и эмодзи
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
|
||||
private Component processUrls(Component message) {
|
||||
String plainText = PlainTextComponentSerializer.plainText().serialize(message);
|
||||
// Fixed regex: escape the dash in character class to avoid illegal range
|
||||
String urlPattern = "(https?://[\\w\\-.~:/?#\\[\\]@!$&'()*+,;=%]+)";
|
||||
java.util.regex.Pattern pattern = java.util.regex.Pattern.compile(urlPattern);
|
||||
java.util.regex.Matcher matcher = pattern.matcher(plainText);
|
||||
String plain = PlainTextComponentSerializer.plainText().serialize(message);
|
||||
java.util.regex.Pattern pattern =
|
||||
java.util.regex.Pattern.compile("(https?://[\\w\\-.~:/?#\\[\\]@!$&'()*+,;=%]+)");
|
||||
java.util.regex.Matcher matcher = pattern.matcher(plain);
|
||||
|
||||
Component result = message;
|
||||
while (matcher.find()) {
|
||||
String url = matcher.group(1);
|
||||
Component urlComponent = Component.text(url, NamedTextColor.AQUA, TextDecoration.UNDERLINED)
|
||||
Component urlComp = Component.text(url, NamedTextColor.AQUA, TextDecoration.UNDERLINED)
|
||||
.hoverEvent(HoverEvent.showText(Component.text("Открыть: " + url, NamedTextColor.GRAY)))
|
||||
.clickEvent(ClickEvent.openUrl(url));
|
||||
|
||||
result = result.replaceText(builder ->
|
||||
builder.matchLiteral(url).replacement(urlComponent)
|
||||
);
|
||||
result = result.replaceText(b -> b.matchLiteral(url).replacement(urlComp));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private Component processEmojis(Component message) {
|
||||
Component result = message;
|
||||
|
||||
// Основные символы
|
||||
result = result.replaceText(builder -> builder.matchLiteral(":heart:").replacement("♥"));
|
||||
result = result.replaceText(builder -> builder.matchLiteral(":star:").replacement("★"));
|
||||
result = result.replaceText(builder -> builder.matchLiteral(":check:").replacement("✓"));
|
||||
result = result.replaceText(builder -> builder.matchLiteral(":cross:").replacement("✗"));
|
||||
result = result.replaceText(builder -> builder.matchLiteral(":arrow:").replacement("→"));
|
||||
result = result.replaceText(builder -> builder.matchLiteral(":skull:").replacement("☠"));
|
||||
result = result.replaceText(builder -> builder.matchLiteral(":note:").replacement("♪"));
|
||||
result = result.replaceText(builder -> builder.matchLiteral(":sun:").replacement("☀"));
|
||||
result = result.replaceText(builder -> builder.matchLiteral(":moon:").replacement("☽"));
|
||||
result = result.replaceText(builder -> builder.matchLiteral(":snowflake:").replacement("❄"));
|
||||
result = result.replaceText(builder -> builder.matchLiteral(":fire:").replacement("▲"));
|
||||
|
||||
// Minecraft символы
|
||||
result = result.replaceText(builder -> builder.matchLiteral(":diamond:").replacement("◆"));
|
||||
result = result.replaceText(builder -> builder.matchLiteral(":sword:").replacement("⚔"));
|
||||
result = result.replaceText(builder -> builder.matchLiteral(":pickaxe:").replacement("⛏"));
|
||||
result = result.replaceText(builder -> builder.matchLiteral(":bow:").replacement("⌐"));
|
||||
result = result.replaceText(builder -> builder.matchLiteral(":shield:").replacement("▣"));
|
||||
result = result.replaceText(builder -> builder.matchLiteral(":potion:").replacement("⚗"));
|
||||
result = result.replaceText(builder -> builder.matchLiteral(":book:").replacement("▤"));
|
||||
result = result.replaceText(builder -> builder.matchLiteral(":crown:").replacement("♔"));
|
||||
result = result.replaceText(builder -> builder.matchLiteral(":gem:").replacement("◊"));
|
||||
result = result.replaceText(builder -> builder.matchLiteral(":emerald:").replacement("◈"));
|
||||
result = result.replaceText(builder -> builder.matchLiteral(":gold:").replacement("◉"));
|
||||
result = result.replaceText(builder -> builder.matchLiteral(":iron:").replacement("◎"));
|
||||
|
||||
// Дополнительные символы
|
||||
result = result.replaceText(builder -> builder.matchLiteral(":thumbsup:").replacement("▲"));
|
||||
result = result.replaceText(builder -> builder.matchLiteral(":thumbsdown:").replacement("▼"));
|
||||
result = result.replaceText(builder -> builder.matchLiteral(":clap:").replacement("※"));
|
||||
result = result.replaceText(builder -> builder.matchLiteral(":wave:").replacement("~"));
|
||||
result = result.replaceText(builder -> builder.matchLiteral(":peace:").replacement("✌"));
|
||||
result = result.replaceText(builder -> builder.matchLiteral(":ok:").replacement("◯"));
|
||||
result = result.replaceText(builder -> builder.matchLiteral(":facepalm:").replacement("⌐_⌐"));
|
||||
result = result.replaceText(builder -> builder.matchLiteral(":shrug:").replacement("¯\\_(ツ)_/¯"));
|
||||
result = result.replaceText(builder -> builder.matchLiteral(":thinking:").replacement("?"));
|
||||
result = result.replaceText(builder -> builder.matchLiteral(":laugh:").replacement("xD"));
|
||||
result = result.replaceText(builder -> builder.matchLiteral(":cry:").replacement(":'("));
|
||||
result = result.replaceText(builder -> builder.matchLiteral(":rage:").replacement(">:("));
|
||||
result = result.replaceText(builder -> builder.matchLiteral(":cool:").replacement("B)"));
|
||||
result = result.replaceText(builder -> builder.matchLiteral(":wink:").replacement(";)"));
|
||||
|
||||
for (java.util.Map.Entry<String, String> e : cfg.getEmojis().entrySet()) {
|
||||
result = result.replaceText(b -> b.matchLiteral(e.getKey()).replacement(e.getValue()));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
// Утилиты
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
|
||||
private TextColor parseColor(String hex) {
|
||||
if (hex == null || hex.isBlank()) return FALLBACK_COLOR;
|
||||
String n = hex.startsWith("#") ? hex : "#" + hex;
|
||||
TextColor c = TextColor.fromHexString(n);
|
||||
return c != null ? c : FALLBACK_COLOR;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue