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