New Arena building and visit site for NPCs

This commit is contained in:
Laura 2023-09-26 12:29:14 +00:00 committed by flo
parent 5d311e13bd
commit f4d48d2689
14 changed files with 1444 additions and 49 deletions

View File

@ -24,6 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added the ability to make pets sit, they wont follow nor defend you in this state
- Portals that spawn in place of the last staircase at old style dungeons to prevent stair cheesing
- Mutliple singleplayer worlds and map generation UI.
- New arena building in desert cities, suitable for PVP, also NPCs like to watch the fights too
### Changed

View File

@ -287,3 +287,4 @@ npc-speech-dist_far = far away
npc-speech-dist_ahead = some way away
npc-speech-dist_near = nearby
npc-speech-dist_near_to = very close
npc-speech-arena = Let's sit over there!

View File

@ -6,6 +6,7 @@
use crate::{
character::CharacterId,
comp::{dialogue::Subject, Content},
util::Dir,
};
use rand::{seq::IteratorRandom, Rng};
use serde::{Deserialize, Serialize};
@ -230,6 +231,8 @@ pub struct RtSimController {
pub actions: VecDeque<NpcAction>,
pub personality: Personality,
pub heading_to: Option<String>,
// TODO: Maybe this should allow for looking at a specific entity target?
pub look_dir: Option<Dir>,
}
impl RtSimController {
@ -248,7 +251,9 @@ pub enum NpcActivity {
Gather(&'static [ChunkResource]),
// TODO: Generalise to other entities? What kinds of animals?
HuntAnimals,
Dance,
Dance(Option<Dir>),
Cheer(Option<Dir>),
Sit(Option<Dir>),
}
/// Represents event-like actions that rtsim NPCs can perform to interact with

View File

@ -23,5 +23,4 @@ pub enum SettlementKindMeta {
DesertCity,
SavannahPit,
CoastalTown,
PirateHideout,
}

View File

@ -14,6 +14,7 @@ use common::{
},
store::Id,
terrain::CoordinateConversions,
util::Dir,
};
use hashbrown::{HashMap, HashSet};
use rand::prelude::*;
@ -57,6 +58,7 @@ pub struct Controller {
pub actions: Vec<NpcAction>,
pub activity: Option<NpcActivity>,
pub new_home: Option<SiteId>,
pub look_dir: Option<Dir>,
}
impl Controller {
@ -72,7 +74,11 @@ impl Controller {
pub fn do_hunt_animals(&mut self) { self.activity = Some(NpcActivity::HuntAnimals); }
pub fn do_dance(&mut self) { self.activity = Some(NpcActivity::Dance); }
pub fn do_dance(&mut self, dir: Option<Dir>) { self.activity = Some(NpcActivity::Dance(dir)); }
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 say(&mut self, target: impl Into<Option<Actor>>, content: comp::Content) {
self.actions.push(NpcAction::Say(target.into(), content));

View File

@ -26,6 +26,7 @@ use common::{
store::Id,
terrain::{CoordinateConversions, SiteKindMeta, TerrainChunkSize},
time::DayPeriod,
util::Dir,
};
use fxhash::FxHasher64;
use itertools::{Either, Itertools};
@ -283,6 +284,9 @@ impl Rule for NpcAi {
.for_each(|(npc_id, controller, inbox, sentiments, known_reports, brain)| {
let npc = &data.npcs[*npc_id];
// Reset look_dir
controller.look_dir = None;
brain.action.tick(&mut NpcCtx {
state: ctx.state,
world: ctx.world,
@ -631,7 +635,7 @@ fn socialize() -> impl Action<EveryRange> {
if matches!(ctx.npc.mode, SimulationMode::Loaded) && socialize.should(ctx) {
// Sometimes dance
if ctx.rng.gen_bool(0.15) {
return just(|ctx, _| ctx.controller.do_dance())
return just(|ctx, _| ctx.controller.do_dance(None))
.repeat()
.stop_if(timeout(6.0))
.debug(|| "dancing")
@ -692,11 +696,11 @@ fn adventure() -> impl Action<DefaultState> {
.unwrap_or_default();
// Travel to the site
important(just(move |ctx, _| ctx.controller.say(None, Content::localized_with_args("npc-speech-moving_on", [("site", site_name.clone())])))
.then(travel_to_site(tgt_site, 0.6))
// Stop for a few minutes
.then(villager(tgt_site).repeat().stop_if(timeout(wait_time)))
.map(|_, _| ())
.boxed(),
.then(travel_to_site(tgt_site, 0.6))
// Stop for a few minutes
.then(villager(tgt_site).repeat().stop_if(timeout(wait_time)))
.map(|_, _| ())
.boxed(),
)
} else {
casual(finish().boxed())
@ -841,6 +845,49 @@ 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);
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})
{
// 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
// there is also no pathways to the stands on the first floor for NPCs.
let arena_center = Vec3::new(arena.center.x, arena.center.y, arena.base).as_::<f32>();
let stand_dist = arena.stand_dist as f32;
let seat_var_width = ctx.rng.gen_range(0..arena.stand_width) as f32;
let seat_var_length = ctx.rng.gen_range(-arena.stand_length..arena.stand_length) as f32;
// Select a seat on one of the 4 arena stands
let seat = match ctx.rng.gen_range(0..4) {
0 => Vec3::new(arena_center.x - stand_dist + seat_var_width, arena_center.y + seat_var_length, arena_center.z),
1 => Vec3::new(arena_center.x + stand_dist - seat_var_width, arena_center.y + seat_var_length, arena_center.z),
2 => Vec3::new(arena_center.x + seat_var_length, arena_center.y - stand_dist + seat_var_width, arena_center.z),
_ => Vec3::new(arena_center.x + seat_var_length, arena_center.y + stand_dist - seat_var_width, arena_center.z),
};
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")))
.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) {
casual(just(move |ctx,_| ctx.controller.do_cheer(look_dir)).repeat().stop_if(timeout(5.0)))
} 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)))
})
.repeat()
.stop_if(timeout(wait_time)))
.map(|_, _| ())
.boxed());
}
} else if matches!(ctx.npc.profession(), Some(Profession::Herbalist)) && ctx.rng.gen_bool(0.8)
{
if let Some(forest_wpos) = find_forest(ctx) {

View File

@ -220,7 +220,9 @@ fn on_tick(ctx: EventCtx<SimulateNpcs, OnTick>) {
NpcActivity::Goto(_, _)
| NpcActivity::Gather(_)
| NpcActivity::HuntAnimals
| NpcActivity::Dance,
| NpcActivity::Dance(_)
| NpcActivity::Cheer(_)
| NpcActivity::Sit(_),
) => {},
None => {},
}
@ -246,7 +248,11 @@ fn on_tick(ctx: EventCtx<SimulateNpcs, OnTick>) {
}
},
Some(
NpcActivity::Gather(_) | NpcActivity::HuntAnimals | NpcActivity::Dance,
NpcActivity::Gather(_)
| NpcActivity::HuntAnimals
| NpcActivity::Dance(_)
| NpcActivity::Cheer(_)
| NpcActivity::Sit(_),
) => {
// TODO: Maybe they should walk around randomly
// when gathering resources?

View File

@ -238,6 +238,7 @@ impl<'a> AgentData<'a> {
}
agent.action_state.timers[ActionTimers::TimerIdle as usize] = 0.0;
'activity: {
match agent.rtsim_controller.activity {
Some(NpcActivity::Goto(travel_to, speed_factor)) => {
@ -371,10 +372,46 @@ impl<'a> AgentData<'a> {
controller.push_action(ControlAction::Dance);
break 'activity; // Don't fall through to idle wandering
},
Some(NpcActivity::Dance) => {
Some(NpcActivity::Dance(dir)) => {
// Look at targets specified by rtsim
if let Some(look_dir) = dir {
controller.inputs.look_dir = look_dir;
if self.ori.look_dir().dot(look_dir.to_vec()) < 0.95 {
controller.inputs.move_dir = look_dir.to_vec().xy() * 0.01;
break 'activity;
} else {
controller.inputs.move_dir = Vec2::zero();
}
}
controller.push_action(ControlAction::Dance);
break 'activity; // Don't fall through to idle wandering
},
Some(NpcActivity::Cheer(dir)) => {
if let Some(look_dir) = dir {
controller.inputs.look_dir = look_dir;
if self.ori.look_dir().dot(look_dir.to_vec()) < 0.95 {
controller.inputs.move_dir = look_dir.to_vec().xy() * 0.01;
break 'activity;
} else {
controller.inputs.move_dir = Vec2::zero();
}
}
controller.push_action(ControlAction::Talk);
break 'activity; // Don't fall through to idle wandering
},
Some(NpcActivity::Sit(dir)) => {
if let Some(look_dir) = dir {
controller.inputs.look_dir = look_dir;
if self.ori.look_dir().dot(look_dir.to_vec()) < 0.95 {
controller.inputs.move_dir = look_dir.to_vec().xy() * 0.01;
break 'activity;
} else {
controller.inputs.move_dir = Vec2::zero();
}
}
controller.push_action(ControlAction::Sit);
break 'activity; // Don't fall through to idle wandering
},
Some(NpcActivity::HuntAnimals) => {
if rng.gen::<f32>() < 0.1 {
self.choose_target(

View File

@ -440,6 +440,7 @@ impl<'a> System<'a> for Sys {
// Update entity state
if let Some(agent) = agent {
agent.rtsim_controller.personality = npc.personality;
agent.rtsim_controller.look_dir = npc.controller.look_dir;
agent.rtsim_controller.activity = npc.controller.activity;
agent
.rtsim_controller

View File

@ -261,19 +261,8 @@ impl Civs {
let world_dims = ctx.sim.get_aabr();
for _ in 0..initial_civ_count * 3 {
attempt(5, || {
let (loc, kind) = match ctx.rng.gen_range(0..84) {
0..=5 => (
find_site_loc(
&mut ctx,
&ProximityRequirementsBuilder::new()
.avoid_all_of(this.castle_enemies(), 40)
.close_to_one_of(this.towns(), 20)
.finalize(&world_dims),
&SiteKind::Castle,
)?,
SiteKind::Castle,
),
28..=31 => {
let (loc, kind) = match ctx.rng.gen_range(0..79) {
0..=4 => {
if index.features().site2_giant_trees {
(
find_site_loc(
@ -298,7 +287,7 @@ impl Civs {
)
}
},
32..=37 => (
5..=10 => (
find_site_loc(
&mut ctx,
&ProximityRequirementsBuilder::new()
@ -308,8 +297,7 @@ impl Civs {
)?,
SiteKind::Gnarling,
),
// 32..=37 => (SiteKind::Citadel, (&castle_enemies, 20)),
38..=43 => (
11..=16 => (
find_site_loc(
&mut ctx,
&ProximityRequirementsBuilder::new()
@ -319,7 +307,7 @@ impl Civs {
)?,
SiteKind::ChapelSite,
),
44..=49 => (
17..=22 => (
find_site_loc(
&mut ctx,
&ProximityRequirementsBuilder::new()
@ -329,17 +317,7 @@ impl Civs {
)?,
SiteKind::Adlet,
),
/*50..=55 => (
find_site_loc(
&mut ctx,
&ProximityRequirementsBuilder::new()
.avoid_all_of(this.mine_site_enemies(), 40)
.finalize(&world_dims),
&SiteKind::DwarvenMine,
)?,
SiteKind::DwarvenMine,
),*/
56..=68 => (
23..=35 => (
find_site_loc(
&mut ctx,
&ProximityRequirementsBuilder::new()
@ -349,7 +327,7 @@ impl Civs {
)?,
SiteKind::PirateHideout,
),
69..=75 => (
36..=42 => (
find_site_loc(
&mut ctx,
&ProximityRequirementsBuilder::new()
@ -359,6 +337,29 @@ impl Civs {
)?,
SiteKind::JungleRuin,
),
/*43..=48 => (
find_site_loc(
&mut ctx,
&ProximityRequirementsBuilder::new()
.avoid_all_of(this.mine_site_enemies(), 40)
.finalize(&world_dims),
&SiteKind::DwarvenMine,
)?,
SiteKind::DwarvenMine,
),
49..=54 => (
find_site_loc(
&mut ctx,
&ProximityRequirementsBuilder::new()
.avoid_all_of(this.castle_enemies(), 40)
.close_to_one_of(this.towns(), 20)
.finalize(&world_dims),
&SiteKind::Castle,
)?,
SiteKind::Castle,
),
55..=60 => (SiteKind::Citadel, (&castle_enemies, 20)),
*/
_ => (
find_site_loc(
&mut ctx,

View File

@ -373,7 +373,6 @@ impl Site {
| SiteKind::CliffTown(_)
| SiteKind::SavannahPit(_)
| SiteKind::CoastalTown(_)
| SiteKind::PirateHideout(_)
| SiteKind::DesertCity(_)
| SiteKind::Settlement(_)
)
@ -417,9 +416,6 @@ impl SiteKind {
SiteKind::CoastalTown(_) => {
Some(SiteKindMeta::Settlement(SettlementKindMeta::CoastalTown))
},
SiteKind::PirateHideout(_) => {
Some(SiteKindMeta::Settlement(SettlementKindMeta::PirateHideout))
},
SiteKind::DesertCity(_) => {
Some(SiteKindMeta::Settlement(SettlementKindMeta::DesertCity))
},

View File

@ -1144,6 +1144,29 @@ impl Site {
site.make_plaza(land, &mut rng);
let size = 17.0 as i32;
let aabr = Aabr {
min: Vec2::broadcast(-size),
max: Vec2::broadcast(size),
};
let desert_city_arena =
plot::DesertCityArena::generate(land, &mut reseed(&mut rng), &site, aabr);
let desert_city_arena_alt = desert_city_arena.alt;
let plot = site.create_plot(Plot {
kind: PlotKind::DesertCityArena(desert_city_arena),
root_tile: aabr.center(),
tiles: aabr_tiles(aabr).collect(),
seed: rng.gen(),
});
site.blit_aabr(aabr, Tile {
kind: TileKind::Building,
plot: Some(plot),
hard_alt: Some(desert_city_arena_alt),
});
let build_chance = Lottery::from(vec![(20.0, 1), (10.0, 2)]);
let mut temples = 0;
@ -1675,6 +1698,9 @@ impl Site {
PlotKind::DesertCityTemple(desert_city_temple) => {
desert_city_temple.render_collect(self, canvas)
},
PlotKind::DesertCityArena(desert_city_arena) => {
desert_city_arena.render_collect(self, canvas)
},
PlotKind::Citadel(citadel) => citadel.render_collect(self, canvas),
PlotKind::Bridge(bridge) => bridge.render_collect(self, canvas),
PlotKind::PirateHideout(pirate_hideout) => {

View File

@ -5,6 +5,7 @@ mod citadel;
mod cliff_tower;
mod coastal_house;
mod coastal_workshop;
mod desert_city_arena;
mod desert_city_multiplot;
mod desert_city_temple;
pub mod dungeon;
@ -23,9 +24,9 @@ mod workshop;
pub use self::{
adlet::AdletStronghold, bridge::Bridge, castle::Castle, citadel::Citadel,
cliff_tower::CliffTower, coastal_house::CoastalHouse, coastal_workshop::CoastalWorkshop,
desert_city_multiplot::DesertCityMultiPlot, desert_city_temple::DesertCityTemple,
dungeon::Dungeon, dwarven_mine::DwarvenMine, giant_tree::GiantTree,
gnarling::GnarlingFortification, house::House, jungle_ruin::JungleRuin,
desert_city_arena::DesertCityArena, desert_city_multiplot::DesertCityMultiPlot,
desert_city_temple::DesertCityTemple, dungeon::Dungeon, dwarven_mine::DwarvenMine,
giant_tree::GiantTree, gnarling::GnarlingFortification, house::House, jungle_ruin::JungleRuin,
pirate_hideout::PirateHideout, savannah_hut::SavannahHut, savannah_pit::SavannahPit,
savannah_workshop::SavannahWorkshop, sea_chapel::SeaChapel, workshop::Workshop,
};
@ -74,6 +75,7 @@ pub enum PlotKind {
Workshop(Workshop),
DesertCityMultiPlot(DesertCityMultiPlot),
DesertCityTemple(DesertCityTemple),
DesertCityArena(DesertCityArena),
SeaChapel(SeaChapel),
JungleRuin(JungleRuin),
Plaza,

File diff suppressed because it is too large Load Diff