Merge branch 'aweinstock/enable-rtsim-airships' into 'master'

Enable RtSim Airships.

See merge request veloren/veloren!1973
This commit is contained in:
Joshua Barretto 2021-03-23 14:29:43 +00:00
commit 21b20ea75e
13 changed files with 272 additions and 211 deletions

View File

@ -66,6 +66,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

@ -123,6 +123,13 @@ pub enum ServerEvent {
drop_item: Option<Item>,
rtsim_entity: Option<RtSimEntity>,
},
CreateShip {
pos: comp::Pos,
ship: comp::ship::Body,
mountable: bool,
agent: Option<comp::Agent>,
rtsim_entity: Option<RtSimEntity>,
},
CreateWaypoint(Vec3<f32>),
ClientDisconnect(EcsEntity),
ChunkRequest(EcsEntity, Vec2<i32>),

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

@ -1006,17 +1006,19 @@ fn handle_spawn_airship(
200.0,
)
});
server
let mut builder = server
.state
.create_ship(pos, comp::ship::Body::DefaultAirship, 1, destination)
.with(comp::Scale(comp::ship::AIRSHIP_SCALE))
.create_ship(pos, comp::ship::Body::DefaultAirship, true)
.with(LightEmitter {
col: Rgb::new(1.0, 0.65, 0.2),
strength: 2.0,
flicker: 1.0,
animated: true,
})
.build();
});
if let Some(pos) = destination {
builder = builder.with(comp::Agent::with_destination(pos))
}
builder.build();
server.notify_client(
client,

View File

@ -109,6 +109,25 @@ pub fn handle_create_npc(
entity.build();
}
#[allow(clippy::too_many_arguments)]
pub fn handle_create_ship(
server: &mut Server,
pos: comp::Pos,
ship: comp::ship::Body,
mountable: bool,
agent: Option<Agent>,
rtsim_entity: Option<RtSimEntity>,
) {
let mut entity = server.state.create_ship(pos, ship, mountable);
if let Some(agent) = agent {
entity = entity.with(agent);
}
if let Some(rtsim_entity) = rtsim_entity {
entity = entity.with(rtsim_entity);
}
entity.build();
}
#[allow(clippy::too_many_arguments)]
pub fn handle_shoot(
server: &mut Server,

View File

@ -2,8 +2,8 @@ use crate::{state_ext::StateExt, Server};
use common::event::{EventBus, ServerEvent};
use common_base::span;
use entity_creation::{
handle_beam, handle_create_npc, handle_create_waypoint, handle_initialize_character,
handle_loaded_character_data, handle_shockwave, handle_shoot,
handle_beam, handle_create_npc, handle_create_ship, handle_create_waypoint,
handle_initialize_character, handle_loaded_character_data, handle_shockwave, handle_shoot,
};
use entity_manipulation::{
handle_aura, handle_buff, handle_combo_change, handle_damage, handle_delete, handle_destroy,
@ -162,6 +162,13 @@ impl Server {
home_chunk,
rtsim_entity,
),
ServerEvent::CreateShip {
pos,
ship,
mountable,
agent,
rtsim_entity,
} => handle_create_ship(self, pos, ship, mountable, agent, rtsim_entity),
ServerEvent::CreateWaypoint(pos) => handle_create_waypoint(self, pos),
ServerEvent::ClientDisconnect(entity) => {
frontend_events.push(handle_client_disconnect(self, entity))

View File

@ -34,9 +34,7 @@ impl Entity {
pub fn get_body(&self) -> comp::Body {
match self.rng(PERM_GENUS).gen::<f32>() {
// we want 5% airships, 45% birds, 50% humans
// TODO: uncomment this to re-enable RtSim airships once physics is interpolated well
// in multiplayer.
//x if x < 0.05 => comp::Body::Ship(comp::ship::Body::DefaultAirship),
x if x < 0.05 => comp::Body::Ship(comp::ship::Body::DefaultAirship),
x if x < 0.50 => {
let species = *(&comp::bird_medium::ALL_SPECIES)
.choose(&mut self.rng(PERM_SPECIES))

View File

@ -2,8 +2,7 @@
use super::*;
use common::{
comp,
comp::inventory::loadout_builder::LoadoutBuilder,
comp::{self, inventory::loadout_builder::LoadoutBuilder},
event::{EventBus, ServerEvent},
resources::{DeltaTime, Time},
terrain::TerrainGrid,
@ -103,35 +102,48 @@ impl<'a> System<'a> for Sys {
.map(|e| e as f32)
+ Vec3::new(0.5, 0.5, 0.0);
let body = entity.get_body();
server_emitter.emit(ServerEvent::CreateNpc {
pos: comp::Pos(spawn_pos),
stats: comp::Stats::new(entity.get_name()),
health: comp::Health::new(body, 10),
loadout: match body {
comp::Body::Humanoid(_) => entity.get_loadout(),
_ => LoadoutBuilder::new().build(),
let pos = comp::Pos(spawn_pos);
let agent = Some(comp::Agent::new(
None,
matches!(body, comp::Body::Humanoid(_)),
None,
&body,
false,
));
let rtsim_entity = Some(RtSimEntity(id));
let event = match body {
comp::Body::Ship(ship) => ServerEvent::CreateShip {
pos,
ship,
mountable: false,
agent,
rtsim_entity,
},
poise: comp::Poise::new(body),
body,
agent: Some(comp::Agent::new(
None,
matches!(body, comp::Body::Humanoid(_)),
None,
&body,
false,
)),
alignment: match body {
comp::Body::Humanoid(_) => comp::Alignment::Npc,
_ => comp::Alignment::Wild,
_ => ServerEvent::CreateNpc {
pos: comp::Pos(spawn_pos),
stats: comp::Stats::new(entity.get_name()),
health: comp::Health::new(body, 10),
loadout: match body {
comp::Body::Humanoid(_) => entity.get_loadout(),
_ => LoadoutBuilder::new().build(),
},
poise: comp::Poise::new(body),
body,
agent,
alignment: match body {
comp::Body::Humanoid(_) => comp::Alignment::Npc,
_ => comp::Alignment::Wild,
},
scale: match body {
comp::Body::Ship(_) => comp::Scale(comp::ship::AIRSHIP_SCALE),
_ => comp::Scale(1.0),
},
drop_item: None,
home_chunk: None,
rtsim_entity,
},
scale: match body {
comp::Body::Ship(_) => comp::Scale(comp::ship::AIRSHIP_SCALE),
_ => comp::Scale(1.0),
},
drop_item: None,
home_chunk: None,
rtsim_entity: Some(RtSimEntity(id)),
});
};
server_emitter.emit(event);
}
// Update rtsim with real entity data

View File

@ -46,8 +46,7 @@ pub trait StateExt {
&mut self,
pos: comp::Pos,
ship: comp::ship::Body,
level: u16,
destination: Option<Vec3<f32>>,
mountable: bool,
) -> EcsEntityBuilder;
/// Build a projectile
fn create_projectile(
@ -231,8 +230,7 @@ impl StateExt for State {
&mut self,
pos: comp::Pos,
ship: comp::ship::Body,
level: u16,
destination: Option<Vec3<f32>>,
mountable: bool,
) -> EcsEntityBuilder {
let mut builder = self
.ecs_mut()
@ -246,19 +244,18 @@ impl StateExt for State {
})
.with(comp::Body::Ship(ship))
.with(comp::Gravity(1.0))
.with(comp::Scale(comp::ship::AIRSHIP_SCALE))
.with(comp::Controller::default())
.with(comp::inventory::Inventory::new_empty())
.with(comp::CharacterState::default())
// TODO: some of these are required in order for the character_behavior system to
// recognize a possesed airship; that system should be refactored to use `.maybe()`
.with(comp::Energy::new(ship.into(), level))
.with(comp::Health::new(ship.into(), level))
.with(comp::Energy::new(ship.into(), 0))
.with(comp::Stats::new("Airship".to_string()))
.with(comp::Buffs::default())
.with(comp::MountState::Unmounted)
.with(comp::Combo::default());
if let Some(pos) = destination {
builder = builder.with(comp::Agent::with_destination(pos))
if mountable {
builder = builder.with(comp::MountState::Unmounted);
}
builder
}

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

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