mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
Tidal warrior AI.
This commit is contained in:
parent
c81e1534f7
commit
ac2f097d80
@ -108,7 +108,7 @@
|
|||||||
secondary: "common.abilities.custom.wendigomagic.singlestrike",
|
secondary: "common.abilities.custom.wendigomagic.singlestrike",
|
||||||
abilities: [],
|
abilities: [],
|
||||||
),
|
),
|
||||||
Custom("Tidal Claws"): (
|
Custom("Tidal Warrior"): (
|
||||||
primary: "common.abilities.custom.tidalwarrior.pincer",
|
primary: "common.abilities.custom.tidalwarrior.pincer",
|
||||||
secondary: "common.abilities.custom.tidalwarrior.scuttle",
|
secondary: "common.abilities.custom.tidalwarrior.scuttle",
|
||||||
abilities: [
|
abilities: [
|
||||||
|
@ -10,7 +10,7 @@ BasicBeam(
|
|||||||
kind: Wet,
|
kind: Wet,
|
||||||
dur_secs: 15.0,
|
dur_secs: 15.0,
|
||||||
strength: Value(4.5),
|
strength: Value(4.5),
|
||||||
chance: 0.25,
|
chance: 1.0,
|
||||||
))),
|
))),
|
||||||
energy_regen: 0,
|
energy_regen: 0,
|
||||||
energy_drain: 0,
|
energy_drain: 0,
|
||||||
|
@ -14,7 +14,7 @@ DashMelee(
|
|||||||
charge_duration: 2.0,
|
charge_duration: 2.0,
|
||||||
swing_duration: 0.1,
|
swing_duration: 0.1,
|
||||||
recover_duration: 0.5,
|
recover_duration: 0.5,
|
||||||
charge_through: false,
|
charge_through: true,
|
||||||
is_interruptible: false,
|
is_interruptible: false,
|
||||||
damage_kind: Crushing,
|
damage_kind: Crushing,
|
||||||
)
|
)
|
||||||
|
@ -1,18 +1,14 @@
|
|||||||
BasicMelee(
|
BasicSummon(
|
||||||
energy_cost: 0,
|
buildup_duration: 0.5,
|
||||||
buildup_duration: 0.3,
|
cast_duration: 1.0,
|
||||||
swing_duration: 0.1,
|
recover_duration: 0.5,
|
||||||
recover_duration: 0.6,
|
summon_amount: 6,
|
||||||
base_damage: 50.0,
|
summon_info: (
|
||||||
base_poise_damage: 100.0,
|
// If this is still HaniwaSentry, code reviewers open a comment. I'll know what to do.
|
||||||
knockback: ( strength: 50.0, direction: Towards),
|
body: Object(HaniwaSentry),
|
||||||
range: 5.0,
|
scale: None,
|
||||||
max_angle: 60.0,
|
health_scaling: 20,
|
||||||
damage_effect: Some(Buff((
|
loadout_config: None,
|
||||||
kind: Crippled,
|
skillset_config: None,
|
||||||
dur_secs: 15.0,
|
),
|
||||||
strength: Value(0.5),
|
|
||||||
chance: 1.0,
|
|
||||||
))),
|
|
||||||
damage_kind: Slashing,
|
|
||||||
)
|
)
|
@ -15,5 +15,5 @@ ItemDef(
|
|||||||
)),
|
)),
|
||||||
quality: Low,
|
quality: Low,
|
||||||
tags: [],
|
tags: [],
|
||||||
ability_spec: Some(Custom("Tidal Claws")),
|
ability_spec: Some(Custom("Tidal Warrior")),
|
||||||
)
|
)
|
@ -235,14 +235,9 @@ impl CharacterBehavior for Data {
|
|||||||
handle_orientation(data, &mut update, 0.4 * self.static_data.ori_modifier);
|
handle_orientation(data, &mut update, 0.4 * self.static_data.ori_modifier);
|
||||||
|
|
||||||
// Forward movement
|
// Forward movement
|
||||||
handle_forced_movement(
|
handle_forced_movement(data, &mut update, ForcedMovement::Forward {
|
||||||
data,
|
|
||||||
&mut update,
|
|
||||||
ForcedMovement::Forward {
|
|
||||||
strength: self.static_data.stage_data[stage_index].forward_movement,
|
strength: self.static_data.stage_data[stage_index].forward_movement,
|
||||||
},
|
});
|
||||||
0.3,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Swings
|
// Swings
|
||||||
update.character = CharacterState::ComboMelee(Data {
|
update.character = CharacterState::ComboMelee(Data {
|
||||||
|
@ -106,14 +106,9 @@ impl CharacterBehavior for Data {
|
|||||||
.min(1.0);
|
.min(1.0);
|
||||||
|
|
||||||
handle_orientation(data, &mut update, 0.6);
|
handle_orientation(data, &mut update, 0.6);
|
||||||
handle_forced_movement(
|
handle_forced_movement(data, &mut update, ForcedMovement::Forward {
|
||||||
data,
|
|
||||||
&mut update,
|
|
||||||
ForcedMovement::Forward {
|
|
||||||
strength: self.static_data.forward_speed * charge_frac.sqrt(),
|
strength: self.static_data.forward_speed * charge_frac.sqrt(),
|
||||||
},
|
});
|
||||||
0.1,
|
|
||||||
);
|
|
||||||
|
|
||||||
// This logic basically just decides if a charge should end, and prevents the
|
// This logic basically just decides if a charge should end, and prevents the
|
||||||
// character state spamming attacks while checking if it has hit something
|
// character state spamming attacks while checking if it has hit something
|
||||||
|
@ -86,17 +86,12 @@ impl CharacterBehavior for Data {
|
|||||||
let progress = 1.0
|
let progress = 1.0
|
||||||
- self.timer.as_secs_f32()
|
- self.timer.as_secs_f32()
|
||||||
/ self.static_data.movement_duration.as_secs_f32();
|
/ self.static_data.movement_duration.as_secs_f32();
|
||||||
handle_forced_movement(
|
handle_forced_movement(data, &mut update, ForcedMovement::Leap {
|
||||||
data,
|
|
||||||
&mut update,
|
|
||||||
ForcedMovement::Leap {
|
|
||||||
vertical: self.static_data.vertical_leap_strength,
|
vertical: self.static_data.vertical_leap_strength,
|
||||||
forward: self.static_data.forward_leap_strength,
|
forward: self.static_data.forward_leap_strength,
|
||||||
progress,
|
progress,
|
||||||
direction: MovementDirection::Look,
|
direction: MovementDirection::Look,
|
||||||
},
|
});
|
||||||
0.15,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Increment duration
|
// Increment duration
|
||||||
// If we were to set a timeout for state, this would be
|
// If we were to set a timeout for state, this would be
|
||||||
|
@ -78,19 +78,14 @@ impl CharacterBehavior for Data {
|
|||||||
},
|
},
|
||||||
StageSection::Movement => {
|
StageSection::Movement => {
|
||||||
// Update velocity
|
// Update velocity
|
||||||
handle_forced_movement(
|
handle_forced_movement(data, &mut update, ForcedMovement::Forward {
|
||||||
data,
|
|
||||||
&mut update,
|
|
||||||
ForcedMovement::Forward {
|
|
||||||
strength: self.static_data.roll_strength
|
strength: self.static_data.roll_strength
|
||||||
* ((1.0
|
* ((1.0
|
||||||
- self.timer.as_secs_f32()
|
- self.timer.as_secs_f32()
|
||||||
/ self.static_data.movement_duration.as_secs_f32())
|
/ self.static_data.movement_duration.as_secs_f32())
|
||||||
/ 2.0
|
/ 2.0
|
||||||
+ 0.5),
|
+ 0.5),
|
||||||
},
|
});
|
||||||
0.0,
|
|
||||||
);
|
|
||||||
|
|
||||||
if self.timer < self.static_data.movement_duration {
|
if self.timer < self.static_data.movement_duration {
|
||||||
// Movement
|
// Movement
|
||||||
|
@ -167,14 +167,9 @@ impl CharacterBehavior for Data {
|
|||||||
self.static_data.movement_behavior,
|
self.static_data.movement_behavior,
|
||||||
MovementBehavior::ForwardGround
|
MovementBehavior::ForwardGround
|
||||||
) {
|
) {
|
||||||
handle_forced_movement(
|
handle_forced_movement(data, &mut update, ForcedMovement::Forward {
|
||||||
data,
|
|
||||||
&mut update,
|
|
||||||
ForcedMovement::Forward {
|
|
||||||
strength: self.static_data.forward_speed,
|
strength: self.static_data.forward_speed,
|
||||||
},
|
});
|
||||||
0.1,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Swings
|
// Swings
|
||||||
|
@ -263,21 +263,15 @@ fn basic_move(data: &JoinData, update: &mut StateUpdate, efficiency: f32) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Handles forced movement
|
/// Handles forced movement
|
||||||
pub fn handle_forced_movement(
|
pub fn handle_forced_movement(data: &JoinData, update: &mut StateUpdate, movement: ForcedMovement) {
|
||||||
data: &JoinData,
|
|
||||||
update: &mut StateUpdate,
|
|
||||||
movement: ForcedMovement,
|
|
||||||
efficiency: f32,
|
|
||||||
) {
|
|
||||||
let efficiency = efficiency * data.stats.move_speed_modifier * data.stats.friction_modifier;
|
|
||||||
|
|
||||||
match movement {
|
match movement {
|
||||||
ForcedMovement::Forward { strength } => {
|
ForcedMovement::Forward { strength } => {
|
||||||
|
let strength = strength * data.stats.move_speed_modifier * data.stats.friction_modifier;
|
||||||
if let Some(accel) = data.physics.on_ground.then_some(data.body.base_accel()) {
|
if let Some(accel) = data.physics.on_ground.then_some(data.body.base_accel()) {
|
||||||
update.vel.0 += Vec2::broadcast(data.dt.0)
|
update.vel.0 += Vec2::broadcast(data.dt.0)
|
||||||
* accel
|
* accel
|
||||||
* (data.inputs.move_dir + Vec2::from(update.ori) * strength)
|
* (data.inputs.move_dir + Vec2::from(update.ori))
|
||||||
* efficiency;
|
* strength;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
ForcedMovement::Leap {
|
ForcedMovement::Leap {
|
||||||
|
@ -87,6 +87,10 @@ struct AttackData {
|
|||||||
angle: f32,
|
angle: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl AttackData {
|
||||||
|
fn in_min_range(&self) -> bool { self.dist_sqrd < self.min_attack_dist.powi(2) }
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Eq, PartialEq)]
|
#[derive(Eq, PartialEq)]
|
||||||
pub enum Tactic {
|
pub enum Tactic {
|
||||||
Melee,
|
Melee,
|
||||||
@ -113,6 +117,7 @@ pub enum Tactic {
|
|||||||
BirdLargeFire,
|
BirdLargeFire,
|
||||||
Minotaur,
|
Minotaur,
|
||||||
ClayGolem,
|
ClayGolem,
|
||||||
|
TidalWarrior,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(SystemData)]
|
#[derive(SystemData)]
|
||||||
@ -1601,6 +1606,7 @@ impl<'a> AgentData<'a> {
|
|||||||
"Mindflayer" => Tactic::Mindflayer,
|
"Mindflayer" => Tactic::Mindflayer,
|
||||||
"Minotaur" => Tactic::Minotaur,
|
"Minotaur" => Tactic::Minotaur,
|
||||||
"Clay Golem" => Tactic::ClayGolem,
|
"Clay Golem" => Tactic::ClayGolem,
|
||||||
|
"Tidal Warrior" => Tactic::TidalWarrior,
|
||||||
_ => Tactic::Melee,
|
_ => Tactic::Melee,
|
||||||
},
|
},
|
||||||
AbilitySpec::Tool(tool_kind) => tool_tactic(*tool_kind),
|
AbilitySpec::Tool(tool_kind) => tool_tactic(*tool_kind),
|
||||||
@ -1843,6 +1849,13 @@ impl<'a> AgentData<'a> {
|
|||||||
&tgt_data,
|
&tgt_data,
|
||||||
&read_data,
|
&read_data,
|
||||||
),
|
),
|
||||||
|
Tactic::TidalWarrior => self.handle_tidal_warrior_attack(
|
||||||
|
agent,
|
||||||
|
controller,
|
||||||
|
&attack_data,
|
||||||
|
&tgt_data,
|
||||||
|
&read_data,
|
||||||
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1854,7 +1867,7 @@ impl<'a> AgentData<'a> {
|
|||||||
tgt_data: &TargetData,
|
tgt_data: &TargetData,
|
||||||
read_data: &ReadData,
|
read_data: &ReadData,
|
||||||
) {
|
) {
|
||||||
if attack_data.dist_sqrd < attack_data.min_attack_dist.powi(2) && attack_data.angle < 45.0 {
|
if attack_data.in_min_range() && attack_data.angle < 45.0 {
|
||||||
controller
|
controller
|
||||||
.actions
|
.actions
|
||||||
.push(ControlAction::basic_input(InputKind::Primary));
|
.push(ControlAction::basic_input(InputKind::Primary));
|
||||||
@ -1883,7 +1896,7 @@ impl<'a> AgentData<'a> {
|
|||||||
tgt_data: &TargetData,
|
tgt_data: &TargetData,
|
||||||
read_data: &ReadData,
|
read_data: &ReadData,
|
||||||
) {
|
) {
|
||||||
if attack_data.dist_sqrd < attack_data.min_attack_dist.powi(2) && attack_data.angle < 45.0 {
|
if attack_data.in_min_range() && attack_data.angle < 45.0 {
|
||||||
controller.inputs.move_dir = Vec2::zero();
|
controller.inputs.move_dir = Vec2::zero();
|
||||||
if agent.action_state.timer > 6.0 {
|
if agent.action_state.timer > 6.0 {
|
||||||
controller
|
controller
|
||||||
@ -1932,7 +1945,7 @@ impl<'a> AgentData<'a> {
|
|||||||
tgt_data: &TargetData,
|
tgt_data: &TargetData,
|
||||||
read_data: &ReadData,
|
read_data: &ReadData,
|
||||||
) {
|
) {
|
||||||
if attack_data.dist_sqrd < attack_data.min_attack_dist.powi(2) && attack_data.angle < 45.0 {
|
if attack_data.in_min_range() && attack_data.angle < 45.0 {
|
||||||
controller.inputs.move_dir = Vec2::zero();
|
controller.inputs.move_dir = Vec2::zero();
|
||||||
if agent.action_state.timer > 4.0 {
|
if agent.action_state.timer > 4.0 {
|
||||||
controller
|
controller
|
||||||
@ -2004,7 +2017,7 @@ impl<'a> AgentData<'a> {
|
|||||||
tgt_data: &TargetData,
|
tgt_data: &TargetData,
|
||||||
read_data: &ReadData,
|
read_data: &ReadData,
|
||||||
) {
|
) {
|
||||||
if attack_data.dist_sqrd < attack_data.min_attack_dist.powi(2) && attack_data.angle < 45.0 {
|
if attack_data.in_min_range() && attack_data.angle < 45.0 {
|
||||||
controller.inputs.move_dir = Vec2::zero();
|
controller.inputs.move_dir = Vec2::zero();
|
||||||
if self
|
if self
|
||||||
.skill_set
|
.skill_set
|
||||||
@ -2226,9 +2239,7 @@ impl<'a> AgentData<'a> {
|
|||||||
tgt_data: &TargetData,
|
tgt_data: &TargetData,
|
||||||
read_data: &ReadData,
|
read_data: &ReadData,
|
||||||
) {
|
) {
|
||||||
if self.body.map(|b| b.is_humanoid()).unwrap_or(false)
|
if self.body.map(|b| b.is_humanoid()).unwrap_or(false) && attack_data.in_min_range() {
|
||||||
&& attack_data.dist_sqrd < attack_data.min_attack_dist.powi(2)
|
|
||||||
{
|
|
||||||
controller
|
controller
|
||||||
.actions
|
.actions
|
||||||
.push(ControlAction::basic_input(InputKind::Roll));
|
.push(ControlAction::basic_input(InputKind::Roll));
|
||||||
@ -2325,7 +2336,7 @@ impl<'a> AgentData<'a> {
|
|||||||
tgt_data: &TargetData,
|
tgt_data: &TargetData,
|
||||||
read_data: &ReadData,
|
read_data: &ReadData,
|
||||||
) {
|
) {
|
||||||
if attack_data.dist_sqrd < attack_data.min_attack_dist.powi(2) && attack_data.angle < 90.0 {
|
if attack_data.in_min_range() && attack_data.angle < 90.0 {
|
||||||
controller.inputs.move_dir = Vec2::zero();
|
controller.inputs.move_dir = Vec2::zero();
|
||||||
controller
|
controller
|
||||||
.actions
|
.actions
|
||||||
@ -2371,8 +2382,7 @@ impl<'a> AgentData<'a> {
|
|||||||
radius: u32,
|
radius: u32,
|
||||||
circle_time: u32,
|
circle_time: u32,
|
||||||
) {
|
) {
|
||||||
if attack_data.dist_sqrd < attack_data.min_attack_dist.powi(2) && thread_rng().gen_bool(0.5)
|
if attack_data.in_min_range() && thread_rng().gen_bool(0.5) {
|
||||||
{
|
|
||||||
controller.inputs.move_dir = Vec2::zero();
|
controller.inputs.move_dir = Vec2::zero();
|
||||||
controller
|
controller
|
||||||
.actions
|
.actions
|
||||||
@ -2679,7 +2689,7 @@ impl<'a> AgentData<'a> {
|
|||||||
tgt_data: &TargetData,
|
tgt_data: &TargetData,
|
||||||
read_data: &ReadData,
|
read_data: &ReadData,
|
||||||
) {
|
) {
|
||||||
if attack_data.angle < 90.0 && attack_data.dist_sqrd < attack_data.min_attack_dist.powi(2) {
|
if attack_data.angle < 90.0 && attack_data.in_min_range() {
|
||||||
controller.inputs.move_dir = Vec2::zero();
|
controller.inputs.move_dir = Vec2::zero();
|
||||||
if agent.action_state.timer < 2.0 {
|
if agent.action_state.timer < 2.0 {
|
||||||
controller
|
controller
|
||||||
@ -2762,7 +2772,7 @@ impl<'a> AgentData<'a> {
|
|||||||
tgt_data: &TargetData,
|
tgt_data: &TargetData,
|
||||||
read_data: &ReadData,
|
read_data: &ReadData,
|
||||||
) {
|
) {
|
||||||
if attack_data.angle < 90.0 && attack_data.dist_sqrd < attack_data.min_attack_dist.powi(2) {
|
if attack_data.angle < 90.0 && attack_data.in_min_range() {
|
||||||
controller.inputs.move_dir = Vec2::zero();
|
controller.inputs.move_dir = Vec2::zero();
|
||||||
controller
|
controller
|
||||||
.actions
|
.actions
|
||||||
@ -3190,7 +3200,7 @@ impl<'a> AgentData<'a> {
|
|||||||
agent.action_state.timer += read_data.dt.0;
|
agent.action_state.timer += read_data.dt.0;
|
||||||
} else if agent.action_state.timer < 6.0
|
} else if agent.action_state.timer < 6.0
|
||||||
&& attack_data.angle < 90.0
|
&& attack_data.angle < 90.0
|
||||||
&& attack_data.dist_sqrd < attack_data.min_attack_dist.powi(2)
|
&& attack_data.in_min_range()
|
||||||
{
|
{
|
||||||
// Triplestrike
|
// Triplestrike
|
||||||
controller
|
controller
|
||||||
@ -3355,6 +3365,88 @@ impl<'a> AgentData<'a> {
|
|||||||
self.path_toward_target(agent, controller, tgt_data, read_data, true, None);
|
self.path_toward_target(agent, controller, tgt_data, read_data, true, None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn handle_tidal_warrior_attack(
|
||||||
|
&self,
|
||||||
|
agent: &mut Agent,
|
||||||
|
controller: &mut Controller,
|
||||||
|
attack_data: &AttackData,
|
||||||
|
tgt_data: &TargetData,
|
||||||
|
read_data: &ReadData,
|
||||||
|
) {
|
||||||
|
const SCUTTLE_RANGE: f32 = 40.0;
|
||||||
|
const BUBBLE_RANGE: f32 = 20.0;
|
||||||
|
const MINION_SUMMON_THRESHOLD: f32 = 0.20;
|
||||||
|
let health_fraction = self.health.map_or(0.5, |h| h.fraction());
|
||||||
|
// Sets counter at start of combat, using `condition` to keep track of whether
|
||||||
|
// it was already intitialized
|
||||||
|
if !agent.action_state.condition {
|
||||||
|
agent.action_state.counter = 1.0 - MINION_SUMMON_THRESHOLD;
|
||||||
|
agent.action_state.condition = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if agent.action_state.counter > health_fraction {
|
||||||
|
// Summon minions at particular thresholds of health
|
||||||
|
controller
|
||||||
|
.actions
|
||||||
|
.push(ControlAction::basic_input(InputKind::Ability(1)));
|
||||||
|
|
||||||
|
if matches!(self.char_state, CharacterState::BasicSummon(c) if matches!(c.stage_section, StageSection::Recover))
|
||||||
|
{
|
||||||
|
agent.action_state.counter -= MINION_SUMMON_THRESHOLD;
|
||||||
|
}
|
||||||
|
} else if attack_data.dist_sqrd < SCUTTLE_RANGE.powi(2) {
|
||||||
|
if matches!(self.char_state, CharacterState::DashMelee(c) if !matches!(c.stage_section, StageSection::Recover))
|
||||||
|
{
|
||||||
|
// Keep scuttling if already in dash melee and not in recover
|
||||||
|
controller
|
||||||
|
.actions
|
||||||
|
.push(ControlAction::basic_input(InputKind::Secondary));
|
||||||
|
} else if attack_data.dist_sqrd < BUBBLE_RANGE.powi(2) {
|
||||||
|
if matches!(self.char_state, CharacterState::BasicBeam(c) if !matches!(c.stage_section, StageSection::Recover) && c.timer < Duration::from_secs(10))
|
||||||
|
{
|
||||||
|
// Keep shooting bubbles at them if already in basic beam and not in recover and
|
||||||
|
// have not been bubbling too long
|
||||||
|
controller
|
||||||
|
.actions
|
||||||
|
.push(ControlAction::basic_input(InputKind::Ability(0)));
|
||||||
|
} else if attack_data.in_min_range() && attack_data.angle < 60.0 {
|
||||||
|
// Pincer them if they're in range and angle
|
||||||
|
controller
|
||||||
|
.actions
|
||||||
|
.push(ControlAction::basic_input(InputKind::Primary));
|
||||||
|
} else if attack_data.angle < 30.0
|
||||||
|
&& can_see_tgt(
|
||||||
|
&*read_data.terrain,
|
||||||
|
self.pos,
|
||||||
|
tgt_data.pos,
|
||||||
|
attack_data.dist_sqrd,
|
||||||
|
)
|
||||||
|
{
|
||||||
|
// Start bubbling them if not close enough to do something else and in angle and
|
||||||
|
// can see target
|
||||||
|
controller
|
||||||
|
.actions
|
||||||
|
.push(ControlAction::basic_input(InputKind::Ability(0)));
|
||||||
|
}
|
||||||
|
} else if attack_data.angle < 90.0
|
||||||
|
&& can_see_tgt(
|
||||||
|
&*read_data.terrain,
|
||||||
|
self.pos,
|
||||||
|
tgt_data.pos,
|
||||||
|
attack_data.dist_sqrd,
|
||||||
|
)
|
||||||
|
{
|
||||||
|
// Start scuttling if not close enough to do something else and in angle and can
|
||||||
|
// see target
|
||||||
|
controller
|
||||||
|
.actions
|
||||||
|
.push(ControlAction::basic_input(InputKind::Secondary));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Always attempt to path towards target
|
||||||
|
self.path_toward_target(agent, controller, tgt_data, read_data, false, None);
|
||||||
|
}
|
||||||
|
|
||||||
fn follow(
|
fn follow(
|
||||||
&self,
|
&self,
|
||||||
agent: &mut Agent,
|
agent: &mut Agent,
|
||||||
|
Loading…
Reference in New Issue
Block a user