|
| 1 | +package net.swofty.type.ravengardgeneric.entity.mob; |
| 2 | + |
| 3 | +import net.kyori.adventure.text.Component; |
| 4 | +import net.kyori.adventure.text.format.TextColor; |
| 5 | +import net.minestom.server.MinecraftServer; |
| 6 | +import net.minestom.server.coordinate.Pos; |
| 7 | +import net.minestom.server.coordinate.Vec; |
| 8 | +import net.minestom.server.entity.Entity; |
| 9 | +import net.minestom.server.entity.EntityType; |
| 10 | +import net.minestom.server.entity.LivingEntity; |
| 11 | +import net.minestom.server.entity.Player; |
| 12 | +import net.minestom.server.entity.metadata.display.AbstractDisplayMeta; |
| 13 | +import net.minestom.server.entity.metadata.display.ItemDisplayMeta; |
| 14 | +import net.minestom.server.entity.metadata.display.TextDisplayMeta; |
| 15 | +import net.minestom.server.instance.Instance; |
| 16 | +import net.minestom.server.item.ItemStack; |
| 17 | +import net.minestom.server.item.Material; |
| 18 | +import net.minestom.server.component.DataComponents; |
| 19 | +import net.swofty.type.generic.HypixelGenericLoader; |
| 20 | +import net.swofty.type.generic.entity.InteractionEntity; |
| 21 | +import net.swofty.type.ravengardgeneric.entity.animation.RavengardAnimationClip; |
| 22 | + |
| 23 | +import java.util.ArrayList; |
| 24 | +import java.util.List; |
| 25 | +import java.util.concurrent.CopyOnWriteArrayList; |
| 26 | + |
| 27 | +/** |
| 28 | + * A combat mob driven by a captured rig: the clip's idle and attack tracks animate the parts, |
| 29 | + * it chases the nearest player and swings when in reach, and the captured ten square health bar |
| 30 | + * floats above it, draining as it takes hits. |
| 31 | + */ |
| 32 | +public class RavengardMob { |
| 33 | + private static final List<RavengardMob> MOBS = new CopyOnWriteArrayList<>(); |
| 34 | + private static final TextColor BAR_FULL = TextColor.color(0x5FEC7B); |
| 35 | + private static final TextColor BAR_EMPTY = TextColor.color(0x3D3D3D); |
| 36 | + private static final int BAR_SEGMENTS = 10; |
| 37 | + private static final double CHASE_RANGE = 14; |
| 38 | + private static final double ATTACK_RANGE = 2.4; |
| 39 | + private static final float PLAYER_DAMAGE_PER_HIT = 8f; |
| 40 | + private static final int HIT_DAMAGE = 20; |
| 41 | + private static final int ATTACK_COOLDOWN_TICKS = 30; |
| 42 | + |
| 43 | + private final RavengardAnimationClip clip; |
| 44 | + private final List<Entity> parts = new ArrayList<>(); |
| 45 | + private final List<Vec> partOffsets = new ArrayList<>(); |
| 46 | + private LivingEntity healthBar; |
| 47 | + private InteractionEntity hitbox; |
| 48 | + private Instance instance; |
| 49 | + |
| 50 | + private Pos position; |
| 51 | + private double health; |
| 52 | + private final double maxHealth; |
| 53 | + private int frame; |
| 54 | + private int attackTicksLeft; |
| 55 | + private int attackCooldown; |
| 56 | + private Vec knockback = Vec.ZERO; |
| 57 | + |
| 58 | + public RavengardMob(RavengardAnimationClip clip, Pos position) { |
| 59 | + this.clip = clip; |
| 60 | + this.position = position; |
| 61 | + this.maxHealth = clip.health() <= 0 ? 100 : clip.health(); |
| 62 | + this.health = maxHealth; |
| 63 | + } |
| 64 | + |
| 65 | + public static List<RavengardMob> mobs() { |
| 66 | + return MOBS; |
| 67 | + } |
| 68 | + |
| 69 | + public void spawn(Instance instance) { |
| 70 | + this.instance = instance; |
| 71 | + for (RavengardAnimationClip.Part part : clip.parts()) { |
| 72 | + RavengardAnimationClip.Base base = part.base(); |
| 73 | + Entity display = new Entity(EntityType.ITEM_DISPLAY); |
| 74 | + display.setNoGravity(true); |
| 75 | + display.editEntityMeta(ItemDisplayMeta.class, meta -> { |
| 76 | + meta.setItemStack(ItemStack.builder(Material.LEATHER_BOOTS) |
| 77 | + .set(DataComponents.ITEM_MODEL, base.model()).build()); |
| 78 | + ItemDisplayMeta.DisplayContext[] contexts = ItemDisplayMeta.DisplayContext.values(); |
| 79 | + int contextOrdinal = base.itemDisplayContext(); |
| 80 | + meta.setDisplayContext(contexts[contextOrdinal >= 0 && contextOrdinal < contexts.length |
| 81 | + ? contextOrdinal : 0]); |
| 82 | + meta.setTranslation(vec(base.translation())); |
| 83 | + meta.setScale(vec(base.scale())); |
| 84 | + meta.setLeftRotation(base.leftRotation()); |
| 85 | + meta.setRightRotation(base.rightRotation()); |
| 86 | + meta.setTransformationInterpolationDuration(clip.interpolationDuration()); |
| 87 | + meta.setPosRotInterpolationDuration(2); |
| 88 | + meta.setViewRange((float) base.viewRange()); |
| 89 | + }); |
| 90 | + double[] off = base.offset(); |
| 91 | + Vec offset = new Vec(off[0], off[1], off[2]); |
| 92 | + display.setInstance(instance, position.add(offset)); |
| 93 | + parts.add(display); |
| 94 | + partOffsets.add(offset); |
| 95 | + } |
| 96 | + |
| 97 | + healthBar = new LivingEntity(EntityType.TEXT_DISPLAY); |
| 98 | + healthBar.setNoGravity(true); |
| 99 | + healthBar.editEntityMeta(TextDisplayMeta.class, meta -> { |
| 100 | + meta.setText(barText()); |
| 101 | + meta.setScale(new Vec(0.3, 0.3, 0.3)); |
| 102 | + meta.setBillboardRenderConstraints(AbstractDisplayMeta.BillboardConstraints.CENTER); |
| 103 | + meta.setBackgroundColor(0); |
| 104 | + meta.setHasNoGravity(true); |
| 105 | + }); |
| 106 | + healthBar.setInstance(instance, position.add(0, 2.25, 0)); |
| 107 | + |
| 108 | + hitbox = new InteractionEntity(0.9f, 2.0f, (player, event) -> hit(player)); |
| 109 | + hitbox.setInstance(instance, position); |
| 110 | + |
| 111 | + MOBS.add(this); |
| 112 | + } |
| 113 | + |
| 114 | + private Component barText() { |
| 115 | + int full = (int) Math.ceil(BAR_SEGMENTS * Math.max(0, health) / maxHealth); |
| 116 | + return Component.text("\u25A0".repeat(full)).color(BAR_FULL) |
| 117 | + .append(Component.text("\u25A0".repeat(BAR_SEGMENTS - full)).color(BAR_EMPTY)); |
| 118 | + } |
| 119 | + |
| 120 | + public void hit(Player player) { |
| 121 | + if (health <= 0) { |
| 122 | + return; |
| 123 | + } |
| 124 | + health -= HIT_DAMAGE; |
| 125 | + Vec away = position.sub(player.getPosition()).asVec().withY(0); |
| 126 | + if (away.lengthSquared() > 0.001) { |
| 127 | + knockback = away.normalize().mul(0.45).withY(0.1); |
| 128 | + } |
| 129 | + healthBar.editEntityMeta(TextDisplayMeta.class, meta -> meta.setText(barText())); |
| 130 | + if (health <= 0) { |
| 131 | + remove(); |
| 132 | + } |
| 133 | + } |
| 134 | + |
| 135 | + public void remove() { |
| 136 | + parts.forEach(Entity::remove); |
| 137 | + if (healthBar != null) healthBar.remove(); |
| 138 | + if (hitbox != null) hitbox.remove(); |
| 139 | + MOBS.remove(this); |
| 140 | + } |
| 141 | + |
| 142 | + public void tick() { |
| 143 | + if (instance == null || health <= 0) { |
| 144 | + return; |
| 145 | + } |
| 146 | + |
| 147 | + Player target = nearestPlayer(); |
| 148 | + boolean attacking = attackTicksLeft > 0; |
| 149 | + boolean moved = false; |
| 150 | + float yaw = position.yaw(); |
| 151 | + |
| 152 | + if (!attacking && target != null) { |
| 153 | + double distance = position.distance(target.getPosition()); |
| 154 | + Vec toTarget = target.getPosition().sub(position).asVec().withY(0); |
| 155 | + if (toTarget.lengthSquared() > 0.01) { |
| 156 | + yaw = (float) Math.toDegrees(Math.atan2(-toTarget.x(), toTarget.z())); |
| 157 | + } |
| 158 | + if (distance <= ATTACK_RANGE && attackCooldown <= 0) { |
| 159 | + attackTicksLeft = attackFrames(); |
| 160 | + attackCooldown = ATTACK_COOLDOWN_TICKS; |
| 161 | + frame = 0; |
| 162 | + target.damage(net.minestom.server.entity.damage.DamageType.MOB_ATTACK, PLAYER_DAMAGE_PER_HIT); |
| 163 | + } else if (distance > ATTACK_RANGE && distance <= CHASE_RANGE && clip.walkSpeed() > 0) { |
| 164 | + Vec step = toTarget.normalize().mul(clip.walkSpeed()); |
| 165 | + position = position.add(step.x(), 0, step.z()).withYaw(yaw); |
| 166 | + moved = true; |
| 167 | + } else { |
| 168 | + position = position.withYaw(yaw); |
| 169 | + } |
| 170 | + } |
| 171 | + |
| 172 | + if (knockback.lengthSquared() > 0.001) { |
| 173 | + position = position.add(knockback.x(), 0, knockback.z()); |
| 174 | + knockback = knockback.mul(0.6); |
| 175 | + moved = true; |
| 176 | + } |
| 177 | + |
| 178 | + if (attackCooldown > 0) attackCooldown--; |
| 179 | + |
| 180 | + String phase = attacking ? "talk" : "idle"; |
| 181 | + List<RavengardAnimationClip.Part> clipParts = clip.parts(); |
| 182 | + for (int i = 0; i < parts.size() && i < clipParts.size(); i++) { |
| 183 | + Entity part = parts.get(i); |
| 184 | + List<RavengardAnimationClip.Frame> frames = clipParts.get(i).phase( |
| 185 | + attacking ? net.swofty.type.ravengardgeneric.entity.animation.RavengardAnimationPhase.TALK |
| 186 | + : net.swofty.type.ravengardgeneric.entity.animation.RavengardAnimationPhase.IDLE); |
| 187 | + Pos partPos = position.add(rotateOffset(partOffsets.get(i), yaw)).withYaw(yaw); |
| 188 | + if (moved || attacking) { |
| 189 | + part.teleport(partPos); |
| 190 | + } |
| 191 | + if (frames != null && !frames.isEmpty()) { |
| 192 | + final RavengardAnimationClip.Frame animationFrame = frames.get(frame % frames.size()); |
| 193 | + final float[] rotated = rotateRig(animationFrame.leftRotation(), yaw); |
| 194 | + final float finalYaw = yaw; |
| 195 | + part.editEntityMeta(ItemDisplayMeta.class, meta -> { |
| 196 | + meta.setTransformationInterpolationStartDelta(0); |
| 197 | + meta.setTranslation(rotateTranslation(vec(animationFrame.translation()), finalYaw)); |
| 198 | + meta.setLeftRotation(rotated); |
| 199 | + }); |
| 200 | + } |
| 201 | + } |
| 202 | + if (moved || attacking) { |
| 203 | + healthBar.teleport(position.add(0, 2.25, 0)); |
| 204 | + hitbox.teleport(position); |
| 205 | + } |
| 206 | + |
| 207 | + frame++; |
| 208 | + if (attacking) { |
| 209 | + attackTicksLeft--; |
| 210 | + if (attackTicksLeft <= 0) frame = 0; |
| 211 | + } |
| 212 | + } |
| 213 | + |
| 214 | + private int attackFrames() { |
| 215 | + int max = 0; |
| 216 | + for (RavengardAnimationClip.Part part : clip.parts()) { |
| 217 | + List<RavengardAnimationClip.Frame> frames = part.phase( |
| 218 | + net.swofty.type.ravengardgeneric.entity.animation.RavengardAnimationPhase.TALK); |
| 219 | + if (frames != null) max = Math.max(max, frames.size()); |
| 220 | + } |
| 221 | + return Math.max(1, max); |
| 222 | + } |
| 223 | + |
| 224 | + private Player nearestPlayer() { |
| 225 | + Player nearest = null; |
| 226 | + double best = CHASE_RANGE * CHASE_RANGE; |
| 227 | + for (var player : HypixelGenericLoader.getLoadedPlayers()) { |
| 228 | + if (player.getInstance() != instance) continue; |
| 229 | + double distance = player.getPosition().distanceSquared(position); |
| 230 | + if (distance < best) { |
| 231 | + best = distance; |
| 232 | + nearest = player; |
| 233 | + } |
| 234 | + } |
| 235 | + return nearest; |
| 236 | + } |
| 237 | + |
| 238 | + private static Vec vec(float[] values) { |
| 239 | + return new Vec(values[0], values[1], values[2]); |
| 240 | + } |
| 241 | + |
| 242 | + private static Vec rotateOffset(Vec offset, float yaw) { |
| 243 | + double theta = Math.toRadians(yaw); |
| 244 | + double sin = Math.sin(theta), cos = Math.cos(theta); |
| 245 | + return new Vec(offset.x() * cos + offset.z() * sin, offset.y(), |
| 246 | + -offset.x() * sin + offset.z() * cos); |
| 247 | + } |
| 248 | + |
| 249 | + private static Vec rotateTranslation(Vec translation, float yaw) { |
| 250 | + return rotateOffset(translation, yaw); |
| 251 | + } |
| 252 | + |
| 253 | + private static float[] rotateRig(float[] rotation, float yaw) { |
| 254 | + double theta = Math.toRadians(-yaw) / 2.0; |
| 255 | + float sin = (float) Math.sin(theta), cos = (float) Math.cos(theta); |
| 256 | + float rx = rotation[0], ry = rotation[1], rz = rotation[2], rw = rotation[3]; |
| 257 | + return new float[]{cos * rx + sin * rz, cos * ry + sin * rw, cos * rz - sin * rx, cos * rw - sin * ry}; |
| 258 | + } |
| 259 | + |
| 260 | + public static void startTicking() { |
| 261 | + MinecraftServer.getSchedulerManager() |
| 262 | + .buildTask(() -> MOBS.forEach(mob -> { |
| 263 | + try { |
| 264 | + mob.tick(); |
| 265 | + } catch (Exception ignored) { |
| 266 | + } |
| 267 | + })) |
| 268 | + .repeat(net.minestom.server.timer.TaskSchedule.tick(1)) |
| 269 | + .schedule(); |
| 270 | + } |
| 271 | +} |
0 commit comments