mirror of
https://gitlab.com/veloren/veloren.git
synced 2025-07-25 21:02:31 +00:00
Merge branch 'isse/rtsim-hp' into 'master'
Health tracking in RTSim See merge request veloren/veloren!4644
This commit is contained in:
@ -47,6 +47,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Water splashes and river particles.
|
||||
- Custom furniture for Coastal Houses.
|
||||
- Airship docks for desert cities, coastal towns and mesa towns.
|
||||
- Health tracking for NPCs in unloaded chunks.
|
||||
|
||||
### Changed
|
||||
|
||||
|
@ -87,6 +87,12 @@ impl Health {
|
||||
/// Returns the fraction of health an entity has remaining
|
||||
pub fn fraction(&self) -> f32 { self.current() / self.maximum().max(1.0) }
|
||||
|
||||
/// Instantly set the health fraction.
|
||||
pub fn set_fraction(&mut self, fraction: f32) {
|
||||
self.current = (self.maximum() * fraction * Self::SCALING_FACTOR_FLOAT) as u32;
|
||||
self.is_dead = self.current == 0;
|
||||
}
|
||||
|
||||
/// Calculates a new maximum value and returns it if the value differs from
|
||||
/// the current maximum.
|
||||
///
|
||||
|
@ -111,7 +111,8 @@ pub struct Npc {
|
||||
pub role: Role,
|
||||
pub home: Option<SiteId>,
|
||||
pub faction: Option<FactionId>,
|
||||
pub is_dead: bool,
|
||||
/// The current health of the NPC, < 0.0 is dead and 1.0 is max.
|
||||
pub health_fraction: f32,
|
||||
|
||||
/// The [`Report`]s that the NPC is aware of.
|
||||
pub known_reports: HashSet<ReportId>,
|
||||
@ -153,7 +154,7 @@ impl Clone for Npc {
|
||||
role: self.role.clone(),
|
||||
home: self.home,
|
||||
faction: self.faction,
|
||||
is_dead: self.is_dead,
|
||||
health_fraction: self.health_fraction,
|
||||
known_reports: self.known_reports.clone(),
|
||||
body: self.body,
|
||||
personality: self.personality,
|
||||
@ -186,7 +187,7 @@ impl Npc {
|
||||
role,
|
||||
home: None,
|
||||
faction: None,
|
||||
is_dead: false,
|
||||
health_fraction: 1.0,
|
||||
known_reports: Default::default(),
|
||||
chunk_pos: None,
|
||||
current_site: None,
|
||||
@ -197,6 +198,8 @@ impl Npc {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_dead(&self) -> bool { self.health_fraction <= 0.0 }
|
||||
|
||||
// TODO: have a dedicated `NpcBuilder` type for this.
|
||||
pub fn with_personality(mut self, personality: Personality) -> Self {
|
||||
self.personality = personality;
|
||||
|
@ -39,6 +39,14 @@ pub struct OnDeath {
|
||||
}
|
||||
impl Event for OnDeath {}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct OnHealthChange {
|
||||
pub actor: Actor,
|
||||
pub cause: Option<Actor>,
|
||||
pub new_health_fraction: f32,
|
||||
}
|
||||
impl Event for OnHealthChange {}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct OnTheft {
|
||||
pub actor: Actor,
|
||||
|
@ -33,7 +33,7 @@ impl Rule for CleanUp {
|
||||
// TODO: Don't do this every tick, find a sensible way to gradually remove dead NPCs after they've been
|
||||
// forgotten
|
||||
data.npcs
|
||||
.retain(|npc_id, npc| if npc.is_dead {
|
||||
.retain(|npc_id, npc| if npc.is_dead() {
|
||||
// Remove NPC from home population
|
||||
if let Some(home) = npc.home.and_then(|home| data.sites.get_mut(home)) {
|
||||
home.population.remove(&npc_id);
|
||||
|
@ -253,7 +253,7 @@ impl Rule for NpcAi {
|
||||
data.npcs
|
||||
.iter_mut()
|
||||
// Don't run AI for dead NPCs
|
||||
.filter(|(_, npc)| !npc.is_dead && !matches!(npc.role, Role::Vehicle))
|
||||
.filter(|(_, npc)| !npc.is_dead() && !matches!(npc.role, Role::Vehicle))
|
||||
// Don't run AI for simulated NPCs every tick
|
||||
.filter(|(_, npc)| matches!(npc.mode, SimulationMode::Loaded) || (npc.seed as u64 + ctx.event.tick) % SIMULATED_TICK_SKIP == 0)
|
||||
.map(|(npc_id, npc)| {
|
||||
|
@ -194,12 +194,15 @@ fn on_tick(ctx: EventCtx<SimulateNpcs, OnTick>) {
|
||||
.npcs
|
||||
.npcs
|
||||
.get(link.mount)
|
||||
.filter(|mount| !mount.is_dead)
|
||||
.filter(|mount| !mount.is_dead())
|
||||
{
|
||||
let wpos = mount.wpos;
|
||||
if let Actor::Npc(rider) = link.rider {
|
||||
if let Some(rider) =
|
||||
data.npcs.npcs.get_mut(rider).filter(|rider| !rider.is_dead)
|
||||
if let Some(rider) = data
|
||||
.npcs
|
||||
.npcs
|
||||
.get_mut(rider)
|
||||
.filter(|rider| !rider.is_dead())
|
||||
{
|
||||
rider.wpos = wpos;
|
||||
mount_activity.insert(link.mount, rider.controller.activity);
|
||||
@ -213,7 +216,7 @@ fn on_tick(ctx: EventCtx<SimulateNpcs, OnTick>) {
|
||||
}
|
||||
}
|
||||
|
||||
for (npc_id, npc) in data.npcs.npcs.iter_mut().filter(|(_, npc)| !npc.is_dead) {
|
||||
for (npc_id, npc) in data.npcs.npcs.iter_mut().filter(|(_, npc)| !npc.is_dead()) {
|
||||
if matches!(npc.mode, SimulationMode::Simulated) {
|
||||
// Consume NPC actions
|
||||
for action in std::mem::take(&mut npc.controller.actions) {
|
||||
|
@ -1,5 +1,5 @@
|
||||
use crate::{
|
||||
event::{EventCtx, OnDeath, OnSetup, OnTick},
|
||||
event::{EventCtx, OnDeath, OnHealthChange, OnSetup, OnTick},
|
||||
RtState, Rule, RuleError,
|
||||
};
|
||||
use common::{
|
||||
@ -14,6 +14,7 @@ impl Rule for SyncNpcs {
|
||||
fn start(rtstate: &mut RtState) -> Result<Self, RuleError> {
|
||||
rtstate.bind::<Self, OnSetup>(on_setup);
|
||||
rtstate.bind::<Self, OnDeath>(on_death);
|
||||
rtstate.bind::<Self, OnHealthChange>(on_health_change);
|
||||
rtstate.bind::<Self, OnTick>(on_tick);
|
||||
|
||||
Ok(Self)
|
||||
@ -71,13 +72,23 @@ fn on_setup(ctx: EventCtx<SyncNpcs, OnSetup>) {
|
||||
}
|
||||
}
|
||||
|
||||
fn on_health_change(ctx: EventCtx<SyncNpcs, OnHealthChange>) {
|
||||
let data = &mut *ctx.state.data_mut();
|
||||
|
||||
if let Actor::Npc(npc_id) = ctx.event.actor {
|
||||
if let Some(npc) = data.npcs.get_mut(npc_id) {
|
||||
npc.health_fraction = ctx.event.new_health_fraction;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn on_death(ctx: EventCtx<SyncNpcs, OnDeath>) {
|
||||
let data = &mut *ctx.state.data_mut();
|
||||
|
||||
if let Actor::Npc(npc_id) = ctx.event.actor {
|
||||
if let Some(npc) = data.npcs.get_mut(npc_id) {
|
||||
// Mark the NPC as dead, allowing us to clear them up later
|
||||
npc.is_dead = true;
|
||||
npc.health_fraction = 0.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -168,35 +168,60 @@ impl ServerEvent for PoiseChangeEvent {
|
||||
}
|
||||
}
|
||||
|
||||
impl ServerEvent for HealthChangeEvent {
|
||||
type SystemData<'a> = (
|
||||
Entities<'a>,
|
||||
Read<'a, EventBus<Outcome>>,
|
||||
Read<'a, Time>,
|
||||
ReadStorage<'a, Pos>,
|
||||
ReadStorage<'a, Uid>,
|
||||
WriteStorage<'a, Agent>,
|
||||
WriteStorage<'a, Health>,
|
||||
WriteStorage<'a, Heads>,
|
||||
);
|
||||
#[cfg(feature = "worldgen")]
|
||||
pub fn entity_as_actor(
|
||||
entity: Entity,
|
||||
rtsim_entities: &ReadStorage<RtSimEntity>,
|
||||
presences: &ReadStorage<Presence>,
|
||||
) -> Option<Actor> {
|
||||
if let Some(rtsim_entity) = rtsim_entities.get(entity).copied() {
|
||||
Some(Actor::Npc(rtsim_entity.0))
|
||||
} else if let Some(PresenceKind::Character(character)) = presences.get(entity).map(|p| p.kind) {
|
||||
Some(Actor::Character(character))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn handle(
|
||||
events: impl ExactSizeIterator<Item = Self>,
|
||||
(entities, outcomes, time, positions, uids, mut agents, mut healths, mut heads): Self::SystemData<
|
||||
'_,
|
||||
>,
|
||||
) {
|
||||
let mut outcomes_emitter = outcomes.emitter();
|
||||
#[derive(SystemData)]
|
||||
pub struct HealthChangeEventData<'a> {
|
||||
entities: Entities<'a>,
|
||||
#[cfg(feature = "worldgen")]
|
||||
rtsim: WriteExpect<'a, RtSim>,
|
||||
outcomes: Read<'a, EventBus<Outcome>>,
|
||||
time: Read<'a, Time>,
|
||||
#[cfg(feature = "worldgen")]
|
||||
id_maps: Read<'a, IdMaps>,
|
||||
#[cfg(feature = "worldgen")]
|
||||
world: ReadExpect<'a, Arc<World>>,
|
||||
#[cfg(feature = "worldgen")]
|
||||
index: ReadExpect<'a, IndexOwned>,
|
||||
positions: ReadStorage<'a, Pos>,
|
||||
uids: ReadStorage<'a, Uid>,
|
||||
#[cfg(feature = "worldgen")]
|
||||
presences: ReadStorage<'a, Presence>,
|
||||
#[cfg(feature = "worldgen")]
|
||||
rtsim_entities: ReadStorage<'a, RtSimEntity>,
|
||||
agents: WriteStorage<'a, Agent>,
|
||||
healths: WriteStorage<'a, Health>,
|
||||
heads: WriteStorage<'a, Heads>,
|
||||
}
|
||||
|
||||
impl ServerEvent for HealthChangeEvent {
|
||||
type SystemData<'a> = HealthChangeEventData<'a>;
|
||||
|
||||
fn handle(events: impl ExactSizeIterator<Item = Self>, mut data: Self::SystemData<'_>) {
|
||||
let mut outcomes_emitter = data.outcomes.emitter();
|
||||
let mut rng = rand::thread_rng();
|
||||
for ev in events {
|
||||
if let Some((mut health, pos, uid, heads)) = (
|
||||
&mut healths,
|
||||
positions.maybe(),
|
||||
uids.maybe(),
|
||||
(&mut heads).maybe(),
|
||||
&mut data.healths,
|
||||
data.positions.maybe(),
|
||||
data.uids.maybe(),
|
||||
(&mut data.heads).maybe(),
|
||||
)
|
||||
.lend_join()
|
||||
.get(ev.entity, &entities)
|
||||
.get(ev.entity, &data.entities)
|
||||
{
|
||||
// If the change amount was not zero
|
||||
let changed = health.change_by(ev.change);
|
||||
@ -208,7 +233,7 @@ impl ServerEvent for HealthChangeEvent {
|
||||
if heads.amount() > 0 && ev.change.amount < 0.0 && heads.amount() > target_heads
|
||||
{
|
||||
for _ in target_heads..heads.amount() {
|
||||
if let Some(head) = heads.remove_one(&mut rng, *time) {
|
||||
if let Some(head) = heads.remove_one(&mut rng, *data.time) {
|
||||
if let Some(uid) = uid {
|
||||
outcomes_emitter.emit(Outcome::HeadLost { uid: *uid, head });
|
||||
}
|
||||
@ -219,6 +244,26 @@ impl ServerEvent for HealthChangeEvent {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "worldgen")]
|
||||
let entity_as_actor =
|
||||
|entity| entity_as_actor(entity, &data.rtsim_entities, &data.presences);
|
||||
#[cfg(feature = "worldgen")]
|
||||
if let Some(actor) = entity_as_actor(ev.entity) {
|
||||
let cause = ev
|
||||
.change
|
||||
.damage_by()
|
||||
.map(|by| by.uid())
|
||||
.and_then(|uid| data.id_maps.uid_entity(uid))
|
||||
.and_then(entity_as_actor);
|
||||
data.rtsim.hook_rtsim_actor_hp_change(
|
||||
&data.world,
|
||||
data.index.as_index_ref(),
|
||||
actor,
|
||||
cause,
|
||||
health.fraction(),
|
||||
);
|
||||
}
|
||||
|
||||
if let (Some(pos), Some(uid)) = (pos, uid) {
|
||||
if changed {
|
||||
outcomes_emitter.emit(Outcome::HealthChange {
|
||||
@ -240,7 +285,7 @@ impl ServerEvent for HealthChangeEvent {
|
||||
// TODO: Find a better way to separate direct damage from DOT here
|
||||
let damage = -ev.change.amount;
|
||||
if damage > 5.0 {
|
||||
if let Some(agent) = agents.get_mut(ev.entity) {
|
||||
if let Some(agent) = data.agents.get_mut(ev.entity) {
|
||||
agent.inbox.push_back(AgentEvent::Hurt);
|
||||
}
|
||||
}
|
||||
@ -893,22 +938,11 @@ impl ServerEvent for DestroyEvent {
|
||||
}
|
||||
|
||||
#[cfg(feature = "worldgen")]
|
||||
let entity_as_actor = |entity| {
|
||||
if let Some(rtsim_entity) = data.rtsim_entities.get(entity).copied() {
|
||||
Some(Actor::Npc(rtsim_entity.0))
|
||||
} else if let Some(PresenceKind::Character(character)) =
|
||||
data.presences.get(entity).map(|p| p.kind)
|
||||
{
|
||||
Some(Actor::Character(character))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
#[cfg(feature = "worldgen")]
|
||||
let actor = entity_as_actor(ev.entity);
|
||||
let entity_as_actor =
|
||||
|entity| entity_as_actor(entity, &data.rtsim_entities, &data.presences);
|
||||
|
||||
#[cfg(feature = "worldgen")]
|
||||
if let Some(actor) = actor
|
||||
if let Some(actor) = entity_as_actor(ev.entity)
|
||||
// Skip the death hook for rtsim entities if they aren't deleted, otherwise
|
||||
// we'll end up with rtsim respawning an entity that wasn't actually
|
||||
// removed, producing 2 entities having the same RtsimEntityId.
|
||||
|
@ -2,7 +2,7 @@ use hashbrown::HashSet;
|
||||
use rand::{seq::IteratorRandom, Rng};
|
||||
use specs::{
|
||||
join::Join, shred, DispatcherBuilder, Entities, Entity as EcsEntity, Read, ReadExpect,
|
||||
ReadStorage, SystemData, Write, WriteExpect, WriteStorage,
|
||||
ReadStorage, SystemData, Write, WriteStorage,
|
||||
};
|
||||
use tracing::{debug, error, warn};
|
||||
use vek::{Rgb, Vec3};
|
||||
@ -14,7 +14,7 @@ use common::{
|
||||
item::{self, flatten_counted_items, tool::AbilityMap, MaterialStatManifest},
|
||||
loot_owner::LootOwnerKind,
|
||||
slot::{self, Slot},
|
||||
InventoryUpdate, LootOwner, PickupItem, PresenceKind,
|
||||
InventoryUpdate, LootOwner, PickupItem,
|
||||
},
|
||||
consts::MAX_PICKUP_RANGE,
|
||||
event::{
|
||||
@ -77,11 +77,14 @@ pub struct InventoryManipData<'a> {
|
||||
events: Events<'a>,
|
||||
block_change: Write<'a, common_state::BlockChange>,
|
||||
trades: Write<'a, Trades>,
|
||||
rtsim: WriteExpect<'a, crate::rtsim::RtSim>,
|
||||
#[cfg(feature = "worldgen")]
|
||||
rtsim: specs::WriteExpect<'a, crate::rtsim::RtSim>,
|
||||
terrain: ReadExpect<'a, common::terrain::TerrainGrid>,
|
||||
id_maps: Read<'a, IdMaps>,
|
||||
time: Read<'a, Time>,
|
||||
#[cfg(feature = "worldgen")]
|
||||
world: ReadExpect<'a, std::sync::Arc<world::World>>,
|
||||
#[cfg(feature = "worldgen")]
|
||||
index: ReadExpect<'a, world::IndexOwned>,
|
||||
program_time: ReadExpect<'a, ProgramTime>,
|
||||
ability_map: ReadExpect<'a, AbilityMap>,
|
||||
@ -110,7 +113,10 @@ pub struct InventoryManipData<'a> {
|
||||
pets: ReadStorage<'a, comp::Pet>,
|
||||
velocities: ReadStorage<'a, comp::Vel>,
|
||||
masses: ReadStorage<'a, comp::Mass>,
|
||||
#[cfg(feature = "worldgen")]
|
||||
presences: ReadStorage<'a, comp::Presence>,
|
||||
#[cfg(feature = "worldgen")]
|
||||
rtsim_entities: ReadStorage<'a, common::rtsim::RtSimEntity>,
|
||||
}
|
||||
|
||||
impl ServerEvent for InventoryManipEvent {
|
||||
@ -355,9 +361,14 @@ impl ServerEvent for InventoryManipEvent {
|
||||
|
||||
if let Some(block) = block {
|
||||
if block.is_collectible() && data.block_change.can_set_block(sprite_pos) {
|
||||
// Send event to rtsim if something was stolen.
|
||||
#[cfg(feature = "worldgen")]
|
||||
if block.is_owned()
|
||||
&& let Some(PresenceKind::Character(character)) =
|
||||
data.presences.get(entity).map(|p| p.kind)
|
||||
&& let Some(actor) = super::entity_manipulation::entity_as_actor(
|
||||
entity,
|
||||
&data.rtsim_entities,
|
||||
&data.presences,
|
||||
)
|
||||
{
|
||||
data.rtsim.hook_pickup_owned_sprite(
|
||||
&data.world,
|
||||
@ -366,7 +377,7 @@ impl ServerEvent for InventoryManipEvent {
|
||||
.get_sprite()
|
||||
.expect("If the block is owned, it is a sprite"),
|
||||
sprite_pos,
|
||||
common::rtsim::Actor::Character(character),
|
||||
actor,
|
||||
);
|
||||
}
|
||||
// If an item was required to collect the sprite, consume it now
|
||||
|
@ -15,7 +15,7 @@ use crossbeam_channel::{unbounded, Receiver, Sender};
|
||||
use enum_map::EnumMap;
|
||||
use rtsim::{
|
||||
data::{npc::SimulationMode, Data, ReadError},
|
||||
event::{OnDeath, OnMountVolume, OnSetup, OnTheft},
|
||||
event::{OnDeath, OnHealthChange, OnMountVolume, OnSetup, OnTheft},
|
||||
RtState,
|
||||
};
|
||||
use specs::DispatcherBuilder;
|
||||
@ -215,6 +215,25 @@ impl RtSim {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hook_rtsim_actor_hp_change(
|
||||
&mut self,
|
||||
world: &World,
|
||||
index: IndexRef,
|
||||
actor: Actor,
|
||||
cause: Option<Actor>,
|
||||
new_hp_fraction: f32,
|
||||
) {
|
||||
self.state.emit(
|
||||
OnHealthChange {
|
||||
actor,
|
||||
cause,
|
||||
new_health_fraction: new_hp_fraction,
|
||||
},
|
||||
world,
|
||||
index,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn hook_rtsim_actor_death(
|
||||
&mut self,
|
||||
world: &World,
|
||||
|
@ -348,6 +348,10 @@ impl<'a> System<'a> for Sys {
|
||||
agent.rtsim_outbox = Some(Default::default());
|
||||
}
|
||||
|
||||
if let Some(health) = &mut npc_builder.health {
|
||||
health.set_fraction(npc.health_fraction);
|
||||
}
|
||||
|
||||
create_npc_emitter.emit(CreateNpcEvent {
|
||||
pos,
|
||||
ori: comp::Ori::from(Dir::new(npc.dir.with_z(0.0))),
|
||||
|
Reference in New Issue
Block a user