@@ -3,14 +3,15 @@ use std::borrow::Cow;
33use compact_str:: format_compact;
44use flecs_ecs:: {
55 core:: {
6- Builder , EntityViewGet , QueryAPI , QueryBuilderImpl , SystemAPI , TableIter , TermBuilderImpl ,
7- World , WorldGet , flecs,
6+ Builder , EntityView , EntityViewGet , QueryAPI , QueryBuilderImpl , SystemAPI , TableIter ,
7+ TermBuilderImpl , World , WorldGet , flecs,
88 } ,
99 macros:: { Component , system} ,
1010 prelude:: Module ,
1111} ;
1212use glam:: IVec3 ;
1313use hyperion:: {
14+ Prev ,
1415 net:: {
1516 Compose , ConnectionId , agnostic,
1617 packets:: { BossBarAction , BossBarS2c } ,
@@ -130,6 +131,7 @@ impl Module for AttackModule {
130131 compose. unicast ( & pkt, * stream, system) . unwrap ( ) ;
131132 } ) ;
132133
134+ // TODO: This code should be split between melee attacks and bow attacks
133135 system ! ( "handle_attacks" , world, & mut EventQueue <event:: AttackEntity >( $) , & Compose ( $) )
134136 . multi_threaded ( )
135137 . each_iter (
@@ -153,8 +155,9 @@ impl Module for AttackModule {
153155 for event in event_queue. drain ( ) {
154156 let target = world. entity_from_id ( event. target ) ;
155157 let origin = world. entity_from_id ( event. origin ) ;
158+ let critical_hit = can_critical_hit ( origin) ;
156159 origin. get :: < ( & ConnectionId , & Position , & mut KillCount , & mut PlayerInventory , & mut Armor , & CombatStats , & PlayerInventory , & Team , & mut Xp ) > ( |( origin_connection, origin_pos, kill_count, inventory, origin_armor, from_stats, from_inventory, origin_team, origin_xp) | {
157- let damage = from_stats. damage + calculate_stats ( from_inventory) . damage ;
160+ let damage = from_stats. damage + calculate_stats ( from_inventory, critical_hit ) . damage ;
158161 target. try_get :: < (
159162 & ConnectionId ,
160163 Option < & mut ImmuneUntil > ,
@@ -186,7 +189,7 @@ impl Module for AttackModule {
186189 return ;
187190 }
188191
189- let calculated_stats = calculate_stats ( target_inventory) ;
192+ let calculated_stats = calculate_stats ( target_inventory, critical_hit ) ;
190193 let armor = stats. armor + calculated_stats. armor ;
191194 let toughness = stats. armor_toughness + calculated_stats. armor_toughness ;
192195 let protection = stats. protection + calculated_stats. protection ;
@@ -197,7 +200,7 @@ impl Module for AttackModule {
197200 health. damage ( damage_after_protection) ;
198201
199202 let pkt_health = play:: HealthUpdateS2c {
200- health : health. abs ( ) ,
203+ health : * * health,
201204 food : VarInt ( 20 ) ,
202205 food_saturation : 5.0
203206 } ;
@@ -220,7 +223,7 @@ impl Module for AttackModule {
220223 source_pos : Option :: None
221224 } ;
222225 let sound = agnostic:: sound (
223- ident ! ( "minecraft:entity.player.attack.knockback" ) ,
226+ if critical_hit { ident ! ( "minecraft:entity.player.attack.crit" ) } else { ident ! ( "minecraft:entity.player.attack. knockback") } ,
224227 * * target_position,
225228 ) . volume ( 1. )
226229 . pitch ( 1. )
@@ -230,6 +233,21 @@ impl Module for AttackModule {
230233 compose. unicast ( & pkt_hurt, * target_connection, system) . unwrap ( ) ;
231234 compose. unicast ( & pkt_health, * target_connection, system) . unwrap ( ) ;
232235
236+ if critical_hit {
237+ let particle_pkt = play:: ParticleS2c {
238+ particle : Cow :: Owned ( Particle :: Crit ) ,
239+ long_distance : true ,
240+ position : target_position. as_dvec3 ( ) + DVec3 :: new ( 0.0 , 1.0 , 0.0 ) ,
241+ max_speed : 0.5 ,
242+ count : 100 ,
243+ offset : Vec3 :: new ( 0.5 , 0.5 , 0.5 ) ,
244+ } ;
245+
246+ // origin is excluded because the crit particles are
247+ // already generated on the client side of the attacker
248+ compose. broadcast ( & particle_pkt, system) . exclude ( * origin_connection) . send ( ) . unwrap ( ) ;
249+ }
250+
233251 if health. is_dead ( ) {
234252 let attacker_name = origin. name ( ) ;
235253 // Even if enable_respawn_screen is false, the client needs this to send ClientCommandC2s and initiate its respawn
@@ -659,9 +677,11 @@ const fn calculate_toughness(item: &ItemStack) -> f32 {
659677 }
660678}
661679
662- fn calculate_stats ( inventory : & PlayerInventory ) -> CombatStats {
680+ // TODO: split this up into separate functions
681+ fn calculate_stats ( inventory : & PlayerInventory , critical_hit : bool ) -> CombatStats {
663682 let hand = inventory. get_cursor ( ) ;
664- let damage = calculate_damage ( & hand. stack ) ;
683+ let multiplier = if critical_hit { 1.5 } else { 1.0 } ;
684+ let damage = calculate_damage ( & hand. stack ) * multiplier;
665685 let armor = calculate_armor ( & inventory. get_helmet ( ) . stack )
666686 + calculate_armor ( & inventory. get_chestplate ( ) . stack )
667687 + calculate_armor ( & inventory. get_leggings ( ) . stack )
@@ -680,3 +700,12 @@ fn calculate_stats(inventory: &PlayerInventory) -> CombatStats {
680700 protection : 0.0 ,
681701 }
682702}
703+
704+ fn can_critical_hit ( player : EntityView < ' _ > ) -> bool {
705+ player. get :: < ( & ( Prev , Position ) , & Position ) > ( |( prev_position, position) | {
706+ // TODO: Do not allow critical hits if the player is on a ladder, vine, or water. None of
707+ // these special blocks are currently on the map.
708+ let position_delta_y = position. y - prev_position. y ;
709+ position_delta_y < 0.0
710+ } )
711+ }
0 commit comments