Make more systems work with an optional health component, to allow disabling health on rtsim airships (so that players can't hammer them out of the sky).

This commit is contained in:
Avi Weinstock 2021-03-22 13:37:15 -04:00 committed by Marcel Märtens
parent e2a74c5e5c
commit 49f39fb752
12 changed files with 207 additions and 176 deletions

View File

@ -64,6 +64,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- A system to add glow and reflection effects to figures (i.e: characters, armour, weapons, etc.)
- Merchants will trade wares with players
- Airships that can be mounted and flown, and also walked on (`/airship` admin command)
- RtSim airships that fly between towns.
### Changed

View File

@ -112,7 +112,7 @@ pub enum ServerEvent {
CreateNpc {
pos: comp::Pos,
stats: comp::Stats,
health: comp::Health,
health: Option<comp::Health>,
poise: comp::Poise,
loadout: comp::inventory::loadout::Loadout,
body: comp::Body,

View File

@ -80,7 +80,7 @@ pub struct JoinData<'a> {
pub dt: &'a DeltaTime,
pub controller: &'a Controller,
pub inputs: &'a ControllerInputs,
pub health: &'a Health,
pub health: Option<&'a Health>,
pub energy: &'a Energy,
pub inventory: &'a Inventory,
pub body: &'a Body,
@ -111,7 +111,7 @@ pub struct JoinStruct<'a> {
pub energy: RestrictedMut<'a, Energy>,
pub inventory: RestrictedMut<'a, Inventory>,
pub controller: &'a mut Controller,
pub health: &'a Health,
pub health: Option<&'a Health>,
pub body: &'a Body,
pub physics: &'a PhysicsState,
pub melee_attack: Option<&'a Melee>,

View File

@ -140,7 +140,7 @@ impl<'a> System<'a> for Sys {
&mut energies.restrict_mut(),
&mut inventories.restrict_mut(),
&mut controllers,
&read_data.healths,
read_data.healths.maybe(),
&read_data.bodies,
&read_data.physics_states,
&read_data.stats,
@ -149,7 +149,7 @@ impl<'a> System<'a> for Sys {
.join()
{
// Being dead overrides all other states
if health.is_dead {
if health.map_or(false, |h| h.is_dead) {
// Do nothing
continue;
}
@ -248,7 +248,7 @@ impl<'a> System<'a> for Sys {
energy,
inventory,
controller: &mut controller,
health: &health,
health,
body: &body,
physics: &physics,
melee_attack: read_data.melee_attacks.get(entity),

View File

@ -855,7 +855,7 @@ fn handle_spawn(
id,
npc::BodyType::from_body(body),
)),
comp::Health::new(body, 1),
Some(comp::Health::new(body, 1)),
comp::Poise::new(body),
inventory,
body,
@ -968,7 +968,14 @@ fn handle_spawn_training_dummy(
server
.state
.create_npc(pos, stats, health, poise, Inventory::new_empty(), body)
.create_npc(
pos,
stats,
Some(health),
poise,
Inventory::new_empty(),
body,
)
.with(comp::Vel(vel))
.with(comp::MountState::Unmounted)
.build();

View File

@ -48,7 +48,7 @@ pub fn handle_create_npc(
server: &mut Server,
pos: Pos,
stats: Stats,
health: Health,
health: Option<Health>,
poise: Poise,
loadout: Loadout,
body: Body,

View File

@ -2,8 +2,7 @@
use super::*;
use common::{
comp,
comp::inventory::loadout_builder::LoadoutBuilder,
comp::{self, inventory::loadout_builder::LoadoutBuilder, ship},
event::{EventBus, ServerEvent},
resources::{DeltaTime, Time},
terrain::TerrainGrid,
@ -106,7 +105,10 @@ impl<'a> System<'a> for Sys {
server_emitter.emit(ServerEvent::CreateNpc {
pos: comp::Pos(spawn_pos),
stats: comp::Stats::new(entity.get_name()),
health: comp::Health::new(body, 10),
health: match body {
comp::Body::Ship(ship::Body::DefaultAirship) => None,
_ => Some(comp::Health::new(body, 10)),
},
loadout: match body {
comp::Body::Humanoid(_) => entity.get_loadout(),
_ => LoadoutBuilder::new().build(),

View File

@ -35,7 +35,7 @@ pub trait StateExt {
&mut self,
pos: comp::Pos,
stats: comp::Stats,
health: comp::Health,
health: Option<comp::Health>,
poise: comp::Poise,
inventory: comp::Inventory,
body: comp::Body,
@ -162,12 +162,13 @@ impl StateExt for State {
&mut self,
pos: comp::Pos,
stats: comp::Stats,
health: comp::Health,
health: Option<comp::Health>,
poise: comp::Poise,
inventory: comp::Inventory,
body: comp::Body,
) -> EcsEntityBuilder {
self.ecs_mut()
let mut res = self
.ecs_mut()
.create_entity_synced()
.with(pos)
.with(comp::Vel(Vec3::zero()))
@ -199,9 +200,11 @@ impl StateExt for State {
.unwrap_or(None)
.unwrap_or(0),
))
.with(stats)
.with(health)
.with(poise)
.with(stats);
if let Some(health) = health {
res = res.with(health);
}
res.with(poise)
.with(comp::Alignment::Npc)
.with(comp::Gravity(1.0))
.with(comp::CharacterState::default())

View File

@ -130,7 +130,7 @@ impl<'a> System<'a> for Sys {
job.cpu_stats.measure(ParMode::Rayon);
(
&read_data.entities,
(&read_data.energies, &read_data.healths),
(&read_data.energies, read_data.healths.maybe()),
&read_data.positions,
&read_data.velocities,
&read_data.orientations,
@ -241,7 +241,7 @@ impl<'a> System<'a> for Sys {
let flees = alignment
.map(|a| !matches!(a, Alignment::Enemy | Alignment::Owned(_)))
.unwrap_or(true);
let damage = health.current() as f32 / health.maximum() as f32;
let damage = health.map_or(1.0, |h| h.current() as f32 / h.maximum() as f32);
let rtsim_entity = read_data
.rtsim_entities
.get(entity)
@ -394,21 +394,62 @@ impl<'a> System<'a> for Sys {
data.idle_tree(agent, controller, &read_data, &mut event_emitter);
}
} else {
// Target an entity that's attacking us if the attack was recent
if health.last_change.0 < DAMAGE_MEMORY_DURATION {
if let comp::HealthSource::Damage { by: Some(by), .. } =
health.last_change.1.cause
{
if let Some(attacker) =
read_data.uid_allocator.retrieve_entity_internal(by.id())
// Target an entity that's attacking us if the attack was recent and we
// have a health component
match health {
Some(health) if health.last_change.0 < DAMAGE_MEMORY_DURATION => {
if let comp::HealthSource::Damage { by: Some(by), .. } =
health.last_change.1.cause
{
if let Some(tgt_pos) = read_data.positions.get(attacker) {
// If the target is dead or in a safezone, remove the target
// and idle.
if should_stop_attacking(
read_data.healths.get(attacker),
read_data.buffs.get(attacker),
) {
if let Some(attacker) =
read_data.uid_allocator.retrieve_entity_internal(by.id())
{
if let Some(tgt_pos) = read_data.positions.get(attacker) {
// If the target is dead or in a safezone, remove the
// target
// and idle.
if should_stop_attacking(
read_data.healths.get(attacker),
read_data.buffs.get(attacker),
) {
agent.target = None;
data.idle_tree(
agent,
controller,
&read_data,
&mut event_emitter,
);
} else {
agent.target = Some(Target {
target: attacker,
hostile: true,
});
data.attack(
agent,
controller,
&read_data.terrain,
tgt_pos,
read_data.bodies.get(attacker),
&read_data.dt,
);
// Remember this encounter if an RtSim entity
if let Some(tgt_stats) =
read_data.stats.get(attacker)
{
if data.rtsim_entity.is_some() {
agent.rtsim_controller.events.push(
RtSimEvent::AddMemory(Memory {
item: MemoryItem::CharacterFight {
name: tgt_stats.name.clone(),
},
time_to_forget: read_data.time.0
+ 300.0,
}),
);
}
}
}
} else {
agent.target = None;
data.idle_tree(
agent,
@ -416,50 +457,21 @@ impl<'a> System<'a> for Sys {
&read_data,
&mut event_emitter,
);
} else {
agent.target = Some(Target {
target: attacker,
hostile: true,
});
data.attack(
agent,
controller,
&read_data.terrain,
tgt_pos,
read_data.bodies.get(attacker),
&read_data.dt,
);
// Remember this encounter if an RtSim entity
if let Some(tgt_stats) = read_data.stats.get(attacker) {
if data.rtsim_entity.is_some() {
agent.rtsim_controller.events.push(
RtSimEvent::AddMemory(Memory {
item: MemoryItem::CharacterFight {
name: tgt_stats.name.clone(),
},
time_to_forget: read_data.time.0
+ 300.0,
}),
);
}
}
}
} else {
agent.target = None;
data.idle_tree(
agent,
controller,
&read_data,
&mut event_emitter,
);
}
} else {
agent.target = None;
data.idle_tree(
agent,
controller,
&read_data,
&mut event_emitter,
);
}
} else {
agent.target = None;
},
_ => {
data.idle_tree(agent, controller, &read_data, &mut event_emitter);
}
} else {
data.idle_tree(agent, controller, &read_data, &mut event_emitter);
},
}
}

View File

@ -181,7 +181,7 @@ impl<'a> System<'a> for Sys {
server_emitter.emit(ServerEvent::CreateNpc {
pos: Pos(entity.pos),
stats,
health,
health: Some(health),
poise,
loadout,
agent: if entity.has_agency {

View File

@ -1339,7 +1339,7 @@ impl Hud {
&pos,
interpolated.maybe(),
&stats,
&healths,
healths.maybe(),
&buffs,
energy.maybe(),
scales.maybe(),
@ -1352,7 +1352,7 @@ impl Hud {
.filter(|t| {
let health = t.4;
let entity = t.0;
entity != me && !health.is_dead
entity != me && !health.map_or(false, |h| h.is_dead)
})
.filter_map(
|(
@ -1380,7 +1380,7 @@ impl Hud {
let display_overhead_info =
(info.target_entity.map_or(false, |e| e == entity)
|| info.selected_entity.map_or(false, |s| s.0 == entity)
|| overhead::should_show_healthbar(health)
|| health.map_or(true, overhead::should_show_healthbar)
|| in_group)
&& dist_sqr
< (if in_group {
@ -1400,9 +1400,9 @@ impl Hud {
health,
buffs,
energy,
combat_rating: combat::combat_rating(
inventory, health, stats, *body, &msm,
),
combat_rating: health.map_or(0.0, |health| {
combat::combat_rating(inventory, health, stats, *body, &msm)
}),
});
let bubble = if dist_sqr < SPEECH_BUBBLE_RANGE.powi(2) {
speech_bubbles.get(uid)
@ -1492,7 +1492,8 @@ impl Hud {
});
// Divide by 10 to stay in the same dimension as the HP display
let hp_dmg_rounded_abs = ((hp_damage + 5) / 10).abs();
let max_hp_frac = hp_damage.abs() as f32 / health.maximum() as f32;
let max_hp_frac =
hp_damage.abs() as f32 / health.map_or(1.0, |h| h.maximum() as f32);
let timer = floaters
.last()
.expect("There must be at least one floater")
@ -1563,8 +1564,8 @@ impl Hud {
let sct_bg_id = sct_bg_walker
.next(&mut self.ids.sct_bgs, &mut ui_widgets.widget_id_generator());
// Calculate total change
let max_hp_frac =
floater.hp_change.abs() as f32 / health.maximum() as f32;
let max_hp_frac = floater.hp_change.abs() as f32
/ health.map_or(1.0, |h| h.maximum() as f32);
// Increase font size based on fraction of maximum health
// "flashes" by having a larger size in the first 100ms
let font_size = 30

View File

@ -57,7 +57,7 @@ widget_ids! {
#[derive(Clone, Copy)]
pub struct Info<'a> {
pub name: &'a str,
pub health: &'a Health,
pub health: Option<&'a Health>,
pub buffs: &'a Buffs,
pub energy: Option<&'a Energy>,
pub combat_rating: f32,
@ -140,7 +140,7 @@ impl<'a> Ingameable for Overhead<'a> {
} else {
0
}
+ if should_show_healthbar(info.health) {
+ if info.health.map_or(false, |h| should_show_healthbar(h)) {
5 + if info.energy.is_some() { 1 } else { 0 }
} else {
0
@ -176,10 +176,11 @@ impl<'a> Widget for Overhead<'a> {
}) = self.info
{
// Used to set healthbar colours based on hp_percentage
let hp_percentage = health.current() as f64 / health.maximum() as f64 * 100.0;
let hp_percentage =
health.map_or(100.0, |h| h.current() as f64 / h.maximum() as f64 * 100.0);
// Compare levels to decide if a skull is shown
let health_current = (health.current() / 10) as f64;
let health_max = (health.maximum() / 10) as f64;
let health_current = health.map_or(1.0, |h| (h.current() / 10) as f64);
let health_max = health.map_or(1.0, |h| (h.maximum() / 10) as f64);
let name_y = if (health_current - health_max).abs() < 1e-6 {
MANA_BAR_Y + 20.0
} else {
@ -296,116 +297,120 @@ impl<'a> Widget for Overhead<'a> {
.parent(id)
.set(state.ids.name, ui);
if should_show_healthbar(health) {
// Show HP Bar
let hp_ani = (self.pulse * 4.0/* speed factor */).cos() * 0.5 + 1.0; //Animation timer
let crit_hp_color: Color = Color::Rgba(0.93, 0.59, 0.03, hp_ani);
match health {
Some(health) if should_show_healthbar(health) => {
// Show HP Bar
let hp_ani = (self.pulse * 4.0/* speed factor */).cos() * 0.5 + 1.0; //Animation timer
let crit_hp_color: Color = Color::Rgba(0.93, 0.59, 0.03, hp_ani);
// Background
Image::new(if self.in_group {self.imgs.health_bar_group_bg} else {self.imgs.enemy_health_bg})
// Background
Image::new(if self.in_group {self.imgs.health_bar_group_bg} else {self.imgs.enemy_health_bg})
.w_h(84.0 * BARSIZE, 10.0 * BARSIZE)
.x_y(0.0, MANA_BAR_Y + 6.5) //-25.5)
.color(Some(Color::Rgba(0.1, 0.1, 0.1, 0.8)))
.parent(id)
.set(state.ids.health_bar_bg, ui);
// % HP Filling
let size_factor = (hp_percentage / 100.0) * BARSIZE;
let w = if self.in_group {
82.0 * size_factor
} else {
73.0 * size_factor
};
let h = 6.0 * BARSIZE;
let x = if self.in_group {
(0.0 + (hp_percentage / 100.0 * 41.0 - 41.0)) * BARSIZE
} else {
(4.5 + (hp_percentage / 100.0 * 36.45 - 36.45)) * BARSIZE
};
Image::new(self.imgs.enemy_bar)
.w_h(w, h)
.x_y(x, MANA_BAR_Y + 8.0)
.color(if self.in_group {
// Different HP bar colors only for group members
Some(match hp_percentage {
x if (0.0..25.0).contains(&x) => crit_hp_color,
x if (25.0..50.0).contains(&x) => LOW_HP_COLOR,
_ => HP_COLOR,
})
} else {
Some(ENEMY_HP_COLOR)
})
.parent(id)
.set(state.ids.health_bar, ui);
let mut txt = format!("{}/{}", health_cur_txt, health_max_txt);
if health.is_dead {
txt = self.i18n.get("hud.group.dead").to_string()
};
Text::new(&txt)
.mid_top_with_margin_on(state.ids.health_bar_bg, 2.0)
.font_size(10)
.font_id(self.fonts.cyri.conrod_id)
.color(TEXT_COLOR)
.parent(id)
.set(state.ids.health_txt, ui);
// % Mana Filling
if let Some(energy) = energy {
let energy_factor = energy.current() as f64 / energy.maximum() as f64;
let size_factor = energy_factor * BARSIZE;
// % HP Filling
let size_factor = (hp_percentage / 100.0) * BARSIZE;
let w = if self.in_group {
80.0 * size_factor
82.0 * size_factor
} else {
72.0 * size_factor
73.0 * size_factor
};
let h = 6.0 * BARSIZE;
let x = if self.in_group {
((0.0 + (energy_factor * 40.0)) - 40.0) * BARSIZE
(0.0 + (hp_percentage / 100.0 * 41.0 - 41.0)) * BARSIZE
} else {
((3.5 + (energy_factor * 36.5)) - 36.45) * BARSIZE
(4.5 + (hp_percentage / 100.0 * 36.45 - 36.45)) * BARSIZE
};
Rectangle::fill_with([w, MANA_BAR_HEIGHT], STAMINA_COLOR)
.x_y(
x, MANA_BAR_Y, //-32.0,
)
Image::new(self.imgs.enemy_bar)
.w_h(w, h)
.x_y(x, MANA_BAR_Y + 8.0)
.color(if self.in_group {
// Different HP bar colors only for group members
Some(match hp_percentage {
x if (0.0..25.0).contains(&x) => crit_hp_color,
x if (25.0..50.0).contains(&x) => LOW_HP_COLOR,
_ => HP_COLOR,
})
} else {
Some(ENEMY_HP_COLOR)
})
.parent(id)
.set(state.ids.mana_bar, ui);
}
.set(state.ids.health_bar, ui);
let mut txt = format!("{}/{}", health_cur_txt, health_max_txt);
if health.is_dead {
txt = self.i18n.get("hud.group.dead").to_string()
};
Text::new(&txt)
.mid_top_with_margin_on(state.ids.health_bar_bg, 2.0)
.font_size(10)
.font_id(self.fonts.cyri.conrod_id)
.color(TEXT_COLOR)
.parent(id)
.set(state.ids.health_txt, ui);
// Foreground
Image::new(if self.in_group {self.imgs.health_bar_group} else {self.imgs.enemy_health})
// % Mana Filling
if let Some(energy) = energy {
let energy_factor = energy.current() as f64 / energy.maximum() as f64;
let size_factor = energy_factor * BARSIZE;
let w = if self.in_group {
80.0 * size_factor
} else {
72.0 * size_factor
};
let x = if self.in_group {
((0.0 + (energy_factor * 40.0)) - 40.0) * BARSIZE
} else {
((3.5 + (energy_factor * 36.5)) - 36.45) * BARSIZE
};
Rectangle::fill_with([w, MANA_BAR_HEIGHT], STAMINA_COLOR)
.x_y(
x, MANA_BAR_Y, //-32.0,
)
.parent(id)
.set(state.ids.mana_bar, ui);
}
// Foreground
Image::new(if self.in_group {self.imgs.health_bar_group} else {self.imgs.enemy_health})
.w_h(84.0 * BARSIZE, 10.0 * BARSIZE)
.x_y(0.0, MANA_BAR_Y + 6.5) //-25.5)
.color(Some(Color::Rgba(1.0, 1.0, 1.0, 0.99)))
.parent(id)
.set(state.ids.health_bar_fg, ui);
let indicator_col = cr_color(combat_rating);
let artifact_diffculty = 122.0;
let indicator_col = cr_color(combat_rating);
let artifact_diffculty = 122.0;
if combat_rating > artifact_diffculty && !self.in_group {
let skull_ani = ((self.pulse * 0.7/* speed factor */).cos() * 0.5 + 0.5) * 10.0; //Animation timer
Image::new(if skull_ani as i32 == 1 && rand::random::<f32>() < 0.9 {
self.imgs.skull_2
if combat_rating > artifact_diffculty && !self.in_group {
let skull_ani =
((self.pulse * 0.7/* speed factor */).cos() * 0.5 + 0.5) * 10.0; //Animation timer
Image::new(if skull_ani as i32 == 1 && rand::random::<f32>() < 0.9 {
self.imgs.skull_2
} else {
self.imgs.skull
})
.w_h(18.0 * BARSIZE, 18.0 * BARSIZE)
.x_y(-39.0 * BARSIZE, MANA_BAR_Y + 7.0)
.color(Some(Color::Rgba(1.0, 1.0, 1.0, 1.0)))
.parent(id)
.set(state.ids.level_skull, ui);
} else {
self.imgs.skull
})
.w_h(18.0 * BARSIZE, 18.0 * BARSIZE)
.x_y(-39.0 * BARSIZE, MANA_BAR_Y + 7.0)
.color(Some(Color::Rgba(1.0, 1.0, 1.0, 1.0)))
.parent(id)
.set(state.ids.level_skull, ui);
} else {
Image::new(if self.in_group {
self.imgs.nothing
} else {
self.imgs.combat_rating_ico
})
.w_h(7.0 * BARSIZE, 7.0 * BARSIZE)
.x_y(-37.0 * BARSIZE, MANA_BAR_Y + 6.0)
.color(Some(indicator_col))
.parent(id)
.set(state.ids.level, ui);
}
Image::new(if self.in_group {
self.imgs.nothing
} else {
self.imgs.combat_rating_ico
})
.w_h(7.0 * BARSIZE, 7.0 * BARSIZE)
.x_y(-37.0 * BARSIZE, MANA_BAR_Y + 6.0)
.color(Some(indicator_col))
.parent(id)
.set(state.ids.level, ui);
}
},
_ => {},
}
}
// Speech bubble