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