Skip to content

Commit a1056b7

Browse files
refactor(ravengard): mobs ride the real entity ai
The mob is an EntityCreature with melee attack and closest player targeting through the engine's pathfinding rather than straight line steering, with the captured rig, animations and health bar glued onto wherever navigation takes it. Vanilla damage drives the bar and knockback, and the attack goal plays the captured swing.
1 parent 6df6feb commit a1056b7

2 files changed

Lines changed: 53 additions & 76 deletions

File tree

type.ravengardgeneric/src/main/java/net/swofty/type/ravengardgeneric/commands/SpawnCommand.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ public void registerUsage(MinestomCommand command) {
3535
}
3636
RavengardAnimationClip clip = RavengardAnimationClip.loadMob(name);
3737
RavengardMob mob = new RavengardMob(clip, player.getPosition());
38-
mob.spawn(player.getInstance());
38+
mob.spawnMob(player.getInstance());
3939
player.sendMessage("§aSpawned §f" + name + "§a.");
4040
}, mobArg);
4141
}

type.ravengardgeneric/src/main/java/net/swofty/type/ravengardgeneric/entity/mob/RavengardMob.java

Lines changed: 52 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
* it chases the nearest player and swings when in reach, and the captured ten square health bar
3030
* floats above it, draining as it takes hits.
3131
*/
32-
public class RavengardMob {
32+
public class RavengardMob extends net.minestom.server.entity.EntityCreature {
3333
private static final List<RavengardMob> MOBS = new CopyOnWriteArrayList<>();
3434
private static final TextColor BAR_FULL = TextColor.color(0x5FEC7B);
3535
private static final TextColor BAR_EMPTY = TextColor.color(0x3D3D3D);
@@ -44,30 +44,68 @@ public class RavengardMob {
4444
private final List<Entity> parts = new ArrayList<>();
4545
private final List<Vec> partOffsets = new ArrayList<>();
4646
private LivingEntity healthBar;
47-
private InteractionEntity hitbox;
4847
private Instance instance;
4948

5049
private Pos position;
5150
private double health;
5251
private final double maxHealth;
5352
private int frame;
5453
private int attackTicksLeft;
55-
private int attackCooldown;
56-
private Vec knockback = Vec.ZERO;
5754

5855
public RavengardMob(RavengardAnimationClip clip, Pos position) {
56+
super(EntityType.ZOMBIE);
5957
this.clip = clip;
6058
this.position = position;
6159
this.maxHealth = clip.health() <= 0 ? 100 : clip.health();
6260
this.health = maxHealth;
61+
setInvisible(true);
62+
setSilent(true);
63+
getAttribute(net.minestom.server.entity.attribute.Attribute.MOVEMENT_SPEED)
64+
.setBaseValue(Math.max(0.15, clip.walkSpeed() * 2));
65+
getAttribute(net.minestom.server.entity.attribute.Attribute.MAX_HEALTH)
66+
.setBaseValue((float) maxHealth);
67+
setHealth((float) maxHealth);
68+
addAIGroup(
69+
List.of(new net.minestom.server.entity.ai.goal.MeleeAttackGoal(this, 1.8,
70+
java.time.Duration.ofMillis(1500))),
71+
List.of(new net.minestom.server.entity.ai.target.LastEntityDamagerTarget(this, 16),
72+
new net.minestom.server.entity.ai.target.ClosestEntityTarget(this, 14,
73+
entity -> entity instanceof Player)));
74+
}
75+
76+
/** MeleeAttackGoal path: play the captured swing and hurt the target. */
77+
@Override
78+
public void attack(Entity target, boolean swingHand) {
79+
if (attackTicksLeft <= 0) {
80+
attackTicksLeft = attackFrames();
81+
frame = 0;
82+
}
83+
if (target instanceof net.minestom.server.entity.LivingEntity living) {
84+
living.damage(net.minestom.server.entity.damage.DamageType.MOB_ATTACK, PLAYER_DAMAGE_PER_HIT);
85+
}
86+
}
87+
88+
/** Damage from any source drains the captured bar; vanilla handles the knockback. */
89+
@Override
90+
public boolean damage(net.minestom.server.registry.RegistryKey<net.minestom.server.entity.damage.DamageType> type, float amount) {
91+
boolean applied = super.damage(type, amount);
92+
health = getHealth();
93+
if (healthBar != null) {
94+
healthBar.editEntityMeta(TextDisplayMeta.class, meta -> meta.setText(barText()));
95+
}
96+
if (health <= 0) {
97+
removeRig();
98+
}
99+
return applied;
63100
}
64101

65102
public static List<RavengardMob> mobs() {
66103
return MOBS;
67104
}
68105

69-
public void spawn(Instance instance) {
106+
public void spawnMob(Instance instance) {
70107
this.instance = instance;
108+
setInstance(instance, position);
71109
for (RavengardAnimationClip.Part part : clip.parts()) {
72110
RavengardAnimationClip.Base base = part.base();
73111
Entity display = new Entity(EntityType.ITEM_DISPLAY);
@@ -105,9 +143,6 @@ public void spawn(Instance instance) {
105143
});
106144
healthBar.setInstance(instance, position.add(0, 2.25, 0));
107145

108-
hitbox = new InteractionEntity(0.9f, 2.0f, (player, event) -> hit(player));
109-
hitbox.setInstance(instance, position);
110-
111146
MOBS.add(this);
112147
}
113148

@@ -117,67 +152,24 @@ private Component barText() {
117152
.append(Component.text("\u25A0".repeat(BAR_SEGMENTS - full)).color(BAR_EMPTY));
118153
}
119154

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() {
155+
public void removeRig() {
136156
parts.forEach(Entity::remove);
137157
if (healthBar != null) healthBar.remove();
138-
if (hitbox != null) hitbox.remove();
139158
MOBS.remove(this);
159+
remove();
140160
}
141161

142-
public void tick() {
143-
if (instance == null || health <= 0) {
162+
/** Glues the rig, bar and animations onto wherever the creature's pathfinding took it. */
163+
public void tickRig() {
164+
if (instance == null || isDead()) {
144165
return;
145166
}
146-
147-
Player target = nearestPlayer();
167+
Pos previous = position;
168+
position = getPosition();
148169
boolean attacking = attackTicksLeft > 0;
149-
boolean moved = false;
170+
boolean moved = !previous.samePoint(position);
150171
float yaw = position.yaw();
151172

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";
181173
List<RavengardAnimationClip.Part> clipParts = clip.parts();
182174
for (int i = 0; i < parts.size() && i < clipParts.size(); i++) {
183175
Entity part = parts.get(i);
@@ -201,7 +193,6 @@ public void tick() {
201193
}
202194
if (moved || attacking) {
203195
healthBar.teleport(position.add(0, 2.25, 0));
204-
hitbox.teleport(position);
205196
}
206197

207198
frame++;
@@ -221,20 +212,6 @@ private int attackFrames() {
221212
return Math.max(1, max);
222213
}
223214

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-
238215
private static Vec vec(float[] values) {
239216
return new Vec(values[0], values[1], values[2]);
240217
}
@@ -261,7 +238,7 @@ public static void startTicking() {
261238
MinecraftServer.getSchedulerManager()
262239
.buildTask(() -> MOBS.forEach(mob -> {
263240
try {
264-
mob.tick();
241+
mob.tickRig();
265242
} catch (Exception ignored) {
266243
}
267244
}))

0 commit comments

Comments
 (0)