Summon and select ominous item spawners

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
k3yhandClaude Opus 5.5 committed 2026-11-01 13:23:14 -05:00
1 parent 1bd24e4042
commit ebec5efda8
8 files changed
+100 -8

No files matched your search

+24 -2
View File
@@ -2089,7 +2089,14 @@ pub fn ominous_item_spawner_to_nbt(
fall_distance: 0.0,
};
let mut tag = base.tag(raw);
put(&mut tag, "item", item_to_nbt(item));
// `ItemStack.CODEC` can't encode `ItemStack.EMPTY`, so an empty item
// writes no key.
if let Tag::Compound(map) = &mut tag {
map.remove(&"item".into());
}
if &*item.item != "minecraft:air" {
put(&mut tag, "item", item_to_nbt(item));
}
put(
&mut tag,
"spawn_item_after_ticks",
@@ -2101,7 +2108,11 @@ pub fn ominous_item_spawner_to_nbt(
/// and `spawn_item_after_ticks` (0 when absent). None without an item:
/// such a spawner would spawn nothing.
pub fn ominous_item_spawner_from_nbt(tag: &Tag) -> Option<(crate::items::ItemStack, Vec3, i64)> {
let item = tag.get("item").and_then(item_from_nbt)?;
// `readAdditionalSaveData`: `ItemStack.EMPTY` when it has none.
let item = tag
.get("item")
.and_then(item_from_nbt)
.unwrap_or_else(|| crate::items::ItemStack::new("minecraft:air", 1));
let after = number(tag.get("spawn_item_after_ticks")).unwrap_or(0);
Some((item, position(tag)?, after))
}
@@ -3037,6 +3048,17 @@ mod tests {
);
assert_eq!((pos, after), (at, 97));
assert!(ominous_item_spawner_from_nbt(&raw).is_none());
// No item is `ItemStack.EMPTY`, which writes no key back.
let empty = ominous_item_spawner_to_nbt(
&crate::items::ItemStack::new("minecraft:air", 1),
at,
0,
[1, 2, 3, 4],
Some(&tag),
);
assert!(empty.get("item").is_none());
let (back, pos, after) = ominous_item_spawner_from_nbt(&empty).unwrap();
assert_eq!((back.item.as_ref(), pos, after), ("minecraft:air", at, 0));
}
#[test]
+20 -1
View File
@@ -67,6 +67,8 @@ pub(super) enum Target {
Tnt(u32),
/// A falling block, by its entity id (`minecraft:falling_block`).
Falling(u32),
/// An ominous item spawner, by its entity id (`minecraft:ominous_item_spawner`).
Spawner(u32),
}
/// An entity of a type Horizon does not simulate: its compound lives in the
/// entity memory (`persist.raw`), its position and rotation here.
@@ -141,7 +143,8 @@ fn non_player_entity_id(t: Target) -> Option<i32> {
| Target::Crystal(id)
| Target::Dragon(id)
| Target::Tnt(id)
| Target::Falling(id)) = t
| Target::Falling(id)
| Target::Spawner(id)) = t
else {
return None;
};
@@ -525,6 +528,11 @@ impl Game<'_> {
.iter()
.find(|f| f.id == id)
.map(|f| [f.position.x, f.position.y, f.position.z]),
Target::Spawner(id) => self
.ominous_spawners
.iter()
.find(|s| s.id == id)
.map(|s| s.position),
}
}
pub(super) fn target_type(&self, t: Target) -> String {
@@ -573,6 +581,7 @@ impl Game<'_> {
Target::Dragon(_) => "minecraft:ender_dragon".into(),
Target::Tnt(_) => "minecraft:tnt".into(),
Target::Falling(_) => "minecraft:falling_block".into(),
Target::Spawner(_) => "minecraft:ominous_item_spawner".into(),
}
}
/// The name of the player the game acts for: a guest's own while their
@@ -864,6 +873,7 @@ impl Game<'_> {
}
candidates.extend(self.tnts.iter().map(|t| Target::Tnt(t.id)));
candidates.extend(self.falling.iter().map(|f| Target::Falling(f.id)));
candidates.extend(self.ominous_spawners.iter().map(|s| Target::Spawner(s.id)));
candidates
}
/// The loaded levels, in the order the server holds them: the
@@ -897,6 +907,7 @@ impl Game<'_> {
Target::Dragon(id),
Target::Tnt(id),
Target::Falling(id),
Target::Spawner(id),
]
.into_iter()
.find(|t| self.level_holds(*t))
@@ -920,6 +931,7 @@ impl Game<'_> {
Target::Dragon(id) => self.dragon.as_ref().is_some_and(|d| d.id == id),
Target::Tnt(id) => self.tnts.iter().any(|t| t.id == id),
Target::Falling(id) => self.falling.iter().any(|f| f.id == id),
Target::Spawner(id) => self.ominous_spawners.iter().any(|s| s.id == id),
}
}
/// Run `f` with the level that holds the entity a target names in place
@@ -1542,6 +1554,11 @@ impl Game<'_> {
f.previous = v;
}
}
Target::Spawner(id) => {
if let Some(s) = self.ominous_spawners.iter_mut().find(|s| s.id == id) {
s.position = [v.x, v.y, v.z];
}
}
// Wall-mounted entities do not move.
Target::Frame(_) | Target::Painting(_) => {}
}
@@ -2041,6 +2058,7 @@ impl Game<'_> {
Target::Dragon(id) => self.kill_dragon(id),
Target::Tnt(id) => self.tnts.retain(|t| t.id != id),
Target::Falling(id) => self.falling.retain(|f| f.id != id),
Target::Spawner(id) => self.ominous_spawners.retain(|s| s.id != id),
}
// The orbs marked for removal are gone (Entity.kill removes them).
let before = self.orb_ids();
@@ -3714,6 +3732,7 @@ mod look_at_tests {
Target::Dragon(7),
Target::Tnt(7),
Target::Falling(7),
Target::Spawner(7),
] {
assert_eq!(non_player_entity_id(t), Some(7), "{t:?}");
}
@@ -207,6 +207,17 @@ impl Game<'_> {
);
crate::game::persist::with_raw(tag, raw)
}
Target::Spawner(id) => {
let s = self.ominous_spawners.iter().find(|s| s.id == id)?;
let [x, y, z] = s.position;
entity_nbt::ominous_item_spawner_to_nbt(
&s.item,
horizon_core::geometry::Vec3::new(x, y, z),
s.spawn_item_after,
s.uuid,
s.raw.as_deref(),
)
}
})
}
/// `Entity.load` of new data (`EntityDataAccessor.setData`): the entity is
@@ -811,6 +822,8 @@ impl Game<'_> {
Some(Target::Tnt(id))
} else if self.falling.iter().any(|f| f.id == id) {
Some(Target::Falling(id))
} else if self.ominous_spawners.iter().any(|s| s.id == id) {
Some(Target::Spawner(id))
} else {
None
}
@@ -344,7 +344,9 @@ impl Game<'_> {
e.meta.push((
8,
serializer::ITEM_STACK,
Value::Item(hub.stack(&Some(o.item.clone()))),
Value::Item(
hub.stack(&Some(o.item.clone()).filter(|i| &*i.item != "minecraft:air")),
),
));
out.push(e);
}
@@ -115,7 +115,8 @@ fn target_id(t: Target) -> Option<u32> {
| Target::Crystal(id)
| Target::Dragon(id)
| Target::Tnt(id)
| Target::Falling(id) => Some(id),
| Target::Falling(id)
| Target::Spawner(id) => Some(id),
Target::Player | Target::Guest(_) => None,
}
}
@@ -21,8 +21,10 @@ const TICKS_SCALING: f32 = 50.0;
const ROTATION_SPEED: f32 = 40.0;
pub(super) struct OminousItemSpawner {
/// The entity id a server tracks it by (0 until it is tracked).
/// Its entity id: from the entity counter on the server, the server's on
/// a client.
pub(super) id: u32,
/// `DATA_ITEM`; `minecraft:air` stands for `ItemStack.EMPTY`.
pub(super) item: ItemStack,
pub(super) position: [f64; 3],
pub(super) spawn_item_after: i64,
@@ -66,8 +68,9 @@ impl Game<'_> {
position: [f64; 3],
delay: i32,
) {
self.next_entity += 1;
self.ominous_spawners.push(OminousItemSpawner {
id: 0,
id: self.next_entity,
item,
position,
spawn_item_after: i64::from(delay),
@@ -84,8 +87,9 @@ impl Game<'_> {
else {
return;
};
self.next_entity += 1;
self.ominous_spawners.push(OminousItemSpawner {
id: 0,
id: self.next_entity,
item,
position: [at.x, at.y, at.z],
spawn_item_after,
@@ -154,6 +158,10 @@ impl Game<'_> {
/// `spawnItem`: a projectile shot down, or an item entity; level event
/// 3021 and the entity_place game event.
fn spawn_ominous_item(&mut self, s: &OminousItemSpawner) {
// An empty item spawns nothing, and no event marks it.
if &*s.item.item == "minecraft:air" {
return;
}
let at = block(s.position);
let p = s.position;
match projectile(&s.item.item) {
@@ -182,6 +182,12 @@ impl Game<'_> {
| Target::Tnt(id)
| Target::Falling(id)
| Target::Other(id) => Some(uuid_text(self.uuid_for(id))),
// It keeps its UUID with itself.
Target::Spawner(id) => self
.ominous_spawners
.iter()
.find(|s| s.id == id)
.map(|s| uuid_text(s.uuid)),
// Orbs and projectiles have no UUID here.
Target::Orb(_) | Target::Projectile(_) => None,
}
@@ -216,6 +222,11 @@ impl Game<'_> {
| Target::Tnt(id)
| Target::Other(id) => self.persist.raw.get(&id),
Target::Player => self.level.player_extra.as_ref(),
Target::Spawner(id) => self
.ominous_spawners
.iter()
.find(|s| s.id == id)
.and_then(|s| s.raw.as_deref()),
Target::Falling(id) => self
.falling
.iter()
+16
View File
@@ -363,6 +363,22 @@ def entities():
cmd('/tp @e[type=minecraft:falling_block] 4.5 215 8.5'),
cmd('/kill @e[type=minecraft:falling_block]'),
cmd('/execute if entity @e[type=minecraft:falling_block]'),
# An ominous item spawner, with an item or empty (`ItemStack.EMPTY`,
# which saves no `item` and spawns nothing when its time comes).
cmd('/summon minecraft:ominous_item_spawner 4.5 210 8.5 {spawn_item_after_ticks:2000L,item:{id:"minecraft:diamond",count:2},Tags:["o"]}'),
cmd('/execute if entity @e[type=minecraft:ominous_item_spawner,tag=o]'),
cmd('/tag @e[type=minecraft:ominous_item_spawner,limit=1] list'),
cmd('/data get entity @e[type=minecraft:ominous_item_spawner,limit=1] item'),
cmd('/data get entity @e[type=minecraft:ominous_item_spawner,limit=1] spawn_item_after_ticks'),
cmd('/tp @e[type=minecraft:ominous_item_spawner] 4.5 215 8.5'),
cmd('/kill @e[type=minecraft:ominous_item_spawner]'),
cmd('/summon minecraft:ominous_item_spawner 4.5 210 8.5'),
cmd('/data get entity @e[type=minecraft:ominous_item_spawner,limit=1] item'),
cmd('/kill @e[type=minecraft:ominous_item_spawner]'),
cmd('/summon minecraft:ominous_item_spawner 4.5 210 8.5 {spawn_item_after_ticks:5L}'),
{'op': 'tick', 'n': 10},
cmd('/execute if entity @e[type=minecraft:ominous_item_spawner]'),
cmd('/execute if entity @e[type=minecraft:item,distance=..3]'),
# And the thrown items.
*[c for kind in ('snowball', 'egg', 'ender_pearl') for c in (
cmd(f'/summon minecraft:{kind} 4.5 210 8.5 {{NoGravity:1b}}'),