Pumpkin/crates/pumpkin/src/world/raid.rs
Alexander Medvedev 6eb02f65b9 chore: more fixes
2026-08-28 13:31:28 +02:00

746 lines
24 KiB
Rust

use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use uuid::Uuid;
use pumpkin_data::data_component_impl::EquipmentSlot;
use pumpkin_data::effect::StatusEffect;
use pumpkin_data::entity::EntityType;
use pumpkin_data::potion::Effect;
use pumpkin_data::sound::Sound;
use pumpkin_util::difficulty::Difficulty;
use pumpkin_util::math::position::BlockPos;
use pumpkin_util::math::vector3::Vector3;
use pumpkin_util::text::TextComponent;
use crate::entity::EntityBase;
use crate::entity::mob::raider::create_ominous_banner;
use crate::entity::r#type::from_type;
use crate::world::World;
use crate::world::bossbar::{Bossbar, BossbarColor, BossbarDivisions};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum RaidStatus {
Ongoing,
Victory,
Loss,
Stopped,
}
impl RaidStatus {
#[must_use]
pub const fn get_serialized_name(self) -> &'static str {
match self {
Self::Ongoing => "ongoing",
Self::Victory => "victory",
Self::Loss => "loss",
Self::Stopped => "stopped",
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum RaiderType {
Vindicator,
Evoker,
Pillager,
Witch,
Ravager,
}
impl RaiderType {
pub const VALUES: [Self; 5] = [
Self::Vindicator,
Self::Evoker,
Self::Pillager,
Self::Witch,
Self::Ravager,
];
#[must_use]
pub const fn entity_type(self) -> &'static EntityType {
match self {
Self::Vindicator => &EntityType::VINDICATOR,
Self::Evoker => &EntityType::EVOKER,
Self::Pillager => &EntityType::PILLAGER,
Self::Witch => &EntityType::WITCH,
Self::Ravager => &EntityType::RAVAGER,
}
}
#[must_use]
pub const fn spawns_per_wave(self) -> &'static [i32; 8] {
match self {
Self::Vindicator => &[0, 0, 2, 0, 1, 4, 2, 5],
Self::Evoker => &[0, 0, 0, 0, 0, 1, 1, 2],
Self::Pillager => &[0, 4, 3, 3, 4, 4, 4, 2],
Self::Witch => &[0, 0, 0, 0, 3, 0, 0, 1],
Self::Ravager => &[0, 0, 0, 1, 0, 1, 0, 2],
}
}
}
pub struct Raid {
pub id: i32,
pub center: BlockPos,
pub status: RaidStatus,
pub active: bool,
pub started: bool,
pub ticks_active: u64,
pub raid_omen_level: i32,
pub groups_spawned: i32,
pub num_groups: i32,
pub raid_cooldown_ticks: i32,
pub post_raid_ticks: i32,
pub celebration_ticks: u32,
pub total_health: f32,
pub bossbar: Bossbar,
pub group_to_leader_map: HashMap<i32, Uuid>,
pub group_raider_map: HashMap<i32, HashSet<Uuid>>,
pub heroes_of_the_village: HashSet<Uuid>,
pub players_in_raid: HashSet<Uuid>,
pub wave_spawn_pos: Option<BlockPos>,
}
impl Raid {
pub const VALID_RAID_RADIUS: f64 = 96.0;
pub const VALID_RAID_RADIUS_SQR: f64 = 9216.0;
pub const RAID_REMOVAL_THRESHOLD_SQR: f64 = 12544.0;
pub const MAX_RAID_OMEN_LEVEL: i32 = 5;
#[must_use]
pub fn new(id: i32, center: BlockPos, difficulty: Difficulty) -> Self {
let mut bossbar = Bossbar::new(TextComponent::translate("event.minecraft.raid", []));
bossbar.color = BossbarColor::Red;
bossbar.division = BossbarDivisions::Notches10;
bossbar.health = 0.0;
let num_groups = Self::get_num_groups(difficulty);
Self {
id,
center,
status: RaidStatus::Ongoing,
active: true,
started: false,
ticks_active: 0,
raid_omen_level: 1,
groups_spawned: 0,
num_groups,
raid_cooldown_ticks: 300,
post_raid_ticks: 0,
celebration_ticks: 0,
total_health: 0.0,
bossbar,
group_to_leader_map: HashMap::new(),
group_raider_map: HashMap::new(),
heroes_of_the_village: HashSet::new(),
players_in_raid: HashSet::new(),
wave_spawn_pos: None,
}
}
#[must_use]
pub const fn get_num_groups(difficulty: Difficulty) -> i32 {
match difficulty {
Difficulty::Peaceful => 0,
Difficulty::Easy => 3,
Difficulty::Normal => 5,
Difficulty::Hard => 7,
}
}
#[must_use]
pub const fn is_over(&self) -> bool {
self.is_victory() || self.is_loss()
}
#[must_use]
pub const fn is_stopped(&self) -> bool {
matches!(self.status, RaidStatus::Stopped)
}
#[must_use]
pub const fn is_victory(&self) -> bool {
matches!(self.status, RaidStatus::Victory)
}
#[must_use]
pub const fn is_loss(&self) -> bool {
matches!(self.status, RaidStatus::Loss)
}
#[must_use]
pub const fn is_started(&self) -> bool {
self.started
}
#[must_use]
pub const fn is_active(&self) -> bool {
self.active
}
#[must_use]
pub const fn get_groups_spawned(&self) -> i32 {
self.groups_spawned
}
#[must_use]
pub const fn get_raid_omen_level(&self) -> i32 {
self.raid_omen_level
}
pub fn set_raid_omen_level(&mut self, level: i32) {
self.raid_omen_level = level.clamp(0, Self::MAX_RAID_OMEN_LEVEL);
}
#[must_use]
pub const fn has_more_waves(&self) -> bool {
if self.has_bonus_wave() {
!self.has_spawned_bonus_wave()
} else {
!self.is_final_wave()
}
}
#[must_use]
pub const fn is_final_wave(&self) -> bool {
self.groups_spawned == self.num_groups
}
#[must_use]
pub const fn has_bonus_wave(&self) -> bool {
self.raid_omen_level > 1
}
#[must_use]
pub const fn has_spawned_bonus_wave(&self) -> bool {
self.groups_spawned > self.num_groups
}
#[must_use]
pub fn should_spawn_bonus_group(&self) -> bool {
self.is_final_wave() && self.get_total_raiders_alive() == 0 && self.has_bonus_wave()
}
#[must_use]
pub fn should_spawn_group(&self) -> bool {
self.raid_cooldown_ticks == 0
&& (self.groups_spawned < self.num_groups || self.should_spawn_bonus_group())
&& self.get_total_raiders_alive() == 0
}
#[must_use]
pub fn get_total_raiders_alive(&self) -> usize {
self.group_raider_map.values().map(HashSet::len).sum()
}
#[must_use]
pub fn get_all_raiders(&self) -> HashSet<Uuid> {
let mut set = HashSet::new();
for wave_set in self.group_raider_map.values() {
set.extend(wave_set);
}
set
}
pub fn stop(&mut self, world: &World) {
self.active = false;
self.status = RaidStatus::Stopped;
self.remove_all_players(world);
}
pub fn remove_all_players(&mut self, world: &World) {
let players = world.players.load();
for player in players.iter() {
if self.players_in_raid.contains(&player.gameprofile.id) {
player.remove_bossbar(self.bossbar.uuid);
}
}
self.players_in_raid.clear();
}
pub fn absorb_raid_omen(&mut self) -> bool {
self.raid_omen_level = (self.raid_omen_level + 1).clamp(1, Self::MAX_RAID_OMEN_LEVEL);
true
}
pub fn add_hero_of_the_village(&mut self, killer_uuid: Uuid) {
self.heroes_of_the_village.insert(killer_uuid);
}
const fn get_default_num_spawns(
&self,
raider_type: RaiderType,
wave: i32,
is_bonus_wave: bool,
) -> i32 {
let spawns = raider_type.spawns_per_wave();
if is_bonus_wave {
spawns[self.num_groups as usize]
} else {
spawns[wave as usize]
}
}
fn get_potential_bonus_spawns(
raider_type: RaiderType,
wave: i32,
difficulty: Difficulty,
is_bonus_wave: bool,
) -> i32 {
let is_easy = difficulty == Difficulty::Easy;
let is_normal = difficulty == Difficulty::Normal;
let bonus_spawns = match raider_type {
RaiderType::Vindicator | RaiderType::Pillager => {
if is_easy {
rand::random::<u32>() % 2
} else if is_normal {
1
} else {
2
}
}
RaiderType::Evoker => 0,
RaiderType::Witch => u32::from(!(is_easy || wave <= 2 || wave == 4)),
RaiderType::Ravager => u32::from(!is_easy && is_bonus_wave),
};
if bonus_spawns > 0 {
(rand::random::<u32>() % (bonus_spawns + 1)) as i32
} else {
0
}
}
pub fn spawn_group(&mut self, world: &Arc<World>, pos: BlockPos) {
let mut leader_set = false;
let group_number = self.groups_spawned + 1;
self.total_health = 0.0;
let is_bonus_group = self.should_spawn_bonus_group();
let difficulty = world.level_info.load().difficulty;
for raider_type in RaiderType::VALUES {
let num_spawns = self.get_default_num_spawns(raider_type, group_number, is_bonus_group)
+ Self::get_potential_bonus_spawns(
raider_type,
group_number,
difficulty,
is_bonus_group,
);
let mut ravagers_spawned = 0;
for _ in 0..num_spawns {
let uuid = Uuid::new_v4();
let spawn_pos = Vector3::new(
f64::from(pos.0.x) + 0.5,
f64::from(pos.0.y) + 1.0,
f64::from(pos.0.z) + 0.5,
);
let entity_base = from_type(raider_type.entity_type(), spawn_pos, world, uuid);
if let Some(mob) = entity_base.get_mob()
&& let Some(raider) = mob.as_raider()
{
if !leader_set && raider.can_be_leader() {
raider.set_patrol_leader(true);
let banner = create_ominous_banner();
let living = &mob.get_mob_entity().living_entity;
if let Ok(mut equipment) = living.entity_equipment.try_lock() {
equipment.put(&EquipmentSlot::HEAD, banner.clone());
}
living.send_equipment_changes(&[(EquipmentSlot::HEAD, banner)]);
self.group_to_leader_map.insert(group_number, uuid);
leader_set = true;
}
raider.set_wave(group_number);
raider.set_can_join_raid(true);
raider.apply_raid_buffs(group_number, false);
}
self.join_raid(group_number, uuid, &entity_base);
world.spawn_entity_non_save(entity_base.clone());
if *raider_type.entity_type() == EntityType::RAVAGER {
let mut riding_type: Option<&'static EntityType> = None;
if group_number == Self::get_num_groups(Difficulty::Normal) {
riding_type = Some(&EntityType::PILLAGER);
} else if group_number >= Self::get_num_groups(Difficulty::Hard) {
if ravagers_spawned == 0 {
riding_type = Some(&EntityType::EVOKER);
} else {
riding_type = Some(&EntityType::VINDICATOR);
}
}
ravagers_spawned += 1;
if let Some(riding_type) = riding_type {
let rider_uuid = Uuid::new_v4();
let rider_base = from_type(riding_type, spawn_pos, world, rider_uuid);
if let Some(mob) = rider_base.get_mob()
&& let Some(raider) = mob.as_raider()
{
raider.set_wave(group_number);
raider.set_can_join_raid(true);
raider.apply_raid_buffs(group_number, false);
}
self.join_raid(group_number, rider_uuid, &rider_base);
world.spawn_entity_non_save(rider_base);
}
}
}
}
self.wave_spawn_pos = None;
self.groups_spawned += 1;
self.update_bossbar(world);
}
pub fn join_raid(&mut self, wave: i32, uuid: Uuid, entity_base: &Arc<dyn EntityBase>) {
self.group_raider_map.entry(wave).or_default().insert(uuid);
if let Some(living) = entity_base.get_living_entity() {
self.total_health += living.health.load();
}
}
#[must_use]
pub fn find_random_spawn_pos(&self, max_tries: usize) -> Option<BlockPos> {
let seconds_remaining = self.raid_cooldown_ticks / 20;
let how_far = 0.22 * (seconds_remaining as f32) - 0.24;
let start_angle = rand::random::<f32>() * std::f32::consts::PI * 2.0;
for i in 0..max_tries {
let angle = start_angle + std::f32::consts::PI * (i as f32) / 8.0;
let spawn_x = self.center.0.x
+ ((angle.cos() * 32.0 * how_far) as i32)
+ (rand::random::<i32>().rem_euclid(3)) * (how_far as i32);
let spawn_z = self.center.0.z
+ ((angle.sin() * 32.0 * how_far) as i32)
+ (rand::random::<i32>().rem_euclid(3)) * (how_far as i32);
let spawn_y = self.center.0.y;
if (spawn_y - self.center.0.y).abs() <= 96 {
let spawn_pos = BlockPos(Vector3::new(spawn_x, spawn_y, spawn_z));
return Some(spawn_pos);
}
}
None
}
pub fn play_sound(&self, world: &World, pos: BlockPos) {
let sound_pos = pos.to_centered_f64();
world.play_sound_fine(
Sound::EventRaidHorn,
pumpkin_data::sound::SoundCategory::Neutral,
&sound_pos,
64.0,
1.0,
);
}
pub fn update_players(&mut self, world: &World) {
let center_f64 = self.center.to_f64();
let players = world.players.load();
let nearby_players: HashSet<Uuid> = players
.iter()
.filter(|player| {
let pos = player.get_entity().pos.load();
pos.squared_distance_to_vec(&center_f64) <= Self::VALID_RAID_RADIUS_SQR
})
.map(|player| player.gameprofile.id)
.collect();
for player in players.iter() {
let id = player.gameprofile.id;
let was_in = self.players_in_raid.contains(&id);
let is_in = nearby_players.contains(&id);
if !was_in && is_in {
player.send_bossbar(&self.bossbar);
self.players_in_raid.insert(id);
} else if was_in && !is_in {
player.remove_bossbar(self.bossbar.uuid);
self.players_in_raid.remove(&id);
}
}
}
pub fn update_living_raiders(&mut self, world: &World) {
let center_f64 = self.center.to_f64();
let entities = world.entities.load();
for raiders in self.group_raider_map.values_mut() {
let mut wave_dead = Vec::new();
for &raider_uuid in raiders.iter() {
let entity = entities
.iter()
.find(|e| e.get_entity().entity_uuid == raider_uuid);
match entity {
Some(e) => {
let is_dead = e
.get_living_entity()
.is_none_or(|living| living.health.load() <= 0.0);
let pos = e.get_entity().pos.load();
let dist_sq = pos.squared_distance_to_vec(&center_f64);
if is_dead || dist_sq >= Self::RAID_REMOVAL_THRESHOLD_SQR {
wave_dead.push(raider_uuid);
}
}
None => {
wave_dead.push(raider_uuid);
}
}
}
for uuid in wave_dead {
raiders.remove(&uuid);
}
}
}
pub fn update_bossbar(&mut self, world: &World) {
let living_health = self.get_health_of_living_raiders(world);
let progress = if self.total_health > 0.0 {
(living_health / self.total_health).clamp(0.0, 1.0)
} else {
0.0
};
self.bossbar.health = progress;
let players = world.players.load();
for player in players.iter() {
if self.players_in_raid.contains(&player.gameprofile.id) {
player.update_bossbar_health(&self.bossbar.uuid, self.bossbar.health);
player.update_bossbar_title(&self.bossbar.uuid, self.bossbar.title.clone());
}
}
}
#[must_use]
pub fn get_health_of_living_raiders(&self, world: &World) -> f32 {
let mut health = 0.0;
for raiders in self.group_raider_map.values() {
for &uuid in raiders {
if let Some(e) = world.get_entity_by_uuid(uuid)
&& let Some(living) = e.get_living_entity()
{
health += living.health.load();
}
}
}
health
}
pub fn tick(&mut self, world: &Arc<World>) {
if self.is_stopped() {
return;
}
if self.status == RaidStatus::Ongoing {
if world.level_info.load().difficulty == Difficulty::Peaceful {
self.stop(world);
return;
}
self.ticks_active += 1;
if self.ticks_active >= 48000 {
self.stop(world);
return;
}
let raiders_alive = self.get_total_raiders_alive();
if raiders_alive == 0 && self.has_more_waves() {
if self.raid_cooldown_ticks <= 0 {
if self.groups_spawned > 0 {
self.raid_cooldown_ticks = 300;
self.bossbar.title = TextComponent::translate("event.minecraft.raid", []);
}
} else {
if self.wave_spawn_pos.is_none() && self.raid_cooldown_ticks % 5 == 0 {
self.wave_spawn_pos = self.find_random_spawn_pos(8);
}
if self.raid_cooldown_ticks == 300 || self.raid_cooldown_ticks % 20 == 0 {
self.update_players(world);
}
self.raid_cooldown_ticks -= 1;
self.bossbar.health =
((300 - self.raid_cooldown_ticks) as f32 / 300.0).clamp(0.0, 1.0);
}
}
if self.ticks_active.is_multiple_of(20) {
self.update_players(world);
self.update_living_raiders(world);
let alive = self.get_total_raiders_alive();
if alive > 0 && alive <= 2 {
self.bossbar.title = TextComponent::translate(
"event.minecraft.raid.raiders_remaining",
[TextComponent::text(alive.to_string())],
);
} else {
self.bossbar.title = TextComponent::translate("event.minecraft.raid", []);
}
self.update_bossbar(world);
}
while self.should_spawn_group() {
let spawn_pos = self
.wave_spawn_pos
.or_else(|| self.find_random_spawn_pos(20))
.unwrap_or(self.center);
self.started = true;
self.spawn_group(world, spawn_pos);
self.play_sound(world, spawn_pos);
}
let raiders_alive = self.get_total_raiders_alive();
if self.is_started() && !self.has_more_waves() && raiders_alive == 0 {
if self.post_raid_ticks < 40 {
self.post_raid_ticks += 1;
} else {
self.status = RaidStatus::Victory;
let effect = Effect {
effect_type: &StatusEffect::HERO_OF_THE_VILLAGE,
duration: 48000,
amplifier: (self.raid_omen_level - 1).max(0) as u8,
ambient: false,
show_particles: false,
show_icon: true,
blend: true,
};
let players = world.players.load();
for player in players.iter() {
if self.heroes_of_the_village.contains(&player.gameprofile.id) {
player.add_effect(effect.clone());
}
}
}
}
} else if self.is_over() {
self.celebration_ticks += 1;
if self.celebration_ticks >= 600 {
self.stop(world);
return;
}
if self.celebration_ticks.is_multiple_of(20) {
self.update_players(world);
if self.is_victory() {
self.bossbar.health = 0.0;
self.bossbar.title =
TextComponent::translate("event.minecraft.raid.victory.full", []);
} else {
self.bossbar.title =
TextComponent::translate("event.minecraft.raid.defeat.full", []);
}
self.update_bossbar(world);
}
}
}
}
#[derive(Default)]
pub struct Raids {
pub raid_map: HashMap<i32, Raid>,
pub next_id: i32,
pub tick_counter: u64,
}
impl Raids {
#[must_use]
pub fn get(&self, raid_id: i32) -> Option<&Raid> {
self.raid_map.get(&raid_id)
}
pub fn get_mut(&mut self, raid_id: i32) -> Option<&mut Raid> {
self.raid_map.get_mut(&raid_id)
}
#[must_use]
pub fn get_raid_at(&self, pos: &BlockPos) -> Option<&Raid> {
let pos_f64 = pos.to_f64();
self.raid_map.values().find(|r| {
r.is_active()
&& r.center.to_f64().squared_distance_to_vec(&pos_f64)
<= Raid::VALID_RAID_RADIUS_SQR
})
}
pub fn get_raid_at_mut(&mut self, pos: &BlockPos) -> Option<&mut Raid> {
let pos_f64 = pos.to_f64();
self.raid_map.values_mut().find(|r| {
r.is_active()
&& r.center.to_f64().squared_distance_to_vec(&pos_f64)
<= Raid::VALID_RAID_RADIUS_SQR
})
}
#[must_use]
pub fn get_nearby_raid(&self, pos: &BlockPos, max_dist_sqr: f64) -> Option<&Raid> {
let pos_f64 = pos.to_f64();
let mut closest = None;
let mut closest_dist = max_dist_sqr;
for raid in self.raid_map.values() {
let dist = raid.center.to_f64().squared_distance_to_vec(&pos_f64);
if raid.is_active() && dist < closest_dist {
closest = Some(raid);
closest_dist = dist;
}
}
closest
}
pub fn create_or_extend_raid(
&mut self,
raid_position: BlockPos,
world: &Arc<World>,
) -> Option<i32> {
let raid_center_pos = raid_position;
let existing_id = self.raid_map.iter().find_map(|(&id, r)| {
let dist = r
.center
.to_f64()
.squared_distance_to_vec(&raid_center_pos.to_f64());
(r.is_active() && dist <= Raid::VALID_RAID_RADIUS_SQR).then_some(id)
});
if let Some(id) = existing_id {
if let Some(raid) = self.raid_map.get_mut(&id) {
raid.absorb_raid_omen();
}
Some(id)
} else {
self.next_id += 1;
let id = self.next_id;
let mut raid = Raid::new(id, raid_center_pos, world.level_info.load().difficulty);
raid.absorb_raid_omen();
self.raid_map.insert(id, raid);
Some(id)
}
}
pub fn tick(&mut self, world: &Arc<World>) {
self.tick_counter += 1;
let mut stopped_ids = Vec::new();
for (&id, raid) in &mut self.raid_map {
raid.tick(world);
if raid.is_stopped() {
stopped_ids.push(id);
}
}
for id in stopped_ids {
if let Some(mut raid) = self.raid_map.remove(&id) {
raid.remove_all_players(world);
}
}
}
}