BubbleRenderer.java 32.7 KB
Newer Older
1 2 3 4
package com.owlmaddie.ui;

import com.mojang.blaze3d.systems.RenderSystem;
import com.owlmaddie.chat.ChatDataManager;
5
import com.owlmaddie.chat.EntityChatData;
6
import com.owlmaddie.chat.PlayerData;
7 8
import com.owlmaddie.skin.PlayerCustomTexture;
import com.owlmaddie.utils.*;
9 10 11 12
import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.font.TextRenderer;
import net.minecraft.client.font.TextRenderer.TextLayerType;
13
import net.minecraft.client.network.ClientPlayerEntity;
14 15 16 17
import net.minecraft.client.render.*;
import net.minecraft.client.render.entity.EntityRenderer;
import net.minecraft.client.util.math.MatrixStack;
import net.minecraft.entity.Entity;
18 19
import net.minecraft.entity.boss.dragon.EnderDragonEntity;
import net.minecraft.entity.boss.dragon.EnderDragonPart;
20
import net.minecraft.entity.mob.MobEntity;
21
import net.minecraft.entity.player.PlayerEntity;
22
import net.minecraft.registry.Registries;
23 24 25 26 27 28 29
import net.minecraft.util.Identifier;
import net.minecraft.util.math.Box;
import net.minecraft.util.math.MathHelper;
import net.minecraft.util.math.Vec3d;
import net.minecraft.world.World;
import org.joml.Matrix4f;
import org.joml.Quaternionf;
30 31
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
32

33
import java.util.ArrayList;
34
import java.util.List;
35
import java.util.UUID;
36 37 38 39 40 41 42
import java.util.stream.Collectors;

/**
 * The {@code BubbleRenderer} class provides static methods to render the chat UI bubble, entity icons,
 * text, friendship status, and other UI-related rendering code.
 */
public class BubbleRenderer {
43
    public static final Logger LOGGER = LoggerFactory.getLogger("creaturechat");
44 45
    protected static TextureLoader textures = new TextureLoader();
    public static int DISPLAY_PADDING = 2;
46 47
    public static int animationFrame = 0;
    public static long lastTick = 0;
48 49
    public static int light = 15728880;
    public static int overlay = OverlayTexture.DEFAULT_UV;
50 51
    public static List<String> whitelist = new ArrayList<>();
    public static List<String> blacklist = new ArrayList<>();
52 53
    private static int queryEntityDataCount = 0;
    private static List<Entity> relevantEntities;
54

55
    public static void drawTextBubbleBackground(String base_name, MatrixStack matrices, float x, float y, float width, float height, int friendship) {
56 57 58 59
        // Set shader & texture
        RenderSystem.setShader(GameRenderer::getPositionColorTexLightmapProgram);

        // Enable depth test and blending
60 61
        RenderSystem.enableBlend();
        RenderSystem.defaultBlendFunc();
62 63
        RenderSystem.enableDepthTest();
        RenderSystem.depthMask(true);
64

65
        // Prepare the tessellator and buffer
66 67 68 69 70
        Tessellator tessellator = Tessellator.getInstance();
        BufferBuilder buffer = tessellator.getBuffer();
        float z = 0.01F;

        // Draw UI text background (based on friendship)
71
        // Draw TOP
72
        if (friendship == -3 && !base_name.endsWith("-player")) {
73
            RenderSystem.setShaderTexture(0, textures.GetUI(base_name + "-enemy"));
74
        } else if (friendship == 3 && !base_name.endsWith("-player")) {
75
            RenderSystem.setShaderTexture(0, textures.GetUI(base_name + "-friend"));
76
        } else {
77
            RenderSystem.setShaderTexture(0, textures.GetUI(base_name));
78 79 80
        }
        drawTexturePart(matrices, buffer, x - 50, y, z, 228, 40);

81
        // Draw MIDDLE
82 83 84
        RenderSystem.setShaderTexture(0, textures.GetUI("text-middle"));
        drawTexturePart(matrices, buffer, x, y + 40, z, width, height);

85
        // Draw BOTTOM
86 87 88
        RenderSystem.setShaderTexture(0, textures.GetUI("text-bottom"));
        drawTexturePart(matrices, buffer, x, y + 40 + height, z, width, 5);

89
        // Disable blending and depth test
90 91 92 93 94
        RenderSystem.disableBlend();
        RenderSystem.disableDepthTest();
    }

    private static void drawTexturePart(MatrixStack matrices, BufferBuilder buffer, float x, float y, float z, float width, float height) {
95 96 97 98 99 100 101 102 103 104
        // Define the vertices with color, texture, light, and overlay
        Matrix4f matrix4f = matrices.peek().getPositionMatrix();

        // Begin drawing quads with the correct vertex format
        buffer.begin(VertexFormat.DrawMode.QUADS, VertexFormats.POSITION_COLOR_TEXTURE_LIGHT);

        buffer.vertex(matrix4f, x, y + height, z).color(255, 255, 255, 255).texture(0, 1).light(light).overlay(overlay).next();  // bottom left
        buffer.vertex(matrix4f, x + width, y + height, z).color(255, 255, 255, 255).texture(1, 1).light(light).overlay(overlay).next();   // bottom right
        buffer.vertex(matrix4f, x + width, y, z).color(255, 255, 255, 255).texture(1, 0).light(light).overlay(overlay).next();  // top right
        buffer.vertex(matrix4f, x, y, z).color(255, 255, 255, 255).texture(0, 0).light(light).overlay(overlay).next(); // top left
105 106 107 108 109 110
        Tessellator.getInstance().draw();
    }

    private static void drawIcon(String ui_icon_name, MatrixStack matrices, float x, float y, float width, float height) {
        // Draw button icon
        Identifier button_texture = textures.GetUI(ui_icon_name);
111 112 113

        // Set shader & texture
        RenderSystem.setShader(GameRenderer::getPositionColorTexLightmapProgram);
114
        RenderSystem.setShaderTexture(0, button_texture);
115 116

        // Enable depth test and blending
117 118
        RenderSystem.enableBlend();
        RenderSystem.defaultBlendFunc();
119 120
        RenderSystem.enableDepthTest();
        RenderSystem.depthMask(true);
121

122
        // Prepare the tessellator and buffer
123
        Tessellator tessellator = Tessellator.getInstance();
124
        BufferBuilder buffer = tessellator.getBuffer();
125

126 127 128 129 130 131 132 133 134 135
        // Get the current matrix position
        Matrix4f matrix4f = matrices.peek().getPositionMatrix();

        // Begin drawing quads with the correct vertex format
        buffer.begin(VertexFormat.DrawMode.QUADS, VertexFormats.POSITION_COLOR_TEXTURE_LIGHT);

        buffer.vertex(matrix4f, x, y + height, 0.0F).color(255, 255, 255, 255).texture(0, 1).light(light).overlay(overlay).next(); // bottom left
        buffer.vertex(matrix4f, x + width, y + height, 0.0F).color(255, 255, 255, 255).texture(1, 1).light(light).overlay(overlay).next(); // bottom right
        buffer.vertex(matrix4f, x + width, y, 0.0F).color(255, 255, 255, 255).texture(1, 0).light(light).overlay(overlay).next(); // top right
        buffer.vertex(matrix4f, x, y, 0.0F).color(255, 255, 255, 255).texture(0, 0).light(light).overlay(overlay).next(); // top left
136 137
        tessellator.draw();

138
        // Disable blending and depth test
139 140 141 142 143 144 145 146
        RenderSystem.disableBlend();
        RenderSystem.disableDepthTest();
    }

    private static void drawFriendshipStatus(MatrixStack matrices, float x, float y, float width, float height, int friendship) {
        // dynamically calculate friendship ui image name
        String ui_icon_name = "friendship" + friendship;

147 148 149 150
        // Set shader
        RenderSystem.setShader(GameRenderer::getPositionColorTexLightmapProgram);

        // Set texture
151 152
        Identifier button_texture = textures.GetUI(ui_icon_name);
        RenderSystem.setShaderTexture(0, button_texture);
153 154

        // Enable depth test and blending
155 156
        RenderSystem.enableBlend();
        RenderSystem.defaultBlendFunc();
157 158
        RenderSystem.enableDepthTest();
        RenderSystem.depthMask(true);
159

160
        // Prepare the tessellator and buffer
161 162
        Tessellator tessellator = Tessellator.getInstance();
        BufferBuilder bufferBuilder = tessellator.getBuffer();
163 164 165 166 167 168

        // Get the current matrix position
        Matrix4f matrix4f = matrices.peek().getPositionMatrix();

        // Begin drawing quads with the correct vertex format
        bufferBuilder.begin(VertexFormat.DrawMode.QUADS, VertexFormats.POSITION_COLOR_TEXTURE_LIGHT);
169 170

        float z = -0.01F;
171 172 173 174
        bufferBuilder.vertex(matrix4f, x, y + height, z).color(255, 255, 255, 255).texture(0, 1).light(light).overlay(overlay).next();  // bottom left
        bufferBuilder.vertex(matrix4f, x + width, y + height, z).color(255, 255, 255, 255).texture(1, 1).light(light).overlay(overlay).next();   // bottom right
        bufferBuilder.vertex(matrix4f, x + width, y, z).color(255, 255, 255, 255).texture(1, 0).light(light).overlay(overlay).next();  // top right
        bufferBuilder.vertex(matrix4f, x, y, z).color(255, 255, 255, 255).texture(0, 0).light(light).overlay(overlay).next(); // top left
175 176
        tessellator.draw();

177
        // Disable blending and depth test
178 179 180 181
        RenderSystem.disableBlend();
        RenderSystem.disableDepthTest();
    }

182
    private static void drawEntityIcon(MatrixStack matrices, Entity entity, float x, float y, float width, float height) {
183 184 185 186 187 188 189 190 191 192
        // Get entity renderer
        EntityRenderer renderer = EntityRendererAccessor.getEntityRenderer(entity);
        String entity_icon_path = renderer.getTexture(entity).getPath();

        // Draw face icon
        Identifier entity_id = textures.GetEntity(entity_icon_path);
        if (entity_id == null) {
            return;
        }

193 194
        // Set shader & texture
        RenderSystem.setShader(GameRenderer::getPositionColorTexLightmapProgram);
195
        RenderSystem.setShaderTexture(0, entity_id);
196 197

        // Enable depth test and blending
198 199
        RenderSystem.enableBlend();
        RenderSystem.defaultBlendFunc();
200 201
        RenderSystem.enableDepthTest();
        RenderSystem.depthMask(true);
202

203
        // Prepare the tessellator and buffer
204 205
        Tessellator tessellator = Tessellator.getInstance();
        BufferBuilder bufferBuilder = tessellator.getBuffer();
206 207 208 209 210 211

        // Get the current matrix position
        Matrix4f matrix4f = matrices.peek().getPositionMatrix();

        // Begin drawing quads with the correct vertex format
        bufferBuilder.begin(VertexFormat.DrawMode.QUADS, VertexFormats.POSITION_COLOR_TEXTURE_LIGHT);
212 213

        float z = -0.01F;
214 215 216 217
        bufferBuilder.vertex(matrix4f, x, y + height, z).color(255, 255, 255, 255).texture(0, 1).light(light).overlay(overlay).next();  // bottom left
        bufferBuilder.vertex(matrix4f, x + width, y + height, z).color(255, 255, 255, 255).texture(1, 1).light(light).overlay(overlay).next();   // bottom right
        bufferBuilder.vertex(matrix4f, x + width, y, z).color(255, 255, 255, 255).texture(1, 0).light(light).overlay(overlay).next();  // top right
        bufferBuilder.vertex(matrix4f, x, y, z).color(255, 255, 255, 255).texture(0, 0).light(light).overlay(overlay).next(); // top left
218 219
        tessellator.draw();

220
        // Disable blending and depth test
221 222 223 224
        RenderSystem.disableBlend();
        RenderSystem.disableDepthTest();
    }

225 226 227 228 229
    private static void drawPlayerIcon(MatrixStack matrices, Entity entity, float x, float y, float width, float height) {
        // Get player skin texture
        EntityRenderer renderer = EntityRendererAccessor.getEntityRenderer(entity);
        Identifier playerTexture = renderer.getTexture(entity);

230
        // Check for black and white pixels (using the Mixin-based check)
231
        boolean customSkinFound = PlayerCustomTexture.hasCustomIcon(playerTexture);
232

233 234
        // Set shader & texture
        RenderSystem.setShader(GameRenderer::getPositionColorTexLightmapProgram);
235
        RenderSystem.setShaderTexture(0, playerTexture);
236 237

        // Enable depth test and blending
238 239
        RenderSystem.enableBlend();
        RenderSystem.defaultBlendFunc();
240 241
        RenderSystem.enableDepthTest();
        RenderSystem.depthMask(true);
242

243
        // Prepare the tessellator and buffer
244 245
        Tessellator tessellator = Tessellator.getInstance();
        BufferBuilder bufferBuilder = tessellator.getBuffer();
246
        bufferBuilder.begin(VertexFormat.DrawMode.QUADS, VertexFormats.POSITION_COLOR_TEXTURE_LIGHT);
247

248
        Matrix4f matrix4f = matrices.peek().getPositionMatrix();
249
        float z = -0.01F;
250

251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320
        if (customSkinFound) {
            // Hidden icon UV coordinates
            float[][] newCoordinates = {
                    {0.0F, 0.0F, 8.0F, 8.0F, 0F, 0F},     // Row 1 left
                    {24.0F, 0.0F, 32.0F, 8.0F, 8F, 0F},   // Row 1 middle
                    {32.0F, 0.0F, 40.0F, 8.0F, 16F, 0F},  // Row 1 right
                    {56.0F, 0.0F, 64.0F, 8.0F, 0F, 8F},   // Row 2 left
                    {56.0F, 20.0F, 64.0F, 28.0F, 8F, 8F}, // Row 2 middle
                    {36.0F, 16.0F, 44.0F, 20.0F, 16F, 8F},// Row 2 right top
                    {56.0F, 16.0F, 64.0F, 20.0F, 16F, 12F},// Row 2 right bottom
                    {56.0F, 28.0F, 64.0F, 36.0F, 0F, 16F}, // Row 3 left
                    {56.0F, 36.0F, 64.0F, 44.0F, 8F, 16F}, // Row 3 middle
                    {56.0F, 44.0F, 64.0F, 52.0F, 16F, 16F},// Row 3 right
            };
            float scaleFactor = 0.77F;

            for (float[] coords : newCoordinates) {
                float newU1 = coords[0] / 64.0F;
                float newV1 = coords[1] / 64.0F;
                float newU2 = coords[2] / 64.0F;
                float newV2 = coords[3] / 64.0F;

                float offsetX = coords[4] * scaleFactor;
                float offsetY = coords[5] * scaleFactor;
                float scaledX = x + offsetX;
                float scaledY = y + offsetY;
                float scaledWidth = (coords[2] - coords[0]) * scaleFactor;
                float scaledHeight = (coords[3] - coords[1]) * scaleFactor;

                bufferBuilder.vertex(matrix4f, scaledX, scaledY + scaledHeight, z)
                        .color(255, 255, 255, 255).texture(newU1, newV2).light(light).overlay(overlay).next();
                bufferBuilder.vertex(matrix4f, scaledX + scaledWidth, scaledY + scaledHeight, z)
                        .color(255, 255, 255, 255).texture(newU2, newV2).light(light).overlay(overlay).next();
                bufferBuilder.vertex(matrix4f, scaledX + scaledWidth, scaledY, z)
                        .color(255, 255, 255, 255).texture(newU2, newV1).light(light).overlay(overlay).next();
                bufferBuilder.vertex(matrix4f, scaledX, scaledY, z)
                        .color(255, 255, 255, 255).texture(newU1, newV1).light(light).overlay(overlay).next();
            }
        } else {
            // Normal face coordinates
            float u1 = 8.0F / 64.0F;
            float v1 = 8.0F / 64.0F;
            float u2 = 16.0F / 64.0F;
            float v2 = 16.0F / 64.0F;

            bufferBuilder.vertex(matrix4f, x, y + height, z)
                    .color(255, 255, 255, 255).texture(u1, v2).light(light).overlay(overlay).next();
            bufferBuilder.vertex(matrix4f, x + width, y + height, z)
                    .color(255, 255, 255, 255).texture(u2, v2).light(light).overlay(overlay).next();
            bufferBuilder.vertex(matrix4f, x + width, y, z)
                    .color(255, 255, 255, 255).texture(u2, v1).light(light).overlay(overlay).next();
            bufferBuilder.vertex(matrix4f, x, y, z)
                    .color(255, 255, 255, 255).texture(u1, v1).light(light).overlay(overlay).next();

            // Hat layer
            float hatU1 = 40.0F / 64.0F;
            float hatV1 = 8.0F / 64.0F;
            float hatU2 = 48.0F / 64.0F;
            float hatV2 = 16.0F / 64.0F;

            z -= 0.01F;

            bufferBuilder.vertex(matrix4f, x, y + height, z)
                    .color(255, 255, 255, 255).texture(hatU1, hatV2).light(light).overlay(overlay).next();
            bufferBuilder.vertex(matrix4f, x + width, y + height, z)
                    .color(255, 255, 255, 255).texture(hatU2, hatV2).light(light).overlay(overlay).next();
            bufferBuilder.vertex(matrix4f, x + width, y, z)
                    .color(255, 255, 255, 255).texture(hatU2, hatV1).light(light).overlay(overlay).next();
            bufferBuilder.vertex(matrix4f, x, y, z)
                    .color(255, 255, 255, 255).texture(hatU1, hatV1).light(light).overlay(overlay).next();
321 322
        }

323
        tessellator.draw();
324 325

        // Disable blending and depth test
326 327 328 329
        RenderSystem.disableBlend();
        RenderSystem.disableDepthTest();
    }

330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349
    private static void drawMessageText(Matrix4f matrix, List<String> lines, int starting_line, int ending_line,
                                 VertexConsumerProvider immediate, float lineSpacing, int fullBright, float yOffset) {
        TextRenderer fontRenderer = MinecraftClient.getInstance().textRenderer;
        int currentLineIndex = 0; // We'll use this to track which line we're on

        for (String lineText : lines) {
            // Only draw lines that are within the specified range
            if (currentLineIndex >= starting_line && currentLineIndex < ending_line) {
                fontRenderer.draw(lineText, -fontRenderer.getWidth(lineText) / 2f, yOffset, 0xffffff,
                        false, matrix, immediate, TextLayerType.NORMAL, 0, fullBright);
                yOffset += fontRenderer.fontHeight + lineSpacing;
            }
            currentLineIndex++;

            if (currentLineIndex > ending_line) {
                break;
            }
        }
    }

350
    private static void drawEntityName(Entity entity, Matrix4f matrix, VertexConsumerProvider immediate,
351
                                int fullBright, float yOffset, boolean truncate) {
352
        TextRenderer fontRenderer = MinecraftClient.getInstance().textRenderer;
353

354
        // Get Name of entity
355
        String nameText = "";
356 357 358
        if (entity instanceof MobEntity) {
            // Custom Name Tag (MobEntity)
            if (entity.getCustomName() != null) {
359
                nameText = entity.getCustomName().getString();
360 361 362
            }
        } else if (entity instanceof PlayerEntity) {
            // Player Name
363
            nameText = entity.getName().getString();
364
        }
365

366
        // Truncate long names
367
        if (nameText.length() > 14 && truncate) {
368
            nameText = nameText.substring(0, 14) + "...";
369
        }
370 371 372

        fontRenderer.draw(nameText, -fontRenderer.getWidth(nameText) / 2f, yOffset, 0xffffff,
                false, matrix, immediate, TextLayerType.NORMAL, 0, fullBright);
373 374
    }

375
    public static void drawTextAboveEntities(WorldRenderContext context, long tick, float partialTicks) {
376 377 378 379 380
        // Set some rendering constants
        float lineSpacing = 1F;
        float textHeaderHeight = 40F;
        float textFooterHeight = 5F;
        int fullBright = 0xF000F0;
381
        double renderDistance = 11.0;
382

383
        // Get camera
384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401
        Camera camera = context.camera();
        Entity cameraEntity = camera.getFocusedEntity();
        if (cameraEntity == null) return;
        World world = cameraEntity.getEntityWorld();

        // Calculate radius of entities
        Vec3d pos = cameraEntity.getPos();
        Box area = new Box(pos.x - renderDistance, pos.y - renderDistance, pos.z - renderDistance,
                pos.x + renderDistance, pos.y + renderDistance, pos.z + renderDistance);

        // Init font render, matrix, and vertex producer
        TextRenderer fontRenderer = MinecraftClient.getInstance().textRenderer;
        MatrixStack matrices = context.matrixStack();
        VertexConsumerProvider immediate = context.consumers();

        // Get camera position
        Vec3d interpolatedCameraPos = new Vec3d(camera.getPos().x, camera.getPos().y, camera.getPos().z);

402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417
        // Increment query counter
        queryEntityDataCount++;

        // This query count helps us cache the list of relevant entities. We can refresh
        // the list every 3rd call to this render function
        if (queryEntityDataCount % 3 == 0 || relevantEntities == null) {
            // Get all entities
            List<Entity> nearbyEntities = world.getOtherEntities(null, area);

            // Filter to include only MobEntity & PlayerEntity but exclude any camera 1st person entity and any entities with passengers
            relevantEntities = nearbyEntities.stream()
                    .filter(entity -> (entity instanceof MobEntity || entity instanceof PlayerEntity))
                    .filter(entity -> !entity.hasPassengers())
                    .filter(entity -> !(entity.equals(cameraEntity) && !camera.isThirdPerson()))
                    .filter(entity -> !(entity.equals(cameraEntity) && entity.isSpectator()))
                    .filter(entity -> {
418 419 420 421
                        // Always include PlayerEntity
                        if (entity instanceof PlayerEntity) {
                            return true;
                        }
422 423 424 425 426 427 428 429 430 431 432 433 434
                        Identifier entityId = Registries.ENTITY_TYPE.getId(entity.getType());
                        String entityIdString = entityId.toString();
                        // Check blacklist first
                        if (blacklist.contains(entityIdString)) {
                            return false;
                        }
                        // If whitelist is not empty, only include entities in the whitelist
                        return whitelist.isEmpty() || whitelist.contains(entityIdString);
                    })
                    .collect(Collectors.toList());

            queryEntityDataCount = 0;
        }
435

436
        for (Entity entity : relevantEntities) {
437

438 439 440
            // Push a new matrix onto the stack.
            matrices.push();

441 442 443
            // Get entity height (adjust for specific classes)
            float entityHeight = EntityHeights.getAdjustedEntityHeight(entity);

444 445 446 447 448 449 450 451
            // Interpolate entity position (smooth motion)
            double paddingAboveEntity = 0.4D;
            Vec3d interpolatedEntityPos = new Vec3d(
                    MathHelper.lerp(partialTicks, entity.prevX, entity.getPos().x),
                    MathHelper.lerp(partialTicks, entity.prevY, entity.getPos().y),
                    MathHelper.lerp(partialTicks, entity.prevZ, entity.getPos().z)
            );

452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479
            // Determine the chat bubble position
            Vec3d bubblePosition;
            if (entity instanceof EnderDragonEntity) {
                // Ender dragons a unique, and we must use the Head for position
                EnderDragonEntity dragon = (EnderDragonEntity) entity;
                EnderDragonPart head = dragon.head;

                // Interpolate the head position
                Vec3d headPos = new Vec3d(
                        MathHelper.lerp(partialTicks, head.prevX, head.getX()),
                        MathHelper.lerp(partialTicks, head.prevY, head.getY()),
                        MathHelper.lerp(partialTicks, head.prevZ, head.getZ())
                );

                // Just use the head's interpolated position directly
                bubblePosition = headPos.add(0, entityHeight + paddingAboveEntity, 0);
            } else {
                // Calculate the forward offset based on the entity's yaw
                float entityYawRadians = (float) Math.toRadians(entity.getYaw(partialTicks));
                Vec3d forwardOffset = new Vec3d(-Math.sin(entityYawRadians), 0.0, Math.cos(entityYawRadians));

                // Calculate the forward offset based on the entity's yaw, scaled to 80% towards the front edge
                Vec3d scaledForwardOffset = forwardOffset.multiply(entity.getWidth() / 2.0 * 0.8);

                // Calculate the position of the chat bubble: above the head and 80% towards the front
                bubblePosition = interpolatedEntityPos.add(scaledForwardOffset)
                        .add(0, entityHeight + paddingAboveEntity, 0);
            }
480 481 482 483 484

            // Translate to the chat bubble's position
            matrices.translate(bubblePosition.x - interpolatedCameraPos.x,
                    bubblePosition.y - interpolatedCameraPos.y,
                    bubblePosition.z - interpolatedCameraPos.z);
485

486 487
            // Calculate the difference vector (from entity + padding above to camera)
            Vec3d difference = interpolatedCameraPos.subtract(new Vec3d(interpolatedEntityPos.x, interpolatedEntityPos.y + entityHeight + paddingAboveEntity, interpolatedEntityPos.z));
488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514

            // Calculate the yaw angle
            double yaw = -(Math.atan2(difference.z, difference.x) + Math.PI / 2D);

            // Convert yaw to Quaternion
            float halfYaw = (float) yaw * 0.5f;
            double sinHalfYaw = MathHelper.sin(halfYaw);
            double cosHalfYaw = MathHelper.cos(halfYaw);
            Quaternionf yawRotation = new Quaternionf(0, sinHalfYaw, 0, cosHalfYaw);

            // Apply the yaw rotation to the matrix stack
            matrices.multiply(yawRotation);

            // Obtain the horizontal distance to the entity
            double horizontalDistance = Math.sqrt(difference.x * difference.x + difference.z * difference.z);
            // Calculate the pitch angle based on the horizontal distance and the y difference
            double pitch = Math.atan2(difference.y, horizontalDistance);

            // Convert pitch to Quaternion
            float halfPitch = (float) pitch * 0.5f;
            double sinHalfPitch = MathHelper.sin(halfPitch);
            double cosHalfPitch = MathHelper.cos(halfPitch);
            Quaternionf pitchRotation = new Quaternionf(sinHalfPitch, 0, 0, cosHalfPitch);

            // Apply the pitch rotation to the matrix stack
            matrices.multiply(pitchRotation);

515
            // Get position matrix
516 517
            Matrix4f matrix = matrices.peek().getPositionMatrix();

518 519 520
            // Get the player
            ClientPlayerEntity player = MinecraftClient.getInstance().player;

521
            // Get chat message (if any)
522
            EntityChatData chatData = null;
523
            PlayerData playerData = null;
524 525
            if (entity instanceof MobEntity) {
                chatData = ChatDataManager.getClientInstance().getOrCreateChatData(entity.getUuidAsString());
526 527 528
                if (chatData != null) {
                    playerData = chatData.getPlayerData(player.getDisplayName().getString());
                }
529 530
            } else if (entity instanceof PlayerEntity) {
                chatData = PlayerMessageManager.getMessage(entity.getUuid());
531
                playerData = new PlayerData(); // no friendship needed for player messages
532
            }
533

534
            float minTextHeight = (ChatDataManager.DISPLAY_NUM_LINES * (fontRenderer.fontHeight + lineSpacing)) + (DISPLAY_PADDING * 2);
535
            float scaledTextHeight = 0;
536 537 538 539

            if (chatData != null) {
                // Set the range of lines to display
                List<String> lines = chatData.getWrappedLines();
540
                float linesDisplayed = 0;
541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565
                int starting_line = chatData.currentLineNumber;
                int ending_line = Math.min(chatData.currentLineNumber + ChatDataManager.DISPLAY_NUM_LINES, lines.size());

                // Determine max line length
                linesDisplayed = ending_line - starting_line;

                // Calculate size of text scaled to world
                scaledTextHeight = linesDisplayed * (fontRenderer.fontHeight + lineSpacing);
                scaledTextHeight = Math.max(scaledTextHeight, minTextHeight);

                // Update Bubble Data for Click Handling using UUID (account for scaling)
                BubbleLocationManager.updateBubbleData(entity.getUuid(), bubblePosition,
                        128F / (1 / 0.02F), (scaledTextHeight + 25F) / (1 / 0.02F), yaw, pitch);

                // Scale down before rendering textures (otherwise font is huge)
                matrices.scale(-0.02F, -0.02F, 0.02F);

                // Translate above the entity
                matrices.translate(0F, -scaledTextHeight - textHeaderHeight - textFooterHeight, 0F);

                // Check if conversation has started
                if (chatData.status == ChatDataManager.ChatStatus.NONE) {
                    // Draw 'start chat' button
                    drawIcon("button-chat", matrices, -16, textHeaderHeight, 32, 17);

566 567 568
                    // Draw Entity (Custom Name)
                    drawEntityName(entity, matrix, immediate, fullBright, 24F + DISPLAY_PADDING, true);

569 570 571 572 573 574
                } else if (chatData.status == ChatDataManager.ChatStatus.PENDING) {
                    // Draw 'pending' button
                    drawIcon("button-dot-" + animationFrame, matrices, -16, textHeaderHeight, 32, 17);

                } else if (chatData.sender == ChatDataManager.ChatSender.ASSISTANT && chatData.status != ChatDataManager.ChatStatus.HIDDEN) {
                    // Draw Entity (Custom Name)
575
                    drawEntityName(entity, matrix, immediate, fullBright, 24F + DISPLAY_PADDING, true);
576 577

                    // Draw text background (no smaller than 50F tall)
578
                    drawTextBubbleBackground("text-top", matrices, -64, 0, 128, scaledTextHeight, playerData.friendship);
579 580 581 582 583

                    // Draw face icon of entity
                    drawEntityIcon(matrices, entity, -82, 7, 32, 32);

                    // Draw Friendship status
584
                    drawFriendshipStatus(matrices, 51, 18, 31, 21, playerData.friendship);
585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600

                    // Draw 'arrows' & 'keyboard' buttons
                    if (chatData.currentLineNumber > 0) {
                        drawIcon("arrow-left", matrices, -63, scaledTextHeight + 29, 16, 16);
                    }
                    if (!chatData.isEndOfMessage()) {
                        drawIcon("arrow-right", matrices, 47, scaledTextHeight + 29, 16, 16);
                    } else {
                        drawIcon("keyboard", matrices, 47, scaledTextHeight + 28, 16, 16);
                    }

                    // Render each line of the text
                    drawMessageText(matrix, lines, starting_line, ending_line, immediate, lineSpacing, fullBright, 40.0F + DISPLAY_PADDING);

                } else if (chatData.sender == ChatDataManager.ChatSender.ASSISTANT && chatData.status == ChatDataManager.ChatStatus.HIDDEN) {
                    // Draw Entity (Custom Name)
601
                    drawEntityName(entity, matrix, immediate, fullBright, 24F + DISPLAY_PADDING, false);
602 603

                    // Draw 'resume chat' button
604
                    if (playerData.friendship == 3) {
605 606
                        // Friend chat bubble
                        drawIcon("button-chat-friend", matrices, -16, textHeaderHeight, 32, 17);
607
                    } else if (playerData.friendship == -3) {
608 609 610 611 612 613
                        // Enemy chat bubble
                        drawIcon("button-chat-enemy", matrices, -16, textHeaderHeight, 32, 17);
                    } else {
                        // Normal chat bubble
                        drawIcon("button-chat", matrices, -16, textHeaderHeight, 32, 17);
                    }
614 615 616

                } else if (chatData.sender == ChatDataManager.ChatSender.USER && chatData.status == ChatDataManager.ChatStatus.DISPLAY) {
                    // Draw Player Name
617
                    drawEntityName(entity, matrix, immediate, fullBright, 24F + DISPLAY_PADDING, true);
618 619

                    // Draw text background
620
                    drawTextBubbleBackground("text-top-player", matrices, -64, 0, 128, scaledTextHeight, playerData.friendship);
621 622 623 624 625 626

                    // Draw face icon of player
                    drawPlayerIcon(matrices, entity, -75, 14, 18, 18);

                    // Render each line of the player's text
                    drawMessageText(matrix, lines, starting_line, ending_line, immediate, lineSpacing, fullBright, 40.0F + DISPLAY_PADDING);
627 628
                }

629 630 631
            } else if (entity instanceof PlayerEntity) {
                // Scale down before rendering textures (otherwise font is huge)
                matrices.scale(-0.02F, -0.02F, 0.02F);
632

633 634 635 636
                boolean showPendingIcon = false;
                if (PlayerMessageManager.isChatUIOpen(entity.getUuid())) {
                    showPendingIcon = true;
                    scaledTextHeight += minTextHeight; // raise height of player name and icon
637 638
                } else {
                    scaledTextHeight -= 15; // lower a bit more (when no pending icon is visible)
639 640
                }

641 642
                // Translate above the player
                matrices.translate(0F, -scaledTextHeight - textHeaderHeight - textFooterHeight, 0F);
643

644 645
                // Draw Player Name (if not self and HUD is visible)
                if (!entity.equals(cameraEntity) && !MinecraftClient.getInstance().options.hudHidden) {
646
                    drawEntityName(entity, matrices.peek().getPositionMatrix(), immediate, fullBright, 24F + DISPLAY_PADDING, true);
647

648 649 650 651
                    if (showPendingIcon) {
                        // Draw 'pending' button (when Chat UI is open)
                        drawIcon("button-dot-" + animationFrame, matrices, -16, textHeaderHeight, 32, 17);
                    }
652 653 654 655 656 657 658 659 660 661
                }
            }

            // Calculate animation frames (0-8) every X ticks
            if (lastTick != tick && tick % 5 == 0) {
                lastTick = tick;
                animationFrame++;
            }
            if (animationFrame > 8) {
                animationFrame = 0;
662 663 664 665 666
            }

            // Pop the matrix to return to the original state.
            matrices.pop();
        }
667 668

        // Get list of Entity UUIDs with chat bubbles rendered
669
        List<UUID> activeEntityUUIDs = relevantEntities.stream()
670 671 672 673 674
                .map(Entity::getUuid)
                .collect(Collectors.toList());

        // Purge entities that were not rendered
        BubbleLocationManager.performCleanup(activeEntityUUIDs);
675 676
    }
}