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