1use crate::{
2 Canvas,
3 sim::{SimChunk, WorldSim},
4 util::{Sampler, UnitChooser, seed_expan},
5};
6use common::{
7 generation::EntityInfo,
8 spot::{RON_SPOT_PROPERTIES, SpotCondition, SpotProperties},
9 terrain::{BiomeKind, Structure, TerrainChunkSize},
10 vol::RectVolSize,
11};
12use rand::prelude::*;
13use rand_chacha::ChaChaRng;
14use std::ops::Range;
15use vek::*;
16
17#[derive(Copy, Clone, Debug)]
35pub enum Spot {
36 DwarvenGrave,
37 SaurokAltar,
38 MyrmidonTemple,
39 GnarlingTotem,
40 WitchHouse,
41 GnomeSpring,
42 WolfBurrow,
43 Igloo,
44 LionRock,
52 TreeStumpForest,
53 DesertBones,
54 Arch,
55 AirshipCrash,
56 FruitTree,
57 Shipwreck,
58 Shipwreck2,
59 FallenTree,
60 GraveSmall,
61 JungleTemple,
62 SaurokTotem,
63 JungleOutpost,
64 RonFile(&'static SpotProperties),
65}
66
67impl Spot {
68 pub fn generate(world: &mut WorldSim) {
69 use BiomeKind::*;
70 for s in RON_SPOT_PROPERTIES.0.iter() {
73 Self::generate_spots(
74 Spot::RonFile(s),
75 world,
76 s.freq,
77 |g, c| is_valid(&s.condition, g, c),
78 s.spawn,
79 );
80 }
81 Self::generate_spots(
82 Spot::WitchHouse,
83 world,
84 1.0,
85 |g, c| {
86 g < 0.25
87 && !c.near_cliffs()
88 && !c.river.near_water()
89 && !c.path.0.is_way()
90 && c.sites.is_empty()
91 && matches!(
92 c.get_biome(),
93 Grassland | Forest | Taiga | Snowland | Jungle
94 )
95 },
96 false,
97 );
98 Self::generate_spots(
99 Spot::Igloo,
100 world,
101 2.0,
102 |g, c| {
103 g < 0.5
104 && !c.near_cliffs()
105 && !c.river.near_water()
106 && !c.path.0.is_way()
107 && c.sites.is_empty()
108 && matches!(c.get_biome(), Snowland)
109 },
110 false,
111 );
112 Self::generate_spots(
113 Spot::SaurokAltar,
114 world,
115 1.0,
116 |g, c| {
117 g < 0.25
118 && !c.near_cliffs()
119 && !c.river.near_water()
120 && !c.path.0.is_way()
121 && c.sites.is_empty()
122 && matches!(c.get_biome(), Jungle | Forest)
123 },
124 false,
125 );
126 Self::generate_spots(
127 Spot::SaurokTotem,
128 world,
129 1.0,
130 |g, c| {
131 g < 0.25
132 && !c.near_cliffs()
133 && !c.river.near_water()
134 && !c.path.0.is_way()
135 && c.sites.is_empty()
136 && matches!(c.get_biome(), Jungle | Forest)
137 },
138 false,
139 );
140 Self::generate_spots(
141 Spot::JungleOutpost,
142 world,
143 1.0,
144 |g, c| {
145 g < 0.25
146 && !c.near_cliffs()
147 && !c.river.near_water()
148 && !c.path.0.is_way()
149 && c.sites.is_empty()
150 && matches!(c.get_biome(), Jungle | Forest)
151 },
152 false,
153 );
154 Self::generate_spots(
155 Spot::JungleTemple,
156 world,
157 0.5,
158 |g, c| {
159 g < 0.25
160 && !c.near_cliffs()
161 && !c.river.near_water()
162 && !c.path.0.is_way()
163 && c.sites.is_empty()
164 && matches!(c.get_biome(), Jungle | Forest)
165 },
166 false,
167 );
168 Self::generate_spots(
169 Spot::MyrmidonTemple,
170 world,
171 1.0,
172 |g, c| {
173 g < 0.1
174 && !c.near_cliffs()
175 && !c.river.near_water()
176 && !c.path.0.is_way()
177 && c.sites.is_empty()
178 && matches!(c.get_biome(), Desert | Jungle)
179 },
180 false,
181 );
182 Self::generate_spots(
183 Spot::GnarlingTotem,
184 world,
185 1.5,
186 |g, c| {
187 g < 0.25
188 && !c.near_cliffs()
189 && !c.river.near_water()
190 && !c.path.0.is_way()
191 && c.sites.is_empty()
192 && matches!(c.get_biome(), Forest | Grassland)
193 },
194 false,
195 );
196 Self::generate_spots(
197 Spot::FallenTree,
198 world,
199 1.5,
200 |g, c| {
201 g < 0.25
202 && !c.near_cliffs()
203 && !c.river.near_water()
204 && !c.path.0.is_way()
205 && c.sites.is_empty()
206 && matches!(c.get_biome(), Forest | Grassland)
207 },
208 false,
209 );
210 Self::generate_spots(
213 Spot::LionRock,
214 world,
215 1.5,
216 |g, c| {
217 g < 0.25
218 && !c.near_cliffs()
219 && !c.river.near_water()
220 && !c.path.0.is_way()
221 && c.sites.is_empty()
222 && matches!(c.get_biome(), Savannah)
223 },
224 false,
225 );
226 Self::generate_spots(
227 Spot::WolfBurrow,
228 world,
229 1.5,
230 |g, c| {
231 g < 0.25
232 && !c.near_cliffs()
233 && !c.river.near_water()
234 && !c.path.0.is_way()
235 && c.sites.is_empty()
236 && matches!(c.get_biome(), Forest | Grassland)
237 },
238 false,
239 );
240 Self::generate_spots(
241 Spot::TreeStumpForest,
242 world,
243 20.0,
244 |g, c| {
245 g < 0.25
246 && !c.near_cliffs()
247 && !c.river.near_water()
248 && !c.path.0.is_way()
249 && c.sites.is_empty()
250 && matches!(c.get_biome(), Jungle | Forest)
251 },
252 true,
253 );
254 Self::generate_spots(
255 Spot::DesertBones,
256 world,
257 6.0,
258 |g, c| {
259 g < 0.25
260 && !c.near_cliffs()
261 && !c.river.near_water()
262 && !c.path.0.is_way()
263 && c.sites.is_empty()
264 && matches!(c.get_biome(), Desert)
265 },
266 false,
267 );
268 Self::generate_spots(
269 Spot::Arch,
270 world,
271 2.0,
272 |g, c| {
273 g < 0.25
274 && !c.near_cliffs()
275 && !c.river.near_water()
276 && !c.path.0.is_way()
277 && c.sites.is_empty()
278 && matches!(c.get_biome(), Desert)
279 },
280 false,
281 );
282 Self::generate_spots(
283 Spot::AirshipCrash,
284 world,
285 0.7,
286 |g, c| {
287 g < 0.25
288 && !c.near_cliffs()
289 && !c.river.near_water()
290 && !c.path.0.is_way()
291 && c.sites.is_empty()
292 && !matches!(c.get_biome(), Mountain | Void | Ocean)
293 },
294 false,
295 );
296 Self::generate_spots(
297 Spot::FruitTree,
298 world,
299 20.0,
300 |g, c| {
301 g < 0.25
302 && !c.near_cliffs()
303 && !c.river.near_water()
304 && !c.path.0.is_way()
305 && c.sites.is_empty()
306 && matches!(c.get_biome(), Forest)
307 },
308 true,
309 );
310 Self::generate_spots(
311 Spot::GnomeSpring,
312 world,
313 1.0,
314 |g, c| {
315 g < 0.25
316 && !c.near_cliffs()
317 && !c.river.near_water()
318 && !c.path.0.is_way()
319 && c.sites.is_empty()
320 && matches!(c.get_biome(), Forest)
321 },
322 false,
323 );
324 Self::generate_spots(
325 Spot::Shipwreck,
326 world,
327 1.0,
328 |g, c| {
329 g < 0.25 && c.is_underwater() && c.sites.is_empty() && c.water_alt > c.alt + 30.0
330 },
331 true,
332 );
333 Self::generate_spots(
334 Spot::Shipwreck2,
335 world,
336 1.0,
337 |g, c| {
338 g < 0.25 && c.is_underwater() && c.sites.is_empty() && c.water_alt > c.alt + 30.0
339 },
340 true,
341 );
342 Self::generate_spots(
344 Spot::GraveSmall,
345 world,
346 2.0,
347 |g, c| {
348 g < 0.25
349 && !c.near_cliffs()
350 && !c.river.near_water()
351 && !c.path.0.is_way()
352 && c.sites.is_empty()
353 && matches!(c.get_biome(), Forest | Taiga | Jungle | Grassland)
354 },
355 false,
356 );
357
358 }
371
372 fn generate_spots(
373 spot: Spot,
375 world: &mut WorldSim,
376 freq: f32,
378 mut valid: impl FnMut(f32, &SimChunk) -> bool,
382 spawn: bool,
384 ) {
385 let world_size = world.get_size();
386 for _ in
387 0..(world_size.product() as f32 * TerrainChunkSize::RECT_SIZE.product() as f32 * freq
388 / 1000.0f32.powi(2))
389 .ceil() as u64
390 {
391 let pos = world_size.map(|e| (world.rng.gen_range(0..e) & !0b11) as i32);
392 if let Some((_, chunk)) = world
393 .get_gradient_approx(pos)
394 .zip(world.get_mut(pos))
395 .filter(|(grad, chunk)| valid(*grad, chunk))
396 {
397 chunk.spot = Some(spot);
398 if !spawn {
399 chunk.tree_density = 0.0;
400 chunk.spawn_rate = 0.0;
401 }
402 }
403 }
404 }
405}
406
407pub fn apply_spots_to(canvas: &mut Canvas, _dynamic_rng: &mut impl Rng) {
408 let nearby_spots = canvas.nearby_spots().collect::<Vec<_>>();
409
410 for (spot_wpos2d, spot, seed) in nearby_spots.iter().copied() {
411 let mut rng = ChaChaRng::from_seed(seed_expan::rng_state(seed));
412
413 let units = UnitChooser::new(seed).get(seed).into();
414
415 #[derive(Default)]
416 struct SpotConfig<'a> {
417 base_structures: Option<&'a str>,
420 entity_radius: f32,
422 entities: &'a [(Range<i32>, &'a str)],
427 }
428
429 let spot_config = match spot {
430 Spot::DwarvenGrave => SpotConfig {
432 base_structures: Some("spots_grasslands.dwarven_grave"),
433 entity_radius: 60.0,
434 entities: &[(6..12, "common.entity.spot.dwarf_grave_robber")],
435 },
436 Spot::SaurokAltar => SpotConfig {
437 base_structures: Some("spots.jungle.saurok-altar"),
438 entity_radius: 12.0,
439 entities: &[
440 (0..3, "common.entity.wild.aggressive.occult_saurok"),
441 (0..3, "common.entity.wild.aggressive.sly_saurok"),
442 (0..3, "common.entity.wild.aggressive.mighty_saurok"),
443 ],
444 },
445 Spot::SaurokTotem => SpotConfig {
446 base_structures: Some("spots.jungle.saurok_totem"),
447 entity_radius: 20.0,
448 entities: &[
449 (0..3, "common.entity.wild.aggressive.occult_saurok"),
450 (0..3, "common.entity.wild.aggressive.sly_saurok"),
451 (0..3, "common.entity.wild.aggressive.mighty_saurok"),
452 ],
453 },
454 Spot::JungleOutpost => SpotConfig {
455 base_structures: Some("spots.jungle.outpost"),
456 entity_radius: 40.0,
457 entities: &[(6..12, "common.entity.spot.grim_salvager")],
458 },
459 Spot::JungleTemple => SpotConfig {
460 base_structures: Some("spots.jungle.temple_small"),
461 entity_radius: 40.0,
462 entities: &[
463 (2..8, "common.entity.wild.aggressive.occult_saurok"),
464 (2..8, "common.entity.wild.aggressive.sly_saurok"),
465 (2..8, "common.entity.wild.aggressive.mighty_saurok"),
466 ],
467 },
468 Spot::MyrmidonTemple => SpotConfig {
469 base_structures: Some("spots.myrmidon-temple"),
470 entity_radius: 10.0,
471 entities: &[
472 (3..5, "common.entity.dungeon.myrmidon.hoplite"),
473 (3..5, "common.entity.dungeon.myrmidon.strategian"),
474 (2..3, "common.entity.dungeon.myrmidon.marksman"),
475 ],
476 },
477 Spot::WitchHouse => SpotConfig {
478 base_structures: Some("spots_general.witch_hut"),
479 entity_radius: 1.0,
480 entities: &[
481 (1..2, "common.entity.spot.witch_dark"),
482 (0..4, "common.entity.wild.peaceful.cat"),
483 (0..3, "common.entity.wild.peaceful.frog"),
484 ],
485 },
486 Spot::Igloo => SpotConfig {
487 base_structures: Some("spots_general.igloo"),
488 entity_radius: 2.0,
489 entities: &[
490 (3..5, "common.entity.dungeon.adlet.hunter"),
491 (3..5, "common.entity.dungeon.adlet.icepicker"),
492 (2..3, "common.entity.dungeon.adlet.tracker"),
493 ],
494 },
495 Spot::GnarlingTotem => SpotConfig {
496 base_structures: Some("site_structures.gnarling.totem"),
497 entity_radius: 30.0,
498 entities: &[
499 (3..5, "common.entity.dungeon.gnarling.mugger"),
500 (3..5, "common.entity.dungeon.gnarling.stalker"),
501 (3..5, "common.entity.dungeon.gnarling.logger"),
502 (2..4, "common.entity.dungeon.gnarling.mandragora"),
503 (1..3, "common.entity.wild.aggressive.deadwood"),
504 (1..2, "common.entity.dungeon.gnarling.woodgolem"),
505 ],
506 },
507 Spot::FallenTree => SpotConfig {
508 base_structures: Some("spots_grasslands.fallen_tree"),
509 entity_radius: 64.0,
510 entities: &[
511 (1..2, "common.entity.dungeon.gnarling.mandragora"),
512 (2..6, "common.entity.wild.aggressive.deadwood"),
513 (0..2, "common.entity.wild.aggressive.mossdrake"),
514 ],
515 },
516 Spot::LionRock => SpotConfig {
518 base_structures: Some("spots_savannah.lion_rock"),
519 entity_radius: 30.0,
520 entities: &[
521 (5..10, "common.entity.spot.female_lion"),
522 (1..2, "common.entity.wild.aggressive.male_lion"),
523 ],
524 },
525 Spot::WolfBurrow => SpotConfig {
526 base_structures: Some("spots_savannah.wolf_burrow"),
527 entity_radius: 10.0,
528 entities: &[(5..8, "common.entity.wild.aggressive.wolf")],
529 },
530 Spot::TreeStumpForest => SpotConfig {
531 base_structures: Some("trees.oak_stumps"),
532 entity_radius: 30.0,
533 entities: &[(0..2, "common.entity.wild.aggressive.deadwood")],
534 },
535 Spot::DesertBones => SpotConfig {
536 base_structures: Some("spots.bones"),
537 entity_radius: 40.0,
538 entities: &[(4..9, "common.entity.wild.aggressive.hyena")],
539 },
540 Spot::Arch => SpotConfig {
541 base_structures: Some("spots.arch"),
542 entity_radius: 50.0,
543 entities: &[],
544 },
545 Spot::AirshipCrash => SpotConfig {
546 base_structures: Some("trees.airship_crash"),
547 entity_radius: 20.0,
548 entities: &[(4..9, "common.entity.spot.grim_salvager")],
549 },
550 Spot::FruitTree => SpotConfig {
551 base_structures: Some("trees.fruit_trees"),
552 entity_radius: 2.0,
553 entities: &[(0..2, "common.entity.wild.peaceful.bear")],
554 },
555 Spot::GnomeSpring => SpotConfig {
556 base_structures: Some("spots.gnome_spring"),
557 entity_radius: 40.0,
558 entities: &[(7..10, "common.entity.spot.gnome.spear")],
559 },
560 Spot::Shipwreck => SpotConfig {
561 base_structures: Some("spots.water.shipwreck"),
562 entity_radius: 2.0,
563 entities: &[(0..2, "common.entity.wild.peaceful.clownfish")],
564 },
565 Spot::Shipwreck2 => SpotConfig {
566 base_structures: Some("spots.water.shipwreck2"),
567 entity_radius: 20.0,
568 entities: &[(0..3, "common.entity.wild.peaceful.clownfish")],
569 },
570 Spot::GraveSmall => SpotConfig {
571 base_structures: Some("spots.grave_small"),
572 entity_radius: 2.0,
573 entities: &[],
574 },
575 Spot::RonFile(properties) => SpotConfig {
576 base_structures: Some(&properties.base_structures),
577 entity_radius: 1.0,
578 entities: &[],
579 },
580 };
581 if let Some(base_structures) = spot_config.base_structures {
583 let structures = Structure::load_group(base_structures).read();
584 let structure = structures.choose(&mut rng).unwrap();
585 let origin = spot_wpos2d.with_z(
586 canvas
587 .col_or_gen(spot_wpos2d)
588 .map(|c| c.alt as i32)
589 .unwrap_or(0),
590 );
591 canvas.blit_structure(origin, structure, seed, units, true);
592 }
593
594 const PHI: f32 = 1.618;
596 for (spawn_count, spec) in spot_config.entities {
597 let spawn_count = rng.gen_range(spawn_count.clone()).max(0);
598
599 let dir_offset = rng.gen::<f32>();
600 for i in 0..spawn_count {
601 let dir = Vec2::new(
602 ((dir_offset + i as f32 * PHI) * std::f32::consts::TAU).sin(),
603 ((dir_offset + i as f32 * PHI) * std::f32::consts::TAU).cos(),
604 );
605 let dist = i as f32 / spawn_count as f32 * spot_config.entity_radius;
606 let wpos2d = spot_wpos2d + (dir * dist).map(|e| e.round() as i32);
607
608 let alt = canvas.col_or_gen(wpos2d).map(|c| c.alt as i32).unwrap_or(0);
609
610 if let Some(wpos) = canvas
611 .area()
612 .contains_point(wpos2d)
613 .then(|| canvas.find_spawn_pos(wpos2d.with_z(alt)))
614 .flatten()
615 {
616 canvas.spawn(
617 EntityInfo::at(wpos.map(|e| e as f32) + Vec3::new(0.5, 0.5, 0.0))
618 .with_asset_expect(spec, &mut rng, None),
619 );
620 }
621 }
622 }
623 }
624}
625
626pub fn is_valid(condition: &SpotCondition, g: f32, c: &SimChunk) -> bool {
627 c.sites.is_empty()
628 && match condition {
629 SpotCondition::MaxGradient(value) => g < *value,
630 SpotCondition::Biome(biomes) => biomes.contains(&c.get_biome()),
631 SpotCondition::NearCliffs => c.near_cliffs(),
632 SpotCondition::NearRiver => c.river.near_water(),
633 SpotCondition::IsWay => c.path.0.is_way(),
634 SpotCondition::IsUnderwater => c.is_underwater(),
635 SpotCondition::Typical => {
636 !c.near_cliffs() && !c.river.near_water() && !c.path.0.is_way()
637 },
638 SpotCondition::MinWaterDepth(depth) => {
639 is_valid(&SpotCondition::IsUnderwater, g, c) && c.water_alt > c.alt + depth
640 },
641 SpotCondition::Not(condition) => !is_valid(condition, g, c),
642 SpotCondition::All(conditions) => conditions.iter().all(|cond| is_valid(cond, g, c)),
643 SpotCondition::Any(conditions) => conditions.iter().any(|cond| is_valid(cond, g, c)),
644 }
645}