veloren_rtsim/rule/
sync_npcs.rs

1use crate::{
2    RtState, Rule, RuleError,
3    event::{EventCtx, OnDeath, OnHealthChange, OnSetup, OnTick},
4};
5use common::{
6    grid::Grid,
7    rtsim::{Actor, NpcInput},
8    terrain::CoordinateConversions,
9};
10
11pub struct SyncNpcs;
12
13impl Rule for SyncNpcs {
14    fn start(rtstate: &mut RtState) -> Result<Self, RuleError> {
15        rtstate.bind::<Self, OnSetup>(on_setup);
16        rtstate.bind::<Self, OnDeath>(on_death);
17        rtstate.bind::<Self, OnHealthChange>(on_health_change);
18        rtstate.bind::<Self, OnTick>(on_tick);
19
20        Ok(Self)
21    }
22}
23
24fn on_setup(ctx: EventCtx<SyncNpcs, OnSetup>) {
25    let data = &mut *ctx.state.data_mut();
26
27    // Create NPC grid
28    data.npcs.npc_grid = Grid::new(ctx.world.sim().get_size().as_(), Default::default());
29
30    // Add NPCs to home population
31    for (npc_id, npc) in data.npcs.npcs.iter() {
32        if let Some(home) = npc.home.and_then(|home| data.sites.get_mut(home)) {
33            home.population.insert(npc_id);
34        }
35    }
36
37    // Update the list of nearest sites by size for each site
38    let sites_iter = data.sites.iter().filter_map(|(site_id, site)| {
39        let site2 = site
40            .world_site
41            .and_then(|ws| ctx.index.sites.get(ws).site2())?;
42        Some((site_id, site, site2))
43    });
44    let nearest_by_size = sites_iter.clone()
45        .map(|(site_id, site, site2)| {
46            let mut other_sites = sites_iter.clone()
47                // Only include sites in the list if they're not the current one and they're more populus
48                .filter(|(other_id, _, other_site2)| *other_id != site_id && other_site2.plots().len() > site2.plots().len())
49                .collect::<Vec<_>>();
50            other_sites.sort_by_key(|(_, other, _)| other.wpos.as_::<i64>().distance_squared(site.wpos.as_::<i64>()));
51            let mut max_size = 0;
52            // Remove sites that aren't in increasing order of size (Stalin sort?!)
53            other_sites.retain(|(_, _, other_site2)| {
54                if other_site2.plots().len() > max_size {
55                    max_size = other_site2.plots().len();
56                    true
57                } else {
58                    false
59                }
60            });
61            let nearest_by_size = other_sites
62                .into_iter()
63                .map(|(site_id, _, _)| site_id)
64                .collect::<Vec<_>>();
65            (site_id, nearest_by_size)
66        })
67        .collect::<Vec<_>>();
68    for (site_id, nearest_by_size) in nearest_by_size {
69        if let Some(site) = data.sites.get_mut(site_id) {
70            site.nearby_sites_by_size = nearest_by_size;
71        }
72    }
73}
74
75fn on_health_change(ctx: EventCtx<SyncNpcs, OnHealthChange>) {
76    let data = &mut *ctx.state.data_mut();
77
78    // As this handler does not correctly handle death, ignore events that set the
79    // health fraction to 0 (dead)
80    if ctx.event.new_health_fraction != 0.0
81        && let Actor::Npc(npc_id) = ctx.event.actor
82    {
83        if let Some(npc) = data.npcs.get_mut(npc_id) {
84            npc.health_fraction = ctx.event.new_health_fraction;
85        }
86    }
87}
88
89fn on_death(ctx: EventCtx<SyncNpcs, OnDeath>) {
90    let data = &mut *ctx.state.data_mut();
91
92    if let Actor::Npc(npc_id) = ctx.event.actor {
93        if let Some(npc) = data.npcs.get_mut(npc_id) {
94            // Mark the NPC as dead, allowing us to clear them up later
95            npc.health_fraction = 0.0;
96        }
97    }
98}
99
100fn on_tick(ctx: EventCtx<SyncNpcs, OnTick>) {
101    let data = &mut *ctx.state.data_mut();
102    for (npc_id, npc) in data.npcs.npcs.iter_mut() {
103        // Update the NPC's current site, if any
104        npc.current_site = ctx
105            .world
106            .sim()
107            .get(npc.wpos.xy().as_().wpos_to_cpos())
108            .and_then(|chunk| {
109                chunk
110                    .sites
111                    .iter()
112                    .find_map(|site| data.sites.world_site_map.get(site).copied())
113            });
114
115        // Share known reports with current site, if it's our home
116        // TODO: Only share new reports
117        if let Some(current_site) = npc.current_site
118            && Some(current_site) == npc.home
119        {
120            if let Some(site) = data.sites.get_mut(current_site) {
121                // TODO: Sites should have an inbox and their own AI code
122                site.known_reports.extend(npc.known_reports.iter().copied());
123                npc.inbox.extend(
124                    site.known_reports
125                        .iter()
126                        .copied()
127                        .filter(|report| !npc.known_reports.contains(report))
128                        .map(NpcInput::Report),
129                );
130            }
131        }
132
133        // Update the NPC's grid cell
134        let chunk_pos = npc.wpos.xy().as_().wpos_to_cpos();
135        if npc.chunk_pos != Some(chunk_pos) {
136            if let Some(cell) = npc
137                .chunk_pos
138                .and_then(|chunk_pos| data.npcs.npc_grid.get_mut(chunk_pos))
139            {
140                if let Some(index) = cell.npcs.iter().position(|id| *id == npc_id) {
141                    cell.npcs.swap_remove(index);
142                }
143            }
144            npc.chunk_pos = Some(chunk_pos);
145            if let Some(cell) = data.npcs.npc_grid.get_mut(chunk_pos) {
146                cell.npcs.push(npc_id);
147            }
148        }
149    }
150}