mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
New Arena building and visit site for NPCs
This commit is contained in:
parent
5d311e13bd
commit
f4d48d2689
@ -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
|
||||
|
||||
|
@ -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!
|
@ -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
|
||||
|
@ -23,5 +23,4 @@ pub enum SettlementKindMeta {
|
||||
DesertCity,
|
||||
SavannahPit,
|
||||
CoastalTown,
|
||||
PirateHideout,
|
||||
}
|
||||
|
@ -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));
|
||||
|
@ -26,6 +26,7 @@ use common::{
|
||||
store::Id,
|
||||
terrain::{CoordinateConversions, SiteKindMeta, TerrainChunkSize},
|
||||
time::DayPeriod,
|
||||
util::Dir,
|
||||
};
|
||||
use fxhash::FxHasher64;
|
||||
use itertools::{Either, Itertools};
|
||||
@ -270,7 +271,7 @@ impl Rule for NpcAi {
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
|
||||
// The sum of the last `SIMULATED_TICK_SKIP` tick deltatimes is the deltatime since
|
||||
// The sum of the last `SIMULATED_TICK_SKIP` tick deltatimes is the deltatime since
|
||||
// simulated npcs ran this tick had their ai ran.
|
||||
let simulated_dt = last_ticks.iter().sum::<f32>();
|
||||
|
||||
@ -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) {
|
||||
|
@ -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?
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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))
|
||||
},
|
||||
|
@ -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) => {
|
||||
|
@ -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,
|
||||
|
1267
world/src/site2/plot/desert_city_arena.rs
Normal file
1267
world/src/site2/plot/desert_city_arena.rs
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user