This commit is contained in:
loki5512344 2026-03-16 12:43:52 +01:00
parent 5088e3ad1c
commit d1d2b93742
82 changed files with 4008 additions and 3032 deletions

View file

@ -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;
}
}