mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 11:06:11 -10:00
Merge pull request #1417 from ludovic111/fix/st2-rules
🤖 Agent: merged after verify and CI parity passed; lan_e2e (hands, styles, feedback, chat, frames, riding, mounts, beds, contacts, worldspawn) passes on the server thread and in-frame with all four merged on main (bugs-st-2).
This commit is contained in:
6 files changed
+117
-53
No files matched your search
@@ -4306,7 +4306,7 @@ impl Game<'_> {
|
||||
/// world's entities and blocks, and the network hub.
|
||||
fn tick_world(&mut self, running: bool) -> Result<()> {
|
||||
self.update_chunk_tickets();
|
||||
horizon_client::spans::time("tick_effects", || self.tick_effects());
|
||||
horizon_client::spans::time("tick_effects", || self.tick_effects(running));
|
||||
// Player.tick's ItemCooldowns.tick for the window's player (a
|
||||
// server's players count theirs down in `tick_guests`).
|
||||
self.shield_cooldown = self.shield_cooldown.saturating_sub(1);
|
||||
@@ -7037,10 +7037,16 @@ impl Game<'_> {
|
||||
// Item.getAttackDamageBonus (a mace's smash), before the critical hit.
|
||||
amount += self.smash_damage_bonus(target_kind);
|
||||
let player = self.player.as_ref();
|
||||
// Player.canCriticalAttack: falling, off the ground, not sprinting.
|
||||
let falling =
|
||||
player.is_some_and(|p| !p.ground.on_ground && p.fall_distance > 0.0 && !p.sprinting);
|
||||
let crit = scale > 0.9 && falling;
|
||||
// Player.canCriticalAttack: falling, off the ground, not in water,
|
||||
// not blind (isMobilityRestricted), not riding, not sprinting.
|
||||
let falling = player.is_some_and(|p| {
|
||||
!p.ground.on_ground
|
||||
&& p.fall_distance > 0.0
|
||||
&& !p.fluids.was_touching_water
|
||||
&& !p.sprinting
|
||||
});
|
||||
let restricted = self.potions.effects.get("blindness").is_some() || self.riding.is_some();
|
||||
let crit = scale > 0.9 && falling && !restricted;
|
||||
if crit {
|
||||
amount *= 1.5;
|
||||
}
|
||||
@@ -7649,6 +7655,8 @@ impl Game<'_> {
|
||||
world.game_time = self.world.game_time;
|
||||
world.day_time = self.world.day_time;
|
||||
world.weather = self.world.weather;
|
||||
// The game rules and the difficulty hold in every dimension.
|
||||
world.health_rules = self.world.health_rules;
|
||||
// An open hub keeps hearing the new level's block changes.
|
||||
world.block_log = self.world.block_log.as_ref().map(|_| Vec::new());
|
||||
world.light_log = self.world.light_log.as_ref().map(|_| Default::default());
|
||||
|
||||
@@ -810,7 +810,10 @@ impl Game<'_> {
|
||||
None => break,
|
||||
},
|
||||
};
|
||||
if self.slot_get(slot) != item && self.slot_set(slot, item) {
|
||||
// ItemStack.isEmpty: air (or a count of 0) is an empty slot.
|
||||
// SlotAccess.set counts every slot it sets, the same item too.
|
||||
let item = item.filter(|s| &*s.item != "minecraft:air" && s.count > 0);
|
||||
if self.slot_set(slot, item) {
|
||||
changed += 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,15 @@ use super::Game;
|
||||
use horizon_client::{enchant::EffectContext, items::ItemStack};
|
||||
use horizon_core::random::RandomSource;
|
||||
|
||||
/// Where Mending found a damaged item (`EnchantedItemInUse.inSlot`).
|
||||
#[derive(Clone, Copy)]
|
||||
enum MendSlot {
|
||||
Main,
|
||||
Off,
|
||||
/// An `armor` index (head first).
|
||||
Armor(usize),
|
||||
}
|
||||
|
||||
impl Game<'_> {
|
||||
/// `Player.getDestroySpeed` after the tool speed: mining efficiency, haste
|
||||
/// and mining fatigue.
|
||||
@@ -314,36 +323,29 @@ impl Game<'_> {
|
||||
pub(super) fn mending(&mut self, amount: u32) -> u32 {
|
||||
let mut amount = amount;
|
||||
loop {
|
||||
// EnchantmentHelper.getRandomItemWith(REPAIR_WITH_XP, damaged): held and armor.
|
||||
let mut candidates: Vec<(bool, usize)> = Vec::new();
|
||||
if let Some(s) = &self.inventory[self.selected]
|
||||
&& s.damage > 0
|
||||
&& s.enchantment_level("minecraft:mending") > 0
|
||||
{
|
||||
candidates.push((false, self.selected));
|
||||
}
|
||||
for (i, s) in self.armor.iter().enumerate() {
|
||||
if let Some(s) = s
|
||||
&& s.damage > 0
|
||||
&& s.enchantment_level("minecraft:mending") > 0
|
||||
{
|
||||
candidates.push((true, i));
|
||||
}
|
||||
}
|
||||
// EnchantmentHelper.getRandomItemWith(REPAIR_WITH_XP, damaged), in
|
||||
// EquipmentSlot order: the main hand, the off hand, then feet, legs,
|
||||
// chest and head (`armor` holds head first).
|
||||
let mending = |s: &Option<ItemStack>| {
|
||||
s.as_ref()
|
||||
.is_some_and(|s| s.damage > 0 && s.enchantment_level("minecraft:mending") > 0)
|
||||
};
|
||||
let candidates: Vec<MendSlot> = [MendSlot::Main, MendSlot::Off]
|
||||
.into_iter()
|
||||
.chain((0..self.armor.len()).rev().map(MendSlot::Armor))
|
||||
.filter(|&slot| mending(self.mend_slot(slot)))
|
||||
.collect();
|
||||
if candidates.is_empty() {
|
||||
return amount;
|
||||
}
|
||||
let (armor, i) = candidates[self
|
||||
let slot = candidates[self
|
||||
.world
|
||||
.random
|
||||
.next_int_bound(candidates.len() as i32)
|
||||
.unwrap_or(0) as usize];
|
||||
let stack = if armor {
|
||||
self.armor[i].clone()
|
||||
} else {
|
||||
self.inventory[i].clone()
|
||||
let Some(stack) = self.mend_slot(slot).clone() else {
|
||||
return amount;
|
||||
};
|
||||
let Some(stack) = stack else { return amount };
|
||||
let ctx = EffectContext {
|
||||
items: Some(self.res.items()),
|
||||
..Default::default()
|
||||
@@ -356,12 +358,7 @@ impl Game<'_> {
|
||||
amount as f32,
|
||||
) as u32;
|
||||
let repaired = to_repair_xp.min(stack.damage);
|
||||
let slot = if armor {
|
||||
&mut self.armor[i]
|
||||
} else {
|
||||
&mut self.inventory[i]
|
||||
};
|
||||
if let Some(s) = slot.as_mut() {
|
||||
if let Some(s) = self.mend_slot_mut(slot).as_mut() {
|
||||
s.damage -= repaired;
|
||||
}
|
||||
if repaired == 0 || to_repair_xp == 0 {
|
||||
@@ -374,6 +371,20 @@ impl Game<'_> {
|
||||
amount = remaining;
|
||||
}
|
||||
}
|
||||
fn mend_slot(&self, slot: MendSlot) -> &Option<ItemStack> {
|
||||
match slot {
|
||||
MendSlot::Main => &self.inventory[self.selected],
|
||||
MendSlot::Off => &self.offhand,
|
||||
MendSlot::Armor(i) => &self.armor[i],
|
||||
}
|
||||
}
|
||||
fn mend_slot_mut(&mut self, slot: MendSlot) -> &mut Option<ItemStack> {
|
||||
match slot {
|
||||
MendSlot::Main => &mut self.inventory[self.selected],
|
||||
MendSlot::Off => &mut self.offhand,
|
||||
MendSlot::Armor(i) => &mut self.armor[i],
|
||||
}
|
||||
}
|
||||
/// Bow enchantments at release: (extra base damage, infinity keeps the arrow).
|
||||
pub(super) fn bow_enchantments(&mut self) -> (f64, bool) {
|
||||
let Some(bow) = self.inventory[self.selected].clone() else {
|
||||
|
||||
@@ -232,6 +232,9 @@ impl Game<'_> {
|
||||
p.world.game_time = self.world.game_time;
|
||||
p.world.day_time = self.world.day_time;
|
||||
p.world.weather = self.world.weather;
|
||||
// The game rules and the difficulty are the server's, not a
|
||||
// level's: natural regeneration holds in every dimension.
|
||||
p.world.health_rules = self.world.health_rules;
|
||||
// The sculk tables' watched blocks are built once per game.
|
||||
p.world.watched = std::mem::take(&mut self.world.watched);
|
||||
self.swap_level(&mut p);
|
||||
|
||||
@@ -297,10 +297,14 @@ impl Game<'_> {
|
||||
}
|
||||
}
|
||||
/// The local player's effects (`tick_mob_effects`), then the potions
|
||||
/// in flight, which a server without a local player still has.
|
||||
pub(super) fn tick_effects(&mut self) {
|
||||
/// in flight, which a server without a local player still has. A frozen
|
||||
/// tick (`/tick freeze`) leaves the potions where they are
|
||||
/// (`TickRateManager.isEntityFrozen`); players' effects still run.
|
||||
pub(super) fn tick_effects(&mut self, running: bool) {
|
||||
self.tick_mob_effects();
|
||||
self.tick_thrown_potions();
|
||||
if running {
|
||||
self.tick_thrown_potions();
|
||||
}
|
||||
}
|
||||
/// `LivingEntity.tickEffects` (server side) for `self.player`, the local
|
||||
/// player or a guest acting (`as_guest`), then its attribute modifiers
|
||||
|
||||
@@ -306,9 +306,18 @@ impl Game<'_> {
|
||||
}
|
||||
}
|
||||
/// `PhantomSpawner.tick`: tired players under an open night sky draw
|
||||
/// phantoms.
|
||||
/// phantoms. A custom spawner ticks only while `spawn_mobs` is on
|
||||
/// (`ServerChunkCache.tickChunks`), and this one only spawns enemies
|
||||
/// (`ServerLevel.isSpawningMonsters`: `spawn_monsters` too). Every
|
||||
/// player of the level counts: the game's own and the hosted ones (a
|
||||
/// server thread's players are all hosted).
|
||||
pub(super) fn spawn_phantoms(&mut self) {
|
||||
if !self.ui.rules.bool("spawn_phantoms") || self.dim != Dim::Overworld {
|
||||
let rules = &self.ui.rules;
|
||||
if !rules.bool("spawn_mobs")
|
||||
|| !rules.bool("spawn_monsters")
|
||||
|| !rules.bool("spawn_phantoms")
|
||||
|| self.dim != Dim::Overworld
|
||||
{
|
||||
return;
|
||||
}
|
||||
self.stats.phantom_tick -= 1;
|
||||
@@ -317,16 +326,42 @@ impl Game<'_> {
|
||||
}
|
||||
let r = &mut self.world.random;
|
||||
self.stats.phantom_tick += (60 + r.next_int_bound(60).unwrap_or(0)) * 20;
|
||||
if self.sky_darken() < 5 || self.spectator() {
|
||||
if self.sky_darken() < 5 {
|
||||
return;
|
||||
}
|
||||
let Some(p) = self.player.as_ref() else {
|
||||
return;
|
||||
};
|
||||
let rest = |s: &Stats| s.get("custom", "minecraft:time_since_rest");
|
||||
// `level.players()`: (block position, time since rest) of each player
|
||||
// who isn't a spectator.
|
||||
let mut players = Vec::new();
|
||||
if let Some(p) = self.player.as_ref()
|
||||
&& !self.spectator()
|
||||
{
|
||||
players.push((p.position, rest(&self.stats)));
|
||||
}
|
||||
if let Some(host) = self.lan.as_ref() {
|
||||
let level = self.dim.id();
|
||||
for g in host.hub.guests.iter().filter(|g| {
|
||||
g.placed
|
||||
&& g.dimension == level
|
||||
&& g.game_mode != horizon_client::lan::guest::SPECTATOR
|
||||
}) {
|
||||
let since = host.play.get(&g.conn).map_or(0, |p| rest(&p.stats));
|
||||
players.push((
|
||||
Vec3::new(g.position[0], g.position[1], g.position[2]),
|
||||
since,
|
||||
));
|
||||
}
|
||||
}
|
||||
for (position, since) in players {
|
||||
self.spawn_phantoms_near(position, since);
|
||||
}
|
||||
}
|
||||
/// One player's turn of `PhantomSpawner.tick`.
|
||||
fn spawn_phantoms_near(&mut self, position: Vec3, since_rest: i32) {
|
||||
let pos = [
|
||||
p.position.x.floor() as i32,
|
||||
p.position.y.floor() as i32,
|
||||
p.position.z.floor() as i32,
|
||||
position.x.floor() as i32,
|
||||
position.y.floor() as i32,
|
||||
position.z.floor() as i32,
|
||||
];
|
||||
if pos[1] < 63 || !self.can_see_sky(pos) {
|
||||
return;
|
||||
@@ -336,15 +371,15 @@ impl Game<'_> {
|
||||
if r.next_float() * 3.0 >= 2.0 {
|
||||
return;
|
||||
}
|
||||
let rest = self.stats.get("custom", "minecraft:time_since_rest").max(1);
|
||||
if r.next_int_bound(rest).unwrap_or(0) < 72000 {
|
||||
if r.next_int_bound(since_rest.max(1)).unwrap_or(0) < 72000 {
|
||||
return;
|
||||
}
|
||||
let at = [
|
||||
pos[0] - 10 + r.next_int_bound(21).unwrap_or(10),
|
||||
pos[1] + 20 + r.next_int_bound(15).unwrap_or(0),
|
||||
pos[2] - 10 + r.next_int_bound(21).unwrap_or(10),
|
||||
];
|
||||
// `playerPos.above(20 + nextInt(15)).east(-10 + nextInt(21)).south(...)`:
|
||||
// the height is drawn first.
|
||||
let up = 20 + r.next_int_bound(15).unwrap_or(0);
|
||||
let east = -10 + r.next_int_bound(21).unwrap_or(10);
|
||||
let south = -10 + r.next_int_bound(21).unwrap_or(10);
|
||||
let at = [pos[0] + east, pos[1] + up, pos[2] + south];
|
||||
if !self
|
||||
.res
|
||||
.facts()
|
||||
|
||||
Reference in new issue
Block a user