2021-03-21 05:53:39 +00:00
|
|
|
use crate::{
|
|
|
|
comp::{
|
|
|
|
self,
|
2021-10-13 02:03:18 +00:00
|
|
|
character_state::OutputEvents,
|
2021-06-06 22:54:15 +00:00
|
|
|
inventory::loadout_builder::{self, LoadoutBuilder},
|
2021-06-03 22:42:50 +00:00
|
|
|
Behavior, BehaviorCapability, CharacterState, Projectile, StateUpdate,
|
2021-03-21 05:53:39 +00:00
|
|
|
},
|
2021-03-27 15:47:56 +00:00
|
|
|
event::{LocalEvent, ServerEvent},
|
|
|
|
outcome::Outcome,
|
2021-06-07 21:58:05 +00:00
|
|
|
skillset_builder::{self, SkillSetBuilder},
|
2021-03-21 05:53:39 +00:00
|
|
|
states::{
|
|
|
|
behavior::{CharacterBehavior, JoinData},
|
|
|
|
utils::*,
|
2021-10-05 00:15:58 +00:00
|
|
|
wielding,
|
2021-03-21 05:53:39 +00:00
|
|
|
},
|
2021-06-03 22:42:50 +00:00
|
|
|
terrain::Block,
|
|
|
|
vol::ReadVol,
|
2021-03-21 05:53:39 +00:00
|
|
|
};
|
2021-06-03 22:42:50 +00:00
|
|
|
use rand::Rng;
|
2021-03-21 05:53:39 +00:00
|
|
|
use serde::{Deserialize, Serialize};
|
2021-08-22 15:23:34 +00:00
|
|
|
use std::{f32::consts::PI, ops::Sub, time::Duration};
|
2021-06-03 22:42:50 +00:00
|
|
|
use vek::*;
|
2021-03-21 05:53:39 +00:00
|
|
|
|
|
|
|
/// Separated out to condense update portions of character state
|
|
|
|
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
|
|
|
|
pub struct StaticData {
|
|
|
|
/// How long the state builds up for
|
|
|
|
pub buildup_duration: Duration,
|
|
|
|
/// How long the state is casting for
|
|
|
|
pub cast_duration: Duration,
|
|
|
|
/// How long the state recovers for
|
|
|
|
pub recover_duration: Duration,
|
|
|
|
/// How many creatures the state should summon
|
|
|
|
pub summon_amount: u32,
|
2021-06-03 22:42:50 +00:00
|
|
|
/// Range of the summons relative to the summonner
|
|
|
|
pub summon_distance: (f32, f32),
|
2021-03-21 05:53:39 +00:00
|
|
|
/// Information about the summoned creature
|
|
|
|
pub summon_info: SummonInfo,
|
|
|
|
/// Miscellaneous information about the ability
|
|
|
|
pub ability_info: AbilityInfo,
|
2021-06-03 22:42:50 +00:00
|
|
|
/// Duration of the summoned entity
|
|
|
|
pub duration: Option<Duration>,
|
2021-03-21 05:53:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
|
|
|
|
pub struct Data {
|
|
|
|
/// Struct containing data that does not change over the course of the
|
|
|
|
/// character state
|
|
|
|
pub static_data: StaticData,
|
|
|
|
/// How many creatures have been summoned
|
|
|
|
pub summon_count: u32,
|
|
|
|
/// Timer for each stage
|
|
|
|
pub timer: Duration,
|
|
|
|
/// What section the character stage is in
|
|
|
|
pub stage_section: StageSection,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl CharacterBehavior for Data {
|
2021-10-13 02:03:18 +00:00
|
|
|
fn behavior(&self, data: &JoinData, output_events: &mut OutputEvents) -> StateUpdate {
|
2021-03-21 05:53:39 +00:00
|
|
|
let mut update = StateUpdate::from(data);
|
|
|
|
|
|
|
|
match self.stage_section {
|
|
|
|
StageSection::Buildup => {
|
|
|
|
if self.timer < self.static_data.buildup_duration {
|
|
|
|
// Build up
|
|
|
|
update.character = CharacterState::BasicSummon(Data {
|
2021-05-30 19:39:30 +00:00
|
|
|
timer: tick_attack_or_default(data, self.timer, None),
|
2021-03-21 05:53:39 +00:00
|
|
|
..*self
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
// Transitions to recover section of stage
|
|
|
|
update.character = CharacterState::BasicSummon(Data {
|
|
|
|
timer: Duration::default(),
|
2021-07-01 19:44:24 +00:00
|
|
|
stage_section: StageSection::Action,
|
2021-03-21 05:53:39 +00:00
|
|
|
..*self
|
|
|
|
});
|
|
|
|
}
|
|
|
|
},
|
2021-07-01 19:44:24 +00:00
|
|
|
StageSection::Action => {
|
2021-03-21 05:53:39 +00:00
|
|
|
if self.timer < self.static_data.cast_duration
|
|
|
|
|| self.summon_count < self.static_data.summon_amount
|
|
|
|
{
|
|
|
|
if self.timer
|
|
|
|
> self.static_data.cast_duration * self.summon_count
|
|
|
|
/ self.static_data.summon_amount
|
|
|
|
{
|
2021-06-07 21:58:05 +00:00
|
|
|
let SummonInfo {
|
|
|
|
body,
|
|
|
|
loadout_config,
|
|
|
|
skillset_config,
|
|
|
|
..
|
|
|
|
} = self.static_data.summon_info;
|
Split LodoutBuilder::build_loadout
LoadoutBuilder::build_loadout is a function which has four parameters
and 3 of them are Option<>, and although fourth (body) isn't Option<>,
it's optional too because it is used only in some combinations of
another arguments.
Because these combinations produces quirky code flow, it will be better
to split it to different methods.
So we did following changes to remove it and rewrite code that was using it
to use better methods.
* Introduce LoadoutPreset as new LoadoutConfig, currently it's only used
in Summon ability, because SummonInfo uses Copy and we can't specify
String for specifying asset path for loadout.
Everything else is rewritten to use asset path to create loadouts.
* More builder methods for LoadoutBuilder.
Namely:
- from_default which is used in server/src/cmd.rs in "/spawn" command.
- with_default_equipment, with_default_maintool to use default
loadout for specific body
- with_preset to use LoadoutPreset
* Add new make_loadout field with `fn (loadout_builder, trading_info) -> loadout_builder`
to EntityInfo which allows to lazily construct loadout without
modifying LoadoutBuilder code
* Fix Merchants not having trade site
We had heuristic that if something has Merchant LoadoutConfig - it's
merchant, which can be false, especially if we create Merchant loadout
lazily
As side note, we do same check for Guards and it fails too.
Too fix it, we introduce new agent::Mark, which explicitly specifies
kind of agent for entity
* `LoadoutBuilder::build_loadout` was written in a such way that depending
on main_tool you will have different loadout. Turns out it was this
way only for Adlets though and this behaviour is reproduced by specifying
different loadouts directly in world code.
2021-06-05 18:05:31 +00:00
|
|
|
|
2021-06-07 21:58:05 +00:00
|
|
|
let loadout = {
|
|
|
|
let loadout_builder =
|
2021-07-28 16:19:10 +00:00
|
|
|
LoadoutBuilder::empty().with_default_maintool(&body);
|
2021-06-14 10:55:25 +00:00
|
|
|
// If preset is none, use default equipment
|
2021-06-07 21:58:05 +00:00
|
|
|
if let Some(preset) = loadout_config {
|
|
|
|
loadout_builder.with_preset(preset).build()
|
|
|
|
} else {
|
2021-06-14 10:55:25 +00:00
|
|
|
loadout_builder.with_default_equipment(&body).build()
|
2021-06-07 21:58:05 +00:00
|
|
|
}
|
|
|
|
};
|
Split LodoutBuilder::build_loadout
LoadoutBuilder::build_loadout is a function which has four parameters
and 3 of them are Option<>, and although fourth (body) isn't Option<>,
it's optional too because it is used only in some combinations of
another arguments.
Because these combinations produces quirky code flow, it will be better
to split it to different methods.
So we did following changes to remove it and rewrite code that was using it
to use better methods.
* Introduce LoadoutPreset as new LoadoutConfig, currently it's only used
in Summon ability, because SummonInfo uses Copy and we can't specify
String for specifying asset path for loadout.
Everything else is rewritten to use asset path to create loadouts.
* More builder methods for LoadoutBuilder.
Namely:
- from_default which is used in server/src/cmd.rs in "/spawn" command.
- with_default_equipment, with_default_maintool to use default
loadout for specific body
- with_preset to use LoadoutPreset
* Add new make_loadout field with `fn (loadout_builder, trading_info) -> loadout_builder`
to EntityInfo which allows to lazily construct loadout without
modifying LoadoutBuilder code
* Fix Merchants not having trade site
We had heuristic that if something has Merchant LoadoutConfig - it's
merchant, which can be false, especially if we create Merchant loadout
lazily
As side note, we do same check for Guards and it fails too.
Too fix it, we introduce new agent::Mark, which explicitly specifies
kind of agent for entity
* `LoadoutBuilder::build_loadout` was written in a such way that depending
on main_tool you will have different loadout. Turns out it was this
way only for Adlets though and this behaviour is reproduced by specifying
different loadouts directly in world code.
2021-06-05 18:05:31 +00:00
|
|
|
|
2021-06-07 21:58:05 +00:00
|
|
|
let skill_set = {
|
|
|
|
let skillset_builder = SkillSetBuilder::default();
|
|
|
|
if let Some(preset) = skillset_config {
|
|
|
|
skillset_builder.with_preset(preset).build()
|
|
|
|
} else {
|
|
|
|
skillset_builder.build()
|
|
|
|
}
|
|
|
|
};
|
Split LodoutBuilder::build_loadout
LoadoutBuilder::build_loadout is a function which has four parameters
and 3 of them are Option<>, and although fourth (body) isn't Option<>,
it's optional too because it is used only in some combinations of
another arguments.
Because these combinations produces quirky code flow, it will be better
to split it to different methods.
So we did following changes to remove it and rewrite code that was using it
to use better methods.
* Introduce LoadoutPreset as new LoadoutConfig, currently it's only used
in Summon ability, because SummonInfo uses Copy and we can't specify
String for specifying asset path for loadout.
Everything else is rewritten to use asset path to create loadouts.
* More builder methods for LoadoutBuilder.
Namely:
- from_default which is used in server/src/cmd.rs in "/spawn" command.
- with_default_equipment, with_default_maintool to use default
loadout for specific body
- with_preset to use LoadoutPreset
* Add new make_loadout field with `fn (loadout_builder, trading_info) -> loadout_builder`
to EntityInfo which allows to lazily construct loadout without
modifying LoadoutBuilder code
* Fix Merchants not having trade site
We had heuristic that if something has Merchant LoadoutConfig - it's
merchant, which can be false, especially if we create Merchant loadout
lazily
As side note, we do same check for Guards and it fails too.
Too fix it, we introduce new agent::Mark, which explicitly specifies
kind of agent for entity
* `LoadoutBuilder::build_loadout` was written in a such way that depending
on main_tool you will have different loadout. Turns out it was this
way only for Adlets though and this behaviour is reproduced by specifying
different loadouts directly in world code.
2021-06-05 18:05:31 +00:00
|
|
|
|
2021-04-14 15:35:34 +00:00
|
|
|
let stats = comp::Stats::new("Summon".to_string());
|
2021-06-03 22:42:50 +00:00
|
|
|
|
|
|
|
let health_scaling = self
|
|
|
|
.static_data
|
|
|
|
.summon_info
|
|
|
|
.health_scaling
|
|
|
|
.map(|health_scaling| comp::Health::new(body, health_scaling));
|
|
|
|
|
|
|
|
// Ray cast to check where summon should happen
|
|
|
|
let summon_frac =
|
|
|
|
self.summon_count as f32 / self.static_data.summon_amount as f32;
|
|
|
|
|
|
|
|
let length = rand::thread_rng().gen_range(
|
2021-06-11 01:15:58 +00:00
|
|
|
self.static_data.summon_distance.0..=self.static_data.summon_distance.1,
|
2021-06-03 22:42:50 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
// Summon in a clockwise fashion
|
|
|
|
let ray_vector = Vec3::new(
|
|
|
|
(summon_frac * 2.0 * PI).sin() * length,
|
|
|
|
(summon_frac * 2.0 * PI).cos() * length,
|
2021-08-22 15:23:34 +00:00
|
|
|
0.0,
|
2021-06-03 22:42:50 +00:00
|
|
|
);
|
|
|
|
|
2021-08-22 15:23:34 +00:00
|
|
|
// Check for collision on the xy plane, subtract 1 to get point before block
|
2021-06-03 22:42:50 +00:00
|
|
|
let obstacle_xy = data
|
|
|
|
.terrain
|
|
|
|
.ray(data.pos.0, data.pos.0 + length * ray_vector)
|
|
|
|
.until(Block::is_solid)
|
|
|
|
.cast()
|
2021-08-22 15:23:34 +00:00
|
|
|
.0
|
|
|
|
.sub(1.0);
|
2021-06-03 22:42:50 +00:00
|
|
|
|
|
|
|
let collision_vector = Vec3::new(
|
|
|
|
data.pos.0.x + (summon_frac * 2.0 * PI).sin() * obstacle_xy,
|
|
|
|
data.pos.0.y + (summon_frac * 2.0 * PI).cos() * obstacle_xy,
|
2021-06-14 23:34:17 +00:00
|
|
|
data.pos.0.z + data.body.eye_height(),
|
2021-06-03 22:42:50 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
// Check for collision in z up to 50 blocks
|
|
|
|
let obstacle_z = data
|
|
|
|
.terrain
|
|
|
|
.ray(collision_vector, collision_vector - Vec3::unit_z() * 50.0)
|
|
|
|
.until(Block::is_solid)
|
|
|
|
.cast()
|
|
|
|
.0;
|
|
|
|
|
|
|
|
// If a duration is specified, create a projectile componenent for the npc
|
|
|
|
let projectile = self.static_data.duration.map(|duration| Projectile {
|
|
|
|
hit_solid: Vec::new(),
|
|
|
|
hit_entity: Vec::new(),
|
|
|
|
time_left: duration,
|
|
|
|
owner: Some(*data.uid),
|
|
|
|
ignore_group: true,
|
|
|
|
is_sticky: false,
|
|
|
|
is_point: false,
|
|
|
|
});
|
|
|
|
|
2021-03-27 15:47:56 +00:00
|
|
|
// Send server event to create npc
|
2021-10-13 02:03:18 +00:00
|
|
|
output_events.emit_server(ServerEvent::CreateNpc {
|
2021-06-03 22:42:50 +00:00
|
|
|
pos: comp::Pos(collision_vector - Vec3::unit_z() * obstacle_z),
|
2021-03-21 05:53:39 +00:00
|
|
|
stats,
|
2021-04-14 15:35:34 +00:00
|
|
|
skill_set,
|
2021-06-03 22:42:50 +00:00
|
|
|
health: health_scaling,
|
2021-03-21 05:53:39 +00:00
|
|
|
poise: comp::Poise::new(body),
|
|
|
|
loadout,
|
|
|
|
body,
|
2021-08-02 12:08:39 +00:00
|
|
|
agent: Some(
|
|
|
|
comp::Agent::from_body(&body)
|
|
|
|
.with_behavior(Behavior::from(BehaviorCapability::SPEAK))
|
|
|
|
.with_no_flee(true),
|
|
|
|
),
|
2021-03-23 22:41:17 +00:00
|
|
|
alignment: comp::Alignment::Owned(*data.uid),
|
2021-03-21 05:53:39 +00:00
|
|
|
scale: self
|
|
|
|
.static_data
|
|
|
|
.summon_info
|
|
|
|
.scale
|
|
|
|
.unwrap_or(comp::Scale(1.0)),
|
2021-07-28 22:36:41 +00:00
|
|
|
anchor: None,
|
2021-09-27 17:36:18 +00:00
|
|
|
loot: crate::lottery::LootSpec::Nothing,
|
2021-03-21 05:53:39 +00:00
|
|
|
rtsim_entity: None,
|
2021-06-03 22:42:50 +00:00
|
|
|
projectile,
|
2021-03-21 05:53:39 +00:00
|
|
|
});
|
|
|
|
|
2021-03-27 15:47:56 +00:00
|
|
|
// Send local event used for frontend shenanigans
|
2021-10-13 02:03:18 +00:00
|
|
|
output_events.emit_local(LocalEvent::CreateOutcome(
|
2021-03-27 15:47:56 +00:00
|
|
|
Outcome::SummonedCreature {
|
|
|
|
pos: data.pos.0,
|
|
|
|
body,
|
|
|
|
},
|
|
|
|
));
|
|
|
|
|
2021-03-21 05:53:39 +00:00
|
|
|
update.character = CharacterState::BasicSummon(Data {
|
2021-05-30 19:39:30 +00:00
|
|
|
timer: tick_attack_or_default(data, self.timer, None),
|
2021-03-21 05:53:39 +00:00
|
|
|
summon_count: self.summon_count + 1,
|
|
|
|
..*self
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
// Cast
|
|
|
|
update.character = CharacterState::BasicSummon(Data {
|
2021-05-30 19:39:30 +00:00
|
|
|
timer: tick_attack_or_default(data, self.timer, None),
|
2021-03-21 05:53:39 +00:00
|
|
|
..*self
|
|
|
|
});
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// Transitions to recover section of stage
|
|
|
|
update.character = CharacterState::BasicSummon(Data {
|
|
|
|
timer: Duration::default(),
|
|
|
|
stage_section: StageSection::Recover,
|
|
|
|
..*self
|
|
|
|
});
|
|
|
|
}
|
|
|
|
},
|
|
|
|
StageSection::Recover => {
|
|
|
|
if self.timer < self.static_data.recover_duration {
|
|
|
|
// Recovery
|
|
|
|
update.character = CharacterState::BasicSummon(Data {
|
2021-05-30 19:39:30 +00:00
|
|
|
timer: tick_attack_or_default(data, self.timer, None),
|
2021-03-21 05:53:39 +00:00
|
|
|
..*self
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
// Done
|
2021-10-05 00:15:58 +00:00
|
|
|
update.character =
|
|
|
|
CharacterState::Wielding(wielding::Data { is_sneaking: false });
|
2021-03-21 05:53:39 +00:00
|
|
|
}
|
|
|
|
},
|
|
|
|
_ => {
|
|
|
|
// If it somehow ends up in an incorrect stage section
|
2021-10-05 00:15:58 +00:00
|
|
|
update.character = CharacterState::Wielding(wielding::Data { is_sneaking: false });
|
2021-03-21 05:53:39 +00:00
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
update
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
|
|
|
|
pub struct SummonInfo {
|
|
|
|
body: comp::Body,
|
|
|
|
scale: Option<comp::Scale>,
|
2021-06-03 22:42:50 +00:00
|
|
|
health_scaling: Option<u16>,
|
2021-06-06 22:54:15 +00:00
|
|
|
// TODO: use assets for specifying skills and loadout?
|
|
|
|
loadout_config: Option<loadout_builder::Preset>,
|
2021-06-07 21:58:05 +00:00
|
|
|
skillset_config: Option<skillset_builder::Preset>,
|
2021-03-21 05:53:39 +00:00
|
|
|
}
|