Skip to content

Commit 2fa8137

Browse files
authored
feat: add critical hits (#826)
Implements #257
1 parent a73f3e0 commit 2fa8137

File tree

1 file changed

+37
-8
lines changed

1 file changed

+37
-8
lines changed

events/tag/src/module/attack.rs

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@ use std::borrow::Cow;
33
use compact_str::format_compact;
44
use 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
};
1212
use glam::IVec3;
1313
use 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

Comments
 (0)