Initial mercenary hiring work

This commit is contained in:
James Melkonian 2021-05-01 02:36:26 -07:00
parent a37174d6f2
commit 2b7e9ceec0
11 changed files with 357 additions and 153 deletions

View File

@ -1,5 +1,5 @@
use crate::{
comp::{humanoid, quadruped_low, quadruped_medium, quadruped_small, Body},
comp::{chat::GenericChatMsg, humanoid, quadruped_low, quadruped_medium, quadruped_small, Body},
path::Chaser,
rtsim::RtSimController,
trade::{PendingTrade, ReducedInventory, SiteId, SitePrices, TradeId, TradeResult},
@ -8,6 +8,7 @@ use crate::{
use specs::{Component, Entity as EcsEntity};
use specs_idvs::IdvStorage;
use std::collections::VecDeque;
use hashbrown::HashMap;
use vek::*;
use super::dialogue::Subject;
@ -266,6 +267,7 @@ impl<'a> From<&'a Body> for Psyche {
#[derive(Clone, Debug)]
/// Events that affect agent behavior from other entities/players/environment
pub enum AgentEvent {
IncomingChat(GenericChatMsg<String>),
/// Engage in conversation with entity with Uid
Talk(Uid, Subject),
TradeInvite(Uid),
@ -290,6 +292,14 @@ pub struct Target {
pub selected_at: f64,
}
#[derive(Clone, Debug)]
pub enum Occupation {
Traveler,
TravelingMercenary,
Merchant,
TravelingMerchant,
}
#[derive(Clone, Debug, Default)]
pub struct Agent {
pub rtsim_controller: RtSimController,
@ -301,6 +311,19 @@ pub struct Agent {
pub inbox: VecDeque<AgentEvent>,
pub action_timer: f32,
pub bearing: Vec2<f32>,
pub offer: Option<InteractionOffer>,
}
#[derive(Clone, Debug)]
pub enum InteractionOffer {
Trade,
MercenaryHire,
FetchQuest(FetchResult),
}
#[derive(Clone, Debug)]
pub struct FetchResult {
items: HashMap<String, u32>,
}
impl Agent {

View File

@ -6,6 +6,7 @@ use specs_idvs::IdvStorage;
pub enum InviteKind {
Group,
Trade,
JoinGroup,
}
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]

View File

@ -37,6 +37,8 @@ pub enum MemoryItem {
// such as clothing worn, weapon used, etc.
CharacterInteraction { name: String },
CharacterFight { name: String },
/// A contract with another character to join their group and fight for them
MercenaryContract { name: String },
Mood { state: MoodState },
}

View File

@ -211,6 +211,27 @@ pub fn handle_invite_accept(server: &mut Server, entity: specs::Entity) {
},
);
},
InviteKind::JoinGroup => {
let mut group_manager = state.ecs().write_resource::<GroupManager>();
group_manager.add_group_member(
entity,
inviter,
&state.ecs().entities(),
&mut state.ecs().write_storage(),
&state.ecs().read_storage(),
&uids,
|entity, group_change| {
clients
.get(entity)
.and_then(|c| {
group_change
.try_map(|e| uids.get(e).copied())
.map(|g| (g, c))
})
.map(|(g, c)| c.send(ServerGeneral::GroupUpdate(g)));
},
);
},
InviteKind::Trade => {
if let (Some(inviter_uid), Some(invitee_uid)) =
(uids.get(inviter).copied(), uids.get(entity).copied())

View File

@ -32,21 +32,37 @@ fn notify_agent_prices(
entity: EcsEntity,
event: AgentEvent,
) {
if let Some((Some(site_id), agent)) = agents.get_mut(entity).map(|a| (a.behavior.trade_site, a))
{
let prices = index.get_site_prices(site_id);
if let AgentEvent::UpdatePendingTrade(boxval) = event {
// Box<(tid, pend, _, inventories)>) = event {
agent
.inbox
.push_front(AgentEvent::UpdatePendingTrade(Box::new((
// Prefer using this Agent's price data, but use the counterparty's price
// data if we don't have price data
boxval.0,
boxval.1,
prices.unwrap_or(boxval.2),
boxval.3,
))));
if let Some(agent) = agents.get_mut(entity) {
if let Some(site_id) = agent.behavior.trade_site
{
let prices = index.get_site_prices(site_id);
if let AgentEvent::UpdatePendingTrade(boxval) = event {
// Box<(tid, pend, _, inventories)>) = event {
agent
.inbox
.push_front(AgentEvent::UpdatePendingTrade(Box::new((
// Prefer using this Agent's price data, but use the counterparty's price
// data if we don't have price data
boxval.0,
boxval.1,
prices.unwrap_or(boxval.2),
boxval.3,
))));
}
} else if agent.offer.is_some() {
if let AgentEvent::UpdatePendingTrade(boxval) = event {
// Box<(tid, pend, _, inventories)>) = event {
agent
.inbox
.push_front(AgentEvent::UpdatePendingTrade(Box::new((
// Prefer using this Agent's price data, but use the counterparty's price
// data if we don't have price data
boxval.0,
boxval.1,
boxval.2,
boxval.3,
))));
}
}
}
}
@ -58,6 +74,7 @@ pub fn handle_process_trade_action(
trade_id: TradeId,
action: TradeAction,
) {
println!("trade action: {:?}", action);
if let Some(uid) = server.state.ecs().uid_from_entity(entity) {
let mut trades = server.state.ecs().write_resource::<Trades>();
if let TradeAction::Decline = action {
@ -86,8 +103,10 @@ pub fn handle_process_trade_action(
trades.process_trade_action(trade_id, uid, action, get_inventory);
}
if let Entry::Occupied(entry) = trades.trades.entry(trade_id) {
println!("trade entry occupied: {:?}", entry);
let parties = entry.get().parties;
if entry.get().should_commit() {
println!("trade should be committed");
let result = commit_trade(server.state.ecs(), entry.get());
entry.remove();
for party in parties.iter() {
@ -101,6 +120,7 @@ pub fn handle_process_trade_action(
}
}
} else {
println!("trade is not ready to be committed");
let mut entities: [Option<specs::Entity>; 2] = [None, None];
let mut inventories: [Option<ReducedInventory>; 2] = [None, None];
let mut prices = None;
@ -132,6 +152,7 @@ pub fn handle_process_trade_action(
drop(agents);
for party in entities.iter() {
if let Some(e) = *party {
println!("sending trade update");
server.notify_client(
e,
ServerGeneral::UpdatePendingTrade(

View File

@ -1,6 +1,6 @@
use super::*;
use common::{
comp::inventory::loadout_builder::LoadoutBuilder,
comp::{agent::Occupation, inventory::loadout_builder::LoadoutBuilder},
resources::Time,
rtsim::{Memory, MemoryItem},
store::Id,
@ -23,6 +23,7 @@ pub struct Entity {
pub controller: RtSimController,
pub brain: Brain,
pub occupation: Option<Occupation>,
}
const PERM_SPECIES: u32 = 0;
@ -30,6 +31,7 @@ const PERM_BODY: u32 = 1;
const PERM_LOADOUT: u32 = 2;
const PERM_LEVEL: u32 = 3;
const PERM_GENUS: u32 = 4;
const PERM_OCCUPATION: u32 = 5;
impl Entity {
pub fn rng(&self, perm: u32) -> impl Rng { RandomPerm::new(self.seed + perm) }
@ -59,6 +61,18 @@ impl Entity {
}
}
pub fn get_occupation(&self) -> Option<Occupation> {
// If the body is humanoid
if self.rng(PERM_GENUS).gen::<f32>() > 0.50 {
match self.rng(PERM_OCCUPATION).gen::<f32>() {
x if x < 0.5 => Some(Occupation::TravelingMercenary),
_ => Some(Occupation::Traveler),
}
} else {
None
}
}
pub fn get_name(&self) -> String {
use common::{generation::get_npc_name, npc::NPC_NAMES};
let npc_names = NPC_NAMES.read();
@ -120,9 +134,15 @@ impl Entity {
)),
};
let chest = Some(comp::Item::new_from_asset_expect(
let chest = if matches!(self.get_occupation(), Some(Occupation::TravelingMercenary)) {
Some(comp::Item::new_from_asset_expect(
"common.items.armor.plate.chest",
))
} else {
Some(comp::Item::new_from_asset_expect(
"common.items.npc_armor.chest.leather_blue",
));
))
};
let pants = Some(comp::Item::new_from_asset_expect(
"common.items.npc_armor.pants.leather_blue",
));

View File

@ -120,6 +120,7 @@ pub fn init(state: &mut State, #[cfg(feature = "worldgen")] world: &world::World
controller: RtSimController::default(),
last_tick: 0,
brain: Default::default(),
occupation: None,
});
}

View File

@ -7,6 +7,7 @@ use common::{
combat,
comp::{
self,
Agent, agent::AgentEvent,
skills::{GeneralSkill, Skill},
Group, Inventory,
},
@ -581,6 +582,11 @@ impl StateExt for State {
client.send_fallible(ServerGeneral::ChatMsg(resolved_msg.clone()));
}
}
for (agent, pos) in (&mut ecs.write_storage::<Agent>(), &positions).join() {
if is_within(comp::ChatMsg::SAY_DISTANCE, pos, speaker_pos) {
agent.inbox.push_front(AgentEvent::IncomingChat(resolved_msg.clone()));
}
}
}
},
comp::ChatType::Region(uid) => {

View File

@ -2,10 +2,11 @@ use crate::rtsim::{Entity as RtSimData, RtSim};
use common::{
comp::{
self,
agent::{AgentEvent, Tactic, Target, DEFAULT_INTERACTION_TIME, TRADE_INTERACTION_TIME},
agent::{InteractionOffer, Occupation, AgentEvent, Tactic, Target, DEFAULT_INTERACTION_TIME, TRADE_INTERACTION_TIME},
buff::{BuffKind, Buffs},
compass::{Direction, Distance},
dialogue::{MoodContext, MoodState, Subject},
chat::ChatType,
group,
inventory::{item::ItemTag, slot::EquipSlot},
invite::{InviteKind, InviteResponse},
@ -562,6 +563,7 @@ impl<'a> AgentData<'a> {
{
self.interact(agent, controller, &read_data, event_emitter);
} else {
agent.offer = None;
agent.action_timer = 0.0;
agent.target = None;
controller.actions.push(ControlAction::Stand);
@ -710,119 +712,121 @@ impl<'a> AgentData<'a> {
}
agent.action_timer = 0.0;
if let Some((travel_to, _destination)) = &agent.rtsim_controller.travel_to {
// if it has an rtsim destination and can fly then it should
// if it is flying and bumps something above it then it should move down
if self.traversal_config.can_fly
&& !read_data
.terrain
.ray(self.pos.0, self.pos.0 + (Vec3::unit_z() * 3.0))
.until(Block::is_solid)
.cast()
.1
.map_or(true, |b| b.is_some())
{
controller
.actions
.push(ControlAction::basic_input(InputKind::Fly));
} else {
controller
.actions
.push(ControlAction::CancelInput(InputKind::Fly))
}
if let Some((bearing, speed)) = agent.chaser.chase(
&*read_data.terrain,
self.pos.0,
self.vel.0,
*travel_to,
TraversalConfig {
min_tgt_dist: 1.25,
..self.traversal_config
},
) {
controller.inputs.move_dir =
bearing.xy().try_normalized().unwrap_or_else(Vec2::zero)
* speed.min(agent.rtsim_controller.speed_factor);
self.jump_if(controller, bearing.z > 1.5 || self.traversal_config.can_fly);
controller.inputs.climb = Some(comp::Climb::Up);
//.filter(|_| bearing.z > 0.1 || self.physics_state.in_liquid().is_some());
controller.inputs.move_z = bearing.z
+ if self.traversal_config.can_fly {
// NOTE: costs 4 us (imbris)
let obstacle_ahead = read_data
.terrain
.ray(
self.pos.0 + Vec3::unit_z(),
self.pos.0
+ bearing.try_normalized().unwrap_or_else(Vec3::unit_y) * 80.0
+ Vec3::unit_z(),
)
.until(Block::is_solid)
.cast()
.1
.map_or(true, |b| b.is_some());
let mut ground_too_close = self
.body
.map(|body| {
#[cfg(feature = "worldgen")]
let height_approx = self.pos.0.y
- read_data
.world
.sim()
.get_alt_approx(self.pos.0.xy().map(|x: f32| x as i32))
.unwrap_or(0.0);
#[cfg(not(feature = "worldgen"))]
let height_approx = self.pos.0.y;
height_approx < body.flying_height()
})
.unwrap_or(false);
const NUM_RAYS: usize = 5;
// NOTE: costs 15-20 us (imbris)
for i in 0..=NUM_RAYS {
let magnitude = self.body.map_or(20.0, |b| b.flying_height());
// Lerp between a line straight ahead and straight down to detect a
// wedge of obstacles we might fly into (inclusive so that both vectors
// are sampled)
if let Some(dir) = Lerp::lerp(
-Vec3::unit_z(),
Vec3::new(bearing.x, bearing.y, 0.0),
i as f32 / NUM_RAYS as f32,
)
.try_normalized()
{
ground_too_close |= read_data
.terrain
.ray(self.pos.0, self.pos.0 + magnitude * dir)
.until(|b: &Block| b.is_solid() || b.is_liquid())
.cast()
.1
.map_or(false, |b| b.is_some())
}
}
if obstacle_ahead || ground_too_close {
1.0 //fly up when approaching obstacles
} else {
-0.1
} //flying things should slowly come down from the stratosphere
} else {
0.05 //normal land traveller offset
};
// Put away weapon
if thread_rng().gen_bool(0.1)
&& matches!(
read_data.char_states.get(*self.entity),
Some(CharacterState::Wielding)
)
if agent.rtsim_controller.travel_to.is_some() && !matches!(self.alignment, Some(Alignment::Owned(_))) {
if let Some((travel_to, _destination)) = &agent.rtsim_controller.travel_to {
// if it has an rtsim destination and can fly then it should
// if it is flying and bumps something above it then it should move down
if self.traversal_config.can_fly
&& !read_data
.terrain
.ray(self.pos.0, self.pos.0 + (Vec3::unit_z() * 3.0))
.until(Block::is_solid)
.cast()
.1
.map_or(true, |b| b.is_some())
{
controller.actions.push(ControlAction::Unwield);
controller
.actions
.push(ControlAction::basic_input(InputKind::Fly));
} else {
controller
.actions
.push(ControlAction::CancelInput(InputKind::Fly))
}
if let Some((bearing, speed)) = agent.chaser.chase(
&*read_data.terrain,
self.pos.0,
self.vel.0,
*travel_to,
TraversalConfig {
min_tgt_dist: 1.25,
..self.traversal_config
},
) {
controller.inputs.move_dir =
bearing.xy().try_normalized().unwrap_or_else(Vec2::zero)
* speed.min(agent.rtsim_controller.speed_factor);
self.jump_if(controller, bearing.z > 1.5 || self.traversal_config.can_fly);
controller.inputs.climb = Some(comp::Climb::Up);
//.filter(|_| bearing.z > 0.1 || self.physics_state.in_liquid().is_some());
controller.inputs.move_z = bearing.z
+ if self.traversal_config.can_fly {
// NOTE: costs 4 us (imbris)
let obstacle_ahead = read_data
.terrain
.ray(
self.pos.0 + Vec3::unit_z(),
self.pos.0
+ bearing.try_normalized().unwrap_or_else(Vec3::unit_y) * 80.0
+ Vec3::unit_z(),
)
.until(Block::is_solid)
.cast()
.1
.map_or(true, |b| b.is_some());
let mut ground_too_close = self
.body
.map(|body| {
#[cfg(feature = "worldgen")]
let height_approx = self.pos.0.y
- read_data
.world
.sim()
.get_alt_approx(self.pos.0.xy().map(|x: f32| x as i32))
.unwrap_or(0.0);
#[cfg(not(feature = "worldgen"))]
let height_approx = self.pos.0.y;
height_approx < body.flying_height()
})
.unwrap_or(false);
const NUM_RAYS: usize = 5;
// NOTE: costs 15-20 us (imbris)
for i in 0..=NUM_RAYS {
let magnitude = self.body.map_or(20.0, |b| b.flying_height());
// Lerp between a line straight ahead and straight down to detect a
// wedge of obstacles we might fly into (inclusive so that both vectors
// are sampled)
if let Some(dir) = Lerp::lerp(
-Vec3::unit_z(),
Vec3::new(bearing.x, bearing.y, 0.0),
i as f32 / NUM_RAYS as f32,
)
.try_normalized()
{
ground_too_close |= read_data
.terrain
.ray(self.pos.0, self.pos.0 + magnitude * dir)
.until(|b: &Block| b.is_solid() || b.is_liquid())
.cast()
.1
.map_or(false, |b| b.is_some())
}
}
if obstacle_ahead || ground_too_close {
1.0 //fly up when approaching obstacles
} else {
-0.1
} //flying things should slowly come down from the stratosphere
} else {
0.05 //normal land traveller offset
};
// Put away weapon
if thread_rng().gen_bool(0.1)
&& matches!(
read_data.char_states.get(*self.entity),
Some(CharacterState::Wielding)
)
{
controller.actions.push(ControlAction::Unwield);
}
}
}
} else {
@ -860,7 +864,11 @@ impl<'a> AgentData<'a> {
};
if agent.bearing.magnitude_squared() > 0.5f32.powi(2) {
controller.inputs.move_dir = agent.bearing * 0.65;
if matches!(Some(Alignment::Owned(_)), data.alignment) {
controller.inputs.move_dir = agent.bearing * 0.30;
} else {
controller.inputs.move_dir = agent.bearing * 0.65;
}
}
// Put away weapon
@ -901,9 +909,71 @@ impl<'a> AgentData<'a> {
// .events
// .push(ControlEvent::InviteResponse(InviteResponse::Decline));
// }
agent.action_timer += read_data.dt.0;
// TODO allow npcs to handle more than one msg per tick
if agent.target.is_some() {
agent.action_timer += read_data.dt.0 / 10.0;
} else {
agent.action_timer += read_data.dt.0;
}
let msg = agent.inbox.pop_back();
if msg.is_some() {
println!("agent message: {:?}", msg);
}
match msg {
Some(AgentEvent::IncomingChat(chat_msg)) => {
if agent.behavior.can(BehaviorCapability::SPEAK) {
if let ChatType::Say(by) = chat_msg.chat_type {
if let Some(target) = read_data.uid_allocator.retrieve_entity_internal(by.id())
{
agent.target = Some(Target {
target,
hostile: false,
selected_at: read_data.time.0,
});
match agent.offer {
Some(InteractionOffer::Trade) => {
if chat_msg.message == "yes".to_string() {
let msg = "Come see my wares".to_string();
event_emitter.emit(ServerEvent::Chat(
UnresolvedChatMsg::npc(*self.uid, msg),
));
controller.events.push(ControlEvent::InitiateInvite(
by,
InviteKind::Trade,
));
}
},
Some(InteractionOffer::MercenaryHire) => {
if chat_msg.message == "yes".to_string() {
if let Some(target) = read_data.uid_allocator.retrieve_entity_internal(by.id()) {
if let Some(tgt_stats) = read_data.stats.get(target) {
agent.rtsim_controller.events.push(
RtSimEvent::AddMemory(Memory {
item: MemoryItem::MercenaryContract {
name: tgt_stats.name.clone(),
},
time_to_forget: read_data.time.0 + 1440.0,
}),
);
}
}
controller.events.push(ControlEvent::InitiateInvite(
by,
InviteKind::Trade,
));
agent.behavior.set(BehaviorState::TRADING);
}
},
_ => {
event_emitter.emit(ServerEvent::Chat(
UnresolvedChatMsg::npc(*self.uid, chat_msg.message),
));
},
}
}
}
}
},
Some(AgentEvent::Talk(by, subject)) => {
if agent.behavior.can(BehaviorCapability::SPEAK) {
if let Some(target) = read_data.uid_allocator.retrieve_entity_internal(by.id())
@ -934,21 +1004,29 @@ impl<'a> AgentData<'a> {
time_to_forget: read_data.time.0 + 600.0,
}),
);
if rtsim_entity
.brain
.remembers_character(&tgt_stats.name)
{
format!(
"Greetings fair {}! It has been far too \
long since last I saw you. I'm going to \
{} right now.",
&tgt_stats.name, destination_name
)
} else {
format!(
"I'm heading to {}! Want to come along?",
destination_name
)
match rtsim_entity.get_occupation() {
Some(Occupation::TravelingMercenary) => {
agent.offer = Some(InteractionOffer::MercenaryHire);
"You look like an adventurer in need of an armed consort. For 1000 c. I will protect you for the next 24 hours.".to_string()
},
_ => {
if rtsim_entity
.brain
.remembers_character(&tgt_stats.name)
{
format!(
"Greetings fair {}! It has been far too \
long since last I saw you. I'm going to \
{} right now.",
&tgt_stats.name, destination_name
)
} else {
format!(
"I'm heading to {}! Want to come along?",
destination_name
)
}
},
}
} else {
format!(
@ -964,6 +1042,7 @@ impl<'a> AgentData<'a> {
event_emitter.emit(ServerEvent::Chat(
UnresolvedChatMsg::npc(*self.uid, msg),
));
agent.offer = Some(InteractionOffer::Trade);
} else {
let msg = "npc.speech.villager".to_string();
event_emitter.emit(ServerEvent::Chat(
@ -1165,6 +1244,14 @@ impl<'a> AgentData<'a> {
}
},
Some(AgentEvent::TradeAccepted(with)) => {
//let msg = "I will guard you with my life until the contract ends or your coffers run dry".to_string();
//event_emitter.emit(ServerEvent::Chat(
// UnresolvedChatMsg::npc(*self.uid, msg),
//));
//controller.events.push(ControlEvent::InitiateInvite(
// by,
// InviteKind::JoinGroup,
//));
if !agent.behavior.is(BehaviorState::TRADING) {
if let Some(target) =
read_data.uid_allocator.retrieve_entity_internal(with.id())
@ -1183,10 +1270,26 @@ impl<'a> AgentData<'a> {
if agent.behavior.is(BehaviorState::TRADING) {
match result {
TradeResult::Completed => {
event_emitter.emit(ServerEvent::Chat(UnresolvedChatMsg::npc(
*self.uid,
"npc.speech.merchant_trade_successful".to_string(),
)))
if matches!(agent.offer, Some(InteractionOffer::MercenaryHire)) {
agent.offer = None;
let msg = "I will guard you with my life until the contract ends or your coffers run dry".to_string();
event_emitter.emit(ServerEvent::Chat(
UnresolvedChatMsg::npc(*self.uid, msg),
));
if let Some(target) = &agent.target {
if let Some(by) = read_data.uids.get(target.target) {
controller.events.push(ControlEvent::InitiateInvite(
*by,
InviteKind::JoinGroup,
));
}
}
} else {
event_emitter.emit(ServerEvent::Chat(UnresolvedChatMsg::npc(
*self.uid,
"npc.speech.merchant_trade_successful".to_string(),
)));
}
},
_ => event_emitter.emit(ServerEvent::Chat(UnresolvedChatMsg::npc(
*self.uid,
@ -1194,6 +1297,7 @@ impl<'a> AgentData<'a> {
))),
}
agent.behavior.unset(BehaviorState::TRADING);
agent.action_timer = DEFAULT_INTERACTION_TIME + read_data.dt.0;
}
},
Some(AgentEvent::UpdatePendingTrade(boxval)) => {

View File

@ -789,6 +789,10 @@ impl<'a> Widget for Group<'a> {
.localized_strings
.get("hud.group.invite_to_join")
.replace("{name}", &name),
InviteKind::JoinGroup => self
.localized_strings
.get("hud.group.invite_to_join_your_group")
.replace("{name}", &name),
InviteKind::Trade => self
.localized_strings
.get("hud.group.invite_to_trade")

View File

@ -146,6 +146,7 @@ impl SessionState {
// not be grammatical in some languages)
let kind_str = match kind {
InviteKind::Group => "Group",
InviteKind::JoinGroup => "Join Group",
InviteKind::Trade => "Trade",
};
let target_name = match client.player_list().get(&target) {