1use std::{f32::consts::PI, ops::Mul};
2
3use common::rtsim::DialogueKind;
4use common_state::{BlockChange, ScheduledBlockChange};
5use specs::{DispatcherBuilder, Join, ReadExpect, ReadStorage, WriteExpect, WriteStorage};
6use tracing::error;
7use vek::*;
8
9use common::{
10 assets::{self, Concatenate},
11 comp::{
12 self,
13 agent::{AgentEvent, Sound, SoundKind},
14 inventory::slot::EquipSlot,
15 item::{MaterialStatManifest, flatten_counted_items},
16 loot_owner::LootOwnerKind,
17 tool::AbilityMap,
18 },
19 consts::{MAX_INTERACT_RANGE, MAX_NPCINTERACT_RANGE, SOUND_TRAVEL_DIST_PER_VOLUME},
20 event::{
21 CreateItemDropEvent, CreateSpriteEvent, DialogueEvent, EventBus, MineBlockEvent,
22 NpcInteractEvent, SetLanternEvent, SetPetStayEvent, SoundEvent, TamePetEvent,
23 ToggleSpriteLightEvent,
24 },
25 link::Is,
26 mounting::Mount,
27 outcome::Outcome,
28 resources::ProgramTime,
29 terrain::{self, Block, SpriteKind, TerrainGrid},
30 uid::Uid,
31 util::Dir,
32 vol::ReadVol,
33};
34
35use crate::{Server, ServerGeneral, Time, client::Client};
36
37use crate::pet::tame_pet;
38use hashbrown::{HashMap, HashSet};
39use lazy_static::lazy_static;
40use serde::Deserialize;
41
42use super::{ServerEvent, event_dispatch, mounting::within_mounting_range};
43
44pub(super) fn register_event_systems(builder: &mut DispatcherBuilder) {
45 event_dispatch::<SetLanternEvent>(builder, &[]);
46 event_dispatch::<NpcInteractEvent>(builder, &[]);
47 event_dispatch::<DialogueEvent>(builder, &[]);
48 event_dispatch::<SetPetStayEvent>(builder, &[]);
49 event_dispatch::<MineBlockEvent>(builder, &[]);
50 event_dispatch::<SoundEvent>(builder, &[]);
51 event_dispatch::<CreateSpriteEvent>(builder, &[]);
52 event_dispatch::<ToggleSpriteLightEvent>(builder, &[]);
53}
54
55impl ServerEvent for SetLanternEvent {
56 type SystemData<'a> = (
57 WriteStorage<'a, comp::LightEmitter>,
58 ReadStorage<'a, comp::Inventory>,
59 ReadStorage<'a, comp::Health>,
60 );
61
62 fn handle(
63 events: impl ExactSizeIterator<Item = Self>,
64 (mut light_emitters, inventories, healths): Self::SystemData<'_>,
65 ) {
66 for SetLanternEvent(entity, enable) in events {
67 let lantern_exists = light_emitters
68 .get(entity)
69 .is_some_and(|light| light.strength > 0.0);
70
71 if lantern_exists != enable {
72 if !enable {
73 light_emitters.remove(entity);
74 }
75 else if healths.get(entity).is_none_or(|h| !h.is_dead) {
77 let lantern_info = inventories
78 .get(entity)
79 .and_then(|inventory| inventory.equipped(EquipSlot::Lantern))
80 .and_then(|item| {
81 if let comp::item::ItemKind::Lantern(l) = &*item.kind() {
82 Some((l.color(), l.strength(), l.flicker()))
83 } else {
84 None
85 }
86 });
87 if let Some((col, strength, flicker)) = lantern_info {
88 let _ = light_emitters.insert(entity, comp::LightEmitter {
89 col,
90 strength,
91 flicker,
92 animated: true,
93 });
94 }
95 }
96 }
97 }
98 }
99}
100
101impl ServerEvent for NpcInteractEvent {
102 type SystemData<'a> = (
103 WriteStorage<'a, comp::Agent>,
104 ReadStorage<'a, comp::Pos>,
105 ReadStorage<'a, Uid>,
106 );
107
108 fn handle(
109 events: impl ExactSizeIterator<Item = Self>,
110 (mut agents, positions, uids): Self::SystemData<'_>,
111 ) {
112 for NpcInteractEvent(interactor, npc_entity, subject) in events {
113 let within_range = {
114 positions
115 .get(interactor)
116 .zip(positions.get(npc_entity))
117 .is_some_and(|(interactor_pos, npc_pos)| {
118 interactor_pos.0.distance_squared(npc_pos.0)
119 <= MAX_NPCINTERACT_RANGE.powi(2)
120 })
121 };
122
123 if within_range
124 && let Some(agent) = agents.get_mut(npc_entity)
125 && agent.target.is_none()
126 {
127 if let Some(interactor_uid) = uids.get(interactor) {
128 agent
129 .inbox
130 .push_back(AgentEvent::Talk(*interactor_uid, subject));
131 }
132 }
133 }
134 }
135}
136
137impl ServerEvent for DialogueEvent {
138 type SystemData<'a> = (
139 ReadStorage<'a, Uid>,
140 ReadStorage<'a, comp::Pos>,
141 ReadStorage<'a, Client>,
142 WriteStorage<'a, comp::Agent>,
143 WriteStorage<'a, comp::Inventory>,
144 ReadExpect<'a, AbilityMap>,
145 ReadExpect<'a, MaterialStatManifest>,
146 );
147
148 fn handle(
149 events: impl ExactSizeIterator<Item = Self>,
150 (uids, positions, clients, mut agents, mut inventories, ability_map, msm): Self::SystemData<
151 '_,
152 >,
153 ) {
154 for DialogueEvent(sender, target, dialogue) in events {
155 let within_range = positions
156 .get(sender)
157 .zip(positions.get(target))
158 .is_some_and(|(sender_pos, target_pos)| {
159 sender_pos.0.distance_squared(target_pos.0) <= MAX_NPCINTERACT_RANGE.powi(2)
160 });
161
162 if within_range && let Some(sender_uid) = uids.get(sender) {
163 match &dialogue.kind {
165 DialogueKind::Start
166 | DialogueKind::End
167 | DialogueKind::Statement(..)
168 | DialogueKind::Question { .. } => {},
169 DialogueKind::Response { response, .. } => {
170 if let Some((item_def, amount)) = &response.given_item {
172 if let Some(target_inv) = inventories.get(target)
174 && target_inv.has_space_for(item_def, *amount)
175 && let Some(mut sender_inv) = inventories.get_mut(sender)
177 && sender_inv.item_count(item_def) >= *amount as u64
178 && let Some(items) = sender_inv.remove_item_amount(item_def, *amount, &ability_map, &msm)
180 && let Some(mut target_inv) = inventories.get_mut(target)
181 {
182 for item in items {
183 if target_inv.push(item).is_err() {
185 error!(
186 "Failed to insert dialogue given item despite target \
187 inventory claiming to have space, dropping remaining \
188 items..."
189 );
190 break;
191 }
192 }
193 } else {
194 break;
196 }
197 }
198 },
199 }
200
201 let dialogue = dialogue.into_validated_unchecked();
202
203 if let Some(agent) = agents.get_mut(target) {
204 agent
205 .inbox
206 .push_back(AgentEvent::Dialogue(*sender_uid, dialogue.clone()));
207 }
208
209 if let Some(client) = clients.get(target) {
210 client.send_fallible(ServerGeneral::Dialogue(*sender_uid, dialogue));
211 }
212 }
213 }
214 }
215}
216
217impl ServerEvent for SetPetStayEvent {
218 type SystemData<'a> = (
219 WriteStorage<'a, comp::Agent>,
220 WriteStorage<'a, comp::CharacterActivity>,
221 ReadStorage<'a, comp::Pos>,
222 ReadStorage<'a, comp::Alignment>,
223 ReadStorage<'a, Is<Mount>>,
224 ReadStorage<'a, Uid>,
225 );
226
227 fn handle(
228 events: impl ExactSizeIterator<Item = Self>,
229 (mut agents, mut character_activities, positions, alignments, is_mounts, uids): Self::SystemData<'_>,
230 ) {
231 for SetPetStayEvent(command_giver, pet, stay) in events {
232 let is_owner = uids.get(command_giver).is_some_and(|owner_uid| {
233 matches!(
234 alignments.get(pet),
235 Some(comp::Alignment::Owned(pet_owner)) if *pet_owner == *owner_uid,
236 )
237 });
238
239 let current_pet_position = positions.get(pet).copied();
240 let stay = stay && current_pet_position.is_some();
241 if is_owner
242 && within_mounting_range(positions.get(command_giver), positions.get(pet))
243 && is_mounts.get(pet).is_none()
244 {
245 character_activities
246 .get_mut(pet)
247 .map(|mut activity| activity.is_pet_staying = stay);
248 agents
249 .get_mut(pet)
250 .map(|s| s.stay_pos = current_pet_position.filter(|_| stay));
251 }
252 }
253 }
254}
255
256#[derive(Deserialize)]
257struct ResourceExperienceManifest(HashMap<String, u32>);
258
259impl assets::Asset for ResourceExperienceManifest {
260 type Loader = assets::RonLoader;
261
262 const EXTENSION: &'static str = "ron";
263}
264impl Concatenate for ResourceExperienceManifest {
265 fn concatenate(self, b: Self) -> Self { Self(self.0.concatenate(b.0)) }
266}
267
268lazy_static! {
269 static ref RESOURCE_EXPERIENCE_MANIFEST: assets::AssetHandle<ResourceExperienceManifest> =
270 assets::AssetCombined::load_expect_combined_static(
271 "server.manifests.resource_experience_manifest"
272 );
273}
274
275impl ServerEvent for MineBlockEvent {
276 type SystemData<'a> = (
277 WriteExpect<'a, BlockChange>,
278 ReadExpect<'a, TerrainGrid>,
279 ReadExpect<'a, MaterialStatManifest>,
280 ReadExpect<'a, AbilityMap>,
281 ReadExpect<'a, EventBus<CreateItemDropEvent>>,
282 ReadExpect<'a, EventBus<SoundEvent>>,
283 ReadExpect<'a, EventBus<Outcome>>,
284 ReadExpect<'a, ProgramTime>,
285 ReadExpect<'a, Time>,
286 WriteStorage<'a, comp::SkillSet>,
287 ReadStorage<'a, Uid>,
288 );
289
290 fn handle(
291 events: impl ExactSizeIterator<Item = Self>,
292 (
293 mut block_change,
294 terrain,
295 msm,
296 ability_map,
297 create_item_drop_events,
298 sound_events,
299 outcomes,
300 program_time,
301 time,
302 mut skill_sets,
303 uids,
304 ): Self::SystemData<'_>,
305 ) {
306 use rand::Rng;
307 let mut rng = rand::thread_rng();
308 let mut create_item_drop_emitter = create_item_drop_events.emitter();
309 let mut sound_event_emitter = sound_events.emitter();
310 let mut outcome_emitter = outcomes.emitter();
311 for ev in events {
312 if block_change.can_set_block(ev.pos) {
313 let block = terrain.get(ev.pos).ok().copied();
314 if let Some(mut block) =
315 block.filter(|b| b.mine_tool().is_some_and(|t| Some(t) == ev.tool))
316 {
317 let damage = if let Ok(damage) = block.get_attr::<terrain::sprite::Damage>() {
319 let updated_damage = damage.0.saturating_add(1);
320 block
321 .set_attr(terrain::sprite::Damage(updated_damage))
322 .expect(
323 "We just read the Damage attribute from the block, writing should \
324 be possible too",
325 );
326
327 Some(updated_damage)
328 } else {
329 None
330 };
331
332 let sprite = block.get_sprite();
333
334 let is_broken = damage
336 .and_then(|damage| Some((sprite?.required_mine_damage(), damage)))
337 .is_some_and(|(required_damage, damage)| {
338 required_damage.is_none_or(|required| damage >= required)
339 });
340
341 let stage_changed = damage
343 .and_then(|damage| Some((sprite?.mine_drop_interval(), damage)))
344 .is_some_and(|(interval, damage)| damage % interval == 0);
345
346 if (stage_changed || is_broken)
347 && let Some(items) = comp::Item::try_reclaim_from_block(block)
348 {
349 let mut items: Vec<_> =
350 flatten_counted_items(&items, &ability_map, &msm).collect();
351 let maybe_uid = uids.get(ev.entity).copied();
352
353 if let Some(mut skillset) = skill_sets.get_mut(ev.entity) {
354 use common::comp::skills::{MiningSkill, SKILL_MODIFIERS, Skill};
355
356 if is_broken
357 && let (Some(tool), Some(uid), exp_reward @ 1..) = (
358 ev.tool,
359 maybe_uid,
360 items
361 .iter()
362 .filter_map(|item| {
363 item.item_definition_id().itemdef_id().and_then(|id| {
364 RESOURCE_EXPERIENCE_MANIFEST
365 .read()
366 .0
367 .get(id)
368 .copied()
369 })
370 })
371 .sum(),
372 )
373 {
374 let skill_group = comp::SkillGroupKind::Weapon(tool);
375 if let Some(level_outcome) =
376 skillset.add_experience(skill_group, exp_reward)
377 {
378 outcome_emitter.emit(Outcome::SkillPointGain {
379 uid,
380 skill_tree: skill_group,
381 total_points: level_outcome,
382 });
383 }
384 outcome_emitter.emit(Outcome::ExpChange {
385 uid,
386 exp: exp_reward,
387 xp_pools: HashSet::from([skill_group]),
388 });
389 }
390
391 let stage_ore_chance = || {
392 let chance_mod = f64::from(SKILL_MODIFIERS.mining_tree.ore_gain);
393 let skill_level = skillset
394 .skill_level(Skill::Pick(MiningSkill::OreGain))
395 .unwrap_or(0);
396
397 chance_mod * f64::from(skill_level)
398 };
399 let stage_gem_chance = || {
400 let chance_mod = f64::from(SKILL_MODIFIERS.mining_tree.gem_gain);
401 let skill_level = skillset
402 .skill_level(Skill::Pick(MiningSkill::GemGain))
403 .unwrap_or(0);
404
405 chance_mod * f64::from(skill_level)
406 };
407
408 if !is_broken {
411 items.retain(|item| {
412 rng.gen_bool(
413 0.5 + item
414 .item_definition_id()
415 .itemdef_id()
416 .map(|id| {
417 if id.contains("mineral.ore.") {
418 stage_ore_chance()
419 } else if id.contains("mineral.gem.") {
420 stage_gem_chance()
421 } else {
422 0.0
423 }
424 })
425 .unwrap_or(0.0),
426 )
427 });
428 }
429 }
430 for item in items {
431 let loot_owner = maybe_uid
432 .map(LootOwnerKind::Player)
433 .map(|owner| comp::LootOwner::new(owner, false));
434 create_item_drop_emitter.emit(CreateItemDropEvent {
435 pos: comp::Pos(ev.pos.map(|e| e as f32) + Vec3::broadcast(0.5)),
436 vel: comp::Vel(
437 Vec2::unit_x()
438 .rotated_z(rng.gen::<f32>() * PI * 2.0)
439 .mul(4.0)
440 .with_z(rng.gen_range(5.0..10.0)),
441 ),
442 ori: comp::Ori::from(Dir::random_2d(&mut rng)),
443 item: comp::PickupItem::new(item, *program_time),
444 loot_owner,
445 });
446 }
447 }
448
449 if damage.is_some() && !is_broken {
450 block_change.set(ev.pos, block);
451 } else {
452 block_change.set(ev.pos, block.into_vacant());
453 }
454 outcome_emitter.emit(if is_broken {
455 Outcome::BreakBlock {
456 pos: ev.pos,
457 tool: ev.tool,
458 color: block.get_color(),
459 }
460 } else {
461 Outcome::DamagedBlock {
462 pos: ev.pos,
463 stage_changed,
464 tool: ev.tool,
465 }
466 });
467
468 sound_event_emitter.emit(SoundEvent {
470 sound: Sound::new(SoundKind::Mine, ev.pos.as_(), 20.0, time.0),
471 });
472 }
473 }
474 }
475 }
476}
477
478impl ServerEvent for SoundEvent {
479 type SystemData<'a> = (
480 ReadExpect<'a, EventBus<Outcome>>,
481 WriteStorage<'a, comp::Agent>,
482 ReadStorage<'a, comp::Pos>,
483 );
484
485 fn handle(
486 events: impl ExactSizeIterator<Item = Self>,
487 (outcomes, mut agents, positions): Self::SystemData<'_>,
488 ) {
489 let mut outcome_emitter = outcomes.emitter();
490 for SoundEvent { sound } in events {
491 for (agent, agent_pos) in (&mut agents, &positions).join() {
494 let agent_dist_sqrd = agent_pos.0.distance_squared(sound.pos);
496 let sound_travel_dist_sqrd = (sound.vol * SOUND_TRAVEL_DIST_PER_VOLUME).powi(2);
497
498 let vol_dropoff = agent_dist_sqrd / sound_travel_dist_sqrd * sound.vol;
499 let propagated_sound = sound.with_new_vol(sound.vol - vol_dropoff);
500
501 let can_hear_sound = propagated_sound.vol > 0.00;
502 let should_hear_sound = agent_dist_sqrd < agent.psyche.listen_dist.powi(2);
503
504 if can_hear_sound && should_hear_sound {
505 agent
506 .inbox
507 .push_back(AgentEvent::ServerSound(propagated_sound));
508 }
509 }
510
511 if let Some(outcome) = match sound.kind {
513 SoundKind::Utterance(kind, body) => Some(Outcome::Utterance {
514 kind,
515 pos: sound.pos,
516 body,
517 }),
518 _ => None,
519 } {
520 outcome_emitter.emit(outcome);
521 }
522 }
523 }
524}
525
526impl ServerEvent for CreateSpriteEvent {
527 type SystemData<'a> = (
528 WriteExpect<'a, BlockChange>,
529 WriteExpect<'a, ScheduledBlockChange>,
530 ReadExpect<'a, TerrainGrid>,
531 ReadExpect<'a, Time>,
532 );
533
534 fn handle(
535 events: impl ExactSizeIterator<Item = Self>,
536 (mut block_change, mut scheduled_block_change, terrain, time): Self::SystemData<'_>,
537 ) {
538 for ev in events {
539 if block_change.can_set_block(ev.pos) {
540 let block = terrain.get(ev.pos).ok().copied();
541 if block.is_some_and(|b| (*b).is_fluid()) {
542 let old_block = block.unwrap_or_else(|| Block::air(SpriteKind::Empty));
543 let new_block = old_block.with_sprite(ev.sprite);
544 block_change.set(ev.pos, new_block);
545 if let Some((timeout, del_offset)) = ev.del_timeout {
547 use rand::Rng;
548 let mut rng = rand::thread_rng();
549 let offset = rng.gen_range(0.0..del_offset);
550 let current_time: f64 = time.0;
551 let replace_time = current_time + (timeout + offset) as f64;
552 if old_block != new_block {
553 scheduled_block_change.set(ev.pos, old_block, replace_time);
554 scheduled_block_change.outcome_set(ev.pos, new_block, replace_time);
555 }
556 }
557 }
558 }
559 }
560 }
561}
562
563impl ServerEvent for ToggleSpriteLightEvent {
564 type SystemData<'a> = (
565 WriteExpect<'a, BlockChange>,
566 ReadExpect<'a, TerrainGrid>,
567 ReadStorage<'a, comp::Pos>,
568 );
569
570 fn handle(
571 events: impl ExactSizeIterator<Item = Self>,
572 (mut block_change, terrain, positions): Self::SystemData<'_>,
573 ) {
574 for ev in events.into_iter() {
575 if let Some(entity_pos) = positions.get(ev.entity)
576 && entity_pos.0.distance_squared(ev.pos.as_()) < MAX_INTERACT_RANGE.powi(2)
577 && block_change.can_set_block(ev.pos)
578 {
579 if let Some(new_block) = terrain
580 .get(ev.pos)
581 .ok()
582 .and_then(|block| block.with_toggle_light(ev.enable))
583 {
584 block_change.set(ev.pos, new_block);
585 }
587 }
588 }
589 }
590}
591
592pub fn handle_tame_pet(server: &mut Server, ev: TamePetEvent) {
593 tame_pet(server.state.ecs(), ev.pet_entity, ev.owner_entity);
596}