tavern rtsim

This commit is contained in:
Isse 2023-10-17 13:59:38 +02:00
parent c1aa9bd1b6
commit a3a19ecc3a
10 changed files with 245 additions and 98 deletions

View File

@ -298,6 +298,12 @@ pub struct VolumeMounting {
pub rider: Uid,
}
impl VolumeMounting {
pub fn is_steering_entity(&self) -> bool {
matches!(self.pos.kind, Volume::Entity(..)) && self.block.is_controller()
}
}
impl Link for VolumeMounting {
type CreateData<'a> = (
Write<'a, VolumeRiders>,

View File

@ -7,6 +7,10 @@ use std::ops::{Mul, MulAssign};
#[derive(Copy, Clone, Debug, Serialize, Deserialize, Default)]
pub struct TimeOfDay(pub f64);
impl TimeOfDay {
pub fn day(&self) -> f64 { self.0.rem_euclid(24.0 * 3600.0) }
}
/// A resource that stores the tick (i.e: physics) time.
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
pub struct Time(pub f64);

View File

@ -252,7 +252,7 @@ pub enum NpcActivity {
HuntAnimals,
Dance(Option<Dir>),
Cheer(Option<Dir>),
Sit(Option<Dir>),
Sit(Option<Dir>, Option<Vec3<i32>>),
}
/// Represents event-like actions that rtsim NPCs can perform to interact with

View File

@ -79,7 +79,9 @@ impl Controller {
pub fn do_cheer(&mut self, dir: Option<Dir>) { self.activity = Some(NpcActivity::Cheer(dir)); }
pub fn do_sit(&mut self, dir: Option<Dir>) { self.activity = Some(NpcActivity::Sit(dir)); }
pub fn do_sit(&mut self, dir: Option<Dir>, pos: Option<Vec3<i32>>) {
self.activity = Some(NpcActivity::Sit(dir, pos));
}
pub fn say(&mut self, target: impl Into<Option<Actor>>, content: comp::Content) {
self.actions.push(NpcAction::Say(target.into(), content));

View File

@ -38,7 +38,7 @@ use vek::*;
use world::{
civ::{self, Track},
site::{Site as WorldSite, SiteKind},
site2::{self, PlotKind, TileKind},
site2::{self, plot::tavern, PlotKind, TileKind},
util::NEIGHBORS,
IndexRef, World,
};
@ -325,7 +325,7 @@ impl Rule for NpcAi {
}
}
fn idle<S: State>() -> impl Action<S> { just(|ctx, _| ctx.controller.do_idle()).debug(|| "idle") }
fn idle<S: State>() -> impl Action<S> + Clone { just(|ctx, _| ctx.controller.do_idle()).debug(|| "idle") }
/// Try to walk toward a 3D position without caring for obstacles.
fn goto<S: State>(wpos: Vec3<f32>, speed_factor: f32, goal_dist: f32) -> impl Action<S> {
@ -578,7 +578,7 @@ fn travel_to_site<S: State>(tgt_site: SiteId, speed_factor: f32) -> impl Action<
.map(|_, _| ())
}
fn talk_to<S: State>(tgt: Actor, _subject: Option<Subject>) -> impl Action<S> {
fn talk_to<S: State>(tgt: Actor, _subject: Option<Subject>) -> impl Action<S> + Clone {
now(move |ctx, _| {
if matches!(tgt, Actor::Npc(_)) && ctx.rng.gen_bool(0.2) {
// Cut off the conversation sometimes to avoid infinite conversations (but only
@ -630,7 +630,7 @@ fn talk_to<S: State>(tgt: Actor, _subject: Option<Subject>) -> impl Action<S> {
})
}
fn socialize() -> impl Action<EveryRange> {
fn socialize() -> impl Action<EveryRange> + Clone {
now(move |ctx, socialize: &mut EveryRange| {
// Skip most socialising actions if we're not loaded
if matches!(ctx.npc.mode, SimulationMode::Loaded) && socialize.should(ctx) {
@ -758,6 +758,8 @@ fn choose_plaza(ctx: &mut NpcCtx, site: SiteId) -> Option<Vec2<f32>> {
})
}
const WALKING_SPEED: f32 = 0.35;
fn villager(visiting_site: SiteId) -> impl Action<DefaultState> {
choose(move |ctx, state: &mut DefaultState| {
// Consider moving home if the home site gets too full
@ -804,8 +806,9 @@ fn villager(visiting_site: SiteId) -> impl Action<DefaultState> {
.then(travel_to_site(new_home, 0.5))
.then(just(move |ctx, _| ctx.controller.set_new_home(new_home))));
}
if DayPeriod::from(ctx.time_of_day.0).is_dark()
let day_period = DayPeriod::from(ctx.time_of_day.0);
let is_weekend = ctx.time_of_day.day() as u64 % 6 == 0;
if day_period.is_dark()
&& !matches!(ctx.npc.profession(), Some(Profession::Guard))
{
return important(
@ -845,18 +848,18 @@ fn villager(visiting_site: SiteId) -> impl Action<DefaultState> {
})
.debug(|| "find somewhere to sleep"),
);
// Villagers with roles should perform those roles
}
// Visiting villagers in DesertCity who are not Merchants should sit down in the Arena during the day
else if matches!(ctx.state.data().sites[visiting_site].world_site.map(|ws| &ctx.index.sites.get(ws).kind), Some(SiteKind::DesertCity(_)))
&& !matches!(ctx.npc.profession(), Some(Profession::Merchant | Profession::Guard))
&& ctx.rng.gen_bool(1.0 / 3.0)
{
let wait_time = ctx.rng.gen_range(100.0..300.0);
// Go do something fun on evenings and holidays, or on random days.
else if
// Ain't no rest for the wicked
!matches!(ctx.npc.profession(), Some(Profession::Guard))
&& (matches!(day_period, DayPeriod::Evening) || is_weekend || ctx.rng.gen_bool(0.05)) {
let mut fun_stuff = Vec::new();
if let Some(ws_id) = ctx.state.data().sites[visiting_site].world_site
&& let Some(ws) = ctx.index.sites.get(ws_id).site2()
&& let Some(arena) = ws.plots().find_map(|p| match p.kind() { PlotKind::DesertCityArena(a) => Some(a), _ => None})
{
&& let Some(ws) = ctx.index.sites.get(ws_id).site2() {
if let Some(arena) = ws.plots().find_map(|p| match p.kind() { PlotKind::DesertCityArena(a) => Some(a), _ => None}) {
let wait_time = ctx.rng.gen_range(100.0..300.0);
// We don't use Z coordinates for seats because they are complicated to calculate from the Ramp procedural generation
// and using goto_2d seems to work just fine. However it also means that NPC will never go seat on the stands
// on the first floor of the arena. This is a compromise that was made because in the current arena procedural generation
@ -874,7 +877,7 @@ fn villager(visiting_site: SiteId) -> impl Action<DefaultState> {
};
let look_dir = Dir::from_unnormalized(arena_center - seat);
// Walk to an arena seat, cheer, sit and dance
return casual(just(move |ctx, _| ctx.controller.say(None, Content::localized("npc-speech-arena")))
let action = casual(just(move |ctx, _| ctx.controller.say(None, Content::localized("npc-speech-arena")))
.then(goto_2d(seat.xy(), 0.6, 1.0).debug(|| "go to arena"))
// Turn toward the centre of the arena and watch the action!
.then(choose(move |ctx, _| if ctx.rng.gen_bool(0.3) {
@ -882,14 +885,105 @@ fn villager(visiting_site: SiteId) -> impl Action<DefaultState> {
} else if ctx.rng.gen_bool(0.15) {
casual(just(move |ctx,_| ctx.controller.do_dance(look_dir)).repeat().stop_if(timeout(5.0)))
} else {
casual(just(move |ctx,_| ctx.controller.do_sit(look_dir)).repeat().stop_if(timeout(15.0)))
casual(just(move |ctx,_| ctx.controller.do_sit(look_dir, None)).repeat().stop_if(timeout(15.0)))
})
.repeat()
.stop_if(timeout(wait_time)))
.map(|_, _| ())
.boxed());
fun_stuff.push(action);
}
} else if matches!(ctx.npc.profession(), Some(Profession::Herbalist)) && ctx.rng.gen_bool(0.8)
if let Some(tavern) = ws.plots().filter_map(|p| match p.kind() { PlotKind::Tavern(a) => Some(a), _ => None }).choose(&mut ctx.rng) {
let wait_time = ctx.rng.gen_range(100.0..300.0);
let (stage_aabr, stage_z) = tavern.rooms.values().flat_map(|room| {
room.details.iter().filter_map(|detail| match detail {
tavern::Detail::Stage { aabr } => Some((*aabr, room.bounds.min.z + 1)),
_ => None,
})
}).choose(&mut ctx.rng).unwrap_or((tavern.bounds, tavern.door_wpos.z));
let bar_pos = tavern.rooms.values().flat_map(|room|
room.details.iter().filter_map(|detail| match detail {
tavern::Detail::Bar { aabr } => {
let side = site2::util::Dir::from_vec2(room.bounds.center().xy() - aabr.center());
let pos = side.select_aabr_with(*aabr, aabr.center()) + side.to_vec2();
Some(pos.with_z(room.bounds.min.z))
}
_ => None,
})
).choose(&mut ctx.rng).unwrap_or(stage_aabr.center().with_z(stage_z));
// Pick a chair that is theirs for the stay
let chair_pos = tavern.rooms.values().flat_map(|room| {
let z = room.bounds.min.z;
room.details.iter().filter_map(move |detail| match detail {
tavern::Detail::Table { pos, chairs } => Some(chairs.into_iter().map(move |dir| pos.with_z(z) + dir.to_vec2())),
_ => None,
})
.flatten()
}
).choose(&mut ctx.rng)
// This path is possible, but highly unlikely.
.unwrap_or(bar_pos);
let stage_aabr = stage_aabr.as_::<f32>();
let stage_z = stage_z as f32;
let action = casual(travel_to_point(tavern.door_wpos.xy().as_() + 0.5, 0.8).then(choose(move |ctx, (last_action, _)| {
let action = [0, 1, 2].into_iter().filter(|i| *last_action != Some(*i)).choose(&mut ctx.rng).expect("We have at least 2 elements");
let socialize = socialize().map_state(|(_, timer)| timer).repeat();
match action {
// Go and dance on a stage.
0 => {
casual(now(move |ctx, (last_action, _)| {
*last_action = Some(action);
goto(stage_aabr.min.map2(stage_aabr.max, |a, b| ctx.rng.gen_range(a..b)).with_z(stage_z), WALKING_SPEED, 1.0)
})
.then(just(move |ctx,_| ctx.controller.do_dance(None)).repeat().stop_if(timeout(ctx.rng.gen_range(20.0..30.0))))
.map(|_, _| ())
)
},
// Go and sit at a table.
1 => {
casual(
now(move |ctx, (last_action, _)| {
*last_action = Some(action);
goto(chair_pos.as_() + 0.5, WALKING_SPEED, 1.0).then(just(move |ctx, _| ctx.controller.do_sit(None, Some(chair_pos)))).then(socialize.clone().stop_if(timeout(ctx.rng.gen_range(30.0..60.0)))).map(|_, _| ())
})
)
},
// Go to the bar.
_ => {
casual(
now(move |ctx, (last_action, _)| {
*last_action = Some(action);
goto(bar_pos.as_() + 0.5, WALKING_SPEED, 1.0).then(socialize.clone().stop_if(timeout(ctx.rng.gen_range(10.0..25.0)))).map(|_, _| ())
})
)
},
}
})
.with_state((None::<u32>, every_range(5.0..10.0)))
.repeat()
.stop_if(timeout(wait_time)))
.map(|_, _| ())
.boxed()
);
fun_stuff.push(action);
}
}
if !fun_stuff.is_empty() {
let i = ctx.rng.gen_range(0..fun_stuff.len());
return fun_stuff.swap_remove(i);
}
}
// Villagers with roles should perform those roles
else if matches!(ctx.npc.profession(), Some(Profession::Herbalist)) && ctx.rng.gen_bool(0.8)
{
if let Some(forest_wpos) = find_forest(ctx) {
return casual(

View File

@ -254,7 +254,7 @@ fn on_tick(ctx: EventCtx<SimulateNpcs, OnTick>) {
| NpcActivity::HuntAnimals
| NpcActivity::Dance(_)
| NpcActivity::Cheer(_)
| NpcActivity::Sit(_),
| NpcActivity::Sit(..),
) => {
// TODO: Maybe they should walk around randomly
// when gathering resources?

View File

@ -29,6 +29,7 @@ use common::{
consts::MAX_MOUNT_RANGE,
effect::{BuffEffect, Effect},
event::{Emitter, ServerEvent},
mounting::VolumePos,
path::TraversalConfig,
rtsim::NpcActivity,
states::basic_beam,
@ -51,9 +52,7 @@ impl<'a> AgentData<'a> {
////////////////////////////////////////
pub fn glider_fall(&self, controller: &mut Controller, read_data: &ReadData) {
if read_data.is_riders.contains(*self.entity) {
controller.push_event(ControlEvent::Unmount);
}
self.dismount(controller, read_data);
controller.push_action(ControlAction::GlideWield);
@ -73,9 +72,7 @@ impl<'a> AgentData<'a> {
}
pub fn fly_upward(&self, controller: &mut Controller, read_data: &ReadData) {
if read_data.is_riders.contains(*self.entity) {
controller.push_event(ControlEvent::Unmount);
}
self.dismount(controller, read_data);
controller.push_basic_input(InputKind::Fly);
controller.inputs.move_z = 1.0;
@ -96,9 +93,7 @@ impl<'a> AgentData<'a> {
path: Path,
speed_multiplier: Option<f32>,
) -> bool {
if read_data.is_riders.contains(*self.entity) {
controller.push_event(ControlEvent::Unmount);
}
self.dismount(controller, read_data);
let partial_path_tgt_pos = |pos_difference: Vec3<f32>| {
self.pos.0
@ -242,6 +237,13 @@ impl<'a> AgentData<'a> {
'activity: {
match agent.rtsim_controller.activity {
Some(NpcActivity::Goto(travel_to, speed_factor)) => {
if !read_data
.is_volume_riders
.get(*self.entity)
.map_or(false, |r| r.is_steering_entity())
{
controller.push_event(ControlEvent::Unmount);
}
// 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
@ -399,7 +401,15 @@ impl<'a> AgentData<'a> {
controller.push_action(ControlAction::Talk);
break 'activity; // Don't fall through to idle wandering
},
Some(NpcActivity::Sit(dir)) => {
Some(NpcActivity::Sit(dir, pos)) => {
if let Some(pos) =
pos.filter(|p| read_data.terrain.get(*p).is_ok_and(|b| b.is_mountable()))
{
if !read_data.is_volume_riders.contains(*self.entity) {
controller
.push_event(ControlEvent::MountVolume(VolumePos::terrain(pos)));
}
} else {
if let Some(look_dir) = dir {
controller.inputs.look_dir = look_dir;
if self.ori.look_dir().dot(look_dir.to_vec()) < 0.95 {
@ -410,6 +420,7 @@ impl<'a> AgentData<'a> {
}
}
controller.push_action(ControlAction::Sit);
}
break 'activity; // Don't fall through to idle wandering
},
Some(NpcActivity::HuntAnimals) => {
@ -581,7 +592,9 @@ impl<'a> AgentData<'a> {
read_data: &ReadData,
tgt_pos: &Pos,
) {
if read_data.is_riders.contains(*self.entity) {
if read_data.is_riders.contains(*self.entity)
|| read_data.is_volume_riders.contains(*self.entity)
{
controller.push_event(ControlEvent::Unmount);
}
@ -637,7 +650,9 @@ impl<'a> AgentData<'a> {
// Proportion of full speed
const MAX_FLEE_SPEED: f32 = 0.65;
if read_data.is_riders.contains(*self.entity) {
if read_data.is_riders.contains(*self.entity)
|| read_data.is_volume_riders.contains(*self.entity)
{
controller.push_event(ControlEvent::Unmount);
}
@ -993,7 +1008,12 @@ impl<'a> AgentData<'a> {
#[cfg(feature = "be-dyn-lib")]
let rng = &mut thread_rng();
if read_data.is_riders.contains(*self.entity) {
if read_data.is_riders.contains(*self.entity)
|| !read_data
.is_volume_riders
.get(*self.entity)
.map_or(false, |r| r.is_steering_entity())
{
controller.push_event(ControlEvent::Unmount);
}
@ -1999,4 +2019,15 @@ impl<'a> AgentData<'a> {
}
}
}
pub fn dismount(&self, controller: &mut Controller, read_data: &ReadData) {
if read_data.is_riders.contains(*self.entity)
|| !read_data
.is_volume_riders
.get(*self.entity)
.map_or(false, |r| r.is_steering_entity())
{
controller.push_event(ControlEvent::Unmount);
}
}
}

View File

@ -69,10 +69,22 @@ impl Animation for SteerAnimation {
let hand_offset = Vec3::new(rot.cos(), 0.0, -rot.sin()) * 0.4 / s_a.scaler * 11.0;
next.hand_l.position = helm_center - hand_offset;
next.hand_l.orientation = hand_rotation;
next.hand_r.position = helm_center + hand_offset;
next.hand_r.orientation = -hand_rotation;
let ori_l = Quaternion::rotation_x(
PI / 2.0 + (next.hand_l.position.z / next.hand_l.position.x).atan(),
);
let ori_r = Quaternion::rotation_x(
PI / 2.0 - (next.hand_r.position.z / next.hand_r.position.x).atan(),
);
next.hand_l.orientation = hand_rotation * ori_l;
next.hand_r.orientation = -hand_rotation * ori_r;
next.shoulder_l.position = Vec3::new(-s_a.shoulder.0, s_a.shoulder.1, s_a.shoulder.2);
next.shoulder_l.orientation = ori_r;
next.shoulder_r.position = Vec3::new(s_a.shoulder.0, s_a.shoulder.1, s_a.shoulder.2);
next.shoulder_r.orientation = ori_l;
next.foot_l.position = Vec3::new(-s_a.foot.0, s_a.foot.1, s_a.foot.2);
next.foot_l.orientation = Quaternion::identity();
@ -80,10 +92,6 @@ impl Animation for SteerAnimation {
next.foot_r.position = Vec3::new(s_a.foot.0, s_a.foot.1, s_a.foot.2);
next.foot_r.orientation = Quaternion::identity();
next.shoulder_l.position = Vec3::new(-s_a.shoulder.0, s_a.shoulder.1, s_a.shoulder.2);
next.shoulder_r.position = Vec3::new(s_a.shoulder.0, s_a.shoulder.1, s_a.shoulder.2);
next.glider.position = Vec3::new(0.0, 0.0, 10.0);
next.glider.scale = Vec3::one() * 0.0;
next.hold.position = Vec3::new(0.4, -0.3, -5.8);

View File

@ -22,7 +22,7 @@ mod savannah_hut;
mod savannah_pit;
mod savannah_workshop;
mod sea_chapel;
mod tavern;
pub mod tavern;
mod troll_cave;
mod workshop;

View File

@ -22,7 +22,7 @@ use crate::{
type Neighbor = Option<Id<Room>>;
struct Wall {
pub struct Wall {
start: Vec2<i32>,
end: Vec2<i32>,
base_alt: i32,
@ -33,6 +33,15 @@ struct Wall {
door: Option<i32>,
}
impl Wall {
pub fn door_pos(&self) -> Option<Vec3<i32>> {
let wall_dir = Dir::from_vec2(self.end - self.start);
self.door
.map(|door| (self.start + wall_dir.to_vec2() * door).with_z(self.base_alt))
}
}
#[derive(Clone, Copy, EnumIter, enum_map::Enum)]
enum RoomKind {
Garden,
@ -54,7 +63,7 @@ impl RoomKind {
}
#[derive(Clone, Copy)]
enum Detail {
pub enum Detail {
Bar {
aabr: Aabr<i32>,
},
@ -67,15 +76,15 @@ enum Detail {
},
}
struct Room {
pub struct Room {
/// Inclusive
bounds: Aabb<i32>,
pub bounds: Aabb<i32>,
kind: RoomKind,
// stairs: Option<Id<Stairs>>,
walls: EnumMap<Dir, Vec<Id<Wall>>>,
// TODO: Remove this, used for debugging
detail_areas: Vec<Aabr<i32>>,
details: Vec<Detail>,
pub details: Vec<Detail>,
}
impl Room {
@ -99,14 +108,14 @@ struct Stairs {
pub struct Tavern {
name: String,
rooms: Store<Room>,
pub rooms: Store<Room>,
stairs: Store<Stairs>,
walls: Store<Wall>,
/// Tile position of the door tile
pub door_tile: Vec2<i32>,
pub(crate) door_wpos: Vec3<i32>,
pub door_wpos: Vec3<i32>,
/// Axis aligned bounding region for the house
bounds: Aabr<i32>,
pub bounds: Aabr<i32>,
}
impl Tavern {
@ -138,9 +147,7 @@ impl Tavern {
let door_tile_center = site.tile_center_wpos(door_tile);
let door_wpos = door_dir.select_aabr_with(ibounds, door_tile_center);
let door_alt = land
.column_sample(door_wpos, index)
.map_or_else(|| land.get_alt_approx(door_wpos), |sample| sample.alt);
let door_alt = land.get_alt_approx(door_wpos);
let door_wpos = door_wpos.with_z(door_alt.ceil() as i32);
/// Place room in bounds.
@ -605,10 +612,7 @@ impl Tavern {
for door_pos in dir_walls.iter().filter_map(|wall_id| {
let wall = &walls[*wall_id];
wall.door.map(|door| {
let wall_dir = Dir::from_vec2(wall.end - wall.start);
wall.start + wall_dir.to_vec2() * door
})
wall.door_pos()
}) {
let orth = dir.orthogonal();
for i in 0..room.detail_areas.len() {
@ -1005,8 +1009,7 @@ impl Structure for Tavern {
.fill(dark_wood.clone());
},
}
if let Some(door) = wall.door {
let door_pos = wall.start + wall_dir.to_vec2() * door;
if let Some(door_pos) = wall.door_pos() {
let min = match wall.from {
None => door_pos - wall.to_dir.to_vec2(),
Some(_) => door_pos,
@ -1056,9 +1059,8 @@ impl Structure for Tavern {
}
},
}
if let Some(door) = wall.door {
let door_pos = wall.start + wall_dir.to_vec2() * door;
let diff = door_pos - wall_aabb.center().xy();
if let Some(door_pos) = wall.door_pos() {
let diff = door_pos.xy() - wall_aabb.center().xy();
let orth = if diff == Vec2::zero() {
wall_dir
} else {