Merge branch 'isse/rtsim-hp' into 'master'

Health tracking in RTSim

See merge request veloren/veloren!4644
This commit is contained in:
Isse
2024-11-13 15:47:25 +00:00
12 changed files with 157 additions and 57 deletions

View File

@ -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

View File

@ -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.
///

View File

@ -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;

View File

@ -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,

View File

@ -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);

View File

@ -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)| {

View File

@ -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) {

View File

@ -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;
}
}
}

View File

@ -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.

View File

@ -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

View File

@ -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,

View File

@ -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))),