2222import com .mojang .math .Axis ;
2323
2424import net .minecraft .client .gui .Font ;
25+ import net .minecraft .core .registries .BuiltInRegistries ;
2526import net .minecraft .network .protocol .Packet ;
27+ import net .minecraft .network .protocol .game .ClientboundDamageEventPacket ;
2628import net .minecraft .network .protocol .game .ClientboundSetEntityDataPacket ;
2729import net .minecraft .util .Mth ;
2830import net .minecraft .util .RandomSource ;
4244import net .wurstclient .settings .ColorSetting ;
4345import net .wurstclient .settings .SliderSetting ;
4446import net .wurstclient .settings .SliderSetting .ValueDisplay ;
47+ import net .wurstclient .util .ChatUtils ;
4548import net .wurstclient .util .RenderUtils ;
4649
4750@ SearchTags ({"damage esp" , "damage numbers" , "damage popups" })
@@ -71,7 +74,13 @@ public final class DamageEspHack extends Hack
7174 private final ColorSetting textColor = new ColorSetting ("Text color" ,
7275 "Color of the floating damage numbers." , new Color (0xFF5555 ));
7376
77+ private final CheckboxSetting chatInfo = new CheckboxSetting ("Chat info" ,
78+ "Show damage dealt by you in chat, including three-second attack burst summaries." ,
79+ false );
80+
7481 private final Map <UUID , Float > trackedHealth = new HashMap <>();
82+ private final Map <Integer , PendingDamage > pendingDamage = new HashMap <>();
83+ private final Map <UUID , DamageBurst > damageBursts = new HashMap <>();
7584 private final List <DamagePopup > popups = new ArrayList <>();
7685 private final RandomSource random = RandomSource .create ();
7786 private static final int HEALTH_DATA_ID = resolveHealthDataId ();
@@ -85,13 +94,16 @@ public DamageEspHack()
8594 addSetting (fadeTime );
8695 addSetting (opacity );
8796 addSetting (textColor );
97+ addSetting (chatInfo );
8898 }
8999
90100 @ Override
91101 protected void onEnable ()
92102 {
93103 trackedHealth .clear ();
94104 popups .clear ();
105+ pendingDamage .clear ();
106+ damageBursts .clear ();
95107 EVENTS .add (UpdateListener .class , this );
96108 EVENTS .add (RenderListener .class , this );
97109 EVENTS .add (PacketInputListener .class , this );
@@ -105,6 +117,8 @@ protected void onDisable()
105117 EVENTS .remove (PacketInputListener .class , this );
106118 trackedHealth .clear ();
107119 popups .clear ();
120+ pendingDamage .clear ();
121+ damageBursts .clear ();
108122 }
109123
110124 @ Override
@@ -116,6 +130,8 @@ public void onReceivedPacket(PacketInputEvent event)
116130
117131 if (packet instanceof ClientboundSetEntityDataPacket entityData )
118132 handleEntityDataPacket (entityData );
133+ else if (packet instanceof ClientboundDamageEventPacket damage )
134+ handleDamageEvent (damage );
119135 }
120136
121137 @ Override
@@ -125,10 +141,15 @@ public void onUpdate()
125141 {
126142 trackedHealth .clear ();
127143 popups .clear ();
144+ pendingDamage .clear ();
145+ damageBursts .clear ();
128146 return ;
129147 }
130148
131149 long now = MC .level .getGameTime ();
150+ flushDamageBursts (now );
151+ pendingDamage .entrySet ()
152+ .removeIf (entry -> now - entry .getValue ().createdTick > 5 );
132153 Set <UUID > seen = new HashSet <>();
133154 for (Entity entity : MC .level .entitiesForRendering ())
134155 {
@@ -226,11 +247,94 @@ private void handleEntityDataPacket(ClientboundSetEntityDataPacket packet)
226247 trackedHealth .put (id , newHealth );
227248 float damage = oldHealth - newHealth ;
228249 if (damage > DAMAGE_EPSILON )
250+ {
229251 spawnPopup (living , damage );
252+ PendingDamage pending = pendingDamage .remove (entityId );
253+ if (pending != null
254+ && pending .createdTick + 5 >= MC .level .getGameTime ())
255+ recordChatDamage (living , damage , pending .weapon );
256+ }
230257 break ;
231258 }
232259 }
233260
261+ private void handleDamageEvent (ClientboundDamageEventPacket packet )
262+ {
263+ if (!chatInfo .isChecked () || MC .player == null || MC .level == null
264+ || packet .sourceCauseId () != MC .player .getId ())
265+ return ;
266+
267+ Entity target = MC .level .getEntity (packet .entityId ());
268+ if (!(target instanceof LivingEntity living ) || !shouldTrack (living ))
269+ return ;
270+
271+ String weapon = getHeldItemName ();
272+ pendingDamage .put (packet .entityId (),
273+ new PendingDamage (weapon , MC .level .getGameTime ()));
274+ }
275+
276+ private void recordChatDamage (LivingEntity target , float damage ,
277+ String weapon )
278+ {
279+ if (!chatInfo .isChecked ())
280+ return ;
281+
282+ String amount = formatDamage (damage );
283+ ChatUtils .message ("You did " + amount + " HP damage to "
284+ + target .getName ().getString () + " with " + weapon + "." );
285+
286+ UUID id = target .getUUID ();
287+ DamageBurst burst = damageBursts .computeIfAbsent (id ,
288+ ignored -> new DamageBurst (target .getName ().getString (), weapon ));
289+ burst .totalDamage += damage ;
290+ if (burst .hitCount > 0 )
291+ burst .totalIntervalTicks +=
292+ MC .level .getGameTime () - burst .lastHitTick ;
293+ burst .hitCount ++;
294+ burst .lastHitTick = MC .level .getGameTime ();
295+ burst .weapon = weapon ;
296+ }
297+
298+ private void flushDamageBursts (long now )
299+ {
300+ if (!chatInfo .isChecked ())
301+ {
302+ damageBursts .clear ();
303+ return ;
304+ }
305+
306+ damageBursts .entrySet ().removeIf (entry -> {
307+ DamageBurst burst = entry .getValue ();
308+ if (now - burst .lastHitTick < 60 )
309+ return false ;
310+ if (burst .hitCount > 1 )
311+ ChatUtils .message ("Attack summary on " + burst .targetName + ": "
312+ + burst .hitCount + " hits, average "
313+ + formatDamage (burst .totalDamage / burst .hitCount )
314+ + " HP, average time between hits "
315+ + formatSeconds (
316+ burst .totalIntervalTicks / (burst .hitCount - 1 ))
317+ + ", total " + formatDamage (burst .totalDamage ) + " HP with "
318+ + burst .weapon + "." );
319+ return true ;
320+ });
321+ }
322+
323+ private String getHeldItemName ()
324+ {
325+ if (MC .player == null || MC .player .getMainHandItem ().isEmpty ())
326+ return "UNKNOWN" ;
327+
328+ String name = BuiltInRegistries .ITEM
329+ .getKey (MC .player .getMainHandItem ().getItem ()).getPath ();
330+ return name .replace ('_' , ' ' ).toUpperCase (Locale .ROOT );
331+ }
332+
333+ private String formatSeconds (double ticks )
334+ {
335+ return String .format (Locale .ROOT , "%.1f seconds" , ticks / 20.0 );
336+ }
337+
234338 private void spawnPopup (LivingEntity entity , float damage )
235339 {
236340 if (MC .level == null )
@@ -299,6 +403,25 @@ private double ageTicks(double currentTick)
299403 }
300404 }
301405
406+ private record PendingDamage (String weapon , long createdTick )
407+ {}
408+
409+ private static final class DamageBurst
410+ {
411+ private final String targetName ;
412+ private String weapon ;
413+ private float totalDamage ;
414+ private int hitCount ;
415+ private long lastHitTick ;
416+ private long totalIntervalTicks ;
417+
418+ private DamageBurst (String targetName , String weapon )
419+ {
420+ this .targetName = targetName ;
421+ this .weapon = weapon ;
422+ }
423+ }
424+
302425 private static int resolveHealthDataId ()
303426 {
304427 try
0 commit comments