mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-08 08:07:53 -10:00
Check the killing blow of player_killed_entity: Return to Sender needs the ghast's fireball
KilledTrigger.TriggerInstance.matches tests killing_blow, a DamageSourcePredicate, against the damage that killed the entity. Horizon ignored it, so any ghast kill awarded Return to Sender (and any breeze kill Blowback, any skeleton kill from 50 blocks Sniper Duel). Fixes #2832 Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
93d77962ee
commit
c4348be8d2
4 files changed
+109
-3
No files matched your search
@@ -818,7 +818,13 @@ pub fn damage(v: &Value, d: &DamageView, w: &dyn World, origin: [f64; 3]) -> boo
|
||||
{
|
||||
return false;
|
||||
}
|
||||
let ty = &v["type"];
|
||||
damage_source(&v["type"], d, w, origin)
|
||||
}
|
||||
/// `DamageSourcePredicate`: the damage type's tags and the entities behind it.
|
||||
pub fn damage_source(ty: &Value, d: &DamageView, w: &dyn World, origin: [f64; 3]) -> bool {
|
||||
if ty.is_null() {
|
||||
return true;
|
||||
}
|
||||
let tags_ok = ty["tags"].as_array().into_iter().flatten().all(|t| {
|
||||
let tag = t["id"].as_str().unwrap_or("");
|
||||
let tag = tag.strip_prefix('#').unwrap_or(tag);
|
||||
@@ -1326,8 +1332,47 @@ mod tests {
|
||||
fn in_tag(&self, _: &str, tag: &str, id: &str) -> bool {
|
||||
(tag == "minecraft:stone_tool_materials" && id == "minecraft:cobblestone")
|
||||
|| (tag == "minecraft:sulfur_cube_archetype/explosive" && id == "minecraft:tnt")
|
||||
|| (tag == "minecraft:is_projectile"
|
||||
&& matches!(id, "minecraft:fireball" | "minecraft:arrow"))
|
||||
}
|
||||
}
|
||||
/// Return to Sender's `killing_blow`: only the ghast's own fireball, as a
|
||||
/// projectile, kills it for the advancement; a sword or an arrow does not.
|
||||
#[test]
|
||||
fn killing_blow_needs_the_fireball() {
|
||||
let blow = serde_json::json!({
|
||||
"direct_entity": {"minecraft:entity_type": "minecraft:fireball"},
|
||||
"tags": [{"expected": true, "id": "#minecraft:is_projectile"}]
|
||||
});
|
||||
let entity = |kind: &str| EntityView {
|
||||
kind: kind.into(),
|
||||
..Default::default()
|
||||
};
|
||||
let player = entity("minecraft:player");
|
||||
let fireball = DamageView {
|
||||
kind: "minecraft:fireball".into(),
|
||||
direct: Some(entity("minecraft:fireball")),
|
||||
source: Some(player.clone()),
|
||||
..Default::default()
|
||||
};
|
||||
assert!(damage_source(&blow, &fireball, &Flat, [0.0; 3]));
|
||||
let sword = DamageView {
|
||||
kind: "minecraft:player_attack".into(),
|
||||
direct: Some(player.clone()),
|
||||
source: Some(player.clone()),
|
||||
is_direct: true,
|
||||
..Default::default()
|
||||
};
|
||||
assert!(!damage_source(&blow, &sword, &Flat, [0.0; 3]));
|
||||
let arrow = DamageView {
|
||||
kind: "minecraft:arrow".into(),
|
||||
direct: Some(entity("minecraft:arrow")),
|
||||
source: Some(player),
|
||||
..Default::default()
|
||||
};
|
||||
assert!(!damage_source(&blow, &arrow, &Flat, [0.0; 3]));
|
||||
assert!(damage_source(&Value::Null, &sword, &Flat, [0.0; 3]));
|
||||
}
|
||||
#[test]
|
||||
fn uh_oh_accepts_either_tnt_event_only_for_adult_sulfur_cubes() {
|
||||
let path = Path::new(env!("CARGO_MANIFEST_DIR")).join(
|
||||
|
||||
@@ -719,6 +719,32 @@ impl Game<'_> {
|
||||
adv::conditions(&c["player"], &pctx, w) && adv::conditions(&c[&key], &ectx, w)
|
||||
});
|
||||
}
|
||||
/// `KilledTrigger` (`player_killed_entity`): the entity killed and the
|
||||
/// killing blow's damage source (none: any `killing_blow` fails).
|
||||
pub(super) fn trigger_killed(&mut self, target: &EntityView, blow: Option<&adv::DamageView>) {
|
||||
let me = self.player_entity();
|
||||
let target = target.clone();
|
||||
let blow = blow.cloned();
|
||||
self.trigger("minecraft:player_killed_entity", move |c, w| {
|
||||
let pctx = adv::Entities {
|
||||
this: Some(&me),
|
||||
origin: me.position,
|
||||
tool: None,
|
||||
block_pos: None,
|
||||
};
|
||||
let ectx = adv::Entities {
|
||||
this: Some(&target),
|
||||
..pctx
|
||||
};
|
||||
let blow_ok = c["killing_blow"].is_null()
|
||||
|| blow
|
||||
.as_ref()
|
||||
.is_some_and(|d| adv::damage_source(&c["killing_blow"], d, w, me.position));
|
||||
adv::conditions(&c["player"], &pctx, w)
|
||||
&& adv::conditions(&c["entity"], &ectx, w)
|
||||
&& blow_ok
|
||||
});
|
||||
}
|
||||
/// `KilledByArrowTrigger`: what the arrow killed and the weapon it came from.
|
||||
pub(super) fn trigger_killed_by_arrow(
|
||||
&mut self,
|
||||
|
||||
@@ -856,7 +856,42 @@ impl Game<'_> {
|
||||
self.stats.custom("mob_kills", 1);
|
||||
self.score_player_killed(super::commands::Target::Mob(m.id));
|
||||
let view = self.mob_entity(m);
|
||||
self.trigger_entity("minecraft:player_killed_entity", "entity", &view);
|
||||
let blow = m.last_damage.map(|(d, _)| self.damage_view(d));
|
||||
self.trigger_killed(&view, blow.as_ref());
|
||||
}
|
||||
/// A mob's damage source as advancement predicates see it: its type, the
|
||||
/// causing entity, and the projectile when one dealt it.
|
||||
fn damage_view(&self, d: Damage) -> horizon_client::advancements::DamageView {
|
||||
let source = match d.attacker {
|
||||
Some(Who::Player) => Some(self.player_entity()),
|
||||
Some(Who::Mob(id)) => self
|
||||
.mobs
|
||||
.iter()
|
||||
.find(|o| o.id == id)
|
||||
.map(|o| self.mob_entity(o)),
|
||||
None => None,
|
||||
};
|
||||
let projectile = match d.kind {
|
||||
DamageKind::Arrow => Some("minecraft:arrow"),
|
||||
DamageKind::Fireball => Some("minecraft:fireball"),
|
||||
_ => None,
|
||||
};
|
||||
let direct = match projectile {
|
||||
Some(kind) => Some(horizon_client::advancements::EntityView {
|
||||
kind: kind.into(),
|
||||
position: d.from.unwrap_or_default(),
|
||||
..Default::default()
|
||||
}),
|
||||
None => source.clone(),
|
||||
};
|
||||
horizon_client::advancements::DamageView {
|
||||
kind: damage_type(d.kind, matches!(d.attacker, Some(Who::Player))).to_owned(),
|
||||
// DamageSource.isDirect: the causing entity is the direct one.
|
||||
is_direct: projectile.is_none(),
|
||||
direct,
|
||||
source,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
/// `dropAllDeathLoot` through `mobs::drops`: the entity loot table from the
|
||||
/// world's random sequence, equipment by drop chance, species extras and
|
||||
|
||||
@@ -1083,7 +1083,7 @@ impl Game<'_> {
|
||||
self.stats.add("killed", "minecraft:ender_dragon", 1);
|
||||
self.stats.custom("mob_kills", 1);
|
||||
self.score_player_killed(super::commands::Target::Mob(id));
|
||||
self.trigger_entity("minecraft:player_killed_entity", "entity", view);
|
||||
self.trigger_killed(view, None);
|
||||
}
|
||||
/// Player melee on the dragon's parts or a crystal under the crosshair.
|
||||
/// The dragon part under the crosshair within reach: its id and distance.
|
||||
|
||||
Reference in new issue
Block a user