1use super::*;
2use crate::sys::terrain::SpawnEntityData;
3use common::{
4 LoadoutBuilder,
5 calendar::Calendar,
6 comp::{
7 self, Body, Item, Presence, PresenceKind, inventory::trade_pricing::TradePricing,
8 slot::ArmorSlot,
9 },
10 event::{CreateNpcEvent, CreateShipEvent, DeleteEvent, EventBus, NpcBuilder},
11 generation::{BodyBuilder, EntityConfig, EntityInfo},
12 resources::{DeltaTime, Time, TimeOfDay},
13 rtsim::{Actor, NpcId, RtSimEntity},
14 slowjob::SlowJobPool,
15 terrain::CoordinateConversions,
16 trade::{Good, SiteInformation},
17 util::Dir,
18};
19use common_ecs::{Job, Origin, Phase, System};
20use rand::Rng;
21use rtsim::data::{
22 Npc, Sites,
23 npc::{Profession, SimulationMode},
24};
25use specs::{Entities, Join, LendJoin, Read, ReadExpect, ReadStorage, WriteExpect, WriteStorage};
26use std::{sync::Arc, time::Duration};
27use tracing::error;
28
29pub fn trader_loadout(
30 loadout_builder: LoadoutBuilder,
31 economy: Option<&SiteInformation>,
32 mut permitted: impl FnMut(Good) -> bool,
33) -> LoadoutBuilder {
34 let rng = &mut rand::thread_rng();
35 let mut backpack = Item::new_from_asset_expect("common.items.armor.misc.back.backpack");
36 let mut bag1 = Item::new_from_asset_expect("common.items.armor.misc.bag.sturdy_red_backpack");
37 let mut bag2 = Item::new_from_asset_expect("common.items.armor.misc.bag.sturdy_red_backpack");
38 let mut bag3 = Item::new_from_asset_expect("common.items.armor.misc.bag.sturdy_red_backpack");
39 let mut bag4 = Item::new_from_asset_expect("common.items.armor.misc.bag.sturdy_red_backpack");
40 let slots = backpack.slots().len() + 4 * bag1.slots().len();
41 let mut stockmap: hashbrown::HashMap<Good, f32> = economy
42 .map(|e| {
43 e.unconsumed_stock
44 .clone()
45 .into_iter()
46 .filter(|(good, _)| permitted(*good))
47 .collect()
48 })
49 .unwrap_or_default();
50 if permitted(Good::Food) {
57 stockmap
58 .entry(Good::Food)
59 .and_modify(|e| *e = e.max(10_000.0))
60 .or_insert(10_000.0);
61 }
62 if permitted(Good::Potions) {
66 stockmap
67 .entry(Good::Potions)
68 .and_modify(|e| *e = e.powf(0.25));
69 }
70 if permitted(Good::Coin) {
73 stockmap
74 .entry(Good::Coin)
75 .and_modify(|e| *e = e.min(rng.gen_range(1000.0..3000.0)));
76 }
77 stockmap
79 .iter_mut()
80 .filter(|(good, _amount)| **good != Good::Coin)
81 .for_each(|(_good, amount)| *amount *= 0.1);
82 let ability_map = &comp::tool::AbilityMap::load().read();
84 let msm = &comp::item::MaterialStatManifest::load().read();
85 let mut wares: Vec<Item> =
86 TradePricing::random_items(&mut stockmap, slots as u32, true, true, 16)
87 .iter()
88 .filter_map(|(n, a)| {
89 let i = Item::new_from_item_definition_id(n.as_ref(), ability_map, msm).ok();
90 i.map(|mut i| {
91 i.set_amount(*a)
92 .map_err(|_| tracing::error!("merchant loadout amount failure"))
93 .ok();
94 i
95 })
96 })
97 .collect();
98 sort_wares(&mut wares);
99 transfer(&mut wares, &mut backpack);
100 transfer(&mut wares, &mut bag1);
101 transfer(&mut wares, &mut bag2);
102 transfer(&mut wares, &mut bag3);
103 transfer(&mut wares, &mut bag4);
104
105 loadout_builder
106 .back(Some(backpack))
107 .bag(ArmorSlot::Bag1, Some(bag1))
108 .bag(ArmorSlot::Bag2, Some(bag2))
109 .bag(ArmorSlot::Bag3, Some(bag3))
110 .bag(ArmorSlot::Bag4, Some(bag4))
111}
112
113fn sort_wares(bag: &mut [Item]) {
114 use common::comp::item::TagExampleInfo;
115
116 bag.sort_by(|a, b| {
117 a.quality()
118 .cmp(&b.quality())
119 .then(
121 Ord::cmp(
122 a.tags().first().map_or("", |tag| tag.name()),
123 b.tags().first().map_or("", |tag| tag.name()),
124 )
125 )
126 .then(#[expect(deprecated)] Ord::cmp(&a.name(), &b.name()))
128 });
129}
130
131fn transfer(wares: &mut Vec<Item>, bag: &mut Item) {
132 let capacity = bag.slots().len();
133 for (s, w) in bag
134 .slots_mut()
135 .iter_mut()
136 .zip(wares.drain(0..wares.len().min(capacity)))
137 {
138 *s = Some(w);
139 }
140}
141
142fn humanoid_config(profession: &Profession) -> &'static str {
143 match profession {
144 Profession::Farmer => "common.entity.village.farmer",
145 Profession::Hunter => "common.entity.village.hunter",
146 Profession::Herbalist => "common.entity.village.herbalist",
147 Profession::Captain => "common.entity.village.captain",
148 Profession::Merchant => "common.entity.village.merchant",
149 Profession::Guard => "common.entity.village.guard",
150 Profession::Adventurer(rank) => match rank {
151 0 => "common.entity.world.traveler0",
152 1 => "common.entity.world.traveler1",
153 2 => "common.entity.world.traveler2",
154 3 => "common.entity.world.traveler3",
155 _ => {
156 error!(
157 "Tried to get configuration for invalid adventurer rank {}",
158 rank
159 );
160 "common.entity.world.traveler3"
161 },
162 },
163 Profession::Blacksmith => "common.entity.village.blacksmith",
164 Profession::Chef => "common.entity.village.chef",
165 Profession::Alchemist => "common.entity.village.alchemist",
166 Profession::Pirate(leader) => match leader {
167 false => "common.entity.spot.pirate",
168 true => "common.entity.spot.buccaneer",
169 },
170 Profession::Cultist => "common.entity.dungeon.cultist.cultist",
171 }
172}
173
174fn loadout_default(
175 loadout: LoadoutBuilder,
176 _economy: Option<&SiteInformation>,
177 _time: Option<&(TimeOfDay, Calendar)>,
178) -> LoadoutBuilder {
179 loadout
180}
181
182fn merchant_loadout(
183 loadout_builder: LoadoutBuilder,
184 economy: Option<&SiteInformation>,
185 _time: Option<&(TimeOfDay, Calendar)>,
186) -> LoadoutBuilder {
187 trader_loadout(loadout_builder, economy, |_| true)
188}
189
190fn farmer_loadout(
191 loadout_builder: LoadoutBuilder,
192 economy: Option<&SiteInformation>,
193 _time: Option<&(TimeOfDay, Calendar)>,
194) -> LoadoutBuilder {
195 trader_loadout(loadout_builder, economy, |good| matches!(good, Good::Food))
196}
197
198fn herbalist_loadout(
199 loadout_builder: LoadoutBuilder,
200 economy: Option<&SiteInformation>,
201 _time: Option<&(TimeOfDay, Calendar)>,
202) -> LoadoutBuilder {
203 trader_loadout(loadout_builder, economy, |good| {
204 matches!(good, Good::Ingredients)
205 })
206}
207
208fn chef_loadout(
209 loadout_builder: LoadoutBuilder,
210 economy: Option<&SiteInformation>,
211 _time: Option<&(TimeOfDay, Calendar)>,
212) -> LoadoutBuilder {
213 trader_loadout(loadout_builder, economy, |good| matches!(good, Good::Food))
214}
215
216fn blacksmith_loadout(
217 loadout_builder: LoadoutBuilder,
218 economy: Option<&SiteInformation>,
219 _time: Option<&(TimeOfDay, Calendar)>,
220) -> LoadoutBuilder {
221 trader_loadout(loadout_builder, economy, |good| {
222 matches!(good, Good::Tools | Good::Armor)
223 })
224}
225
226fn alchemist_loadout(
227 loadout_builder: LoadoutBuilder,
228 economy: Option<&SiteInformation>,
229 _time: Option<&(TimeOfDay, Calendar)>,
230) -> LoadoutBuilder {
231 trader_loadout(loadout_builder, economy, |good| {
232 matches!(good, Good::Potions)
233 })
234}
235
236fn profession_extra_loadout(
237 profession: Option<&Profession>,
238) -> fn(
239 LoadoutBuilder,
240 Option<&SiteInformation>,
241 time: Option<&(TimeOfDay, Calendar)>,
242) -> LoadoutBuilder {
243 match profession {
244 Some(Profession::Merchant) => merchant_loadout,
245 Some(Profession::Farmer) => farmer_loadout,
246 Some(Profession::Herbalist) => herbalist_loadout,
247 Some(Profession::Chef) => chef_loadout,
248 Some(Profession::Blacksmith) => blacksmith_loadout,
249 Some(Profession::Alchemist) => alchemist_loadout,
250 _ => loadout_default,
251 }
252}
253
254fn profession_agent_mark(profession: Option<&Profession>) -> Option<comp::agent::Mark> {
255 match profession {
256 Some(
257 Profession::Merchant
258 | Profession::Farmer
259 | Profession::Herbalist
260 | Profession::Chef
261 | Profession::Blacksmith
262 | Profession::Alchemist,
263 ) => Some(comp::agent::Mark::Merchant),
264 Some(Profession::Guard) => Some(comp::agent::Mark::Guard),
265 _ => None,
266 }
267}
268
269fn get_npc_entity_info(
270 npc: &Npc,
271 sites: &Sites,
272 index: IndexRef,
273 time: Option<&(TimeOfDay, Calendar)>,
274) -> EntityInfo {
275 let pos = comp::Pos(npc.wpos);
276
277 let mut rng = npc.rng(Npc::PERM_ENTITY_CONFIG);
278 if let Some(profession) = npc.profession() {
279 let economy = npc.home.and_then(|home| {
280 let site = sites.get(home)?.world_site?;
281 index.sites.get(site).trade_information(site)
282 });
283
284 let config_asset = humanoid_config(&profession);
285
286 let entity_config = EntityConfig::from_asset_expect_owned(config_asset)
287 .with_body(BodyBuilder::Exact(npc.body));
288 EntityInfo::at(pos.0)
289 .with_entity_config(entity_config, Some(config_asset), &mut rng, time)
290 .with_alignment(
291 if matches!(profession, Profession::Cultist | Profession::Pirate(_)) {
292 comp::Alignment::Enemy
293 } else {
294 comp::Alignment::Npc
295 },
296 )
297 .with_economy(economy.as_ref())
298 .with_lazy_loadout(profession_extra_loadout(Some(&profession)))
299 .with_alias(npc.get_name())
300 .with_agent_mark(profession_agent_mark(Some(&profession)))
301 } else {
302 let config_asset = match npc.body {
303 Body::BirdLarge(body) => match body.species {
304 comp::bird_large::Species::Phoenix => "common.entity.wild.aggressive.phoenix",
305 comp::bird_large::Species::Cockatrice => "common.entity.wild.aggressive.cockatrice",
306 comp::bird_large::Species::Roc => "common.entity.wild.aggressive.roc",
307 comp::bird_large::Species::CloudWyvern => {
308 "common.entity.wild.aggressive.cloudwyvern"
309 },
310 comp::bird_large::Species::FlameWyvern => {
311 "common.entity.wild.aggressive.flamewyvern"
312 },
313 comp::bird_large::Species::FrostWyvern => {
314 "common.entity.wild.aggressive.frostwyvern"
315 },
316 comp::bird_large::Species::SeaWyvern => "common.entity.wild.aggressive.seawyvern",
317 comp::bird_large::Species::WealdWyvern => {
318 "common.entity.wild.aggressive.wealdwyvern"
319 },
320 },
321 Body::BipedLarge(body) => match body.species {
322 comp::biped_large::Species::Ogre => "common.entity.wild.aggressive.ogre",
323 comp::biped_large::Species::Cyclops => "common.entity.wild.aggressive.cyclops",
324 comp::biped_large::Species::Wendigo => "common.entity.wild.aggressive.wendigo",
325 comp::biped_large::Species::Werewolf => "common.entity.wild.aggressive.werewolf",
326 comp::biped_large::Species::Cavetroll => "common.entity.wild.aggressive.cave_troll",
327 comp::biped_large::Species::Mountaintroll => {
328 "common.entity.wild.aggressive.mountain_troll"
329 },
330 comp::biped_large::Species::Swamptroll => {
331 "common.entity.wild.aggressive.swamp_troll"
332 },
333 comp::biped_large::Species::Blueoni => "common.entity.wild.aggressive.blue_oni",
334 comp::biped_large::Species::Redoni => "common.entity.wild.aggressive.red_oni",
335 comp::biped_large::Species::Tursus => "common.entity.wild.aggressive.tursus",
336 comp::biped_large::Species::Gigasfrost => {
337 "common.entity.world.world_bosses.gigas_frost"
338 },
339 comp::biped_large::Species::Gigasfire => {
340 "common.entity.world.world_bosses.gigas_fire"
341 },
342 species => unimplemented!("rtsim spawning for {:?}", species),
343 },
344 body => unimplemented!("rtsim spawning for {:?}", body),
345 };
346 let entity_config = EntityConfig::from_asset_expect_owned(config_asset)
347 .with_body(BodyBuilder::Exact(npc.body));
348
349 EntityInfo::at(pos.0).with_entity_config(entity_config, Some(config_asset), &mut rng, time)
350 }
351}
352
353#[derive(Default)]
354pub struct Sys;
355impl<'a> System<'a> for Sys {
356 type SystemData = (
357 Entities<'a>,
358 Read<'a, DeltaTime>,
359 Read<'a, Time>,
360 Read<'a, TimeOfDay>,
361 Read<'a, EventBus<CreateShipEvent>>,
362 Read<'a, EventBus<CreateNpcEvent>>,
363 Read<'a, EventBus<DeleteEvent>>,
364 WriteExpect<'a, RtSim>,
365 ReadExpect<'a, Arc<world::World>>,
366 ReadExpect<'a, world::IndexOwned>,
367 ReadExpect<'a, SlowJobPool>,
368 ReadStorage<'a, comp::Pos>,
369 ReadStorage<'a, RtSimEntity>,
370 WriteStorage<'a, comp::Agent>,
371 ReadStorage<'a, Presence>,
372 ReadExpect<'a, Calendar>,
373 <rtsim::OnTick as rtsim::Event>::SystemData<'a>,
374 );
375
376 const NAME: &'static str = "rtsim::tick";
377 const ORIGIN: Origin = Origin::Server;
378 const PHASE: Phase = Phase::Create;
379
380 fn run(
381 _job: &mut Job<Self>,
382 (
383 entities,
384 dt,
385 time,
386 time_of_day,
387 create_ship_events,
388 create_npc_events,
389 delete_events,
390 mut rtsim,
391 world,
392 index,
393 slow_jobs,
394 positions,
395 rtsim_entities,
396 mut agents,
397 presences,
398 calendar,
399 mut tick_data,
400 ): Self::SystemData,
401 ) {
402 let mut create_ship_emitter = create_ship_events.emitter();
403 let mut create_npc_emitter = create_npc_events.emitter();
404 let mut delete_emitter = delete_events.emitter();
405 let rtsim = &mut *rtsim;
406 let calendar_data = (*time_of_day, (*calendar).clone());
407
408 {
410 let mut data = rtsim.state.data_mut();
411
412 data.time_of_day = *time_of_day;
414
415 data.npcs.character_map.clear();
418 for (presence, wpos) in (&presences, &positions).join() {
419 if let PresenceKind::Character(character) = &presence.kind {
420 let chunk_pos = wpos.0.xy().as_().wpos_to_cpos();
421 data.npcs
422 .character_map
423 .entry(chunk_pos)
424 .or_default()
425 .push((*character, wpos.0));
426 }
427 }
428 }
429
430 rtsim.state.tick(
432 &mut tick_data,
433 &world,
434 index.as_index_ref(),
435 *time_of_day,
436 *time,
437 dt.0,
438 );
439
440 if rtsim
442 .last_saved
443 .is_none_or(|ls| ls.elapsed() > Duration::from_secs(60))
444 {
445 let _ = slow_jobs;
447 rtsim.save(false);
448 }
449
450 let chunk_states = rtsim.state.resource::<ChunkStates>();
451 let data = &mut *rtsim.state.data_mut();
452
453 let mut create_event = |id: NpcId, npc: &Npc, steering: Option<NpcBuilder>| match npc.body {
454 Body::Ship(body) => {
455 create_ship_emitter.emit(CreateShipEvent {
456 pos: comp::Pos(npc.wpos),
457 ori: comp::Ori::from(Dir::new(npc.dir.with_z(0.0))),
458 ship: body,
459 rtsim_entity: Some(RtSimEntity(id)),
460 driver: steering,
461 });
462 },
463 _ => {
464 let entity_info = get_npc_entity_info(
465 npc,
466 &data.sites,
467 index.as_index_ref(),
468 Some(&calendar_data),
469 );
470
471 let (mut npc_builder, pos) = SpawnEntityData::from_entity_info(entity_info)
472 .into_npc_data_inner()
473 .expect("Entity loaded from assets cannot be special")
474 .to_npc_builder();
475
476 if let Some(agent) = &mut npc_builder.agent {
477 agent.rtsim_outbox = Some(Default::default());
478 }
479
480 if let Some(health) = &mut npc_builder.health {
481 health.set_fraction(npc.health_fraction);
482 }
483
484 create_npc_emitter.emit(CreateNpcEvent {
485 pos,
486 ori: comp::Ori::from(Dir::new(npc.dir.with_z(0.0))),
487 npc: npc_builder.with_rtsim(RtSimEntity(id)).with_rider(steering),
488 });
489 },
490 };
491
492 for mount in data.npcs.mounts.iter_mounts() {
494 let mount_npc = data.npcs.npcs.get_mut(mount).expect("This should exist");
495 let chunk = mount_npc.wpos.xy().as_::<i32>().wpos_to_cpos();
496
497 if matches!(mount_npc.mode, SimulationMode::Simulated)
498 && chunk_states.0.get(chunk).is_some_and(|c| c.is_some())
499 {
500 mount_npc.mode = SimulationMode::Loaded;
501
502 let mut actor_info = |actor: Actor| {
503 let npc_id = actor.npc()?;
504 let npc = data.npcs.npcs.get_mut(npc_id)?;
505 if matches!(npc.mode, SimulationMode::Simulated) {
506 npc.mode = SimulationMode::Loaded;
507 let entity_info = get_npc_entity_info(
508 npc,
509 &data.sites,
510 index.as_index_ref(),
511 Some(&calendar_data),
512 );
513
514 let mut npc_builder = SpawnEntityData::from_entity_info(entity_info)
515 .into_npc_data_inner()
516 .expect("Entity loaded from assets cannot be special")
520 .to_npc_builder()
521 .0
522 .with_rtsim(RtSimEntity(npc_id));
523
524 if let Some(agent) = &mut npc_builder.agent {
525 agent.rtsim_outbox = Some(Default::default());
526 }
527
528 Some(npc_builder)
529 } else {
530 error!("Npc is loaded but vehicle is unloaded");
531 None
532 }
533 };
534
535 let steerer = data
536 .npcs
537 .mounts
538 .get_steerer_link(mount)
539 .and_then(|link| actor_info(link.rider));
540
541 let mount_npc = data.npcs.npcs.get(mount).expect("This should exist");
542 create_event(mount, mount_npc, steerer);
543 }
544 }
545
546 for (npc_id, npc) in data.npcs.npcs.iter_mut() {
548 let chunk = npc.wpos.xy().as_::<i32>().wpos_to_cpos();
549
550 if matches!(npc.mode, SimulationMode::Simulated)
553 && chunk_states.0.get(chunk).is_some_and(|c| c.is_some())
554 && data.npcs.mounts.get_mount_link(npc_id).is_none()
556 {
557 npc.mode = SimulationMode::Loaded;
558 create_event(npc_id, npc, None);
559 }
560 }
561
562 for (entity, pos, rtsim_entity, agent) in (
564 &entities,
565 &positions,
566 &rtsim_entities,
567 (&mut agents).maybe(),
568 )
569 .join()
570 {
571 if let Some(npc) = data.npcs.get_mut(rtsim_entity.0) {
572 match npc.mode {
573 SimulationMode::Loaded => {
574 npc.wpos = pos.0;
576
577 if let Some(agent) = agent {
579 agent.rtsim_controller.personality = npc.personality;
580 agent.rtsim_controller.look_dir = npc.controller.look_dir;
581 agent.rtsim_controller.activity = npc.controller.activity;
582 agent
583 .rtsim_controller
584 .actions
585 .extend(std::mem::take(&mut npc.controller.actions));
586 if let Some(rtsim_outbox) = &mut agent.rtsim_outbox {
587 npc.inbox.append(rtsim_outbox);
588 }
589 }
590 },
591 SimulationMode::Simulated => {
592 delete_emitter.emit(DeleteEvent(entity));
593 },
594 }
595 }
596 }
597 }
598}