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