Merge branch 'zesterer/worldsim' into 'master'

World simulation, generation, and pathfinding improvements (including castles and caves)

See merge request veloren/veloren!1282
This commit is contained in:
Joshua Barretto 2020-08-12 21:40:56 +00:00
commit 75c1d44010
90 changed files with 3541 additions and 1117 deletions

View File

@ -49,11 +49,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Loading-Screen tips
- Feeding animation for some animals
- Power stat to weapons which affects weapon damage
- Add detection of entities under the cursor
- Add detection of entities under the cursor
- Functional group-system with exp-sharing and disabled damage to group members
- Some Campfire, fireball & bomb; particle, light & sound effects.
- Added firework recipe
- Added setting to change resolution
- Rare (unfinished) castles
- Caves with monsters and treasure
- Furniture and decals in towns
### Changed
@ -86,6 +89,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Dehardcoded many item variants
- Tooltips avoid the mouse better and disappear when hovered
- Improved social window functions and visuals
- Changed agent behaviour to allow fleeing
- Waypoints now spawn on dungeon staircases
### Removed

21
Cargo.lock generated
View File

@ -1189,6 +1189,26 @@ version = "1.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb1f6b1ce1c140482ea30ddd3335fc0024ac7ee112895426e0a629a6c20adfe3"
[[package]]
name = "enum-iterator"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c79a6321a1197d7730510c7e3f6cb80432dfefecb32426de8cea0aa19b4bb8d7"
dependencies = [
"enum-iterator-derive",
]
[[package]]
name = "enum-iterator-derive"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e94aa31f7c0dc764f57896dc615ddd76fc13b0d5dca7eb6cc5e018a5a09ec06"
dependencies = [
"proc-macro2 1.0.18",
"quote 1.0.7",
"syn 1.0.33",
]
[[package]]
name = "error-chain"
version = "0.12.2"
@ -4627,6 +4647,7 @@ dependencies = [
"criterion",
"crossbeam",
"dot_vox",
"enum-iterator",
"find_folder",
"hashbrown",
"image",

View File

@ -0,0 +1,12 @@
[
(20, Velorite),
(30, VeloriteFrag),
(60, Stones),
(15, PurpleFlower),
(130, ShortGrass),
(120, Mushroom),
(8, ShinyGem),
(15, Chest),
(2, Crate),
(0.25, Scarecrow),
]

View File

@ -7,17 +7,14 @@
(1, "common.items.food.coconut"),
// miscellaneous
(0.4, "common.items.ore.velorite"),
(0.6, "common.items.ore.veloritefrag"),
(0.6, "common.items.ore.veloritefrag"),
(0.1, "common.items.consumable.potion_minor"),
(0.01, "common.items.utility.collar"),
(0.01, "common.items.utility.bomb_pile"),
(0.1, "common.items.utility.bomb"),
// crafting ingredients
(0.5, "common.items.crafting_ing.shiny_gem"),
(2, "common.items.crafting_ing.leather_scraps"),
(1, "common.items.crafting_ing.empty_vial"),
(2, "common.items.crafting_ing.stones"),
(3, "common.items.crafting_ing.twigs"),
// swords
(0.1, "common.items.weapons.sword.starter_sword"),
(0.1, "common.items.weapons.sword.wood_sword"),
@ -137,7 +134,7 @@
// belts
(0.17, "common.items.armor.belt.cloth_blue_0"),
(0.17, "common.items.armor.belt.cloth_green_0"),
(0.17, "common.items.armor.belt.cloth_purple_0"),
(0.17, "common.items.armor.belt.cloth_purple_0"),
(0.08, "common.items.armor.belt.druid"),
(0.06, "common.items.armor.belt.leather_0"),
(0.06, "common.items.armor.belt.leather_2"),
@ -159,7 +156,7 @@
(0.025, "common.items.armor.chest.worker_red_0"),
(0.025, "common.items.armor.chest.worker_red_1"),
(0.025, "common.items.armor.chest.worker_yellow_0"),
(0.025, "common.items.armor.chest.worker_yellow_1"),
(0.025, "common.items.armor.chest.worker_yellow_1"),
(0.08, "common.items.armor.chest.druid"),
(0.06, "common.items.armor.chest.leather_0"),
(0.06, "common.items.armor.chest.leather_2"),
@ -171,7 +168,7 @@
// shoes
(0.15, "common.items.armor.foot.cloth_blue_0"),
(0.15, "common.items.armor.foot.cloth_green_0"),
(0.15, "common.items.armor.foot.cloth_purple_0"),
(0.15, "common.items.armor.foot.cloth_purple_0"),
(0.08, "common.items.armor.foot.druid"),
(0.06, "common.items.armor.foot.leather_0"),
(0.06, "common.items.armor.foot.leather_2"),
@ -184,7 +181,7 @@
(0.125, "common.items.armor.pants.cloth_blue_0"),
(0.125, "common.items.armor.pants.cloth_green_0"),
(0.125, "common.items.armor.pants.cloth_purple_0"),
(0.125, "common.items.armor.pants.worker_blue_0"),
(0.125, "common.items.armor.pants.worker_blue_0"),
(0.08, "common.items.armor.pants.druid"),
(0.04, "common.items.armor.pants.leather_0"),
(0.04, "common.items.armor.pants.leather_2"),
@ -198,7 +195,7 @@
(0.125, "common.items.armor.shoulder.cloth_blue_0"),
(0.125, "common.items.armor.shoulder.cloth_blue_1"),
(0.125, "common.items.armor.shoulder.cloth_green_0"),
(0.125, "common.items.armor.shoulder.cloth_purple_0"),
(0.125, "common.items.armor.shoulder.cloth_purple_0"),
(0.06, "common.items.armor.shoulder.druidshoulder"),
(0.06, "common.items.armor.shoulder.leather_strips"),
(0.04, "common.items.armor.shoulder.leather_0"),
@ -217,7 +214,7 @@
//gloves
(0.17, "common.items.armor.hand.cloth_blue_0"),
(0.17, "common.items.armor.hand.cloth_green_0"),
(0.17, "common.items.armor.hand.cloth_purple_0"),
(0.17, "common.items.armor.hand.cloth_purple_0"),
(0.08, "common.items.armor.hand.druid"),
(0.06, "common.items.armor.hand.leather_0"),
(0.06, "common.items.armor.hand.leather_2"),

BIN
assets/voxygen/voxel/sprite/castle/drop_gate_bars-0.vox (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/voxel/sprite/castle/drop_gate_bottom-0.vox (Stored with Git LFS) Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
assets/voxygen/voxel/sprite/grass/grass_snow_0.vox (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/voxel/sprite/grass/grass_snow_1.vox (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/voxel/sprite/grass/grass_snow_2.vox (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/voxel/sprite/grass/grass_snow_3.vox (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/voxel/sprite/grass/grass_snow_4.vox (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/voxel/sprite/grass/grass_snow_5.vox (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/voxel/sprite/grass/grass_snow_6.vox (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/voxel/sprite/grass/grass_snow_7.vox (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/voxel/sprite/grass/grass_snow_8.vox (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/voxel/sprite/grass/grass_snow_9.vox (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -150,7 +150,7 @@ impl Client {
recipe_book,
} => {
// TODO: Display that versions don't match in Voxygen
if &server_info.git_hash != *common::util::GIT_HASH {
if server_info.git_hash != *common::util::GIT_HASH {
warn!(
"Server is running {}[{}], you are running {}[{}], versions \
might be incompatible!",
@ -396,7 +396,7 @@ impl Client {
}
pub fn pick_up(&mut self, entity: EcsEntity) {
if let Some(uid) = self.state.read_component_copied(entity) {
if let Some(uid) = self.state.read_component_cloned(entity) {
self.singleton_stream
.send(ClientMsg::ControlEvent(ControlEvent::InventoryManip(
InventoryManip::Pickup(uid),
@ -520,7 +520,7 @@ impl Client {
}
pub fn mount(&mut self, entity: EcsEntity) {
if let Some(uid) = self.state.read_component_copied(entity) {
if let Some(uid) = self.state.read_component_cloned(entity) {
self.singleton_stream
.send(ClientMsg::ControlEvent(ControlEvent::Mount(uid)))
.unwrap();
@ -1294,7 +1294,7 @@ impl Client {
pub fn entity(&self) -> EcsEntity { self.entity }
/// Get the player's Uid.
pub fn uid(&self) -> Option<Uid> { self.state.read_component_copied(self.entity) }
pub fn uid(&self) -> Option<Uid> { self.state.read_component_cloned(self.entity) }
/// Get the client state
pub fn get_client_state(&self) -> ClientState { self.client_state }
@ -1347,7 +1347,7 @@ impl Client {
pub fn is_admin(&self) -> bool {
let client_uid = self
.state
.read_component_copied::<Uid>(self.entity)
.read_component_cloned::<Uid>(self.entity)
.expect("Client doesn't have a Uid!!!");
self.player_list

View File

@ -31,6 +31,7 @@ indexmap = "1.3.0"
sum_type = "0.2.0"
authc = { git = "https://gitlab.com/veloren/auth.git", rev = "b943c85e4a38f5ec60cd18c34c73097640162bfe" }
slab = "0.4.2"
enum-iterator = "0.6"
[dev-dependencies]
criterion = "0.3"

View File

@ -121,12 +121,10 @@ impl<S: Clone + Eq + Hash, H: BuildHasher + Clone> Astar<S, H> {
{
let iter_limit = self.max_iters.min(self.iter + iters);
while self.iter < iter_limit {
if let Some(PathEntry { node, cost }) = self.potential_nodes.pop() {
self.cheapest_cost = Some(cost);
if let Some(PathEntry { node, .. }) = self.potential_nodes.pop() {
if satisfied(&node) {
return PathResult::Path(self.reconstruct_path_to(node));
} else {
self.cheapest_node = Some(node.clone());
for neighbor in neighbors(&node) {
let node_cheapest = self.cheapest_scores.get(&node).unwrap_or(&f32::MAX);
let neighbor_cheapest =
@ -136,12 +134,18 @@ impl<S: Clone + Eq + Hash, H: BuildHasher + Clone> Astar<S, H> {
if cost < *neighbor_cheapest {
self.came_from.insert(neighbor.clone(), node.clone());
self.cheapest_scores.insert(neighbor.clone(), cost);
let neighbor_cost = cost + heuristic(&neighbor);
let h = heuristic(&neighbor);
let neighbor_cost = cost + h;
self.final_scores.insert(neighbor.clone(), neighbor_cost);
if self.cheapest_cost.map(|cc| h < cc).unwrap_or(true) {
self.cheapest_node = Some(node.clone());
self.cheapest_cost = Some(h);
};
if self.visited.insert(neighbor.clone()) {
self.potential_nodes.push(PathEntry {
node: neighbor.clone(),
node: neighbor,
cost: neighbor_cost,
});
}

View File

@ -1,4 +1,4 @@
use crate::{assets, comp, npc};
use crate::{assets, comp, npc, terrain};
use lazy_static::lazy_static;
use std::{
collections::HashMap,
@ -56,6 +56,7 @@ pub enum ChatCommand {
KillNpcs,
Lantern,
Light,
MakeBlock,
Motd,
Object,
Players,
@ -98,6 +99,7 @@ pub static CHAT_COMMANDS: &[ChatCommand] = &[
ChatCommand::KillNpcs,
ChatCommand::Lantern,
ChatCommand::Light,
ChatCommand::MakeBlock,
ChatCommand::Motd,
ChatCommand::Object,
ChatCommand::Players,
@ -149,6 +151,11 @@ lazy_static! {
.map(|s| s.to_string())
.collect();
static ref BLOCK_KINDS: Vec<String> = terrain::block::BLOCK_KINDS
.keys()
.cloned()
.collect();
/// List of item specifiers. Useful for tab completing
static ref ITEM_SPECS: Vec<String> = {
let path = assets::ASSETS_PATH.join("common").join("items");
@ -281,6 +288,11 @@ impl ChatCommand {
"Spawn entity with light",
Admin,
),
ChatCommand::MakeBlock => cmd(
vec![Enum("block", BLOCK_KINDS.clone(), Required)],
"Make a block",
Admin,
),
ChatCommand::Motd => cmd(
vec![Message(Optional)],
"View the server description",
@ -386,6 +398,7 @@ impl ChatCommand {
ChatCommand::KillNpcs => "kill_npcs",
ChatCommand::Lantern => "lantern",
ChatCommand::Light => "light",
ChatCommand::MakeBlock => "make_block",
ChatCommand::Motd => "motd",
ChatCommand::Object => "object",
ChatCommand::Players => "players",

View File

@ -1,4 +1,4 @@
use crate::{path::Chaser, sync::Uid};
use crate::{comp::Body, path::Chaser, sync::Uid};
use specs::{Component, Entity as EcsEntity};
use specs_idvs::IdvStorage;
use vek::*;
@ -54,6 +54,33 @@ impl Component for Alignment {
type Storage = IdvStorage<Self>;
}
#[derive(Clone, Debug, Default)]
pub struct Psyche {
pub aggro: f32, // 0.0 = always flees, 1.0 = always attacks
}
impl<'a> From<&'a Body> for Psyche {
fn from(body: &'a Body) -> Self {
Self {
aggro: match body {
Body::Humanoid(_) => 0.5,
Body::QuadrupedSmall(_) => 0.35,
Body::QuadrupedMedium(_) => 0.5,
Body::QuadrupedLow(_) => 0.65,
Body::BirdMedium(_) => 1.0,
Body::BirdSmall(_) => 0.2,
Body::FishMedium(_) => 0.15,
Body::FishSmall(_) => 0.0,
Body::BipedLarge(_) => 1.0,
Body::Object(_) => 0.0,
Body::Golem(_) => 1.0,
Body::Critter(_) => 0.1,
Body::Dragon(_) => 1.0,
},
}
}
}
#[derive(Clone, Debug, Default)]
pub struct Agent {
pub patrol_origin: Option<Vec3<f32>>,
@ -61,6 +88,7 @@ pub struct Agent {
/// Does the agent talk when e.g. hit by the player
// TODO move speech patterns into a Behavior component
pub can_speak: bool,
pub psyche: Psyche,
}
impl Agent {
@ -69,11 +97,12 @@ impl Agent {
self
}
pub fn new(origin: Vec3<f32>, can_speak: bool) -> Self {
pub fn new(origin: Vec3<f32>, can_speak: bool, body: &Body) -> Self {
let patrol_origin = Some(origin);
Agent {
patrol_origin,
can_speak,
psyche: Psyche::from(body),
..Default::default()
}
}

View File

@ -1,5 +1,4 @@
pub mod armor;
pub mod lottery;
pub mod tool;
// Reexports
@ -8,6 +7,7 @@ pub use tool::{Hands, Tool, ToolCategory, ToolKind};
use crate::{
assets::{self, Asset},
effect::Effect,
lottery::Lottery,
terrain::{Block, BlockKind},
};
use serde::{Deserialize, Serialize};
@ -171,7 +171,7 @@ impl Item {
BlockKind::ShortGrass => Some(assets::load_expect_cloned("common.items.grasses.short")),
BlockKind::Coconut => Some(assets::load_expect_cloned("common.items.food.coconut")),
BlockKind::Chest => {
let chosen = assets::load_expect::<lottery::Lottery<_>>("common.loot_table");
let chosen = assets::load_expect::<Lottery<String>>("common.loot_table");
let chosen = chosen.choose();
Some(assets::load_expect_cloned(chosen))

View File

@ -503,7 +503,7 @@ impl Inventory {
}
}
if missing.len() == 0 {
if missing.is_empty() {
Ok(slot_claims)
} else {
Err(missing)

View File

@ -80,7 +80,7 @@ pub struct PhysicsState {
pub on_ceiling: bool,
pub on_wall: Option<Vec3<f32>>,
pub touch_entity: Option<Uid>,
pub in_fluid: bool,
pub in_fluid: Option<f32>, // Depth
}
impl PhysicsState {

View File

@ -116,6 +116,8 @@ impl EntityInfo {
},
Body::Dragon(body) => Some(get_npc_name(&NPC_NAMES.dragon, body.species)),
Body::QuadrupedLow(body) => Some(get_npc_name(&NPC_NAMES.quadruped_low, body.species)),
Body::Golem(body) => Some(get_npc_name(&NPC_NAMES.golem, body.species)),
Body::BipedLarge(body) => Some(get_npc_name(&NPC_NAMES.biped_large, body.species)),
_ => None,
}
.map(|s| {

View File

@ -22,6 +22,7 @@ pub mod event;
pub mod figure;
pub mod generation;
pub mod loadout_builder;
pub mod lottery;
pub mod msg;
pub mod npc;
pub mod outcome;

View File

@ -1,25 +1,19 @@
use crate::assets::{self, Asset};
use rand::prelude::*;
use serde::{Deserialize, Serialize};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use std::{fs::File, io::BufReader};
// Generate a random float between 0 and 1
pub fn rand() -> f32 {
let mut rng = rand::thread_rng();
rng.gen()
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Lottery<T> {
items: Vec<(f32, T)>,
total: f32,
}
impl Asset for Lottery<String> {
impl<T: DeserializeOwned + Send + Sync> Asset for Lottery<T> {
const ENDINGS: &'static [&'static str] = &["ron"];
fn parse(buf_reader: BufReader<File>) -> Result<Self, assets::Error> {
ron::de::from_reader::<BufReader<File>, Vec<(f32, String)>>(buf_reader)
ron::de::from_reader::<BufReader<File>, Vec<(f32, T)>>(buf_reader)
.map(|items| Lottery::from_rates(items.into_iter()))
.map_err(assets::Error::parse_error)
}
@ -37,8 +31,8 @@ impl<T> Lottery<T> {
Self { items, total }
}
pub fn choose(&self) -> &T {
let x = rand() * self.total;
pub fn choose_seeded(&self, seed: u32) -> &T {
let x = ((seed % 65536) as f32 / 65536.0) * self.total;
&self.items[self
.items
.binary_search_by(|(y, _)| y.partial_cmp(&x).unwrap())
@ -46,19 +40,18 @@ impl<T> Lottery<T> {
.1
}
pub fn choose(&self) -> &T { self.choose_seeded(thread_rng().gen()) }
pub fn iter(&self) -> impl Iterator<Item = &(f32, T)> { self.items.iter() }
}
#[cfg(test)]
mod tests {
use crate::{
assets,
comp::inventory::item::{lottery::Lottery, Item},
};
use super::*;
use crate::{assets, comp::Item};
#[test]
fn test_loot_table() {
let test = assets::load_expect::<Lottery<_>>("common.loot_table");
let test = test;
let test = assets::load_expect::<Lottery<String>>("common.loot_table");
for (_, item) in test.iter() {
assert!(

View File

@ -68,6 +68,17 @@ pub struct TraversalConfig {
pub min_tgt_dist: f32,
}
const DIAGONALS: [Vec2<i32>; 8] = [
Vec2::new(1, 0),
Vec2::new(1, 1),
Vec2::new(0, 1),
Vec2::new(-1, 1),
Vec2::new(-1, 0),
Vec2::new(-1, -1),
Vec2::new(0, -1),
Vec2::new(1, -1),
];
impl Route {
pub fn path(&self) -> &Path<Vec3<i32>> { &self.path }
@ -88,45 +99,29 @@ impl Route {
V: BaseVol<Vox = Block> + ReadVol,
{
let (next0, next1, next_tgt, be_precise) = loop {
// If we've reached the end of the path, stop
self.next(0)?;
let next0 = self
.next(0)
.unwrap_or_else(|| pos.map(|e| e.floor() as i32));
let next1 = self.next(1).unwrap_or(next0);
// Stop using obstructed paths
if vol.get(next0).map(|b| b.is_solid()).unwrap_or(false) {
if !walkable(vol, next1) {
return None;
}
let diagonals = [
Vec2::new(1, 0),
Vec2::new(1, 1),
Vec2::new(0, 1),
Vec2::new(-1, 1),
Vec2::new(-1, 0),
Vec2::new(-1, -1),
Vec2::new(0, -1),
Vec2::new(1, -1),
];
let next1 = self.next(1).unwrap_or(next0);
let be_precise = diagonals.iter().any(|pos| {
!walkable(vol, next0 + Vec3::new(pos.x, pos.y, 0))
&& !walkable(vol, next0 + Vec3::new(pos.x, pos.y, -1))
&& !walkable(vol, next0 + Vec3::new(pos.x, pos.y, -2))
&& !walkable(vol, next0 + Vec3::new(pos.x, pos.y, 1))
let be_precise = DIAGONALS.iter().any(|pos| {
(-1..2).all(|z| {
vol.get(next0 + Vec3::new(pos.x, pos.y, z))
.map(|b| !b.is_solid())
.unwrap_or(false)
})
});
let next0_tgt = next0.map(|e| e as f32) + Vec3::new(0.5, 0.5, 0.0);
let next1_tgt = next1.map(|e| e as f32) + Vec3::new(0.5, 0.5, 0.0);
let next_tgt = next0_tgt;
// Maybe skip a node (useful with traversing downhill)
let closest_tgt = if next0_tgt.distance_squared(pos) < next1_tgt.distance_squared(pos) {
next0_tgt
} else {
next1_tgt
};
let next_tgt = next0.map(|e| e as f32) + Vec3::new(0.5, 0.5, 0.0);
let closest_tgt = next_tgt.map2(pos, |tgt, pos| pos.clamped(tgt.floor(), tgt.ceil()));
// Determine whether we're close enough to the next to to consider it completed
let dist_sqrd = pos.xy().distance_squared(closest_tgt.xy());
@ -135,12 +130,12 @@ impl Route {
&& (pos.z - closest_tgt.z < 1.2 || (pos.z - closest_tgt.z < 2.9 && vel.z < -0.05))
&& vel.z <= 0.0
// Only consider the node reached if there's nothing solid between us and it
&& vol
&& (vol
.ray(pos + Vec3::unit_z() * 1.5, closest_tgt + Vec3::unit_z() * 1.5)
.until(|block| block.is_solid())
.cast()
.0
> pos.distance(closest_tgt) * 0.9
> pos.distance(closest_tgt) * 0.9 || dist_sqrd < 0.5)
&& self.next_idx < self.path.len()
{
// Node completed, move on to the next one
@ -312,7 +307,7 @@ impl Route {
#[derive(Default, Clone, Debug)]
pub struct Chaser {
last_search_tgt: Option<Vec3<f32>>,
route: Option<Route>,
route: Option<(Route, bool)>,
/// We use this hasher (AAHasher) because:
/// (1) we care about DDOS attacks (ruling out FxHash);
/// (2) we don't care about determinism across computers (we can use
@ -335,14 +330,24 @@ impl Chaser {
let pos_to_tgt = pos.distance(tgt);
// If we're already close to the target then there's nothing to do
if ((pos - tgt) * Vec3::new(1.0, 1.0, 2.0)).magnitude_squared()
let end = self
.route
.as_ref()
.and_then(|(r, _)| r.path.end().copied())
.map(|e| e.map(|e| e as f32 + 0.5))
.unwrap_or(tgt);
if ((pos - end) * Vec3::new(1.0, 1.0, 2.0)).magnitude_squared()
< traversal_cfg.min_tgt_dist.powf(2.0)
{
self.route = None;
return None;
}
let bearing = if let Some(end) = self.route.as_ref().and_then(|r| r.path().end().copied()) {
let bearing = if let Some((end, complete)) = self
.route
.as_ref()
.and_then(|(r, complete)| Some((r.path().end().copied()?, *complete)))
{
let end_to_tgt = end.map(|e| e as f32).distance(tgt);
// If the target has moved significantly since the path was generated then it's
// time to search for a new path. Also, do this randomly from time
@ -350,21 +355,14 @@ impl Chaser {
// theory this shouldn't happen, but in practice the world is full
// of unpredictable obstacles that are more than willing to mess up
// our day. TODO: Come up with a better heuristic for this
if end_to_tgt > pos_to_tgt * 0.3 + 5.0
/* || thread_rng().gen::<f32>() < 0.005 */
if (end_to_tgt > pos_to_tgt * 0.3 + 5.0 && complete)
|| thread_rng().gen::<f32>() < 0.001
{
None
} else {
self.route
.as_mut()
.and_then(|r| r.traverse(vol, pos, vel, traversal_cfg))
// In theory this filter isn't needed, but in practice agents often try to take
// stale paths that start elsewhere. This code makes sure that we're only using
// paths that start near us, avoiding the agent doubling back to chase a stale
// path.
.filter(|(bearing, _)| bearing.xy()
.magnitude_squared() < 1.75f32.powf(2.0)
&& thread_rng().gen::<f32>() > 0.025)
.and_then(|(r, _)| r.traverse(vol, pos, vel, traversal_cfg))
}
} else {
None
@ -373,6 +371,8 @@ impl Chaser {
if let Some((bearing, speed)) = bearing {
Some((bearing, speed))
} else {
let tgt_dir = (tgt - pos).xy().try_normalized().unwrap_or_default();
// Only search for a path if the target has moved from their last position. We
// don't want to be thrashing the pathfinding code for targets that
// we're unable to access!
@ -383,16 +383,45 @@ impl Chaser {
|| self.astar.is_some()
|| self.route.is_none()
{
let (start_pos, path) = find_path(&mut self.astar, vol, pos, tgt);
// Don't use a stale path
if start_pos.distance_squared(pos) < 4.0f32.powf(2.0) {
self.route = path.map(Route::from);
} else {
self.route = None;
}
self.last_search_tgt = Some(tgt);
let (path, complete) = find_path(&mut self.astar, vol, pos, tgt);
self.route = path.map(|path| {
let start_index = path
.iter()
.enumerate()
.min_by_key(|(_, node)| {
node.xy()
.map(|e| e as f32)
.distance_squared(pos.xy() + tgt_dir)
as i32
})
.map(|(idx, _)| idx);
(
Route {
path,
next_idx: start_index.unwrap_or(0),
},
complete,
)
});
}
Some(((tgt - pos) * Vec3::new(1.0, 1.0, 0.0), 0.75))
let walking_towards_edge = (-3..2).all(|z| {
vol.get(
(pos + Vec3::<f32>::from(tgt_dir) * 2.5).map(|e| e as i32) + Vec3::unit_z() * z,
)
.map(|b| !b.is_solid())
.unwrap_or(false)
});
if !walking_towards_edge {
Some(((tgt - pos) * Vec3::new(1.0, 1.0, 0.0), 0.75))
} else {
None
}
}
}
}
@ -415,13 +444,14 @@ where
.unwrap_or(true)
}
#[allow(clippy::float_cmp)] // TODO: Pending review in #587
/// Attempt to search for a path to a target, returning the path (if one was
/// found) and whether it is complete (reaches the target)
fn find_path<V>(
astar: &mut Option<Astar<Vec3<i32>, DefaultHashBuilder>>,
vol: &V,
startf: Vec3<f32>,
endf: Vec3<f32>,
) -> (Vec3<f32>, Option<Path<Vec3<i32>>>)
) -> (Option<Path<Vec3<i32>>>, bool)
where
V: BaseVol<Vox = Block> + ReadVol,
{
@ -443,29 +473,33 @@ where
get_walkable_z(endf.map(|e| e.floor() as i32)),
) {
(Some(start), Some(end)) => (start, end),
_ => return (startf, None),
_ => return (None, false),
};
let heuristic = |pos: &Vec3<i32>| (pos.distance_squared(end) as f32).sqrt();
let neighbors = |pos: &Vec3<i32>| {
let pos = *pos;
const DIRS: [Vec3<i32>; 17] = [
const DIRS: [Vec3<i32>; 21] = [
Vec3::new(0, 1, 0), // Forward
Vec3::new(0, 1, 1), // Forward upward
Vec3::new(0, 1, 2), // Forward Upwardx2
Vec3::new(0, 1, -1), // Forward downward
Vec3::new(0, 1, -2), // Forward downwardx2
Vec3::new(1, 0, 0), // Right
Vec3::new(1, 0, 1), // Right upward
Vec3::new(1, 0, 2), // Right Upwardx2
Vec3::new(1, 0, -1), // Right downward
Vec3::new(1, 0, -2), // Right downwardx2
Vec3::new(0, -1, 0), // Backwards
Vec3::new(0, -1, 1), // Backward Upward
Vec3::new(0, -1, 2), // Backward Upwardx2
Vec3::new(0, -1, -1), // Backward downward
Vec3::new(0, -1, -2), // Backward downwardx2
Vec3::new(-1, 0, 0), // Left
Vec3::new(-1, 0, 1), // Left upward
Vec3::new(-1, 0, 2), // Left Upwardx2
Vec3::new(-1, 0, -1), // Left downward
Vec3::new(-1, 0, -2), // Left downwardx2
Vec3::new(0, 0, -1), // Downwards
];
@ -541,19 +575,19 @@ where
*astar = Some(new_astar);
(startf, match path_result {
match path_result {
PathResult::Path(path) => {
*astar = None;
Some(path)
(Some(path), true)
},
PathResult::None(path) => {
*astar = None;
Some(path)
(Some(path), false)
},
PathResult::Exhausted(path) => {
*astar = None;
Some(path)
(Some(path), false)
},
PathResult::Pending => None,
})
PathResult::Pending => (None, false),
}
}

View File

@ -11,12 +11,17 @@ pub struct Spiral2d {
impl Spiral2d {
#[allow(clippy::new_without_default)] // TODO: Pending review in #587
pub fn new() -> Self { Self { layer: 0, i: 0 } }
pub fn radius(self, radius: i32) -> impl Iterator<Item = Vec2<i32>> {
self.take((radius * 2 + 1).pow(2) as usize)
.filter(move |pos| pos.magnitude_squared() < (radius + 1).pow(2))
}
}
impl Iterator for Spiral2d {
type Item = Vec2<i32>;
#[allow(clippy::erasing_op)]
#[allow(clippy::erasing_op, clippy::identity_op)]
fn next(&mut self) -> Option<Self::Item> {
let layer_size = (self.layer * 8 + 4 * self.layer.min(1) - 4).max(1);
if self.i >= layer_size {

View File

@ -199,8 +199,8 @@ impl State {
}
/// Read a component attributed to a particular entity.
pub fn read_component_copied<C: Component + Copy>(&self, entity: EcsEntity) -> Option<C> {
self.ecs.read_storage().get(entity).copied()
pub fn read_component_cloned<C: Component + Copy>(&self, entity: EcsEntity) -> Option<C> {
self.ecs.read_storage().get(entity).cloned()
}
/// Get a read-only reference to the storage of a particular component type.

View File

@ -24,7 +24,12 @@ impl CharacterBehavior for Data {
update.character = CharacterState::GlideWield;
return update;
}
if data.physics.in_fluid {
if data
.physics
.in_fluid
.map(|depth| depth > 0.5)
.unwrap_or(false)
{
update.character = CharacterState::Idle;
}
// If there is a wall in front of character and they are trying to climb go to

View File

@ -19,7 +19,12 @@ impl CharacterBehavior for Data {
if !data.physics.on_ground {
update.character = CharacterState::Glide;
}
if data.physics.in_fluid {
if data
.physics
.in_fluid
.map(|depth| depth > 0.5)
.unwrap_or(false)
{
update.character = CharacterState::Idle;
}

View File

@ -8,7 +8,7 @@ use crate::{
sys::{character_behavior::JoinData, phys::GRAVITY},
util::Dir,
};
use vek::vec::Vec2;
use vek::*;
pub const MOVEMENT_THRESHOLD_VEL: f32 = 3.0;
const BASE_HUMANOID_AIR_ACCEL: f32 = 8.0;
@ -67,8 +67,8 @@ impl Body {
/// Handles updating `Components` to move player based on state of `JoinData`
pub fn handle_move(data: &JoinData, update: &mut StateUpdate, efficiency: f32) {
if data.physics.in_fluid {
swim_move(data, update, efficiency);
if let Some(depth) = data.physics.in_fluid {
swim_move(data, update, efficiency, depth);
} else {
basic_move(data, update, efficiency);
}
@ -104,7 +104,7 @@ pub fn handle_orientation(data: &JoinData, update: &mut StateUpdate, rate: f32)
}
/// Updates components to move player as if theyre swimming
fn swim_move(data: &JoinData, update: &mut StateUpdate, efficiency: f32) {
fn swim_move(data: &JoinData, update: &mut StateUpdate, efficiency: f32, depth: f32) {
// Update velocity
update.vel.0 += Vec2::broadcast(data.dt.0)
* data.inputs.move_dir
@ -119,8 +119,9 @@ fn swim_move(data: &JoinData, update: &mut StateUpdate, efficiency: f32) {
// Swim
if data.inputs.swimup.is_pressed() {
update.vel.0.z =
(update.vel.0.z + data.dt.0 * GRAVITY * 4.0).min(BASE_HUMANOID_WATER_SPEED);
update.vel.0.z = (update.vel.0.z
+ data.dt.0 * GRAVITY * 4.0 * depth.clamped(0.0, 1.0).powf(3.0))
.min(BASE_HUMANOID_WATER_SPEED);
}
// Swim
if data.inputs.swimdown.is_pressed() {
@ -192,14 +193,28 @@ pub fn attempt_swap_loadout(data: &JoinData, update: &mut StateUpdate) {
/// Checks that player can wield the glider and updates `CharacterState` if so
pub fn attempt_glide_wield(data: &JoinData, update: &mut StateUpdate) {
if data.physics.on_ground && !data.physics.in_fluid && data.body.is_humanoid() {
if data.physics.on_ground
&& !data
.physics
.in_fluid
.map(|depth| depth > 1.0)
.unwrap_or(false)
&& data.body.is_humanoid()
{
update.character = CharacterState::GlideWield;
}
}
/// Checks that player can jump and sends jump event if so
pub fn handle_jump(data: &JoinData, update: &mut StateUpdate) {
if data.inputs.jump.is_pressed() && data.physics.on_ground && !data.physics.in_fluid {
if data.inputs.jump.is_pressed()
&& data.physics.on_ground
&& !data
.physics
.in_fluid
.map(|depth| depth > 1.0)
.unwrap_or(false)
{
update
.local_events
.push_front(LocalEvent::Jump(data.entity));

View File

@ -50,20 +50,17 @@ impl<T> Store<T> {
}
pub fn ids(&self) -> impl Iterator<Item = Id<T>> {
// NOTE: Assumes usize fits into 8 bytes.
(0..self.items.len() as u64).map(|i| Id(i, PhantomData))
(0..self.items.len()).map(|i| Id(i as u64, PhantomData))
}
pub fn iter(&self) -> impl Iterator<Item = &T> { self.items.iter() }
pub fn values(&self) -> impl Iterator<Item = &T> { self.items.iter() }
pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut T> { self.items.iter_mut() }
pub fn values_mut(&mut self) -> impl Iterator<Item = &mut T> { self.items.iter_mut() }
pub fn iter_ids(&self) -> impl Iterator<Item = (Id<T>, &T)> {
self.items
.iter()
.enumerate()
// NOTE: Assumes usize fits into 8 bytes.
.map(|(i, item)| (Id(i as u64, PhantomData), item))
pub fn iter(&self) -> impl Iterator<Item = (Id<T>, &T)> { self.ids().zip(self.values()) }
pub fn iter_mut(&mut self) -> impl Iterator<Item = (Id<T>, &mut T)> {
self.ids().zip(self.values_mut())
}
pub fn insert(&mut self, item: T) -> Id<T> {

View File

@ -151,6 +151,7 @@ impl<'a> System<'a> for Sys {
const SEARCH_DIST: f32 = 48.0;
const SIGHT_DIST: f32 = 128.0;
const MIN_ATTACK_DIST: f32 = 3.5;
const MAX_FLEE_DIST: f32 = 32.0;
let scale = scales.get(entity).map(|s| s.0).unwrap_or(1.0);
@ -158,7 +159,7 @@ impl<'a> System<'a> for Sys {
// and so can afford to be less precise when trying to move around
// the world (especially since they would otherwise get stuck on
// obstacles that smaller entities would not).
let node_tolerance = scale + vel.0.xy().magnitude() * 0.2;
let node_tolerance = scale * 1.5;
let slow_factor = body.map(|b| b.base_accel() / 250.0).unwrap_or(0.0).min(1.0);
let mut do_idle = false;
@ -240,6 +241,8 @@ impl<'a> System<'a> for Sys {
bearing.xy().try_normalized().unwrap_or(Vec2::zero())
* speed.min(0.2 + (dist - AVG_FOLLOW_DIST) / 8.0);
inputs.jump.set_state(bearing.z > 1.5);
inputs.swimup.set_state(bearing.z > 0.5);
inputs.swimdown.set_state(bearing.z < 0.5);
}
} else {
do_idle = true;
@ -297,7 +300,47 @@ impl<'a> System<'a> for Sys {
}
let dist_sqrd = pos.0.distance_squared(tgt_pos.0);
if dist_sqrd < (MIN_ATTACK_DIST * scale).powf(2.0) {
let damage = stats
.get(entity)
.map(|s| s.health.current() as f32 / s.health.maximum() as f32)
.unwrap_or(0.5);
// Flee
let flees = alignment
.map(|a| !matches!(a, Alignment::Enemy | Alignment::Owned(_)))
.unwrap_or(true);
if 1.0 - agent.psyche.aggro > damage && flees {
if dist_sqrd < MAX_FLEE_DIST.powf(2.0) {
if let Some((bearing, speed)) = chaser.chase(
&*terrain,
pos.0,
vel.0,
// Away from the target (ironically)
pos.0
+ (pos.0 - tgt_pos.0)
.try_normalized()
.unwrap_or_else(Vec3::unit_y)
* 8.0,
TraversalConfig {
node_tolerance,
slow_factor,
on_ground: physics_state.on_ground,
min_tgt_dist: 1.25,
},
) {
inputs.move_dir = Vec2::from(bearing)
.try_normalized()
.unwrap_or(Vec2::zero())
* speed;
inputs.jump.set_state(bearing.z > 1.5);
inputs.swimup.set_state(bearing.z > 0.5);
inputs.swimdown.set_state(bearing.z < 0.5);
}
} else {
do_idle = true;
}
} else if dist_sqrd < (MIN_ATTACK_DIST * scale).powf(2.0) {
// Close-range attack
inputs.move_dir = Vec2::from(tgt_pos.0 - pos.0)
.try_normalized()
@ -360,6 +403,8 @@ impl<'a> System<'a> for Sys {
.unwrap_or(Vec2::zero())
* speed;
inputs.jump.set_state(bearing.z > 1.5);
inputs.swimup.set_state(bearing.z > 0.5);
inputs.swimdown.set_state(bearing.z < 0.5);
}
if dist_sqrd < 16.0f32.powf(2.0)
@ -427,7 +472,7 @@ impl<'a> System<'a> for Sys {
// Attack a target that's attacking us
if let Some(my_stats) = stats.get(entity) {
// Only if the attack was recent
if my_stats.health.last_change.0 < 5.0 {
if my_stats.health.last_change.0 < 3.0 {
if let comp::HealthSource::Attack { by }
| comp::HealthSource::Projectile { owner: Some(by) } =
my_stats.health.last_change.1.cause
@ -436,20 +481,26 @@ impl<'a> System<'a> for Sys {
if let Some(attacker) = uid_allocator.retrieve_entity_internal(by.id())
{
if stats.get(attacker).map_or(false, |a| !a.is_dead) {
if agent.can_speak {
let msg = "npc.speech.villager_under_attack".to_string();
event_bus.emit_now(ServerEvent::Chat(
UnresolvedChatMsg::npc(*uid, msg),
));
}
match agent.activity {
Activity::Attack { target, .. } if target == attacker => {},
_ => {
if agent.can_speak {
let msg =
"npc.speech.villager_under_attack".to_string();
event_bus.emit_now(ServerEvent::Chat(
UnresolvedChatMsg::npc(*uid, msg),
));
}
agent.activity = Activity::Attack {
target: attacker,
chaser: Chaser::default(),
time: time.0,
been_close: false,
powerup: 0.0,
};
agent.activity = Activity::Attack {
target: attacker,
chaser: Chaser::default(),
time: time.0,
been_close: false,
powerup: 0.0,
};
},
}
}
}
}

View File

@ -12,6 +12,7 @@ use crate::{
use specs::{
saveload::MarkerAllocator, Entities, Join, Read, ReadExpect, ReadStorage, System, WriteStorage,
};
use std::ops::Range;
use vek::*;
pub const GRAVITY: f32 = 9.81 * 5.0;
@ -93,7 +94,7 @@ impl<'a> System<'a> for Sys {
let mut event_emitter = event_bus.emitter();
// Apply movement inputs
for (entity, scale, sticky, collider, mut pos, mut vel, _ori, _) in (
for (entity, _scale, sticky, collider, mut pos, mut vel, _ori, _) in (
&entities,
scales.maybe(),
stickies.maybe(),
@ -112,7 +113,8 @@ impl<'a> System<'a> for Sys {
continue;
}
let scale = scale.map(|s| s.0).unwrap_or(1.0);
// TODO: Use this
//let scale = scale.map(|s| s.0).unwrap_or(1.0);
let old_vel = *vel;
// Integrate forces
@ -123,7 +125,7 @@ impl<'a> System<'a> for Sys {
} else {
0.0
})
.max(if physics_state.in_fluid {
.max(if physics_state.in_fluid.is_some() {
FRIC_FLUID
} else {
0.0
@ -133,7 +135,11 @@ impl<'a> System<'a> for Sys {
.is_some();
let downward_force = if !in_loaded_chunk {
0.0 // No gravity in unloaded chunks
} else if physics_state.in_fluid {
} else if physics_state
.in_fluid
.map(|depth| depth > 0.75)
.unwrap_or(false)
{
(1.0 - BOUYANCY) * GRAVITY
} else {
GRAVITY
@ -157,9 +163,9 @@ impl<'a> System<'a> for Sys {
z_max,
} => {
// Scale collider
let radius = *radius * scale;
let z_min = *z_min * scale;
let z_max = *z_max * scale;
let radius = *radius; // * scale;
let z_min = *z_min; // * scale;
let z_max = *z_max; // * scale;
// Probe distances
let hdist = radius.ceil() as i32;
@ -175,18 +181,23 @@ impl<'a> System<'a> for Sys {
.flatten()
.flatten();
// Function for determining whether the player at a specific position collides
// with the ground
let collision_with = |pos: Vec3<f32>,
hit: &dyn Fn(&Block) -> bool,
near_iter| {
for (i, j, k) in near_iter {
// Function for iterating over the blocks the player at a specific position
// collides with
fn collision_iter<'a>(
pos: Vec3<f32>,
terrain: &'a TerrainGrid,
hit: &'a dyn Fn(&Block) -> bool,
near_iter: impl Iterator<Item = (i32, i32, i32)> + 'a,
radius: f32,
z_range: Range<f32>,
) -> impl Iterator<Item = Aabb<f32>> + 'a {
near_iter.filter_map(move |(i, j, k)| {
let block_pos = pos.map(|e| e.floor() as i32) + Vec3::new(i, j, k);
if let Some(block) = terrain.get(block_pos).ok().copied().filter(hit) {
let player_aabb = Aabb {
min: pos + Vec3::new(-radius, -radius, z_min),
max: pos + Vec3::new(radius, radius, z_max),
min: pos + Vec3::new(-radius, -radius, z_range.start),
max: pos + Vec3::new(radius, radius, z_range.end),
};
let block_aabb = Aabb {
min: block_pos.map(|e| e as f32),
@ -195,11 +206,21 @@ impl<'a> System<'a> for Sys {
};
if player_aabb.collides_with_aabb(block_aabb) {
return true;
return Some(block_aabb);
}
}
}
false
None
})
};
// Function for determining whether the player at a specific position collides
// with blocks with the given criteria
let collision_with = |pos: Vec3<f32>,
hit: &dyn Fn(&Block) -> bool,
near_iter| {
collision_iter(pos, &terrain, hit, near_iter, radius, z_min..z_max).count()
> 0
};
let was_on_ground = physics_state.on_ground;
@ -400,8 +421,16 @@ impl<'a> System<'a> for Sys {
}
// Figure out if we're in water
physics_state.in_fluid =
collision_with(pos.0, &|block| block.is_fluid(), near_iter.clone());
physics_state.in_fluid = collision_iter(
pos.0,
&terrain,
&|block| block.is_fluid(),
near_iter.clone(),
radius,
z_min..z_max,
)
.max_by_key(|block_aabb| (block_aabb.max.z * 100.0) as i32)
.map(|block_aabb| block_aabb.max.z - pos.0.z);
},
Collider::Point => {
let (dist, block) = terrain.ray(pos.0, pos.0 + pos_delta).ignore_error().cast();

View File

@ -1,9 +1,11 @@
use crate::vol::Vox;
use enum_iterator::IntoEnumIterator;
use lazy_static::lazy_static;
use serde::{Deserialize, Serialize};
use std::ops::Deref;
use std::{collections::HashMap, convert::TryFrom, fmt, ops::Deref};
use vek::*;
#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, Serialize, Deserialize)]
#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, Serialize, Deserialize, IntoEnumIterator)]
#[repr(u8)]
pub enum BlockKind {
Air,
@ -86,6 +88,25 @@ pub enum BlockKind {
Stones,
Twigs,
ShinyGem,
DropGate,
DropGateBottom,
GrassSnow,
}
impl fmt::Display for BlockKind {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{:?}", self) }
}
lazy_static! {
pub static ref BLOCK_KINDS: HashMap<String, BlockKind> = BlockKind::into_enum_iter()
.map(|bk| (bk.to_string(), bk))
.collect();
}
impl<'a> TryFrom<&'a str> for BlockKind {
type Error = ();
fn try_from(s: &'a str) -> Result<Self, Self::Error> { BLOCK_KINDS.get(s).copied().ok_or(()) }
}
impl BlockKind {
@ -173,6 +194,9 @@ impl BlockKind {
BlockKind::Stones => true,
BlockKind::Twigs => true,
BlockKind::ShinyGem => true,
BlockKind::DropGate => false,
BlockKind::DropGateBottom => false,
BlockKind::GrassSnow => true,
_ => false,
}
}
@ -184,6 +208,16 @@ impl BlockKind {
}
}
pub fn get_glow(&self) -> Option<u8> {
// TODO: When we have proper volumetric lighting
// match self {
// BlockKind::StreetLamp | BlockKind::StreetLampTall => Some(20),
// BlockKind::Velorite | BlockKind::VeloriteFrag => Some(10),
// _ => None,
// }
None
}
pub fn is_opaque(&self) -> bool {
match self {
BlockKind::Air => false,
@ -261,6 +295,9 @@ impl BlockKind {
BlockKind::Stones => false,
BlockKind::Twigs => false,
BlockKind::ShinyGem => false,
BlockKind::DropGate => false,
BlockKind::DropGateBottom => false,
BlockKind::GrassSnow => false,
_ => true,
}
}
@ -335,14 +372,18 @@ impl BlockKind {
BlockKind::Stones => false,
BlockKind::Twigs => false,
BlockKind::ShinyGem => false,
BlockKind::DropGate => true,
BlockKind::DropGateBottom => false,
BlockKind::GrassSnow => false,
_ => true,
}
}
pub fn is_explodable(&self) -> bool {
match self {
BlockKind::Leaves | BlockKind::Grass | BlockKind::Rock => true,
_ => false,
BlockKind::Leaves | BlockKind::Grass | BlockKind::Rock | BlockKind::GrassSnow => true,
BlockKind::Air => false,
bk => bk.is_air(), // Temporary catch for terrain sprites
}
}
@ -363,9 +404,9 @@ impl BlockKind {
BlockKind::Radish => 0.18,
BlockKind::Door => 3.0,
BlockKind::Bed => 1.54,
BlockKind::Bench => 1.45,
BlockKind::ChairSingle => 1.36,
BlockKind::ChairDouble => 1.36,
BlockKind::Bench => 0.5,
BlockKind::ChairSingle => 0.5,
BlockKind::ChairDouble => 0.5,
BlockKind::CoatRack => 2.36,
BlockKind::Crate => 0.90,
BlockKind::DrawerSmall => 1.0,
@ -438,6 +479,29 @@ impl Block {
| BlockKind::Window2
| BlockKind::Window3
| BlockKind::Window4
| BlockKind::Bed
| BlockKind::Bench
| BlockKind::ChairSingle
| BlockKind::ChairDouble
| BlockKind::CoatRack
| BlockKind::Crate
| BlockKind::DrawerLarge
| BlockKind::DrawerMedium
| BlockKind::DrawerSmall
| BlockKind::DungeonWallDecor
| BlockKind::HangingBasket
| BlockKind::HangingSign
| BlockKind::WallLamp
| BlockKind::Planter
| BlockKind::Shelf
| BlockKind::TableSide
| BlockKind::TableDining
| BlockKind::TableDouble
| BlockKind::WardrobeSingle
| BlockKind::WardrobeDouble
| BlockKind::Pot
| BlockKind::DropGate
| BlockKind::DropGateBottom
| BlockKind::Door => Some(self.color[0] & 0b111),
_ => None,
}

View File

@ -1 +1 @@
economy.csv

View File

@ -13,13 +13,14 @@ use common::{
npc::{self, get_npc_name},
state::TimeOfDay,
sync::{Uid, WorldSyncExt},
terrain::TerrainChunkSize,
terrain::{Block, BlockKind, TerrainChunkSize},
util::Dir,
vol::RectVolSize,
LoadoutBuilder,
};
use rand::Rng;
use specs::{Builder, Entity as EcsEntity, Join, WorldExt};
use std::convert::TryFrom;
use vek::*;
use world::util::Sampler;
@ -83,6 +84,7 @@ fn get_handler(cmd: &ChatCommand) -> CommandHandler {
ChatCommand::KillNpcs => handle_kill_npcs,
ChatCommand::Lantern => handle_lantern,
ChatCommand::Light => handle_light,
ChatCommand::MakeBlock => handle_make_block,
ChatCommand::Motd => handle_motd,
ChatCommand::Object => handle_object,
ChatCommand::Players => handle_players,
@ -179,6 +181,39 @@ fn handle_give_item(
}
}
fn handle_make_block(
server: &mut Server,
client: EcsEntity,
target: EcsEntity,
args: String,
action: &ChatCommand,
) {
if let Some(block_name) = scan_fmt_some!(&args, &action.arg_fmt(), String) {
if let Ok(bk) = BlockKind::try_from(block_name.as_str()) {
match server.state.read_component_cloned::<comp::Pos>(target) {
Some(pos) => server.state.set_block(
pos.0.map(|e| e.floor() as i32),
Block::new(bk, Rgb::broadcast(255)),
),
None => server.notify_client(
client,
ChatType::CommandError.server_msg(String::from("You have no position.")),
),
}
} else {
server.notify_client(
client,
ChatType::CommandError.server_msg(format!("Invalid block kind: {}", block_name)),
);
}
} else {
server.notify_client(
client,
ChatType::CommandError.server_msg(action.help_string()),
);
}
}
fn handle_motd(
server: &mut Server,
client: EcsEntity,
@ -227,7 +262,7 @@ fn handle_jump(
action: &ChatCommand,
) {
if let Ok((x, y, z)) = scan_fmt!(&args, &action.arg_fmt(), f32, f32, f32) {
match server.state.read_component_copied::<comp::Pos>(target) {
match server.state.read_component_cloned::<comp::Pos>(target) {
Some(current_pos) => {
server
.state
@ -252,7 +287,7 @@ fn handle_goto(
if let Ok((x, y, z)) = scan_fmt!(&args, &action.arg_fmt(), f32, f32, f32) {
if server
.state
.read_component_copied::<comp::Pos>(target)
.read_component_cloned::<comp::Pos>(target)
.is_some()
{
server
@ -463,9 +498,9 @@ fn handle_tp(
);
return;
};
if let Some(_pos) = server.state.read_component_copied::<comp::Pos>(target) {
if let Some(_pos) = server.state.read_component_cloned::<comp::Pos>(target) {
if let Some(player) = opt_player {
if let Some(pos) = server.state.read_component_copied::<comp::Pos>(player) {
if let Some(pos) = server.state.read_component_cloned::<comp::Pos>(player) {
server.state.write_component(target, pos);
server.state.write_component(target, comp::ForceUpdate);
} else {
@ -510,7 +545,7 @@ fn handle_spawn(
(Some(opt_align), Some(npc::NpcBody(id, mut body)), opt_amount, opt_ai) => {
let uid = server
.state
.read_component_copied(target)
.read_component_cloned(target)
.expect("Expected player to have a UID");
if let Some(alignment) = parse_alignment(uid, &opt_align) {
let amount = opt_amount
@ -521,7 +556,7 @@ fn handle_spawn(
let ai = opt_ai.unwrap_or_else(|| "true".to_string());
match server.state.read_component_copied::<comp::Pos>(target) {
match server.state.read_component_cloned::<comp::Pos>(target) {
Some(pos) => {
let agent =
if let comp::Alignment::Owned(_) | comp::Alignment::Npc = alignment {
@ -631,7 +666,7 @@ fn handle_spawn_training_dummy(
_args: String,
_action: &ChatCommand,
) {
match server.state.read_component_copied::<comp::Pos>(target) {
match server.state.read_component_cloned::<comp::Pos>(target) {
Some(pos) => {
let vel = Vec3::new(
rand::thread_rng().gen_range(-2.0, 3.0),
@ -672,7 +707,7 @@ fn handle_spawn_campfire(
_args: String,
_action: &ChatCommand,
) {
match server.state.read_component_copied::<comp::Pos>(target) {
match server.state.read_component_cloned::<comp::Pos>(target) {
Some(pos) => {
server
.state
@ -1031,7 +1066,7 @@ fn handle_explosion(
let ecs = server.state.ecs();
match server.state.read_component_copied::<comp::Pos>(target) {
match server.state.read_component_cloned::<comp::Pos>(target) {
Some(pos) => {
ecs.read_resource::<EventBus<ServerEvent>>()
.emit_now(ServerEvent::Explosion {
@ -1056,7 +1091,7 @@ fn handle_waypoint(
_args: String,
_action: &ChatCommand,
) {
match server.state.read_component_copied::<comp::Pos>(target) {
match server.state.read_component_cloned::<comp::Pos>(target) {
Some(pos) => {
let time = server.state.ecs().read_resource();
let _ = server
@ -1092,7 +1127,7 @@ fn handle_adminify(
Some(player) => {
let is_admin = if server
.state
.read_component_copied::<comp::Admin>(player)
.read_component_cloned::<comp::Admin>(player)
.is_some()
{
ecs.write_storage::<comp::Admin>().remove(player);
@ -1438,7 +1473,7 @@ fn handle_debug_column(
let spawn_rate = sim.get_interpolated(wpos, |chunk| chunk.spawn_rate)?;
let chunk_pos = wpos.map2(TerrainChunkSize::RECT_SIZE, |e, sz: u32| e / sz as i32);
let chunk = sim.get(chunk_pos)?;
let col = sampler.get(wpos)?;
let col = sampler.get((wpos, server.world.index()))?;
let downhill = chunk.downhill;
let river = &chunk.river;
let flux = chunk.flux;
@ -1636,7 +1671,7 @@ fn handle_remove_lights(
action: &ChatCommand,
) {
let opt_radius = scan_fmt_some!(&args, &action.arg_fmt(), f32);
let opt_player_pos = server.state.read_component_copied::<comp::Pos>(target);
let opt_player_pos = server.state.read_component_cloned::<comp::Pos>(target);
let mut to_delete = vec![];
match opt_player_pos {

View File

@ -2,9 +2,10 @@ use crate::{client::Client, Server, SpawnPoint, StateExt};
use common::{
assets,
comp::{
self, item::lottery::Lottery, object, Alignment, Body, Damage, DamageSource, Group,
HealthChange, HealthSource, Player, Pos, Stats,
self, object, Alignment, Body, Damage, DamageSource, Group, HealthChange, HealthSource,
Player, Pos, Stats,
},
lottery::Lottery,
msg::{PlayerListUpdate, ServerMsg},
outcome::Outcome,
state::BlockChange,
@ -183,7 +184,7 @@ pub fn handle_destroy(server: &mut Server, entity: EcsEntity, cause: HealthSourc
item_drops.remove(entity);
item_drop.0
} else {
let chosen = assets::load_expect::<Lottery<_>>("common.loot_table");
let chosen = assets::load_expect::<Lottery<String>>("common.loot_table");
let chosen = chosen.choose();
assets::load_expect_cloned(chosen)
@ -251,7 +252,7 @@ pub fn handle_respawn(server: &Server, entity: EcsEntity) {
.is_some()
{
let respawn_point = state
.read_component_copied::<comp::Waypoint>(entity)
.read_component_cloned::<comp::Waypoint>(entity)
.map(|wp| wp.get_pos())
.unwrap_or(state.ecs().read_resource::<SpawnPoint>().0);

View File

@ -168,10 +168,10 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv
thrown_items.push((
*pos,
state
.read_component_copied::<comp::Vel>(entity)
.read_component_cloned::<comp::Vel>(entity)
.unwrap_or_default(),
state
.read_component_copied::<comp::Ori>(entity)
.read_component_cloned::<comp::Ori>(entity)
.unwrap_or_default(),
*kind,
));
@ -186,7 +186,7 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv
state.read_storage::<comp::Pos>().get(entity)
{
let uid = state
.read_component_copied(entity)
.read_component_cloned(entity)
.expect("Expected player to have a UID");
if (
&state.read_storage::<comp::Alignment>(),
@ -342,7 +342,7 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv
dropped_items.push((
*pos,
state
.read_component_copied::<comp::Ori>(entity)
.read_component_cloned::<comp::Ori>(entity)
.unwrap_or_default(),
item,
));
@ -374,10 +374,10 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv
for _ in 0..amount {
dropped_items.push((
state
.read_component_copied::<comp::Pos>(entity)
.read_component_cloned::<comp::Pos>(entity)
.unwrap_or_default(),
state
.read_component_copied::<comp::Ori>(entity)
.read_component_cloned::<comp::Ori>(entity)
.unwrap_or_default(),
item.clone(),
));
@ -419,7 +419,7 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv
},
};
let uid = state.read_component_copied::<Uid>(entity);
let uid = state.read_component_cloned::<Uid>(entity);
let mut new_entity = state
.create_object(Default::default(), match kind {

View File

@ -20,7 +20,7 @@ pub fn handle_exit_ingame(server: &mut Server, entity: EcsEntity) {
// Note: If other `ServerEvent`s are referring to this entity they will be
// disrupted
let maybe_client = state.ecs().write_storage::<Client>().remove(entity);
let maybe_uid = state.read_component_copied::<Uid>(entity);
let maybe_uid = state.read_component_cloned::<Uid>(entity);
let maybe_player = state.ecs().write_storage::<comp::Player>().remove(entity);
let maybe_group = state
.ecs()

View File

@ -184,7 +184,7 @@ impl Server {
..WorldOpts::default()
});
#[cfg(feature = "worldgen")]
let map = world.sim().get_map();
let map = world.get_map_data();
#[cfg(not(feature = "worldgen"))]
let world = World::generate(settings.world_seed);
@ -219,11 +219,11 @@ impl Server {
// get a z cache for the collumn in which we want to spawn
let mut block_sampler = world.sample_blocks();
let z_cache = block_sampler
.get_z_cache(spawn_location)
.get_z_cache(spawn_location, world.index())
.expect(&format!("no z_cache found for chunk: {}", spawn_chunk));
// get the minimum and maximum z values at which there could be soild blocks
let (min_z, _, max_z) = z_cache.get_z_limits(&mut block_sampler);
let (min_z, _, max_z) = z_cache.get_z_limits(&mut block_sampler, world.index());
// round range outwards, so no potential air block is missed
let min_z = min_z.floor() as i32;
let max_z = max_z.ceil() as i32;
@ -239,6 +239,7 @@ impl Server {
Vec3::new(spawn_location.x, spawn_location.y, *z),
Some(&z_cache),
false,
world.index(),
)
.map(|b| b.is_air())
.unwrap_or(false)

View File

@ -173,7 +173,7 @@ impl StateExt for State {
self.write_component(entity, comp::CharacterState::default());
self.write_component(
entity,
comp::Alignment::Owned(self.read_component_copied(entity).unwrap()),
comp::Alignment::Owned(self.read_component_cloned(entity).unwrap()),
);
// Set the character id for the player
@ -213,7 +213,7 @@ impl StateExt for State {
// Notify clients of a player list update
let client_uid = self
.read_component_copied::<Uid>(entity)
.read_component_cloned::<Uid>(entity)
.map(|u| u)
.expect("Client doesn't have a Uid!!!");

View File

@ -319,12 +319,12 @@ impl<'a> System<'a> for Sys {
pos: Pos(entity.pos),
stats,
loadout,
body,
agent: if entity.has_agency {
Some(comp::Agent::new(entity.pos, can_speak))
Some(comp::Agent::new(entity.pos, can_speak, &body))
} else {
None
},
body,
alignment,
scale: comp::Scale(scale),
drop_item: entity.loot_drop,

View File

@ -12,7 +12,6 @@ impl Animation for BetaAnimation {
const UPDATE_FN: &'static [u8] = b"character_beta\0";
#[cfg_attr(feature = "be-dyn-lib", export_name = "character_beta")]
#[allow(clippy::unnested_or_patterns)] // TODO: Pending review in #587
fn update_skeleton_inner(
skeleton: &Self::Skeleton,
(active_tool_kind, second_tool_kind, _velocity, _global_time): Self::Dependency,
@ -42,65 +41,60 @@ impl Animation for BetaAnimation {
.sqrt())
* ((anim_time as f32 * lab as f32 * 14.0).sin());
match active_tool_kind {
//TODO: Inventory
Some(ToolKind::Axe(_))
| Some(ToolKind::Hammer(_))
| Some(ToolKind::Sword(_))
| Some(ToolKind::Dagger(_)) => {
//INTENTION: SWORD
next.head.offset =
Vec3::new(0.0, -2.0 + skeleton_attr.head.0, skeleton_attr.head.1);
next.head.ori = Quaternion::rotation_z(slow * -0.18)
* Quaternion::rotation_x(-0.1 + slow * -0.28)
* Quaternion::rotation_y(0.2 + slow * 0.18);
next.head.scale = Vec3::one() * skeleton_attr.head_scale;
if let Some(
ToolKind::Axe(_) | ToolKind::Hammer(_) | ToolKind::Sword(_) | ToolKind::Dagger(_),
) = active_tool_kind
{
//INTENTION: SWORD
next.head.offset = Vec3::new(0.0, -2.0 + skeleton_attr.head.0, skeleton_attr.head.1);
next.head.ori = Quaternion::rotation_z(slow * -0.18)
* Quaternion::rotation_x(-0.1 + slow * -0.28)
* Quaternion::rotation_y(0.2 + slow * 0.18);
next.head.scale = Vec3::one() * skeleton_attr.head_scale;
next.chest.offset = Vec3::new(0.0 + foot * 2.0, 0.0, 7.0);
next.chest.ori = Quaternion::rotation_z(slow * 0.2)
* Quaternion::rotation_x(slow * 0.2)
* Quaternion::rotation_y(slow * -0.1);
next.chest.offset = Vec3::new(0.0 + foot * 2.0, 0.0, 7.0);
next.chest.ori = Quaternion::rotation_z(slow * 0.2)
* Quaternion::rotation_x(slow * 0.2)
* Quaternion::rotation_y(slow * -0.1);
next.belt.offset = Vec3::new(0.0, 0.0, -2.0);
next.belt.ori = Quaternion::rotation_z(slow * 0.1)
* Quaternion::rotation_x(slow * 0.1)
* Quaternion::rotation_y(slow * -0.04);
next.belt.offset = Vec3::new(0.0, 0.0, -2.0);
next.belt.ori = Quaternion::rotation_z(slow * 0.1)
* Quaternion::rotation_x(slow * 0.1)
* Quaternion::rotation_y(slow * -0.04);
next.shorts.offset = Vec3::new(0.0, 0.0, -5.0);
next.shorts.ori = Quaternion::rotation_z(slow * 0.1)
* Quaternion::rotation_x(slow * 0.1)
* Quaternion::rotation_y(slow * -0.05);
next.shorts.offset = Vec3::new(0.0, 0.0, -5.0);
next.shorts.ori = Quaternion::rotation_z(slow * 0.1)
* Quaternion::rotation_x(slow * 0.1)
* Quaternion::rotation_y(slow * -0.05);
next.l_hand.offset = Vec3::new(-0.75, -1.0, -2.5);
next.l_hand.ori = Quaternion::rotation_x(1.27);
next.l_hand.scale = Vec3::one() * 1.04;
next.r_hand.offset = Vec3::new(0.75, -1.5, -5.5);
next.r_hand.ori = Quaternion::rotation_x(1.27);
next.r_hand.scale = Vec3::one() * 1.05;
next.main.offset = Vec3::new(0.0, 6.0, -1.0);
next.main.ori = Quaternion::rotation_x(-0.3);
next.l_hand.offset = Vec3::new(-0.75, -1.0, -2.5);
next.l_hand.ori = Quaternion::rotation_x(1.27);
next.l_hand.scale = Vec3::one() * 1.04;
next.r_hand.offset = Vec3::new(0.75, -1.5, -5.5);
next.r_hand.ori = Quaternion::rotation_x(1.27);
next.r_hand.scale = Vec3::one() * 1.05;
next.main.offset = Vec3::new(0.0, 6.0, -1.0);
next.main.ori = Quaternion::rotation_x(-0.3);
next.control.offset = Vec3::new(-8.0 + slow * 1.5, 1.5 + slow * 1.0, 0.0);
next.control.ori = Quaternion::rotation_x(-1.4)
* Quaternion::rotation_y(slow * 2.0 + 0.7)
* Quaternion::rotation_z(1.7 - slow * 0.4 + fast * 0.6);
next.control.scale = Vec3::one();
next.l_foot.offset = Vec3::new(
-skeleton_attr.foot.0,
footquick * -9.5,
skeleton_attr.foot.2,
);
next.l_foot.ori = Quaternion::rotation_x(footquick * 0.3)
* Quaternion::rotation_y(footquick * -0.6);
next.control.offset = Vec3::new(-8.0 + slow * 1.5, 1.5 + slow * 1.0, 0.0);
next.control.ori = Quaternion::rotation_x(-1.4)
* Quaternion::rotation_y(slow * 2.0 + 0.7)
* Quaternion::rotation_z(1.7 - slow * 0.4 + fast * 0.6);
next.control.scale = Vec3::one();
next.l_foot.offset = Vec3::new(
-skeleton_attr.foot.0,
footquick * -9.5,
skeleton_attr.foot.2,
);
next.l_foot.ori =
Quaternion::rotation_x(footquick * 0.3) * Quaternion::rotation_y(footquick * -0.6);
next.r_foot.offset =
Vec3::new(skeleton_attr.foot.0, footquick * 9.5, skeleton_attr.foot.2);
next.r_foot.ori = Quaternion::rotation_x(footquick * -0.3)
* Quaternion::rotation_y(footquick * 0.2);
next.torso.offset = Vec3::new(0.0, 0.0, 0.1) * skeleton_attr.scaler;
next.torso.scale = Vec3::one() / 11.0 * skeleton_attr.scaler;
},
_ => {},
next.r_foot.offset =
Vec3::new(skeleton_attr.foot.0, footquick * 9.5, skeleton_attr.foot.2);
next.r_foot.ori =
Quaternion::rotation_x(footquick * -0.3) * Quaternion::rotation_y(footquick * 0.2);
next.torso.offset = Vec3::new(0.0, 0.0, 0.1) * skeleton_attr.scaler;
next.torso.scale = Vec3::one() / 11.0 * skeleton_attr.scaler;
}
next.l_shoulder.offset = Vec3::new(

View File

@ -16,7 +16,6 @@ impl Animation for SpinAnimation {
const UPDATE_FN: &'static [u8] = b"character_spin\0";
#[cfg_attr(feature = "be-dyn-lib", export_name = "character_spin")]
#[allow(clippy::unnested_or_patterns)] // TODO: Pending review in #587
fn update_skeleton_inner(
skeleton: &Self::Skeleton,
(active_tool_kind, second_tool_kind, _global_time): Self::Dependency,
@ -39,60 +38,56 @@ impl Animation for SpinAnimation {
let spin = (anim_time as f32 * 2.8 * lab as f32).sin();
let spinhalf = (anim_time as f32 * 1.4 * lab as f32).sin();
match active_tool_kind {
//TODO: Inventory
Some(ToolKind::Axe(_))
| Some(ToolKind::Hammer(_))
| Some(ToolKind::Sword(_))
| Some(ToolKind::Dagger(_)) => {
//INTENTION: SWORD
next.l_hand.offset = Vec3::new(-0.75, -1.0, -2.5);
next.l_hand.ori = Quaternion::rotation_x(1.27);
next.l_hand.scale = Vec3::one() * 1.04;
next.r_hand.offset = Vec3::new(0.75, -1.5, -5.5);
next.r_hand.ori = Quaternion::rotation_x(1.27);
next.r_hand.scale = Vec3::one() * 1.05;
next.main.offset = Vec3::new(0.0, 6.0, -1.0);
next.main.ori = Quaternion::rotation_x(-0.3)
* Quaternion::rotation_y(0.0)
* Quaternion::rotation_z(0.0);
next.main.scale = Vec3::one();
if let Some(
ToolKind::Axe(_) | ToolKind::Hammer(_) | ToolKind::Sword(_) | ToolKind::Dagger(_),
) = active_tool_kind
{
//INTENTION: SWORD
next.l_hand.offset = Vec3::new(-0.75, -1.0, -2.5);
next.l_hand.ori = Quaternion::rotation_x(1.27);
next.l_hand.scale = Vec3::one() * 1.04;
next.r_hand.offset = Vec3::new(0.75, -1.5, -5.5);
next.r_hand.ori = Quaternion::rotation_x(1.27);
next.r_hand.scale = Vec3::one() * 1.05;
next.main.offset = Vec3::new(0.0, 6.0, -1.0);
next.main.ori = Quaternion::rotation_x(-0.3)
* Quaternion::rotation_y(0.0)
* Quaternion::rotation_z(0.0);
next.main.scale = Vec3::one();
next.control.offset = Vec3::new(-4.5 + spinhalf * 4.0, 11.0, 8.0);
next.control.ori = Quaternion::rotation_x(-1.7)
* Quaternion::rotation_y(0.2 + spin * -2.0)
* Quaternion::rotation_z(1.4 + spin * 0.1);
next.control.scale = Vec3::one();
next.head.offset = Vec3::new(
0.0,
-2.0 + skeleton_attr.head.0 + spin * -0.8,
skeleton_attr.head.1,
);
next.head.ori = Quaternion::rotation_z(spin * -0.25)
* Quaternion::rotation_x(0.0 + spin * -0.1)
* Quaternion::rotation_y(spin * -0.2);
next.chest.offset = Vec3::new(0.0, skeleton_attr.chest.0, skeleton_attr.chest.1);
next.chest.ori = Quaternion::rotation_z(spin * 0.1)
* Quaternion::rotation_x(0.0 + spin * 0.1)
* Quaternion::rotation_y(decel * -0.2);
next.chest.scale = Vec3::one();
next.control.offset = Vec3::new(-4.5 + spinhalf * 4.0, 11.0, 8.0);
next.control.ori = Quaternion::rotation_x(-1.7)
* Quaternion::rotation_y(0.2 + spin * -2.0)
* Quaternion::rotation_z(1.4 + spin * 0.1);
next.control.scale = Vec3::one();
next.head.offset = Vec3::new(
0.0,
-2.0 + skeleton_attr.head.0 + spin * -0.8,
skeleton_attr.head.1,
);
next.head.ori = Quaternion::rotation_z(spin * -0.25)
* Quaternion::rotation_x(0.0 + spin * -0.1)
* Quaternion::rotation_y(spin * -0.2);
next.chest.offset = Vec3::new(0.0, skeleton_attr.chest.0, skeleton_attr.chest.1);
next.chest.ori = Quaternion::rotation_z(spin * 0.1)
* Quaternion::rotation_x(0.0 + spin * 0.1)
* Quaternion::rotation_y(decel * -0.2);
next.chest.scale = Vec3::one();
next.belt.offset = Vec3::new(0.0, 0.0, -2.0);
next.belt.ori = next.chest.ori * -0.1;
next.belt.scale = Vec3::one();
next.belt.offset = Vec3::new(0.0, 0.0, -2.0);
next.belt.ori = next.chest.ori * -0.1;
next.belt.scale = Vec3::one();
next.shorts.offset = Vec3::new(0.0, 0.0, -5.0);
next.belt.ori = next.chest.ori * -0.08;
next.shorts.scale = Vec3::one();
next.torso.offset = Vec3::new(0.0, 0.0, 0.1) * skeleton_attr.scaler;
next.torso.ori = Quaternion::rotation_z((spin * 7.0).max(0.3))
* Quaternion::rotation_x(0.0)
* Quaternion::rotation_y(0.0);
next.torso.scale = Vec3::one() / 11.0 * skeleton_attr.scaler;
},
_ => {},
next.shorts.offset = Vec3::new(0.0, 0.0, -5.0);
next.belt.ori = next.chest.ori * -0.08;
next.shorts.scale = Vec3::one();
next.torso.offset = Vec3::new(0.0, 0.0, 0.1) * skeleton_attr.scaler;
next.torso.ori = Quaternion::rotation_z((spin * 7.0).max(0.3))
* Quaternion::rotation_x(0.0)
* Quaternion::rotation_y(0.0);
next.torso.scale = Vec3::one() / 11.0 * skeleton_attr.scaler;
}
next.l_foot.offset = Vec3::new(-skeleton_attr.foot.0, foot * 1.0, skeleton_attr.foot.2);
next.l_foot.ori = Quaternion::rotation_x(foot * -1.2);
next.l_foot.scale = Vec3::one();

View File

@ -1,3 +1,4 @@
#![feature(or_patterns)]
#[cfg(all(feature = "be-dyn-lib", feature = "use-dyn-lib"))]
compile_error!("Can't use both \"be-dyn-lib\" and \"use-dyn-lib\" features at once");

View File

@ -85,7 +85,7 @@ fn maps_idle() {
on_ceiling: false,
on_wall: None,
touch_entity: None,
in_fluid: false,
in_fluid: None,
},
&PreviousEntityState {
event: SfxEvent::Idle,
@ -107,7 +107,7 @@ fn maps_run_with_sufficient_velocity() {
on_ceiling: false,
on_wall: None,
touch_entity: None,
in_fluid: false,
in_fluid: None,
},
&PreviousEntityState {
event: SfxEvent::Idle,
@ -129,7 +129,7 @@ fn does_not_map_run_with_insufficient_velocity() {
on_ceiling: false,
on_wall: None,
touch_entity: None,
in_fluid: false,
in_fluid: None,
},
&PreviousEntityState {
event: SfxEvent::Idle,
@ -151,7 +151,7 @@ fn does_not_map_run_with_sufficient_velocity_but_not_on_ground() {
on_ceiling: false,
on_wall: None,
touch_entity: None,
in_fluid: false,
in_fluid: None,
},
&PreviousEntityState {
event: SfxEvent::Idle,
@ -176,7 +176,7 @@ fn maps_roll() {
on_ceiling: false,
on_wall: None,
touch_entity: None,
in_fluid: false,
in_fluid: None,
},
&PreviousEntityState {
event: SfxEvent::Run,
@ -198,7 +198,7 @@ fn maps_land_on_ground_to_run() {
on_ceiling: false,
on_wall: None,
touch_entity: None,
in_fluid: false,
in_fluid: None,
},
&PreviousEntityState {
event: SfxEvent::Idle,
@ -220,7 +220,7 @@ fn maps_glider_open() {
on_ceiling: false,
on_wall: None,
touch_entity: None,
in_fluid: false,
in_fluid: None,
},
&PreviousEntityState {
event: SfxEvent::Jump,
@ -242,7 +242,7 @@ fn maps_glide() {
on_ceiling: false,
on_wall: None,
touch_entity: None,
in_fluid: false,
in_fluid: None,
},
&PreviousEntityState {
event: SfxEvent::Glide,
@ -264,7 +264,7 @@ fn maps_glider_close_when_closing_mid_flight() {
on_ceiling: false,
on_wall: None,
touch_entity: None,
in_fluid: false,
in_fluid: None,
},
&PreviousEntityState {
event: SfxEvent::Glide,
@ -287,7 +287,7 @@ fn maps_glider_close_when_landing() {
on_ceiling: false,
on_wall: None,
touch_entity: None,
in_fluid: false,
in_fluid: None,
},
&PreviousEntityState {
event: SfxEvent::Glide,
@ -308,7 +308,7 @@ fn maps_quadrupeds_running() {
on_ceiling: false,
on_wall: None,
touch_entity: None,
in_fluid: false,
in_fluid: None,
},
Vec3::new(0.5, 0.8, 0.0),
);

View File

@ -505,7 +505,7 @@ impl<'a> Widget for Social<'a> {
})
.or_else(|| {
self.selected_entity
.and_then(|s| self.client.state().read_component_copied(s.0))
.and_then(|s| self.client.state().read_component_cloned(s.0))
})
.filter(|selected| {
// Prevent inviting entities already in the same group
@ -564,7 +564,7 @@ impl<'a> Widget for Social<'a> {
});
}
}
} // End of Online Tab
} // End of Online Tab
events
}

View File

@ -4,7 +4,7 @@ use crate::{
};
use common::{
terrain::{Block, BlockKind},
vol::{ReadVol, RectRasterableVol, Vox},
vol::{DefaultVolIterator, ReadVol, RectRasterableVol, Vox},
volumes::vol_grid_2d::{CachedVolGrid2d, VolGrid2d},
};
use std::{collections::VecDeque, fmt::Debug};
@ -26,13 +26,16 @@ impl Blendable for BlockKind {
}
}
const SUNLIGHT: u8 = 24;
const MAX_LIGHT_DIST: i32 = SUNLIGHT as i32;
fn calc_light<V: RectRasterableVol<Vox = Block> + ReadVol + Debug>(
bounds: Aabb<i32>,
vol: &VolGrid2d<V>,
lit_blocks: impl Iterator<Item = (Vec3<i32>, u8)>,
) -> impl FnMut(Vec3<i32>) -> f32 + '_ {
const UNKNOWN: u8 = 255;
const OPAQUE: u8 = 254;
const SUNLIGHT: u8 = 24;
let outer = Aabb {
min: bounds.min - Vec3::new(SUNLIGHT as i32 - 1, SUNLIGHT as i32 - 1, 1),
@ -47,7 +50,13 @@ fn calc_light<V: RectRasterableVol<Vox = Block> + ReadVol + Debug>(
move |x, y, z| (z * h * w + x * h + y) as usize
};
// Light propagation queue
let mut prop_que = VecDeque::new();
let mut prop_que = lit_blocks
.map(|(pos, light)| {
let rpos = pos - outer.min;
light_map[lm_idx(rpos.x, rpos.y, rpos.z)] = light;
(rpos.x as u8, rpos.y as u8, rpos.z as u16)
})
.collect::<VecDeque<_>>();
// Start sun rays
for x in 0..outer.size().w {
for y in 0..outer.size().h {
@ -216,7 +225,13 @@ impl<'a, V: RectRasterableVol<Vox = Block> + ReadVol + Debug>
&'a self,
range: Self::Supplement,
) -> (Mesh<Self::Pipeline>, Mesh<Self::TranslucentPipeline>) {
let mut light = calc_light(range, self);
// Find blocks that should glow
let lit_blocks =
DefaultVolIterator::new(self, range.min - MAX_LIGHT_DIST, range.max + MAX_LIGHT_DIST)
.filter_map(|(pos, block)| block.get_glow().map(|glow| (pos, glow)));
// Calculate chunk lighting
let mut light = calc_light(range, self, lit_blocks);
let mut lowest_opaque = range.size().d;
let mut highest_opaque = 0;

View File

@ -581,7 +581,7 @@ impl FigureMgr {
let target_base = match (
physics.on_ground,
vel.0.magnitude_squared() > MOVING_THRESHOLD_SQR, // Moving
physics.in_fluid, // In water
physics.in_fluid.is_some(), // In water
) {
// Standing
(true, false, false) => anim::character::StandAnimation::update_skeleton(
@ -823,7 +823,7 @@ impl FigureMgr {
)
},
CharacterState::Wielding { .. } => {
if physics.in_fluid {
if physics.in_fluid.is_some() {
anim::character::SwimWieldAnimation::update_skeleton(
&target_base,
(active_tool_kind, second_tool_kind, vel.0.magnitude(), time),
@ -949,7 +949,7 @@ impl FigureMgr {
let target_base = match (
physics.on_ground,
vel.0.magnitude_squared() > MOVING_THRESHOLD_SQR, // Moving
physics.in_fluid, // In water
physics.in_fluid.is_some(), // In water
) {
// Standing
(true, false, false) => {
@ -1047,7 +1047,7 @@ impl FigureMgr {
let target_base = match (
physics.on_ground,
vel.0.magnitude_squared() > MOVING_THRESHOLD_SQR, // Moving
physics.in_fluid, // In water
physics.in_fluid.is_some(), // In water
) {
// Standing
(true, false, false) => {
@ -1143,7 +1143,7 @@ impl FigureMgr {
let target_base = match (
physics.on_ground,
vel.0.magnitude_squared() > MOVING_THRESHOLD_SQR, // Moving
physics.in_fluid, // In water
physics.in_fluid.is_some(), // In water
) {
// Standing
(true, false, false) => {
@ -1237,7 +1237,7 @@ impl FigureMgr {
let target_base = match (
physics.on_ground,
vel.0.magnitude_squared() > MOVING_THRESHOLD_SQR, // Moving
physics.in_fluid, // In water
physics.in_fluid.is_some(), // In water
) {
// Standing
(true, false, false) => anim::bird_medium::IdleAnimation::update_skeleton(
@ -1329,7 +1329,7 @@ impl FigureMgr {
let target_base = match (
physics.on_ground,
vel.0.magnitude_squared() > MOVING_THRESHOLD_SQR, // Moving
physics.in_fluid, // In water
physics.in_fluid.is_some(), // In water
) {
// Standing
(true, false, false) => anim::fish_medium::IdleAnimation::update_skeleton(
@ -1404,7 +1404,7 @@ impl FigureMgr {
let target_base = match (
physics.on_ground,
vel.0.magnitude_squared() > MOVING_THRESHOLD_SQR, // Moving
physics.in_fluid, // In water
physics.in_fluid.is_some(), // In water
) {
// Standing
(true, false, false) => anim::dragon::IdleAnimation::update_skeleton(
@ -1478,7 +1478,7 @@ impl FigureMgr {
let target_base = match (
physics.on_ground,
vel.0.magnitude_squared() > MOVING_THRESHOLD_SQR, // Moving
physics.in_fluid, // In water
physics.in_fluid.is_some(), // In water
) {
// Standing
(true, false, false) => anim::critter::IdleAnimation::update_skeleton(
@ -1553,7 +1553,7 @@ impl FigureMgr {
let target_base = match (
physics.on_ground,
vel.0.magnitude_squared() > MOVING_THRESHOLD_SQR, // Moving
physics.in_fluid, // In water
physics.in_fluid.is_some(), // In water
) {
// Standing
(true, false, false) => anim::bird_small::IdleAnimation::update_skeleton(
@ -1628,7 +1628,7 @@ impl FigureMgr {
let target_base = match (
physics.on_ground,
vel.0.magnitude_squared() > MOVING_THRESHOLD_SQR, // Moving
physics.in_fluid, // In water
physics.in_fluid.is_some(), // In water
) {
// Standing
(true, false, false) => anim::fish_small::IdleAnimation::update_skeleton(
@ -1703,7 +1703,7 @@ impl FigureMgr {
let target_base = match (
physics.on_ground,
vel.0.magnitude_squared() > MOVING_THRESHOLD_SQR, // Moving
physics.in_fluid, // In water
physics.in_fluid.is_some(), // In water
) {
// Standing
(true, false, false) => anim::biped_large::IdleAnimation::update_skeleton(
@ -1795,7 +1795,7 @@ impl FigureMgr {
let target_base = match (
physics.on_ground,
vel.0.magnitude_squared() > MOVING_THRESHOLD_SQR, // Moving
physics.in_fluid, // In water
physics.in_fluid.is_some(), // In water
) {
// Standing
(true, false, false) => anim::golem::IdleAnimation::update_skeleton(

View File

@ -19,6 +19,7 @@ use crossbeam::channel;
use dot_vox::DotVoxData;
use hashbrown::HashMap;
use std::{f32, fmt::Debug, i32, marker::PhantomData, time::Duration};
use tracing::warn;
use treeculler::{BVol, Frustum, AABB};
use vek::*;
@ -142,7 +143,7 @@ fn sprite_config_for(kind: BlockKind) -> Option<SpriteConfig> {
wind_sway: 0.1,
}),
BlockKind::LargeGrass => Some(SpriteConfig {
variations: 3,
variations: 1,
wind_sway: 0.5,
}),
@ -350,6 +351,18 @@ fn sprite_config_for(kind: BlockKind) -> Option<SpriteConfig> {
variations: 3,
wind_sway: 0.0,
}),
BlockKind::DropGate => Some(SpriteConfig {
variations: 1,
wind_sway: 0.0,
}),
BlockKind::DropGateBottom => Some(SpriteConfig {
variations: 1,
wind_sway: 0.0,
}),
BlockKind::GrassSnow => Some(SpriteConfig {
variations: 10,
wind_sway: 0.2,
}),
_ => None,
}
}
@ -2195,7 +2208,7 @@ impl<V: RectRasterableVol> Terrain<V> {
(BlockKind::Bed, 0),
make_models(
"voxygen.voxel.sprite.furniture.bed-0",
Vec3::new(-9.5, -6.0, 0.0),
Vec3::new(-9.5, -14.5, 0.0),
Vec3::one(),
),
),
@ -2452,7 +2465,7 @@ impl<V: RectRasterableVol> Terrain<V> {
(BlockKind::HangingBasket, 0),
make_models(
"voxygen.voxel.sprite.furniture.hanging_basket-0",
Vec3::new(-6.5, -4.5, 0.0),
Vec3::new(-6.5, -3.5, 0.0),
Vec3::one(),
),
),
@ -2469,7 +2482,7 @@ impl<V: RectRasterableVol> Terrain<V> {
(BlockKind::HangingSign, 0),
make_models(
"voxygen.voxel.sprite.furniture.hanging_sign-0",
Vec3::new(-3.5, -17.0, 0.0),
Vec3::new(-3.5, -28.0, -4.0),
Vec3::one(),
),
),
@ -2486,7 +2499,7 @@ impl<V: RectRasterableVol> Terrain<V> {
(BlockKind::WallLamp, 1),
make_models(
"voxygen.voxel.sprite.furniture.lamp_wall-1",
Vec3::new(-9.0, -10.5, 0.0),
Vec3::new(-10.5, -9.0, 0.0),
Vec3::one(),
),
),
@ -2603,7 +2616,7 @@ impl<V: RectRasterableVol> Terrain<V> {
(BlockKind::TableDining, 0),
make_models(
"voxygen.voxel.sprite.furniture.table_dining-0",
Vec3::new(-13.5, -13.5, 0.0),
Vec3::new(-8.5, -8.5, 0.0),
Vec3::one(),
),
),
@ -2611,7 +2624,7 @@ impl<V: RectRasterableVol> Terrain<V> {
(BlockKind::TableDining, 1),
make_models(
"voxygen.voxel.sprite.furniture.table_dining-1",
Vec3::new(-13.5, -13.5, 0.0),
Vec3::new(-8.5, -8.5, 0.0),
Vec3::one(),
),
),
@ -2629,7 +2642,7 @@ impl<V: RectRasterableVol> Terrain<V> {
(BlockKind::WardrobeSingle, 0),
make_models(
"voxygen.voxel.sprite.furniture.wardrobe_single-0",
Vec3::new(-6.0, -5.5, 0.0),
Vec3::new(-5.5, -6.0, 0.0),
Vec3::one(),
),
),
@ -2637,7 +2650,7 @@ impl<V: RectRasterableVol> Terrain<V> {
(BlockKind::WardrobeSingle, 1),
make_models(
"voxygen.voxel.sprite.furniture.wardrobe_single-1",
Vec3::new(-6.5, -5.5, 0.0),
Vec3::new(-5.5, -6.5, 0.0),
Vec3::one(),
),
),
@ -2646,7 +2659,7 @@ impl<V: RectRasterableVol> Terrain<V> {
(BlockKind::WardrobeDouble, 0),
make_models(
"voxygen.voxel.sprite.furniture.wardrobe_double-0",
Vec3::new(-6.5, -10.5, 0.0),
Vec3::new(-10.5, -6.5, 0.0),
Vec3::one(),
),
),
@ -2654,7 +2667,7 @@ impl<V: RectRasterableVol> Terrain<V> {
(BlockKind::WardrobeDouble, 1),
make_models(
"voxygen.voxel.sprite.furniture.wardrobe_double-1",
Vec3::new(-6.0, -10.5, 0.0),
Vec3::new(-10.5, -6.0, 0.0),
Vec3::one(),
),
),
@ -2733,6 +2746,104 @@ impl<V: RectRasterableVol> Terrain<V> {
Vec3::one(),
),
),
// Drop Gate Parts
(
(BlockKind::DropGate, 0),
make_models(
"voxygen.voxel.sprite.castle.drop_gate_bars-0",
Vec3::new(-5.5, -5.5, 0.0),
Vec3::one(),
),
),
(
(BlockKind::DropGateBottom, 0),
make_models(
"voxygen.voxel.sprite.castle.drop_gate_bottom-0",
Vec3::new(-5.5, -5.5, 0.0),
Vec3::one(),
),
),
// Snow covered Grass
(
(BlockKind::GrassSnow, 0),
make_models(
"voxygen.voxel.sprite.grass.grass_snow_0",
Vec3::new(-2.5, -2.5, 0.0),
Vec3::one(),
),
),
(
(BlockKind::GrassSnow, 1),
make_models(
"voxygen.voxel.sprite.grass.grass_snow_1",
Vec3::new(-2.5, -2.5, 0.0),
Vec3::one(),
),
),
(
(BlockKind::GrassSnow, 2),
make_models(
"voxygen.voxel.sprite.grass.grass_snow_2",
Vec3::new(-2.5, -2.5, 0.0),
Vec3::one(),
),
),
(
(BlockKind::GrassSnow, 3),
make_models(
"voxygen.voxel.sprite.grass.grass_snow_3",
Vec3::new(-2.5, -2.5, 0.0),
Vec3::one(),
),
),
(
(BlockKind::GrassSnow, 4),
make_models(
"voxygen.voxel.sprite.grass.grass_snow_4",
Vec3::new(-2.5, -2.5, 0.0),
Vec3::one(),
),
),
(
(BlockKind::GrassSnow, 5),
make_models(
"voxygen.voxel.sprite.grass.grass_snow_5",
Vec3::new(-2.5, -2.5, 0.0),
Vec3::one(),
),
),
(
(BlockKind::GrassSnow, 6),
make_models(
"voxygen.voxel.sprite.grass.grass_snow_6",
Vec3::new(-2.5, -2.5, 0.0),
Vec3::one(),
),
),
(
(BlockKind::GrassSnow, 7),
make_models(
"voxygen.voxel.sprite.grass.grass_snow_7",
Vec3::new(-2.5, -2.5, 0.0),
Vec3::one(),
),
),
(
(BlockKind::GrassSnow, 8),
make_models(
"voxygen.voxel.sprite.grass.grass_snow_8",
Vec3::new(-2.5, -2.5, 0.0),
Vec3::one(),
),
),
(
(BlockKind::GrassSnow, 9),
make_models(
"voxygen.voxel.sprite.grass.grass_snow_9",
Vec3::new(-2.5, -2.5, 0.0),
Vec3::one(),
),
),
]
.into_iter()
.collect(),
@ -2820,26 +2931,6 @@ impl<V: RectRasterableVol> Terrain<V> {
.iter()
.map(|(p, _)| *p)
{
let chunk_pos = scene_data.state.terrain().pos_key(pos);
// Only mesh if this chunk has all its neighbors
let mut neighbours = true;
for i in -1..2 {
for j in -1..2 {
neighbours &= scene_data
.state
.terrain()
.get_key(chunk_pos + Vec2::new(i, j))
.is_some();
}
}
if neighbours {
self.mesh_todo.insert(chunk_pos, ChunkMeshState {
pos: chunk_pos,
started_tick: current_tick,
active_worker: None,
});
}
// Handle block changes on chunk borders
// Remesh all neighbours because we have complex lighting now
// TODO: if lighting is on the server this can be updated to only remesh when
@ -2850,26 +2941,24 @@ impl<V: RectRasterableVol> Terrain<V> {
let neighbour_pos = pos + Vec3::new(x, y, 0);
let neighbour_chunk_pos = scene_data.state.terrain().pos_key(neighbour_pos);
if neighbour_chunk_pos != chunk_pos {
// Only remesh if this chunk has all its neighbors
let mut neighbours = true;
for i in -1..2 {
for j in -1..2 {
neighbours &= scene_data
.state
.terrain()
.get_key(neighbour_chunk_pos + Vec2::new(i, j))
.is_some();
}
}
if neighbours {
self.mesh_todo.insert(neighbour_chunk_pos, ChunkMeshState {
pos: neighbour_chunk_pos,
started_tick: current_tick,
active_worker: None,
});
// Only remesh if this chunk has all its neighbors
let mut neighbours = true;
for i in -1..2 {
for j in -1..2 {
neighbours &= scene_data
.state
.terrain()
.get_key(neighbour_chunk_pos + Vec2::new(i, j))
.is_some();
}
}
if neighbours {
self.mesh_todo.insert(neighbour_chunk_pos, ChunkMeshState {
pos: neighbour_chunk_pos,
started_tick: current_tick,
active_worker: None,
});
}
}
}
}
@ -3138,23 +3227,27 @@ impl<V: RectRasterableVol> Terrain<V> {
let dist_sqrd = Vec2::from(focus_pos).distance_squared(chunk_center);
if dist_sqrd < sprite_render_distance.powf(2.0) {
for (kind, instances) in &chunk.sprite_instances {
renderer.render_sprites(
if dist_sqrd < sprite_high_detail_distance.powf(2.0) {
&self.sprite_models[&kind][0]
} else if dist_sqrd < sprite_hid_detail_distance.powf(2.0) {
&self.sprite_models[&kind][1]
} else if dist_sqrd < sprite_mid_detail_distance.powf(2.0) {
&self.sprite_models[&kind][2]
} else if dist_sqrd < sprite_low_detail_distance.powf(2.0) {
&self.sprite_models[&kind][3]
} else {
&self.sprite_models[&kind][4]
},
globals,
&instances,
lights,
shadows,
);
if let Some(models) = self.sprite_models.get(&kind) {
renderer.render_sprites(
if dist_sqrd < sprite_high_detail_distance.powf(2.0) {
&models[0]
} else if dist_sqrd < sprite_hid_detail_distance.powf(2.0) {
&models[1]
} else if dist_sqrd < sprite_mid_detail_distance.powf(2.0) {
&models[2]
} else if dist_sqrd < sprite_low_detail_distance.powf(2.0) {
&models[3]
} else {
&models[4]
},
globals,
&instances,
lights,
shadows,
);
} else {
warn!("Sprite model for {:?} does not exists", kind);
}
}
}
}

View File

@ -28,7 +28,7 @@ fn main() {
let pos = focus + Vec2::new(i as i32, j as i32) * scale;
let (alt, place) = sampler
.get(pos)
.get((pos, world.index()))
.map(|sample| {
(
sample.alt.sub(64.0).add(gain).mul(0.7).max(0.0).min(255.0) as u8,

View File

@ -85,7 +85,7 @@ fn main() {
lakes,
oceans,
quads,
} = config.generate(sampler, |pos, (r, g, b, a)| {
} = config.generate(sampler, world.index(), |pos, (r, g, b, a)| {
let i = pos.x;
let j = pos.y;
buf[j * W + i] = u32::from_le_bytes([b, g, r, a]);
@ -104,7 +104,7 @@ fn main() {
..config
};
let mut buf = vec![0u8; 4 * len];
config.generate(sampler, |pos, (r, g, b, a)| {
config.generate(sampler, world.index(), |pos, (r, g, b, a)| {
let i = pos.x;
let j = pos.y;
(&mut buf[(j * x + i) * 4..]).write(&[r, g, b, a]).unwrap();

View File

@ -3,13 +3,13 @@ mod natural;
use crate::{
column::{ColumnGen, ColumnSample},
util::{RandomField, Sampler, SmallCache},
CONFIG,
Index,
};
use common::{
terrain::{structure::StructureBlock, Block, BlockKind, Structure},
vol::{ReadVol, Vox},
};
use std::ops::{Add, Div, Mul, Neg};
use std::ops::{Div, Mul};
use vek::*;
pub struct BlockGen<'a> {
@ -29,8 +29,11 @@ impl<'a> BlockGen<'a> {
column_gen: &ColumnGen<'a>,
cache: &'b mut SmallCache<Option<ColumnSample<'a>>>,
wpos: Vec2<i32>,
index: &Index,
) -> Option<&'b ColumnSample<'a>> {
cache.get(wpos, |wpos| column_gen.get(wpos)).as_ref()
cache
.get(wpos, |wpos| column_gen.get((wpos, index)))
.as_ref()
}
fn get_cliff_height(
@ -40,11 +43,13 @@ impl<'a> BlockGen<'a> {
close_cliffs: &[(Vec2<i32>, u32); 9],
cliff_hill: f32,
tolerance: f32,
index: &Index,
) -> f32 {
close_cliffs.iter().fold(
0.0f32,
|max_height, (cliff_pos, seed)| match Self::sample_column(column_gen, cache, *cliff_pos)
{
|max_height, (cliff_pos, seed)| match Self::sample_column(
column_gen, cache, *cliff_pos, index,
) {
Some(cliff_sample) if cliff_sample.is_cliffs && cliff_sample.spawn_rate > 0.5 => {
let cliff_pos3d = Vec3::from(*cliff_pos);
@ -84,14 +89,14 @@ impl<'a> BlockGen<'a> {
)
}
pub fn get_z_cache(&mut self, wpos: Vec2<i32>) -> Option<ZCache<'a>> {
pub fn get_z_cache(&mut self, wpos: Vec2<i32>, index: &'a Index) -> Option<ZCache<'a>> {
let BlockGen {
column_cache,
column_gen,
} = self;
// Main sample
let sample = column_gen.get(wpos)?;
let sample = column_gen.get((wpos, index))?;
// Tree samples
let mut structures = [None, None, None, None, None, None, None, None, None];
@ -101,7 +106,7 @@ impl<'a> BlockGen<'a> {
.zip(structures.iter_mut())
.for_each(|(close_structure, structure)| {
if let Some(st) = *close_structure {
let st_sample = Self::sample_column(column_gen, column_cache, st.pos);
let st_sample = Self::sample_column(column_gen, column_cache, st.pos, index);
if let Some(st_sample) = st_sample {
let st_sample = st_sample.clone();
let st_info = match st.meta {
@ -111,6 +116,7 @@ impl<'a> BlockGen<'a> {
st.pos,
st.seed,
&st_sample,
index,
),
Some(meta) => Some(StructureInfo {
pos: Vec3::from(st.pos) + Vec3::unit_z() * st_sample.alt as i32,
@ -137,6 +143,7 @@ impl<'a> BlockGen<'a> {
wpos: Vec3<i32>,
z_cache: Option<&ZCache>,
only_structures: bool,
index: &Index,
) -> Option<Block> {
let BlockGen {
column_cache,
@ -156,16 +163,14 @@ impl<'a> BlockGen<'a> {
//tree_density,
//forest_kind,
//close_structures,
cave_xy,
cave_alt,
marble,
marble_small,
// marble,
// marble_small,
rock,
//cliffs,
cliff_hill,
close_cliffs,
temp,
humidity,
// temp,
// humidity,
stone_col,
..
} = sample;
@ -175,7 +180,7 @@ impl<'a> BlockGen<'a> {
let wposf = wpos.map(|e| e as f64);
let (block, _height) = if !only_structures {
let (_definitely_underground, height, on_cliff, basement_height, water_height) =
let (_definitely_underground, height, _on_cliff, basement_height, water_height) =
if (wposf.z as f32) < alt - 64.0 * chaos {
// Shortcut warping
(true, alt, false, basement, water_level)
@ -208,6 +213,7 @@ impl<'a> BlockGen<'a> {
&close_cliffs,
cliff_hill,
0.0,
index,
);
(
@ -269,81 +275,84 @@ impl<'a> BlockGen<'a> {
},
col.map(|e| (e * 255.0) as u8),
))
} else if (wposf.z as f32) < height + 0.9
&& temp < CONFIG.desert_temp
&& (wposf.z as f32 > water_height + 3.0)
&& marble > 0.6
&& marble_small > 0.55
&& (marble * 3173.7).fract() < 0.6
&& humidity > CONFIG.desert_hum
{
let treasures = [BlockKind::Chest, BlockKind::Velorite];
// } else if (wposf.z as f32) < height + 0.9
// && temp < CONFIG.desert_temp
// && (wposf.z as f32 > water_height + 3.0)
// && marble > 0.6
// && marble_small > 0.55
// && (marble * 3173.7).fract() < 0.6
// && humidity > CONFIG.desert_hum
// && false
// {
// let treasures = [BlockKind::Chest, BlockKind::Velorite];
let flowers = [
BlockKind::BlueFlower,
BlockKind::PinkFlower,
BlockKind::PurpleFlower,
BlockKind::RedFlower,
BlockKind::WhiteFlower,
BlockKind::YellowFlower,
BlockKind::Sunflower,
BlockKind::Mushroom, //TODO: Better spawnrules
BlockKind::LeafyPlant,
BlockKind::Blueberry,
BlockKind::LingonBerry,
BlockKind::Fern,
/*BlockKind::Twigs, // TODO: Better spawnrules
*BlockKind::Stones, // TODO: Better spawnrules
*BlockKind::ShinyGem, // TODO: Better spawnrules */
];
let grasses = [
BlockKind::LongGrass,
BlockKind::MediumGrass,
BlockKind::ShortGrass,
];
// let flowers = [
// BlockKind::BlueFlower,
// BlockKind::PinkFlower,
// BlockKind::PurpleFlower,
// BlockKind::RedFlower,
// BlockKind::WhiteFlower,
// BlockKind::YellowFlower,
// BlockKind::Sunflower,
// BlockKind::Mushroom, //TODO: Better spawnrules
// BlockKind::LeafyPlant,
// BlockKind::Blueberry,
// BlockKind::LingonBerry,
// BlockKind::Fern,
// /*BlockKind::Twigs, // TODO: Better spawnrules
// *BlockKind::Stones, // TODO: Better spawnrules
// *BlockKind::ShinyGem, // TODO: Better spawnrules */
// ];
// let grasses = [
// BlockKind::LongGrass,
// BlockKind::MediumGrass,
// BlockKind::ShortGrass,
// ];
Some(Block::new(
if on_cliff && (height * 1271.0).fract() < 0.015 {
treasures[(height * 731.3) as usize % treasures.len()]
} else if (height * 1271.0).fract() < 0.1 {
flowers[(height * 0.2) as usize % flowers.len()]
} else {
grasses[(height * 103.3) as usize % grasses.len()]
},
Rgb::broadcast(0),
))
} else if (wposf.z as f32) < height + 0.9
&& temp > CONFIG.desert_temp
&& (marble * 4423.5).fract() < 0.0005
{
let large_cacti = [
BlockKind::LargeCactus,
BlockKind::MedFlatCactus,
BlockKind::Welwitch,
];
// Some(Block::new(
// if on_cliff && (height * 1271.0).fract() < 0.015 {
// treasures[(height * 731.3) as usize %
// treasures.len()] } else if (height *
// 1271.0).fract() < 0.1 { flowers[(height *
// 0.2) as usize % flowers.len()] } else {
// grasses[(height * 103.3) as usize % grasses.len()]
// },
// Rgb::broadcast(0),
// ))
// } else if (wposf.z as f32) < height + 0.9
// && temp > CONFIG.desert_temp
// && (marble * 4423.5).fract() < 0.0005
// && false
// {
// let large_cacti = [
// BlockKind::LargeCactus,
// BlockKind::MedFlatCactus,
// BlockKind::Welwitch,
// ];
let small_cacti = [
BlockKind::BarrelCactus,
BlockKind::RoundCactus,
BlockKind::ShortCactus,
BlockKind::ShortFlatCactus,
BlockKind::DeadBush,
];
// let small_cacti = [
// BlockKind::BarrelCactus,
// BlockKind::RoundCactus,
// BlockKind::ShortCactus,
// BlockKind::ShortFlatCactus,
// BlockKind::DeadBush,
// ];
Some(Block::new(
if (height * 1271.0).fract() < 0.5 {
large_cacti[(height * 0.2) as usize % large_cacti.len()]
} else {
small_cacti[(height * 0.3) as usize % small_cacti.len()]
},
Rgb::broadcast(0),
))
// Some(Block::new(
// if (height * 1271.0).fract() < 0.5 {
// large_cacti[(height * 0.2) as usize %
// large_cacti.len()] } else {
// small_cacti[(height * 0.3) as usize %
// small_cacti.len()] },
// Rgb::broadcast(0),
// ))
} else {
None
}
.or_else(|| {
// Rocks
if (height + 2.5 - wposf.z as f32).div(7.5).abs().powf(2.0) < rock {
#[allow(clippy::identity_op)]
let field0 = RandomField::new(world.seed + 0);
let field1 = RandomField::new(world.seed + 1);
let field2 = RandomField::new(world.seed + 2);
@ -361,23 +370,6 @@ impl<'a> BlockGen<'a> {
None
}
})
.and_then(|block| {
// Caves
// Underground
let cave = cave_xy.powf(2.0)
* (wposf.z as f32 - cave_alt)
.div(40.0)
.powf(4.0)
.neg()
.add(1.0)
> 0.9993;
if cave && wposf.z as f32 > water_height + 3.0 {
None
} else {
Some(block)
}
})
.or_else(|| {
// Water
if (wposf.z as f32) < water_height {
@ -412,15 +404,8 @@ pub struct ZCache<'a> {
}
impl<'a> ZCache<'a> {
pub fn get_z_limits(&self, block_gen: &mut BlockGen) -> (f32, f32, f32) {
let cave_depth =
if self.sample.cave_xy.abs() > 0.9 && self.sample.water_level <= self.sample.alt {
(self.sample.alt - self.sample.cave_alt + 8.0).max(0.0)
} else {
0.0
};
let min = self.sample.alt - (self.sample.chaos.min(1.0) * 16.0 + cave_depth);
pub fn get_z_limits(&self, block_gen: &mut BlockGen, index: &Index) -> (f32, f32, f32) {
let min = self.sample.alt - (self.sample.chaos.min(1.0) * 16.0);
let min = min - 4.0;
let cliff = BlockGen::get_cliff_height(
@ -430,6 +415,7 @@ impl<'a> ZCache<'a> {
&self.sample.close_cliffs,
self.sample.cliff_hill,
32.0,
index,
);
let rocks = if self.sample.rock > 0.0 { 12.0 } else { 0.0 };
@ -550,7 +536,7 @@ pub fn block_from_structure(
structure_seed: u32,
sample: &ColumnSample,
) -> Option<Block> {
let field = RandomField::new(structure_seed + 0);
let field = RandomField::new(structure_seed);
let lerp = ((field.get(Vec3::from(structure_pos)).rem_euclid(256)) as f32 / 255.0) * 0.85
+ ((field.get(pos + std::i32::MAX / 2).rem_euclid(256)) as f32 / 255.0) * 0.15;

View File

@ -3,7 +3,7 @@ use crate::{
all::ForestKind,
column::{ColumnGen, ColumnSample},
util::{RandomPerm, Sampler, SmallCache, UnitChooser},
CONFIG,
Index, CONFIG,
};
use common::terrain::Structure;
use lazy_static::lazy_static;
@ -20,6 +20,7 @@ pub fn structure_gen<'a>(
st_pos: Vec2<i32>,
st_seed: u32,
st_sample: &ColumnSample,
index: &'a Index,
) -> Option<StructureInfo> {
// Assuming it's a tree... figure out when it SHOULDN'T spawn
let random_seed = (st_seed as f64) / (u32::MAX as f64);
@ -27,7 +28,7 @@ pub fn structure_gen<'a>(
|| st_sample.alt < st_sample.water_level
|| st_sample.spawn_rate < 0.5
|| st_sample.water_dist.map(|d| d < 8.0).unwrap_or(false)
|| st_sample.path.map(|(d, _)| d < 12.0).unwrap_or(false)
|| st_sample.path.map(|(d, _, _, _)| d < 12.0).unwrap_or(false)
{
return None;
}
@ -39,6 +40,7 @@ pub fn structure_gen<'a>(
&st_sample.close_cliffs,
st_sample.cliff_hill,
0.0,
index,
);
let wheight = st_sample.alt.max(cliff_height);

View File

@ -5,8 +5,9 @@ mod econ;
use self::{Occupation::*, Stock::*};
use crate::{
sim::WorldSim,
site::{Dungeon, Settlement, Site as WorldSite},
util::{attempt, seed_expan, CARDINALS, NEIGHBORS},
site::{Castle, Dungeon, Settlement, Site as WorldSite},
util::{attempt, seed_expan, MapVec, CARDINALS, NEIGHBORS},
Index,
};
use common::{
astar::Astar,
@ -70,11 +71,15 @@ impl<'a, R: Rng> GenCtx<'a, R> {
}
impl Civs {
pub fn generate(seed: u32, sim: &mut WorldSim) -> Self {
pub fn generate(seed: u32, sim: &mut WorldSim, index: &mut Index) -> Self {
let mut this = Self::default();
let rng = ChaChaRng::from_seed(seed_expan::rng_state(seed));
let mut ctx = GenCtx { sim, rng };
for _ in 0..100 {
this.generate_cave(&mut ctx);
}
for _ in 0..INITIAL_CIV_COUNT {
debug!("Creating civilisation...");
if this.birth_civ(&mut ctx.reseed()).is_none() {
@ -85,9 +90,13 @@ impl Civs {
for _ in 0..INITIAL_CIV_COUNT * 3 {
attempt(5, || {
let loc = find_site_loc(&mut ctx, None)?;
let (kind, size) = match ctx.rng.gen_range(0, 8) {
0 => (SiteKind::Castle, 3),
_ => (SiteKind::Dungeon, 0),
};
let loc = find_site_loc(&mut ctx, None, size)?;
this.establish_site(&mut ctx.reseed(), loc, |place| Site {
kind: SiteKind::Dungeon,
kind,
center: loc,
place,
@ -103,7 +112,7 @@ impl Civs {
last_exports: Stocks::from_default(0.0),
export_targets: Stocks::from_default(0.0),
trade_states: Stocks::default(),
//trade_states: Stocks::default(),
coin: 1000.0,
})
});
@ -116,7 +125,7 @@ impl Civs {
}
// Flatten ground around sites
for site in this.sites.iter() {
for site in this.sites.values() {
let radius = 48i32;
let wpos = site.center * TerrainChunkSize::RECT_SIZE.map(|e: u32| e as i32);
@ -124,10 +133,12 @@ impl Civs {
let flatten_radius = match &site.kind {
SiteKind::Settlement => 10.0,
SiteKind::Dungeon => 2.0,
SiteKind::Castle => 5.0,
};
let (raise, raise_dist): (f32, i32) = match &site.kind {
SiteKind::Settlement => (10.0, 6),
SiteKind::Castle => (0.0, 6),
_ => (0.0, 0),
};
@ -141,9 +152,11 @@ impl Civs {
0.0
}; // Raise the town centre up a little
let pos = site.center + offs;
let factor = (1.0
let factor = ((1.0
- (site.center - pos).map(|e| e as f32).magnitude() / flatten_radius)
* 1.15;
* 1.25)
.min(1.0);
let rng = &mut ctx.rng;
ctx.sim
.get_mut(pos)
// Don't disrupt chunks that are near water
@ -154,6 +167,7 @@ impl Civs {
chunk.basement += diff;
chunk.rockiness = 0.0;
chunk.warp_factor = 0.0;
chunk.surface_veg *= 1.0 - factor * rng.gen_range(0.25, 0.9);
});
}
}
@ -161,33 +175,37 @@ impl Civs {
// Place sites in world
let mut cnt = 0;
for site in this.sites.iter() {
for sim_site in this.sites.values() {
cnt += 1;
let wpos = site.center.map2(TerrainChunkSize::RECT_SIZE, |e, sz: u32| {
e * sz as i32 + sz as i32 / 2
});
let wpos = sim_site
.center
.map2(TerrainChunkSize::RECT_SIZE, |e, sz: u32| {
e * sz as i32 + sz as i32 / 2
});
let mut rng = ctx.reseed().rng;
let world_site = match &site.kind {
let site = index.sites.insert(match &sim_site.kind {
SiteKind::Settlement => {
WorldSite::from(Settlement::generate(wpos, Some(ctx.sim), &mut rng))
WorldSite::settlement(Settlement::generate(wpos, Some(ctx.sim), &mut rng))
},
SiteKind::Dungeon => {
WorldSite::from(Dungeon::generate(wpos, Some(ctx.sim), &mut rng))
WorldSite::dungeon(Dungeon::generate(wpos, Some(ctx.sim), &mut rng))
},
};
SiteKind::Castle => {
WorldSite::castle(Castle::generate(wpos, Some(ctx.sim), &mut rng))
},
});
let site_ref = &index.sites[site];
let radius_chunks =
(world_site.radius() / TerrainChunkSize::RECT_SIZE.x as f32).ceil() as usize;
(site_ref.radius() / TerrainChunkSize::RECT_SIZE.x as f32).ceil() as usize;
for pos in Spiral2d::new()
.map(|offs| site.center + offs)
.map(|offs| sim_site.center + offs)
.take((radius_chunks * 2).pow(2))
{
ctx.sim
.get_mut(pos)
.map(|chunk| chunk.sites.push(world_site.clone()));
ctx.sim.get_mut(pos).map(|chunk| chunk.sites.push(site));
}
debug!(?site.center, "Placed site at location");
debug!(?sim_site.center, "Placed site at location");
}
info!(?cnt, "all sites placed");
@ -196,20 +214,84 @@ impl Civs {
this
}
// TODO: Move this
fn generate_cave(&self, ctx: &mut GenCtx<impl Rng>) {
let mut pos = ctx
.sim
.get_size()
.map(|sz| ctx.rng.gen_range(0, sz as i32) as f32);
let mut vel = pos
.map2(ctx.sim.get_size(), |pos, sz| sz as f32 / 2.0 - pos)
.try_normalized()
.unwrap_or_else(Vec2::unit_y);
let path = (-100..100)
.filter_map(|i: i32| {
let depth = (i.abs() as f32 / 100.0 * std::f32::consts::PI / 2.0).cos();
vel = (vel
+ Vec2::new(
ctx.rng.gen_range(-0.35, 0.35),
ctx.rng.gen_range(-0.35, 0.35),
))
.try_normalized()
.unwrap_or_else(Vec2::unit_y);
let old_pos = pos.map(|e| e as i32);
pos = (pos + vel * 0.5)
.clamped(Vec2::zero(), ctx.sim.get_size().map(|e| e as f32 - 1.0));
Some((pos.map(|e| e as i32), depth)).filter(|(pos, _)| *pos != old_pos)
})
.collect::<Vec<_>>();
for locs in path.windows(3) {
let to_prev_idx = NEIGHBORS
.iter()
.enumerate()
.find(|(_, dir)| **dir == locs[0].0 - locs[1].0)
.expect("Track locations must be neighbors")
.0;
let to_next_idx = NEIGHBORS
.iter()
.enumerate()
.find(|(_, dir)| **dir == locs[2].0 - locs[1].0)
.expect("Track locations must be neighbors")
.0;
ctx.sim.get_mut(locs[0].0).unwrap().cave.0.neighbors |=
1 << ((to_prev_idx as u8 + 4) % 8);
ctx.sim.get_mut(locs[1].0).unwrap().cave.0.neighbors |=
(1 << (to_prev_idx as u8)) | (1 << (to_next_idx as u8));
ctx.sim.get_mut(locs[2].0).unwrap().cave.0.neighbors |=
1 << ((to_next_idx as u8 + 4) % 8);
}
for loc in path.iter() {
let mut chunk = ctx.sim.get_mut(loc.0).unwrap();
let depth = loc.1 * 250.0 - 20.0;
chunk.cave.1.alt =
chunk.alt - depth + ctx.rng.gen_range(-4.0, 4.0) * (depth > 10.0) as i32 as f32;
chunk.cave.1.width = ctx.rng.gen_range(6.0, 32.0);
chunk.cave.0.offset = Vec2::new(ctx.rng.gen_range(-16, 17), ctx.rng.gen_range(-16, 17));
if chunk.cave.1.alt + chunk.cave.1.width + 5.0 > chunk.alt {
chunk.spawn_rate = 0.0;
}
}
}
pub fn place(&self, id: Id<Place>) -> &Place { self.places.get(id) }
pub fn sites(&self) -> impl Iterator<Item = &Site> + '_ { self.sites.iter() }
pub fn sites(&self) -> impl Iterator<Item = &Site> + '_ { self.sites.values() }
#[allow(dead_code)]
#[allow(clippy::print_literal)] // TODO: Pending review in #587
fn display_info(&self) {
for (id, civ) in self.civs.iter_ids() {
for (id, civ) in self.civs.iter() {
println!("# Civilisation {:?}", id);
println!("Name: {}", "<unnamed>");
println!("Homeland: {:#?}", self.places.get(civ.homeland));
}
for (id, site) in self.sites.iter_ids() {
for (id, site) in self.sites.iter() {
println!("# Site {:?}", id);
println!("{:#?}", site);
}
@ -272,7 +354,7 @@ impl Civs {
fn birth_civ(&mut self, ctx: &mut GenCtx<impl Rng>) -> Option<Id<Civ>> {
let site = attempt(5, || {
let loc = find_site_loc(ctx, None)?;
let loc = find_site_loc(ctx, None, 1)?;
self.establish_site(ctx, loc, |place| Site {
kind: SiteKind::Settlement,
center: loc,
@ -290,7 +372,7 @@ impl Civs {
last_exports: Stocks::from_default(0.0),
export_targets: Stocks::from_default(0.0),
trade_states: Stocks::default(),
//trade_states: Stocks::default(),
coin: 1000.0,
})
})?;
@ -367,7 +449,7 @@ impl Civs {
loc: Vec2<i32>,
site_fn: impl FnOnce(Id<Place>) -> Site,
) -> Option<Id<Site>> {
const SITE_AREA: Range<usize> = 64..256;
const SITE_AREA: Range<usize> = 1..4; //64..256;
let place = match ctx.sim.get(loc).and_then(|site| site.place) {
Some(place) => place,
@ -380,13 +462,14 @@ impl Civs {
const MAX_NEIGHBOR_DISTANCE: f32 = 500.0;
let mut nearby = self
.sites
.iter_ids()
.iter()
.filter(|(_, p)| matches!(p.kind, SiteKind::Settlement | SiteKind::Castle))
.map(|(id, p)| (id, (p.center.distance_squared(loc) as f32).sqrt()))
.filter(|(_, dist)| *dist < MAX_NEIGHBOR_DISTANCE)
.collect::<Vec<_>>();
nearby.sort_by_key(|(_, dist)| *dist as i32);
if let SiteKind::Settlement = self.sites[site].kind {
if let SiteKind::Settlement | SiteKind::Castle = self.sites[site].kind {
for (nearby, _) in nearby.into_iter().take(5) {
// Find a novel path
if let Some((path, cost)) = find_path(ctx, loc, self.sites.get(nearby).center) {
@ -412,17 +495,15 @@ impl Civs {
.expect("Track locations must be neighbors")
.0;
ctx.sim.get_mut(locs[0]).unwrap().path.neighbors |=
ctx.sim.get_mut(locs[0]).unwrap().path.0.neighbors |=
1 << ((to_prev_idx as u8 + 4) % 8);
ctx.sim.get_mut(locs[2]).unwrap().path.neighbors |=
ctx.sim.get_mut(locs[2]).unwrap().path.0.neighbors |=
1 << ((to_next_idx as u8 + 4) % 8);
let mut chunk = ctx.sim.get_mut(locs[1]).unwrap();
chunk.path.neighbors |=
chunk.path.0.neighbors |=
(1 << (to_prev_idx as u8)) | (1 << (to_next_idx as u8));
chunk.path.offset = Vec2::new(
ctx.rng.gen_range(-16.0, 16.0),
ctx.rng.gen_range(-16.0, 16.0),
);
chunk.path.0.offset =
Vec2::new(ctx.rng.gen_range(-16, 17), ctx.rng.gen_range(-16, 17));
}
// Take note of the track
@ -440,7 +521,7 @@ impl Civs {
}
fn tick(&mut self, _ctx: &mut GenCtx<impl Rng>, years: f32) {
for site in self.sites.iter_mut() {
for site in self.sites.values_mut() {
site.simulate(years, &self.places.get(site.place).nat_res);
}
@ -557,7 +638,7 @@ fn walk_in_dir(sim: &WorldSim, a: Vec2<i32>, dir: Vec2<i32>) -> Option<f32> {
} else {
0.0
};
let wild_cost = if b_chunk.path.is_path() {
let wild_cost = if b_chunk.path.0.is_way() {
0.0 // Traversing existing paths has no additional cost!
} else {
2.0
@ -589,6 +670,7 @@ fn loc_suitable_for_site(sim: &WorldSim, loc: Vec2<i32>) -> bool {
if let Some(chunk) = sim.get(loc) {
!chunk.river.is_ocean()
&& !chunk.river.is_lake()
&& !chunk.river.is_river()
&& sim
.get_gradient_approx(loc)
.map(|grad| grad < 1.0)
@ -599,9 +681,12 @@ fn loc_suitable_for_site(sim: &WorldSim, loc: Vec2<i32>) -> bool {
}
/// Attempt to search for a location that's suitable for site construction
#[allow(clippy::useless_conversion)] // TODO: Pending review in #587
#[allow(clippy::or_fun_call)] // TODO: Pending review in #587
fn find_site_loc(ctx: &mut GenCtx<impl Rng>, near: Option<(Vec2<i32>, f32)>) -> Option<Vec2<i32>> {
fn find_site_loc(
ctx: &mut GenCtx<impl Rng>,
near: Option<(Vec2<i32>, f32)>,
size: i32,
) -> Option<Vec2<i32>> {
const MAX_ATTEMPTS: usize = 100;
let mut loc = None;
for _ in 0..MAX_ATTEMPTS {
@ -621,16 +706,16 @@ fn find_site_loc(ctx: &mut GenCtx<impl Rng>, near: Option<(Vec2<i32>, f32)>) ->
),
});
if loc_suitable_for_site(&ctx.sim, test_loc) {
return Some(test_loc);
for offset in Spiral2d::new().take((size * 2 + 1).pow(2) as usize) {
if loc_suitable_for_site(&ctx.sim, test_loc + offset) {
return Some(test_loc);
}
}
loc = ctx.sim.get(test_loc).and_then(|c| {
Some(
c.downhill?
.map2(Vec2::from(TerrainChunkSize::RECT_SIZE), |e, sz: u32| {
e / (sz as i32)
}),
.map2(TerrainChunkSize::RECT_SIZE, |e, sz: u32| e / (sz as i32)),
)
});
}
@ -717,16 +802,13 @@ pub struct Site {
last_exports: Stocks<f32>,
export_targets: Stocks<f32>,
trade_states: Stocks<TradeState>,
//trade_states: Stocks<TradeState>,
coin: f32,
}
impl fmt::Display for Site {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self.kind {
SiteKind::Settlement => writeln!(f, "Settlement")?,
SiteKind::Dungeon => writeln!(f, "Dungeon")?,
}
writeln!(f, "{:?}", self.kind)?;
writeln!(f, "- population: {}", self.population.floor() as u32)?;
writeln!(f, "- coin: {}", self.coin.floor() as u32)?;
writeln!(f, "Stocks")?;
@ -766,6 +848,7 @@ impl fmt::Display for Site {
pub enum SiteKind {
Settlement,
Dungeon,
Castle,
}
impl Site {
@ -996,79 +1079,3 @@ impl Default for TradeState {
}
pub type Stocks<T> = MapVec<Stock, T>;
#[derive(Clone, Debug)]
pub struct MapVec<K, T> {
/// We use this hasher (FxHasher32) because
/// (1) we don't care about DDOS attacks (ruling out SipHash);
/// (2) we care about determinism across computers (ruling out AAHash);
/// (3) we have 1-byte keys (for which FxHash is supposedly fastest).
entries: HashMap<K, T, BuildHasherDefault<FxHasher32>>,
default: T,
}
/// Need manual implementation of Default since K doesn't need that bound.
impl<K, T: Default> Default for MapVec<K, T> {
fn default() -> Self {
Self {
entries: Default::default(),
default: Default::default(),
}
}
}
impl<K: Copy + Eq + Hash, T: Clone> MapVec<K, T> {
pub fn from_list<'a>(i: impl IntoIterator<Item = &'a (K, T)>, default: T) -> Self
where
K: 'a,
T: 'a,
{
Self {
entries: i.into_iter().cloned().collect(),
default,
}
}
pub fn from_default(default: T) -> Self {
Self {
entries: HashMap::default(),
default,
}
}
pub fn get_mut(&mut self, entry: K) -> &mut T {
let default = &self.default;
self.entries.entry(entry).or_insert_with(|| default.clone())
}
pub fn get(&self, entry: K) -> &T { self.entries.get(&entry).unwrap_or(&self.default) }
pub fn map<U: Default>(self, mut f: impl FnMut(K, T) -> U) -> MapVec<K, U> {
MapVec {
entries: self
.entries
.into_iter()
.map(|(s, v)| (s, f(s, v)))
.collect(),
default: U::default(),
}
}
pub fn iter(&self) -> impl Iterator<Item = (K, &T)> + '_ {
self.entries.iter().map(|(s, v)| (*s, v))
}
pub fn iter_mut(&mut self) -> impl Iterator<Item = (K, &mut T)> + '_ {
self.entries.iter_mut().map(|(s, v)| (*s, v))
}
}
impl<K: Copy + Eq + Hash, T: Clone> std::ops::Index<K> for MapVec<K, T> {
type Output = T;
fn index(&self, entry: K) -> &Self::Output { self.get(entry) }
}
impl<K: Copy + Eq + Hash, T: Clone> std::ops::IndexMut<K> for MapVec<K, T> {
fn index_mut(&mut self, entry: K) -> &mut Self::Output { self.get_mut(entry) }
}

View File

@ -1,9 +1,12 @@
use crate::{
all::ForestKind,
block::StructureMeta,
sim::{local_cells, uniform_idx_as_vec2, vec2_as_uniform_idx, RiverKind, SimChunk, WorldSim},
sim::{
local_cells, uniform_idx_as_vec2, vec2_as_uniform_idx, Cave, Path, RiverKind, SimChunk,
WorldSim,
},
util::Sampler,
CONFIG,
Index, CONFIG,
};
use common::{terrain::TerrainChunkSize, vol::RectVolSize};
use noise::NoiseFn;
@ -11,7 +14,7 @@ use roots::find_roots_cubic;
use std::{
cmp::Reverse,
f32, f64,
ops::{Add, Div, Mul, Neg, Sub},
ops::{Add, Div, Mul, Sub},
};
use tracing::error;
use vek::*;
@ -165,16 +168,18 @@ pub fn quadratic_nearest_point(
min_root
}
impl<'a> Sampler<'a> for ColumnGen<'a> {
type Index = Vec2<i32>;
impl<'a, 'b> Sampler<'b> for ColumnGen<'a>
where
'a: 'b,
{
type Index = (Vec2<i32>, &'b Index);
type Sample = Option<ColumnSample<'a>>;
#[allow(clippy::float_cmp)] // TODO: Pending review in #587
#[allow(clippy::if_same_then_else)] // TODO: Pending review in #587
#[allow(clippy::nonminimal_bool)] // TODO: Pending review in #587
#[allow(clippy::single_match)] // TODO: Pending review in #587
#[allow(clippy::bind_instead_of_map)] // TODO: Pending review in #587
fn get(&self, wpos: Vec2<i32>) -> Option<ColumnSample<'a>> {
fn get(&self, (wpos, index): Self::Index) -> Option<ColumnSample<'a>> {
let wposf = wpos.map(|e| e as f64);
let chunk_pos = wpos.map2(TerrainChunkSize::RECT_SIZE, |e, sz: u32| e / sz as i32);
@ -193,6 +198,7 @@ impl<'a> Sampler<'a> for ColumnGen<'a> {
let tree_density = sim.get_interpolated(wpos, |chunk| chunk.tree_density)?;
let spawn_rate = sim.get_interpolated(wpos, |chunk| chunk.spawn_rate)?;
let alt = sim.get_interpolated_monotone(wpos, |chunk| chunk.alt)?;
let surface_veg = sim.get_interpolated_monotone(wpos, |chunk| chunk.surface_veg)?;
let chunk_warp_factor = sim.get_interpolated_monotone(wpos, |chunk| chunk.warp_factor)?;
let sim_chunk = sim.get(chunk_pos)?;
let neighbor_coef = TerrainChunkSize::RECT_SIZE.map(|e| e as f64);
@ -608,7 +614,7 @@ impl<'a> Sampler<'a> for ColumnGen<'a> {
.unwrap_or_else(|| {
max_border_river
.river_kind
.and_then(|river_kind| {
.map(|river_kind| {
match river_kind {
RiverKind::Ocean => {
let (
@ -642,7 +648,7 @@ impl<'a> Sampler<'a> for ColumnGen<'a> {
let river_dist = wposf.distance(river_pos);
let _river_height_factor =
river_dist / (river_width * 0.5);
return Some((
return (
true,
Some((river_dist - river_width * 0.5) as f32),
alt_for_river
@ -650,10 +656,10 @@ impl<'a> Sampler<'a> for ColumnGen<'a> {
lake_water_alt - river_gouge,
alt_for_river.max(lake_water_alt),
0.0,
));
);
}
Some((
(
river_scale_factor <= 1.0,
Some(
(wposf.distance(river_pos) - river_width * 0.5)
@ -663,7 +669,7 @@ impl<'a> Sampler<'a> for ColumnGen<'a> {
downhill_water_alt,
alt_for_river,
river_scale_factor as f32,
))
)
},
RiverKind::Lake { .. } => {
let lake_dist = (max_border_river_pos.map(|e| e as f64)
@ -685,7 +691,7 @@ impl<'a> Sampler<'a> for ColumnGen<'a> {
|| in_bounds
{
let gouge_factor = 0.0;
return Some((
return (
in_bounds
|| downhill_water_alt
.max(river_chunk.water_alt)
@ -697,16 +703,16 @@ impl<'a> Sampler<'a> for ColumnGen<'a> {
alt_for_river,
river_scale_factor as f32
* (1.0 - gouge_factor),
));
);
} else {
return Some((
return (
false,
Some(lake_dist as f32),
alt_for_river,
downhill_water_alt,
alt_for_river,
river_scale_factor as f32,
));
);
};
let lake_dist = dist.y;
@ -718,7 +724,7 @@ impl<'a> Sampler<'a> for ColumnGen<'a> {
river_t as f32,
);
if dist == Vec2::zero() {
return Some((
return (
true,
Some(lake_dist as f32),
alt_for_river
@ -726,7 +732,7 @@ impl<'a> Sampler<'a> for ColumnGen<'a> {
lake_water_alt - river_gouge,
alt_for_river.max(lake_water_alt),
0.0,
));
);
}
if lake_dist <= TerrainChunkSize::RECT_SIZE.x as f64 * 1.0
|| in_bounds
@ -739,7 +745,7 @@ impl<'a> Sampler<'a> for ColumnGen<'a> {
let in_bounds_ = lake_dist
<= TerrainChunkSize::RECT_SIZE.x as f64 * 0.5;
if gouge_factor == 1.0 {
return Some((
return (
true,
Some(lake_dist as f32),
alt.min(lake_water_alt - 1.0 - river_gouge),
@ -747,9 +753,9 @@ impl<'a> Sampler<'a> for ColumnGen<'a> {
- river_gouge,
alt.max(lake_water_alt),
0.0,
));
);
} else {
return Some((
return (
true,
None,
alt_for_river,
@ -761,17 +767,17 @@ impl<'a> Sampler<'a> for ColumnGen<'a> {
alt_for_river,
river_scale_factor as f32
* (1.0 - gouge_factor),
));
);
}
}
Some((
(
river_scale_factor <= 1.0,
Some(lake_dist as f32),
alt_for_river,
downhill_water_alt,
alt_for_river,
river_scale_factor as f32,
))
)
},
RiverKind::River { .. } => {
let (_, _, river_width, (_, (river_pos, _), _)) =
@ -779,14 +785,14 @@ impl<'a> Sampler<'a> for ColumnGen<'a> {
let river_dist = wposf.distance(river_pos);
// FIXME: Make water altitude accurate.
Some((
(
river_scale_factor <= 1.0,
Some((river_dist - river_width * 0.5) as f32),
alt_for_river,
downhill_water_alt,
alt_for_river,
river_scale_factor as f32,
))
)
},
}
})
@ -857,8 +863,8 @@ impl<'a> Sampler<'a> for ColumnGen<'a> {
let cold_stone = Rgb::new(0.57, 0.67, 0.8);
let hot_stone = Rgb::new(0.07, 0.07, 0.06);
let warm_stone = Rgb::new(0.77, 0.77, 0.64);
let beach_sand = Rgb::new(0.9, 0.82, 0.6);
let desert_sand = Rgb::new(0.95, 0.75, 0.5);
let beach_sand = Rgb::new(0.8, 0.75, 0.5);
let desert_sand = Rgb::new(0.7, 0.4, 0.25);
let snow = Rgb::new(0.8, 0.85, 1.0);
let stone_col = Rgb::new(195, 187, 201);
@ -1031,34 +1037,6 @@ impl<'a> Sampler<'a> for ColumnGen<'a> {
(alt, ground, sub_surface_color)
};
// Caves
let cave_at = |wposf: Vec2<f64>| {
(sim.gen_ctx.cave_0_nz.get(
Vec3::new(wposf.x, wposf.y, alt as f64 * 8.0)
.div(800.0)
.into_array(),
) as f32)
.powf(2.0)
.neg()
.add(1.0)
.mul((1.32 - chaos).min(1.0))
};
let cave_xy = cave_at(wposf);
let cave_alt = alt - 24.0
+ (sim
.gen_ctx
.cave_1_nz
.get(Vec2::new(wposf.x, wposf.y).div(48.0).into_array()) as f32)
* 8.0
+ (sim
.gen_ctx
.cave_1_nz
.get(Vec2::new(wposf.x, wposf.y).div(500.0).into_array()) as f32)
.add(1.0)
.mul(0.5)
.powf(15.0)
.mul(150.0);
let near_ocean = max_river.and_then(|(_, _, river_data, _)| {
if (river_data.is_lake() || river_data.river_kind == Some(RiverKind::Ocean))
&& ((alt <= water_level.max(CONFIG.sea_level + 5.0) && !is_cliffs) || !near_cliffs)
@ -1076,6 +1054,7 @@ impl<'a> Sampler<'a> for ColumnGen<'a> {
};
let path = sim.get_nearest_path(wpos);
let cave = sim.get_nearest_cave(wpos);
Some(ColumnSample {
alt,
@ -1085,11 +1064,15 @@ impl<'a> Sampler<'a> for ColumnGen<'a> {
water_level,
warp_factor,
surface_color: Rgb::lerp(
Rgb::lerp(cliff, sand, alt.sub(basement).mul(0.25)),
// Land
ground,
// Beach
((ocean_level - 1.0) / 2.0).max(0.0),
sub_surface_color,
Rgb::lerp(
Rgb::lerp(cliff, sand, alt.sub(basement).mul(0.25)),
// Land
ground,
// Beach
((ocean_level - 1.0) / 2.0).max(0.0),
),
surface_veg,
),
sub_surface_color,
// No growing directly on bedrock.
@ -1098,7 +1081,7 @@ impl<'a> Sampler<'a> for ColumnGen<'a> {
tree_density: if sim_chunk
.sites
.iter()
.all(|site| site.spawn_rules(wpos).trees)
.all(|site| index.sites[*site].spawn_rules(wpos).trees)
{
Lerp::lerp(0.0, tree_density, alt.sub(2.0).sub(basement).mul(0.5))
} else {
@ -1106,8 +1089,6 @@ impl<'a> Sampler<'a> for ColumnGen<'a> {
},
forest_kind: sim_chunk.forest_kind,
close_structures: self.gen_close_structures(wpos),
cave_xy,
cave_alt,
marble,
marble_small,
rock,
@ -1121,6 +1102,7 @@ impl<'a> Sampler<'a> for ColumnGen<'a> {
stone_col,
water_dist,
path,
cave,
chunk: sim_chunk,
})
@ -1140,8 +1122,6 @@ pub struct ColumnSample<'a> {
pub tree_density: f32,
pub forest_kind: ForestKind,
pub close_structures: [Option<StructureData>; 9],
pub cave_xy: f32,
pub cave_alt: f32,
pub marble: f32,
pub marble_small: f32,
pub rock: f32,
@ -1154,7 +1134,8 @@ pub struct ColumnSample<'a> {
pub spawn_rate: f32,
pub stone_col: Rgb<u8>,
pub water_dist: Option<f32>,
pub path: Option<(f32, Vec2<f32>)>,
pub path: Option<(f32, Vec2<f32>, Path, Vec2<f32>)>,
pub cave: Option<(f32, Vec2<f32>, Cave, Vec2<f32>)>,
pub chunk: &'a SimChunk,
}

36
world/src/index.rs Normal file
View File

@ -0,0 +1,36 @@
use crate::site::Site;
use common::store::Store;
use noise::{Seedable, SuperSimplex};
pub struct Index {
pub seed: u32,
pub time: f32,
pub noise: Noise,
pub sites: Store<Site>,
}
impl Index {
pub fn new(seed: u32) -> Self {
Self {
seed,
time: 0.0,
noise: Noise::new(seed),
sites: Store::default(),
}
}
}
pub struct Noise {
pub cave_nz: SuperSimplex,
pub scatter_nz: SuperSimplex,
}
impl Noise {
#[allow(clippy::identity_op)]
fn new(seed: u32) -> Self {
Self {
cave_nz: SuperSimplex::new().set_seed(seed + 0),
scatter_nz: SuperSimplex::new().set_seed(seed + 1),
}
}
}

View File

@ -1,18 +1,156 @@
use crate::{
column::ColumnSample,
sim::SimChunk,
util::{RandomField, Sampler},
Index,
};
use common::{
assets, comp,
generation::{ChunkSupplement, EntityInfo},
lottery::Lottery,
terrain::{Block, BlockKind},
vol::{BaseVol, ReadVol, RectSizedVol, Vox, WriteVol},
};
use std::f32;
use noise::NoiseFn;
use rand::prelude::*;
use std::{
f32,
ops::{Mul, Sub},
};
use vek::*;
fn close(x: f32, tgt: f32, falloff: f32) -> f32 {
(1.0 - (x - tgt).abs() / falloff).max(0.0).powf(0.5)
}
pub fn apply_scatter_to<'a>(
wpos2d: Vec2<i32>,
mut get_column: impl FnMut(Vec2<i32>) -> Option<&'a ColumnSample<'a>>,
vol: &mut (impl BaseVol<Vox = Block> + RectSizedVol + ReadVol + WriteVol),
index: &Index,
chunk: &SimChunk,
) {
use BlockKind::*;
#[allow(clippy::type_complexity)]
let scatter: &[(_, bool, fn(&SimChunk) -> (f32, Option<(f32, f32)>))] = &[
// (density, Option<(wavelen, threshold)>)
(BlueFlower, false, |c| {
(
close(c.temp, -0.3, 0.7).min(close(c.humidity, 0.6, 0.35)) * 0.05,
Some((48.0, 0.6)),
)
}),
(PinkFlower, false, |c| {
(
close(c.temp, 0.15, 0.5).min(close(c.humidity, 0.6, 0.35)) * 0.05,
Some((48.0, 0.6)),
)
}),
(DeadBush, false, |c| {
(
close(c.temp, 0.8, 0.3).min(close(c.humidity, 0.0, 0.4)) * 0.015,
None,
)
}),
(Twigs, false, |c| {
((c.tree_density - 0.5).max(0.0) * 0.0025, None)
}),
(Stones, false, |c| {
((c.rockiness - 0.5).max(0.0) * 0.005, None)
}),
(ShortGrass, false, |c| {
(
close(c.temp, 0.3, 0.4).min(close(c.humidity, 0.6, 0.35)) * 0.05,
Some((48.0, 0.4)),
)
}),
(MediumGrass, false, |c| {
(
close(c.temp, 0.0, 0.6).min(close(c.humidity, 0.6, 0.35)) * 0.05,
Some((48.0, 0.2)),
)
}),
(LongGrass, false, |c| {
(
close(c.temp, 0.4, 0.4).min(close(c.humidity, 0.8, 0.2)) * 0.05,
Some((48.0, 0.1)),
)
}),
(GrassSnow, false, |c| {
(
close(c.temp, -0.4, 0.4).min(close(c.rockiness, 0.0, 0.5)) * 0.1,
Some((48.0, 0.6)),
)
}),
];
for y in 0..vol.size_xy().y as i32 {
for x in 0..vol.size_xy().x as i32 {
let offs = Vec2::new(x, y);
let wpos2d = wpos2d + offs;
// Sample terrain
let col_sample = if let Some(col_sample) = get_column(offs) {
col_sample
} else {
continue;
};
let underwater = col_sample.water_level > col_sample.alt;
let bk = scatter
.iter()
.enumerate()
.find_map(|(i, (bk, is_underwater, f))| {
let (density, patch) = f(chunk);
let is_patch = patch
.map(|(wavelen, threshold)| {
index.noise.scatter_nz.get(
wpos2d
.map(|e| e as f64 / wavelen as f64 + i as f64 * 43.0)
.into_array(),
) < threshold as f64
})
.unwrap_or(false);
if density <= 0.0
|| is_patch
|| !RandomField::new(i as u32)
.chance(Vec3::new(wpos2d.x, wpos2d.y, 0), density)
|| underwater != *is_underwater
{
None
} else {
Some(*bk)
}
});
if let Some(bk) = bk {
let mut z = col_sample.alt as i32 - 4;
for _ in 0..8 {
if vol
.get(Vec3::new(offs.x, offs.y, z))
.map(|b| !b.is_solid())
.unwrap_or(true)
{
let _ = vol.set(
Vec3::new(offs.x, offs.y, z),
Block::new(bk, Rgb::broadcast(0)),
);
break;
}
z += 1;
}
}
}
}
}
pub fn apply_paths_to<'a>(
wpos2d: Vec2<i32>,
mut get_column: impl FnMut(Vec2<i32>) -> Option<&'a ColumnSample<'a>>,
vol: &mut (impl BaseVol<Vox = Block> + RectSizedVol + ReadVol + WriteVol),
_index: &Index,
) {
for y in 0..vol.size_xy().y as i32 {
for x in 0..vol.size_xy().x as i32 {
@ -37,7 +175,9 @@ pub fn apply_paths_to<'a>(
})
};
if let Some((path_dist, path_nearest)) = col_sample.path.filter(|(dist, _)| *dist < 5.0)
if let Some((path_dist, path_nearest, path, _)) = col_sample
.path
.filter(|(dist, _, path, _)| *dist < path.width)
{
let inset = 0;
@ -75,14 +215,14 @@ pub fn apply_paths_to<'a>(
if bridge_offset >= 2.0 && path_dist >= 3.0 || z < inset - 1 {
Block::new(BlockKind::Normal, noisy_color(Rgb::new(80, 80, 100), 8))
} else {
let path_color = col_sample
.sub_surface_color
.map(|e| (e * 255.0 * 0.7) as u8);
let path_color = path.surface_color(
col_sample.sub_surface_color.map(|e| (e * 255.0) as u8),
);
Block::new(BlockKind::Normal, noisy_color(path_color, 8))
},
);
}
let head_space = (8 - (path_dist * 0.25).powf(6.0).round() as i32).max(1);
let head_space = path.head_space(path_dist);
for z in inset..inset + head_space {
let pos = Vec3::new(offs.x, offs.y, surface_z + z);
if vol.get(pos).unwrap().kind() != BlockKind::Water {
@ -93,3 +233,194 @@ pub fn apply_paths_to<'a>(
}
}
}
pub fn apply_caves_to<'a>(
wpos2d: Vec2<i32>,
mut get_column: impl FnMut(Vec2<i32>) -> Option<&'a ColumnSample<'a>>,
vol: &mut (impl BaseVol<Vox = Block> + RectSizedVol + ReadVol + WriteVol),
index: &Index,
) {
for y in 0..vol.size_xy().y as i32 {
for x in 0..vol.size_xy().x as i32 {
let offs = Vec2::new(x, y);
let wpos2d = wpos2d + offs;
// Sample terrain
let col_sample = if let Some(col_sample) = get_column(offs) {
col_sample
} else {
continue;
};
let surface_z = col_sample.riverless_alt.floor() as i32;
if let Some((cave_dist, _, cave, _)) = col_sample
.cave
.filter(|(dist, _, cave, _)| *dist < cave.width)
{
let cave_x = (cave_dist / cave.width).min(1.0);
// Relative units
let cave_floor = 0.0 - 0.5 * (1.0 - cave_x.powf(2.0)).max(0.0).sqrt() * cave.width;
let cave_height = (1.0 - cave_x.powf(2.0)).max(0.0).sqrt() * cave.width;
// Abs units
let cave_base = (cave.alt + cave_floor) as i32;
let cave_roof = (cave.alt + cave_height) as i32;
for z in cave_base..cave_roof {
if cave_x < 0.95
|| index.noise.cave_nz.get(
Vec3::new(wpos2d.x, wpos2d.y, z)
.map(|e| e as f64 * 0.15)
.into_array(),
) < 0.0
{
let _ = vol.set(Vec3::new(offs.x, offs.y, z), Block::empty());
}
}
// Stalagtites
let stalagtites = index
.noise
.cave_nz
.get(wpos2d.map(|e| e as f64 * 0.125).into_array())
.sub(0.5)
.max(0.0)
.mul(
(col_sample.alt - cave_roof as f32 - 5.0)
.mul(0.15)
.clamped(0.0, 1.0) as f64,
)
.mul(45.0) as i32;
for z in cave_roof - stalagtites..cave_roof {
let _ = vol.set(
Vec3::new(offs.x, offs.y, z),
Block::new(BlockKind::Rock, Rgb::broadcast(200)),
);
}
let cave_depth = (col_sample.alt - cave.alt).max(0.0);
let difficulty = cave_depth / 100.0;
// Scatter things in caves
if RandomField::new(index.seed).chance(wpos2d.into(), 0.002 * difficulty.powf(1.5))
&& cave_base < surface_z as i32 - 25
{
let kind = *assets::load_expect::<Lottery<BlockKind>>("common.cave_scatter")
.choose_seeded(RandomField::new(index.seed + 1).get(wpos2d.into()));
let _ = vol.set(
Vec3::new(offs.x, offs.y, cave_base),
Block::new(kind, Rgb::zero()),
);
}
}
}
}
}
pub fn apply_caves_supplement<'a>(
rng: &mut impl Rng,
wpos2d: Vec2<i32>,
mut get_column: impl FnMut(Vec2<i32>) -> Option<&'a ColumnSample<'a>>,
vol: &(impl BaseVol<Vox = Block> + RectSizedVol + ReadVol + WriteVol),
index: &Index,
supplement: &mut ChunkSupplement,
) {
for y in 0..vol.size_xy().y as i32 {
for x in 0..vol.size_xy().x as i32 {
let offs = Vec2::new(x, y);
let wpos2d = wpos2d + offs;
// Sample terrain
let col_sample = if let Some(col_sample) = get_column(offs) {
col_sample
} else {
continue;
};
let surface_z = col_sample.riverless_alt.floor() as i32;
if let Some((cave_dist, _, cave, _)) = col_sample
.cave
.filter(|(dist, _, cave, _)| *dist < cave.width)
{
let cave_x = (cave_dist / cave.width).min(1.0);
// Relative units
let cave_floor = 0.0 - 0.5 * (1.0 - cave_x.powf(2.0)).max(0.0).sqrt() * cave.width;
// Abs units
let cave_base = (cave.alt + cave_floor) as i32;
let cave_depth = (col_sample.alt - cave.alt).max(0.0);
let difficulty = cave_depth / 200.0;
// Scatter things in caves
if RandomField::new(index.seed).chance(wpos2d.into(), 0.00005 * difficulty)
&& cave_base < surface_z as i32 - 40
{
let entity = EntityInfo::at(Vec3::new(
wpos2d.x as f32,
wpos2d.y as f32,
cave_base as f32,
))
.with_alignment(comp::Alignment::Enemy)
.with_body(match rng.gen_range(0, 6) {
0 => {
let species = match rng.gen_range(0, 2) {
0 => comp::quadruped_small::Species::Truffler,
_ => comp::quadruped_small::Species::Hyena,
};
comp::quadruped_small::Body::random_with(rng, &species).into()
},
1 => {
let species = match rng.gen_range(0, 3) {
0 => comp::quadruped_medium::Species::Tarasque,
1 => comp::quadruped_medium::Species::Frostfang,
_ => comp::quadruped_medium::Species::Bonerattler,
};
comp::quadruped_medium::Body::random_with(rng, &species).into()
},
2 => {
let species = match rng.gen_range(0, 3) {
0 => comp::quadruped_low::Species::Maneater,
1 => comp::quadruped_low::Species::Rocksnapper,
_ => comp::quadruped_low::Species::Salamander,
};
comp::quadruped_low::Body::random_with(rng, &species).into()
},
3 => {
let species = match rng.gen_range(0, 3) {
0 => comp::critter::Species::Fungome,
1 => comp::critter::Species::Axolotl,
_ => comp::critter::Species::Rat,
};
comp::critter::Body::random_with(rng, &species).into()
},
4 => {
#[allow(clippy::match_single_binding)]
let species = match rng.gen_range(0, 1) {
_ => comp::golem::Species::StoneGolem,
};
comp::golem::Body::random_with(rng, &species).into()
},
_ => {
let species = match rng.gen_range(0, 4) {
0 => comp::biped_large::Species::Ogre,
1 => comp::biped_large::Species::Cyclops,
2 => comp::biped_large::Species::Wendigo,
_ => comp::biped_large::Species::Troll,
};
comp::biped_large::Body::random_with(rng, &species).into()
},
})
.with_automatic_name();
supplement.add_entity(entity);
}
}
}
}
}

View File

@ -8,8 +8,10 @@ mod block;
pub mod civ;
mod column;
pub mod config;
pub mod index;
pub mod layer;
pub mod sim;
pub mod sim2;
pub mod site;
pub mod util;
@ -19,6 +21,7 @@ pub use crate::config::CONFIG;
use crate::{
block::BlockGen,
column::{ColumnGen, ColumnSample},
index::Index,
util::{Grid, Sampler},
};
use common::{
@ -39,26 +42,35 @@ pub enum Error {
pub struct World {
sim: sim::WorldSim,
civs: civ::Civs,
index: Index,
}
impl World {
pub fn generate(seed: u32, opts: sim::WorldOpts) -> Self {
let mut sim = sim::WorldSim::generate(seed, opts);
let civs = civ::Civs::generate(seed, &mut sim);
Self { sim, civs }
let mut index = Index::new(seed);
let civs = civ::Civs::generate(seed, &mut sim, &mut index);
sim2::simulate(&mut index, &mut sim);
Self { sim, civs, index }
}
pub fn sim(&self) -> &sim::WorldSim { &self.sim }
pub fn civs(&self) -> &civ::Civs { &self.civs }
pub fn index(&self) -> &Index { &self.index }
pub fn tick(&self, _dt: Duration) {
// TODO
}
pub fn get_map_data(&self) -> Vec<u32> { self.sim.get_map(&self.index) }
pub fn sample_columns(
&self,
) -> impl Sampler<Index = Vec2<i32>, Sample = Option<ColumnSample>> + '_ {
) -> impl Sampler<Index = (Vec2<i32>, &Index), Sample = Option<ColumnSample>> + '_ {
ColumnGen::new(&self.sim)
}
@ -77,7 +89,7 @@ impl World {
let grid_border = 4;
let zcache_grid = Grid::populate_from(
TerrainChunkSize::RECT_SIZE.map(|e| e as i32) + grid_border * 2,
|offs| sampler.get_z_cache(chunk_wpos2d - grid_border + offs),
|offs| sampler.get_z_cache(chunk_wpos2d - grid_border + offs, &self.index),
);
let air = Block::empty();
@ -132,7 +144,8 @@ impl World {
_ => continue,
};
let (min_z, only_structures_min_z, max_z) = z_cache.get_z_limits(&mut sampler);
let (min_z, only_structures_min_z, max_z) =
z_cache.get_z_limits(&mut sampler, &self.index);
(base_z..min_z as i32).for_each(|z| {
let _ = chunk.set(Vec3::new(x, y, z), stone);
@ -144,7 +157,7 @@ impl World {
let only_structures = lpos.z >= only_structures_min_z as i32;
if let Some(block) =
sampler.get_with_z_cache(wpos, Some(&z_cache), only_structures)
sampler.get_with_z_cache(wpos, Some(&z_cache), only_structures, &self.index)
{
let _ = chunk.set(lpos, block);
}
@ -162,14 +175,15 @@ impl World {
let mut rng = rand::thread_rng();
// Apply site generation
sim_chunk
.sites
.iter()
.for_each(|site| site.apply_to(chunk_wpos2d, sample_get, &mut chunk));
// Apply layers (paths, caves, etc.)
layer::apply_scatter_to(chunk_wpos2d, sample_get, &mut chunk, &self.index, sim_chunk);
layer::apply_paths_to(chunk_wpos2d, sample_get, &mut chunk, &self.index);
layer::apply_caves_to(chunk_wpos2d, sample_get, &mut chunk, &self.index);
// Apply paths
layer::apply_paths_to(chunk_wpos2d, sample_get, &mut chunk);
// Apply site generation
sim_chunk.sites.iter().for_each(|site| {
self.index.sites[*site].apply_to(chunk_wpos2d, sample_get, &mut chunk)
});
let gen_entity_pos = || {
let lpos2d = TerrainChunkSize::RECT_SIZE
@ -215,9 +229,24 @@ impl World {
supplement.add_entity(EntityInfo::at(gen_entity_pos()).into_waypoint());
}
// Apply layer supplement
layer::apply_caves_supplement(
&mut rng,
chunk_wpos2d,
sample_get,
&chunk,
&self.index,
&mut supplement,
);
// Apply site supplementary information
sim_chunk.sites.iter().for_each(|site| {
site.apply_supplement(&mut rng, chunk_wpos2d, sample_get, &mut supplement)
self.index.sites[*site].apply_supplement(
&mut rng,
chunk_wpos2d,
sample_get,
&mut supplement,
)
});
Ok((chunk, supplement))

View File

@ -1,6 +1,6 @@
use crate::{
sim::{RiverKind, WorldSim, WORLD_SIZE},
CONFIG,
Index, CONFIG,
};
use common::{terrain::TerrainChunkSize, vol::RectVolSize};
use std::{f32, f64};
@ -114,6 +114,7 @@ impl MapConfig {
pub fn generate(
&self,
sampler: &WorldSim,
index: &Index,
mut write_pixel: impl FnMut(Vec2<usize>, (u8, u8, u8, u8)),
) -> MapDebug {
let MapConfig {
@ -156,6 +157,7 @@ impl MapConfig {
downhill,
river_kind,
is_path,
is_cave,
near_site,
) = sampler
.get(pos)
@ -168,9 +170,11 @@ impl MapConfig {
sample.temp,
sample.downhill,
sample.river.river_kind,
sample.path.is_path(),
sample.path.0.is_way(),
sample.cave.0.is_way(),
sample.sites.iter().any(|site| {
site.get_origin()
index.sites[*site]
.get_origin()
.distance_squared(pos * TerrainChunkSize::RECT_SIZE.x as i32)
< 64i32.pow(2)
}),
@ -186,6 +190,7 @@ impl MapConfig {
None,
false,
false,
false,
));
let humidity = humidity.min(1.0).max(0.0);
let temperature = temperature.min(1.0).max(-1.0) * 0.5 + 0.5;
@ -315,6 +320,8 @@ impl MapConfig {
(0x57, 0x39, 0x33, 0xFF)
} else if is_path {
(0x37, 0x29, 0x23, 0xFF)
} else if is_cave {
(0x37, 0x37, 0x37, 0xFF)
} else {
rgba
};

View File

@ -2,8 +2,8 @@ mod diffusion;
mod erosion;
mod location;
mod map;
mod path;
mod util;
mod way;
// Reexports
use self::erosion::Compute;
@ -15,12 +15,12 @@ pub use self::{
},
location::Location,
map::{MapConfig, MapDebug},
path::PathData,
util::{
cdf_irwin_hall, downhill, get_oceans, local_cells, map_edge_factor, neighbors,
uniform_idx_as_vec2, uniform_noise, uphill, vec2_as_uniform_idx, InverseCdf, ScaleBias,
NEIGHBOR_DELTA,
},
way::{Cave, Path, Way},
};
use crate::{
@ -28,7 +28,7 @@ use crate::{
civ::Place,
site::Site,
util::{seed_expan, FastNoise, RandomField, StructureGen2d, LOCALITY, NEIGHBORS},
CONFIG,
Index, CONFIG,
};
use common::{
assets,
@ -1305,14 +1305,14 @@ impl WorldSim {
/// Draw a map of the world based on chunk information. Returns a buffer of
/// u32s.
pub fn get_map(&self) -> Vec<u32> {
pub fn get_map(&self, index: &Index) -> Vec<u32> {
let mut v = vec![0u32; WORLD_SIZE.x * WORLD_SIZE.y];
// TODO: Parallelize again.
MapConfig {
gain: self.max_height,
..MapConfig::default()
}
.generate(&self, |pos, (r, g, b, a)| {
.generate(&self, index, |pos, (r, g, b, a)| {
v[pos.y * WORLD_SIZE.x + pos.x] = u32::from_le_bytes([r, g, b, a]);
});
v
@ -1699,7 +1699,14 @@ impl WorldSim {
Some(z0 + z1 + z2 + z3)
}
pub fn get_nearest_path(&self, wpos: Vec2<i32>) -> Option<(f32, Vec2<f32>)> {
/// Return the distance to the nearest way in blocks, along with the
/// closest point on the way, the way metadata, and the tangent vector
/// of that way.
pub fn get_nearest_way<M: Clone + Lerp<Output = M>>(
&self,
wpos: Vec2<i32>,
get_way: impl Fn(&SimChunk) -> Option<(Way, M)>,
) -> Option<(f32, Vec2<f32>, M, Vec2<f32>)> {
let chunk_pos = wpos.map2(TerrainChunkSize::RECT_SIZE, |e, sz: u32| {
e.div_euclid(sz as i32)
});
@ -1709,32 +1716,35 @@ impl WorldSim {
})
};
let get_way = &get_way;
LOCALITY
.iter()
.filter_map(|ctrl| {
let chunk = self.get(chunk_pos + *ctrl)?;
let ctrl_pos =
get_chunk_centre(chunk_pos + *ctrl).map(|e| e as f32) + chunk.path.offset;
let (way, meta) = get_way(self.get(chunk_pos + *ctrl)?)?;
let ctrl_pos = get_chunk_centre(chunk_pos + *ctrl).map(|e| e as f32)
+ way.offset.map(|e| e as f32);
let chunk_connections = chunk.path.neighbors.count_ones();
let chunk_connections = way.neighbors.count_ones();
if chunk_connections == 0 {
return None;
}
let (start_pos, _start_idx) = if chunk_connections != 2 {
(ctrl_pos, None)
let (start_pos, _start_idx, start_meta) = if chunk_connections != 2 {
(ctrl_pos, None, meta.clone())
} else {
let (start_idx, start_rpos) = NEIGHBORS
.iter()
.copied()
.enumerate()
.find(|(i, _)| chunk.path.neighbors & (1 << *i as u8) != 0)
.find(|(i, _)| way.neighbors & (1 << *i as u8) != 0)
.unwrap();
let start_pos_chunk = chunk_pos + *ctrl + start_rpos;
let (start_way, start_meta) = get_way(self.get(start_pos_chunk)?)?;
(
get_chunk_centre(start_pos_chunk).map(|e| e as f32)
+ self.get(start_pos_chunk)?.path.offset,
+ start_way.offset.map(|e| e as f32),
Some(start_idx),
start_meta,
)
};
@ -1742,11 +1752,12 @@ impl WorldSim {
NEIGHBORS
.iter()
.enumerate()
.filter(move |(i, _)| chunk.path.neighbors & (1 << *i as u8) != 0)
.filter(move |(i, _)| way.neighbors & (1 << *i as u8) != 0)
.filter_map(move |(_, end_rpos)| {
let end_pos_chunk = chunk_pos + *ctrl + end_rpos;
let (end_way, end_meta) = get_way(self.get(end_pos_chunk)?)?;
let end_pos = get_chunk_centre(end_pos_chunk).map(|e| e as f32)
+ self.get(end_pos_chunk)?.path.offset;
+ end_way.offset.map(|e| e as f32);
let bez = QuadraticBezier2 {
start: (start_pos + ctrl_pos) / 2.0,
@ -1759,13 +1770,28 @@ impl WorldSim {
.clamped(0.0, 1.0);
let pos = bez.evaluate(nearest_interval);
let dist_sqrd = pos.distance_squared(wpos.map(|e| e as f32));
Some((dist_sqrd, pos))
let meta = if nearest_interval < 0.5 {
Lerp::lerp(start_meta.clone(), meta.clone(), 0.5 + nearest_interval)
} else {
Lerp::lerp(meta.clone(), end_meta, nearest_interval - 0.5)
};
Some((dist_sqrd, pos, meta, move || {
bez.evaluate_derivative(nearest_interval).normalized()
}))
}),
)
})
.flatten()
.min_by_key(|(dist_sqrd, _)| (dist_sqrd * 1024.0) as i32)
.map(|(dist, pos)| (dist.sqrt(), pos))
.min_by_key(|(dist_sqrd, _, _, _)| (dist_sqrd * 1024.0) as i32)
.map(|(dist, pos, meta, calc_tangent)| (dist.sqrt(), pos, meta, calc_tangent()))
}
pub fn get_nearest_path(&self, wpos: Vec2<i32>) -> Option<(f32, Vec2<f32>, Path, Vec2<f32>)> {
self.get_nearest_way(wpos, |chunk| Some(chunk.path))
}
pub fn get_nearest_cave(&self, wpos: Vec2<i32>) -> Option<(f32, Vec2<f32>, Cave, Vec2<f32>)> {
self.get_nearest_way(wpos, |chunk| Some(chunk.cave))
}
}
@ -1787,10 +1813,14 @@ pub struct SimChunk {
pub spawn_rate: f32,
pub river: RiverData,
pub warp_factor: f32,
pub surface_veg: f32,
pub sites: Vec<Site>,
pub sites: Vec<Id<Site>>,
pub place: Option<Id<Place>>,
pub path: PathData,
pub path: (Way, Path),
pub cave: (Way, Cave),
pub contains_waypoint: bool,
}
@ -2022,10 +2052,12 @@ impl SimChunk {
spawn_rate: 1.0,
river,
warp_factor: 1.0,
surface_veg: 1.0,
sites: Vec::new(),
place: None,
path: PathData::default(),
path: Default::default(),
cave: Default::default(),
contains_waypoint: false,
}
}

View File

@ -1,21 +0,0 @@
use vek::*;
#[derive(Debug)]
pub struct PathData {
pub offset: Vec2<f32>, /* Offset from centre of chunk: must not be more than half chunk
* width in any direction */
pub neighbors: u8, // One bit for each neighbor
}
impl PathData {
pub fn is_path(&self) -> bool { self.neighbors != 0 }
}
impl Default for PathData {
fn default() -> Self {
Self {
offset: Vec2::zero(),
neighbors: 0,
}
}
}

72
world/src/sim/way.rs Normal file
View File

@ -0,0 +1,72 @@
use vek::*;
#[derive(Copy, Clone, Debug, Default)]
pub struct Way {
/// Offset from chunk center in blocks (no more than half chunk width)
pub offset: Vec2<i8>,
/// Neighbor connections, one bit each
pub neighbors: u8,
}
impl Way {
pub fn is_way(&self) -> bool { self.neighbors != 0 }
pub fn clear(&mut self) { self.neighbors = 0; }
}
#[derive(Copy, Clone, Debug)]
pub struct Path {
pub width: f32, // Actually radius
}
impl Default for Path {
fn default() -> Self { Self { width: 5.0 } }
}
impl Lerp for Path {
type Output = Self;
fn lerp_unclamped(from: Self, to: Self, factor: f32) -> Self::Output {
Self {
width: Lerp::lerp(from.width, to.width, factor),
}
}
}
impl Path {
/// Return the number of blocks of headspace required at the given path
/// distance
/// TODO: make this generic over width
pub fn head_space(&self, dist: f32) -> i32 {
(8 - (dist * 0.25).powf(6.0).round() as i32).max(1)
}
/// Get the surface colour of a path given the surrounding surface color
pub fn surface_color(&self, col: Rgb<u8>) -> Rgb<u8> { col.map(|e| (e as f32 * 0.7) as u8) }
}
#[derive(Copy, Clone, Debug)]
pub struct Cave {
pub width: f32, // Actually radius
pub alt: f32, // Actually radius
}
impl Default for Cave {
fn default() -> Self {
Self {
width: 32.0,
alt: 0.0,
}
}
}
impl Lerp for Cave {
type Output = Self;
fn lerp_unclamped(from: Self, to: Self, factor: f32) -> Self::Output {
Self {
width: Lerp::lerp(from.width, to.width, factor),
alt: Lerp::lerp(from.alt, to.alt, factor),
}
}
}

286
world/src/sim2/mod.rs Normal file
View File

@ -0,0 +1,286 @@
use crate::{
sim::WorldSim,
site::{
economy::{Good, Labor},
Site,
},
util::MapVec,
Index,
};
use common::store::Id;
use tracing::debug;
const MONTH: f32 = 30.0;
const YEAR: f32 = 12.0 * MONTH;
const TICK_PERIOD: f32 = 3.0 * MONTH; // 3 months
const HISTORY_DAYS: f32 = 500.0 * YEAR; // 500 years
const GENERATE_CSV: bool = false;
pub fn simulate(index: &mut Index, world: &mut WorldSim) {
use std::io::Write;
let mut f = if GENERATE_CSV {
let mut f = std::fs::File::create("economy.csv").unwrap();
write!(f, "Population,").unwrap();
for g in Good::list() {
write!(f, "{:?} Value,", g).unwrap();
}
for g in Good::list() {
write!(f, "{:?} LaborVal,", g).unwrap();
}
for g in Good::list() {
write!(f, "{:?} Stock,", g).unwrap();
}
for g in Good::list() {
write!(f, "{:?} Surplus,", g).unwrap();
}
for l in Labor::list() {
write!(f, "{:?} Labor,", l).unwrap();
}
for l in Labor::list() {
write!(f, "{:?} Productivity,", l).unwrap();
}
for l in Labor::list() {
write!(f, "{:?} Yields,", l).unwrap();
}
writeln!(f).unwrap();
Some(f)
} else {
None
};
for i in 0..(HISTORY_DAYS / TICK_PERIOD) as i32 {
if (index.time / YEAR) as i32 % 50 == 0 && (index.time % YEAR) as i32 == 0 {
debug!("Year {}", (index.time / YEAR) as i32);
}
tick(index, world, TICK_PERIOD);
if let Some(f) = f.as_mut() {
if i % 5 == 0 {
let site = index.sites.values().next().unwrap();
write!(f, "{},", site.economy.pop).unwrap();
for g in Good::list() {
write!(f, "{:?},", site.economy.values[*g].unwrap_or(-1.0)).unwrap();
}
for g in Good::list() {
write!(f, "{:?},", site.economy.labor_values[*g].unwrap_or(-1.0)).unwrap();
}
for g in Good::list() {
write!(f, "{:?},", site.economy.stocks[*g]).unwrap();
}
for g in Good::list() {
write!(f, "{:?},", site.economy.marginal_surplus[*g]).unwrap();
}
for l in Labor::list() {
write!(f, "{:?},", site.economy.labors[*l] * site.economy.pop).unwrap();
}
for l in Labor::list() {
write!(f, "{:?},", site.economy.productivity[*l]).unwrap();
}
for l in Labor::list() {
write!(f, "{:?},", site.economy.yields[*l]).unwrap();
}
writeln!(f).unwrap();
}
}
}
}
pub fn tick(index: &mut Index, _world: &mut WorldSim, dt: f32) {
for site in index.sites.ids() {
tick_site_economy(index, site, dt);
}
index.time += dt;
}
/// Simulate a site's economy. This simulation is roughly equivalent to the
/// Lange-Lerner model's solution to the socialist calculation problem. The
/// simulation begins by assigning arbitrary values to each commodity and then
/// incrementally updates them according to the final scarcity of the commodity
/// at the end of the tick. This results in the formulation of values that are
/// roughly analgous to prices for each commodity. The workforce is then
/// reassigned according to the respective commodity values. The simulation also
/// includes damping terms that prevent cyclical inconsistencies in value
/// rationalisation magnifying enough to crash the economy. We also ensure that
/// a small number of workers are allocated to every industry (even inactive
/// ones) each tick. This is not an accident: a small amount of productive
/// capacity in one industry allows the economy to quickly pivot to a different
/// prodution configuration should an additional commodity that acts as
/// production input become available. This means that the economy will
/// dynamically react to environmental changes. If a product becomes available
/// through a mechanism such as trade, an entire arm of the economy may
/// materialise to take advantage of this.
pub fn tick_site_economy(index: &mut Index, site: Id<Site>, dt: f32) {
let site = &mut index.sites[site];
let orders = site.economy.get_orders();
let productivity = site.economy.get_productivity();
let mut demand = MapVec::from_default(0.0);
for (labor, orders) in &orders {
let scale = if let Some(labor) = labor {
site.economy.labors[*labor]
} else {
1.0
} * site.economy.pop;
for (good, amount) in orders {
demand[*good] += *amount * scale;
}
}
let mut supply = site.economy.stocks.clone(); //MapVec::from_default(0.0);
for (labor, (output_good, _)) in productivity.iter() {
supply[*output_good] +=
site.economy.yields[labor] * site.economy.labors[labor] * site.economy.pop;
}
let stocks = &site.economy.stocks;
site.economy.surplus = demand
.clone()
.map(|g, demand| supply[g] + stocks[g] - demand);
site.economy.marginal_surplus = demand.clone().map(|g, demand| supply[g] - demand);
// Update values according to the surplus of each stock
// Note that values are used for workforce allocation and are not the same thing
// as price
let values = &mut site.economy.values;
site.economy.surplus.iter().for_each(|(good, surplus)| {
// Value rationalisation
let val = 2.0f32.powf(1.0 - *surplus / demand[good]);
let smooth = 0.8;
values[good] = if val > 0.001 && val < 1000.0 {
Some(smooth * values[good].unwrap_or(val) + (1.0 - smooth) * val)
} else {
None
};
});
// Update export targets based on relative values
// let value_avg = values
// .iter()
// .map(|(_, v)| (*v).unwrap_or(0.0))
// .sum::<f32>()
// .max(0.01)
// / values.iter().filter(|(_, v)| v.is_some()).count() as f32;
//let export_targets = &mut site.economy.export_targets;
//let last_exports = &self.last_exports;
// site.economy.values.iter().for_each(|(stock, value)| {
// let rvalue = (*value).map(|v| v - value_avg).unwrap_or(0.0);
// //let factor = if export_targets[stock] > 0.0 { 1.0 / rvalue } else {
// rvalue }; //export_targets[stock] = last_exports[stock] - rvalue *
// 0.1; // + (trade_states[stock].sell_belief.price -
// trade_states[stock].buy_belief.price) * 0.025; });
//let pop = site.economy.pop;
// Redistribute workforce according to relative good values
let labor_ratios = productivity.clone().map(|labor, (output_good, _)| {
site.economy.values[output_good].unwrap_or(0.0)
* site.economy.productivity[labor]
//(site.economy.prices[output_good] - site.economy.material_costs[output_good]) * site.economy.yields[labor]
//* demand[output_good] / supply[output_good].max(0.001)
});
let labor_ratio_sum = labor_ratios.iter().map(|(_, r)| *r).sum::<f32>().max(0.01);
productivity.iter().for_each(|(labor, _)| {
let smooth = 0.8;
site.economy.labors[labor] = smooth * site.economy.labors[labor]
+ (1.0 - smooth)
* (labor_ratios[labor].max(labor_ratio_sum / 1000.0) / labor_ratio_sum);
});
// Production
let stocks_before = site.economy.stocks.clone();
let mut total_labor_values = MapVec::<_, f32>::default();
let mut total_outputs = MapVec::<_, f32>::default();
for (labor, orders) in orders.iter() {
let scale = if let Some(labor) = labor {
site.economy.labors[*labor]
} else {
1.0
} * site.economy.pop;
// For each order, we try to find the minimum satisfaction rate - this limits
// how much we can produce! For example, if we need 0.25 fish and
// 0.75 oats to make 1 unit of food, but only 0.5 units of oats are
// available then we only need to consume 2/3rds
// of other ingredients and leave the rest in stock
// In effect, this is the productivity
let labor_productivity = orders
.iter()
.map(|(good, amount)| {
// What quantity is this order requesting?
let _quantity = *amount * scale;
// What proportion of this order is the economy able to satisfy?
(stocks_before[*good] / demand[*good]).min(1.0)
})
.min_by(|a, b| a.partial_cmp(b).unwrap())
.unwrap_or_else(|| panic!("Industry {:?} requires at least one input order", labor));
let mut total_materials_cost = 0.0;
for (good, amount) in orders {
// What quantity is this order requesting?
let quantity = *amount * scale;
// What amount gets actually used in production?
let used = quantity * labor_productivity;
// Material cost of each factor of production
total_materials_cost += used * site.economy.labor_values[*good].unwrap_or(0.0);
// Deplete stocks accordingly
site.economy.stocks[*good] = (site.economy.stocks[*good] - used).max(0.0);
}
// Industries produce things
if let Some(labor) = labor {
let (stock, rate) = productivity[*labor];
let workers = site.economy.labors[*labor] * site.economy.pop;
let final_rate = rate;
let yield_per_worker =
labor_productivity * final_rate * (1.0 + workers / 100.0).min(3.0);
site.economy.yields[*labor] = yield_per_worker;
site.economy.productivity[*labor] = labor_productivity;
let total_output = yield_per_worker * workers;
site.economy.stocks[stock] += total_output;
// Materials cost per unit
site.economy.material_costs[stock] = total_materials_cost / total_output.max(0.001);
// Labor costs
let wages = 1.0;
let total_labor_cost = workers * wages;
total_labor_values[stock] += total_materials_cost + total_labor_cost;
total_outputs[stock] += total_output;
}
}
// Update labour values per unit
site.economy.labor_values = total_labor_values.map(|stock, tlv| {
let total_output = total_outputs[stock];
if total_output > 0.01 {
Some(tlv / total_outputs[stock])
} else {
None
}
});
// Decay stocks
site.economy
.stocks
.iter_mut()
.for_each(|(c, v)| *v *= 1.0 - c.decay_rate());
// Decay stocks
site.economy.replenish(index.time);
// Births/deaths
const NATURAL_BIRTH_RATE: f32 = 0.05;
const DEATH_RATE: f32 = 0.005;
let birth_rate = if site.economy.surplus[Good::Food] > 0.0 {
NATURAL_BIRTH_RATE
} else {
0.0
};
site.economy.pop += dt / YEAR * site.economy.pop * (birth_rate - DEATH_RATE);
}

View File

@ -0,0 +1,39 @@
use common::{terrain::Block, vol::Vox};
#[derive(Copy, Clone)]
pub struct BlockMask {
block: Block,
priority: i32,
}
impl BlockMask {
pub fn new(block: Block, priority: i32) -> Self { Self { block, priority } }
pub fn nothing() -> Self {
Self {
block: Block::empty(),
priority: 0,
}
}
pub fn with_priority(mut self, priority: i32) -> Self {
self.priority = priority;
self
}
pub fn resolve_with(self, other: Self) -> Self {
if self.priority >= other.priority {
self
} else {
other
}
}
pub fn finish(self) -> Option<Block> {
if self.priority > 0 {
Some(self.block)
} else {
None
}
}
}

View File

@ -0,0 +1,416 @@
use super::SpawnRules;
use crate::{
column::ColumnSample,
sim::WorldSim,
site::settlement::building::{
archetype::keep::{Attr, Keep as KeepArchetype},
Archetype, Ori,
},
};
use common::{
generation::ChunkSupplement,
terrain::{Block, BlockKind},
vol::{BaseVol, ReadVol, RectSizedVol, Vox, WriteVol},
};
use core::f32;
use rand::prelude::*;
use vek::*;
struct Keep {
offset: Vec2<i32>,
locus: i32,
storeys: i32,
is_tower: bool,
alt: i32,
}
struct Tower {
offset: Vec2<i32>,
alt: i32,
}
pub struct Castle {
origin: Vec2<i32>,
//seed: u32,
radius: i32,
towers: Vec<Tower>,
keeps: Vec<Keep>,
rounded_towers: bool,
ridged: bool,
flags: bool,
evil: bool,
}
pub struct GenCtx<'a, R: Rng> {
sim: Option<&'a mut WorldSim>,
rng: &'a mut R,
}
impl Castle {
#[allow(clippy::let_and_return)] // TODO: Pending review in #587
pub fn generate(wpos: Vec2<i32>, sim: Option<&mut WorldSim>, rng: &mut impl Rng) -> Self {
let ctx = GenCtx { sim, rng };
let boundary_towers = ctx.rng.gen_range(5, 10);
let keep_count = ctx.rng.gen_range(1, 4);
let boundary_noise = ctx.rng.gen_range(-2i32, 8).max(1) as f32;
let radius = 150;
let this = Self {
origin: wpos,
// alt: ctx
// .sim
// .as_ref()
// .and_then(|sim| sim.get_alt_approx(wpos))
// .unwrap_or(0.0) as i32
// + 6,
//seed: ctx.rng.gen(),
radius,
towers: (0..boundary_towers)
.map(|i| {
let angle = (i as f32 / boundary_towers as f32) * f32::consts::PI * 2.0;
let dir = Vec2::new(angle.cos(), angle.sin());
let dist = radius as f32 + ((angle * boundary_noise).sin() - 1.0) * 40.0;
let mut offset = (dir * dist).map(|e| e as i32);
// Try to move the tower around until it's not intersecting a path
for i in (1..80).step_by(5) {
if ctx
.sim
.as_ref()
.and_then(|sim| sim.get_nearest_path(wpos + offset))
.map(|(dist, _, _, _)| dist > 24.0)
.unwrap_or(true)
{
break;
}
offset = (dir * dist)
.map(|e| (e + ctx.rng.gen_range(-1.0, 1.0) * i as f32) as i32);
}
Tower {
offset,
alt: ctx
.sim
.as_ref()
.and_then(|sim| sim.get_alt_approx(wpos + offset))
.unwrap_or(0.0) as i32
+ 2,
}
})
.collect(),
rounded_towers: ctx.rng.gen(),
ridged: ctx.rng.gen(),
flags: ctx.rng.gen(),
evil: ctx.rng.gen(),
keeps: (0..keep_count)
.map(|i| {
let angle = (i as f32 / keep_count as f32) * f32::consts::PI * 2.0;
let dir = Vec2::new(angle.cos(), angle.sin());
let dist =
(radius as f32 + ((angle * boundary_noise).sin() - 1.0) * 40.0) * 0.3;
let locus = ctx.rng.gen_range(20, 26);
let offset = (dir * dist).map(|e| e as i32);
let storeys = ctx.rng.gen_range(1, 8).clamped(3, 5);
Keep {
offset,
locus,
storeys,
is_tower: true,
alt: ctx
.sim
.as_ref()
.and_then(|sim| sim.get_alt_approx(wpos + offset))
.unwrap_or(0.0) as i32
+ 2,
}
})
.collect(),
};
this
}
pub fn contains_point(&self, wpos: Vec2<i32>) -> bool {
let lpos = wpos - self.origin;
for i in 0..self.towers.len() {
let tower0 = &self.towers[i];
let tower1 = &self.towers[(i + 1) % self.towers.len()];
if lpos.determine_side(Vec2::zero(), tower0.offset) > 0
&& lpos.determine_side(Vec2::zero(), tower1.offset) <= 0
&& lpos.determine_side(tower0.offset, tower1.offset) > 0
{
return true;
}
}
false
}
pub fn get_origin(&self) -> Vec2<i32> { self.origin }
pub fn radius(&self) -> f32 { 1200.0 }
#[allow(clippy::needless_update)] // TODO: Pending review in #587
pub fn spawn_rules(&self, wpos: Vec2<i32>) -> SpawnRules {
SpawnRules {
trees: wpos.distance_squared(self.origin) > self.radius.pow(2),
..SpawnRules::default()
}
}
pub fn apply_to<'a>(
&'a self,
wpos2d: Vec2<i32>,
mut get_column: impl FnMut(Vec2<i32>) -> Option<&'a ColumnSample<'a>>,
vol: &mut (impl BaseVol<Vox = Block> + RectSizedVol + ReadVol + WriteVol),
) {
for y in 0..vol.size_xy().y as i32 {
for x in 0..vol.size_xy().x as i32 {
let offs = Vec2::new(x, y);
let wpos2d = wpos2d + offs;
let rpos = wpos2d - self.origin;
if rpos.magnitude_squared() > (self.radius + 64).pow(2) {
continue;
}
let col_sample = if let Some(col) = get_column(offs) {
col
} else {
continue;
};
// Inner ground
if self.contains_point(wpos2d) {
let surface_z = col_sample.alt as i32;
for z in -5..3 {
let pos = Vec3::new(offs.x, offs.y, surface_z + z);
if z > 0 {
if vol.get(pos).unwrap().kind() != BlockKind::Water {
let _ = vol.set(pos, Block::empty());
}
} else {
let _ = vol.set(
pos,
Block::new(
BlockKind::Normal,
col_sample.sub_surface_color.map(|e| (e * 255.0) as u8),
),
);
}
}
}
let (wall_dist, wall_pos, wall_alt, wall_ori, _towers) = (0..self.towers.len())
.map(|i| {
let tower0 = &self.towers[i];
let tower1 = &self.towers[(i + 1) % self.towers.len()];
let wall = LineSegment2 {
start: tower0.offset.map(|e| e as f32),
end: tower1.offset.map(|e| e as f32),
};
let projected = wall
.projected_point(rpos.map(|e| e as f32))
.map(|e| e.floor() as i32);
let tower0_dist = tower0
.offset
.map(|e| e as f32)
.distance(projected.map(|e| e as f32));
let tower1_dist = tower1
.offset
.map(|e| e as f32)
.distance(projected.map(|e| e as f32));
let tower_lerp = tower0_dist / (tower0_dist + tower1_dist);
let wall_ori = if (tower0.offset.x - tower1.offset.x).abs()
< (tower0.offset.y - tower1.offset.y).abs()
{
Ori::North
} else {
Ori::East
};
(
wall.distance_to_point(rpos.map(|e| e as f32)) as i32,
projected,
Lerp::lerp(tower0.alt as f32, tower1.alt as f32, tower_lerp) as i32,
wall_ori,
[tower0, tower1],
)
})
.min_by_key(|x| x.0)
.unwrap();
let border_pos = (wall_pos - rpos).map(|e| e.abs());
let wall_rpos = if wall_ori == Ori::East {
rpos
} else {
rpos.yx()
};
let head_space = col_sample
.path
.map(|(dist, _, path, _)| path.head_space(dist))
.unwrap_or(0);
let wall_sample = if let Some(col) = get_column(offs + wall_pos - rpos) {
col
} else {
col_sample
};
// Make sure particularly weird terrain doesn't give us underground walls
let wall_alt = wall_alt + (wall_sample.alt as i32 - wall_alt - 10).max(0);
let keep_archetype = KeepArchetype {
flag_color: if self.evil {
Rgb::new(80, 10, 130)
} else {
Rgb::new(200, 80, 40)
},
stone_color: if self.evil {
Rgb::new(65, 60, 55)
} else {
Rgb::new(100, 100, 110)
},
};
for z in -10..64 {
let wpos = Vec3::new(wpos2d.x, wpos2d.y, col_sample.alt as i32 + z);
// Boundary wall
let wall_z = wpos.z - wall_alt;
if z < head_space {
continue;
}
let mut mask = keep_archetype.draw(
Vec3::from(wall_rpos) + Vec3::unit_z() * wpos.z - wall_alt,
wall_dist,
border_pos,
rpos - wall_pos,
wall_z,
wall_ori,
4,
0,
&Attr {
storeys: 2,
is_tower: false,
flag: self.flags,
ridged: false,
rounded: true,
has_doors: false,
},
);
// Boundary towers
for tower in &self.towers {
let tower_wpos = Vec3::new(
self.origin.x + tower.offset.x,
self.origin.y + tower.offset.y,
tower.alt,
);
let tower_locus = 10;
let border_pos = (tower_wpos - wpos).xy().map(|e| e.abs());
mask = mask.resolve_with(keep_archetype.draw(
if (tower_wpos.x - wpos.x).abs() < (tower_wpos.y - wpos.y).abs() {
wpos - tower_wpos
} else {
Vec3::new(
wpos.y - tower_wpos.y,
wpos.x - tower_wpos.x,
wpos.z - tower_wpos.z,
)
},
border_pos.reduce_max() - tower_locus,
Vec2::new(border_pos.reduce_min(), border_pos.reduce_max()),
(wpos - tower_wpos).xy(),
wpos.z - tower.alt,
if border_pos.x > border_pos.y {
Ori::East
} else {
Ori::North
},
tower_locus,
0,
&Attr {
storeys: 3,
is_tower: true,
flag: self.flags,
ridged: self.ridged,
rounded: self.rounded_towers,
has_doors: false,
},
));
}
// Keeps
for keep in &self.keeps {
let keep_wpos = Vec3::new(
self.origin.x + keep.offset.x,
self.origin.y + keep.offset.y,
keep.alt,
);
let border_pos = (keep_wpos - wpos).xy().map(|e| e.abs());
mask = mask.resolve_with(keep_archetype.draw(
if (keep_wpos.x - wpos.x).abs() < (keep_wpos.y - wpos.y).abs() {
wpos - keep_wpos
} else {
Vec3::new(
wpos.y - keep_wpos.y,
wpos.x - keep_wpos.x,
wpos.z - keep_wpos.z,
)
},
border_pos.reduce_max() - keep.locus,
Vec2::new(border_pos.reduce_min(), border_pos.reduce_max()),
(wpos - keep_wpos).xy(),
wpos.z - keep.alt,
if border_pos.x > border_pos.y {
Ori::East
} else {
Ori::North
},
keep.locus,
0,
&Attr {
storeys: keep.storeys,
is_tower: keep.is_tower,
flag: self.flags,
ridged: self.ridged,
rounded: self.rounded_towers,
has_doors: true,
},
));
}
if let Some(block) = mask.finish() {
let _ = vol.set(Vec3::new(offs.x, offs.y, wpos.z), block);
}
}
}
}
}
#[allow(clippy::or_fun_call)] // TODO: Pending review in #587
pub fn apply_supplement<'a>(
&'a self,
_rng: &mut impl Rng,
_wpos2d: Vec2<i32>,
_get_column: impl FnMut(Vec2<i32>) -> Option<&'a ColumnSample<'a>>,
_supplement: &mut ChunkSupplement,
) {
// TODO
}
}

View File

@ -23,19 +23,6 @@ use rand::prelude::*;
use std::sync::Arc;
use vek::*;
impl WorldSim {
#[allow(dead_code)]
fn can_host_dungeon(&self, pos: Vec2<i32>) -> bool {
self.get(pos)
.map(|chunk| !chunk.near_cliffs && !chunk.river.is_river() && !chunk.river.is_lake())
.unwrap_or(false)
&& self
.get_gradient_approx(pos)
.map(|grad| grad > 0.25 && grad < 1.5)
.unwrap_or(false)
}
}
pub struct Dungeon {
origin: Vec2<i32>,
alt: i32,
@ -164,20 +151,6 @@ impl Dungeon {
max: rpos + TerrainChunkSize::RECT_SIZE.map(|e| e as i32),
};
if area.contains_point(Vec2::zero()) {
let offs = Vec2::new(rng.gen_range(-1.0, 1.0), rng.gen_range(-1.0, 1.0))
.try_normalized()
.unwrap_or(Vec2::unit_y())
* 12.0;
supplement.add_entity(
EntityInfo::at(
Vec3::new(self.origin.x, self.origin.y, self.alt + 16).map(|e| e as f32)
+ Vec3::from(offs),
)
.into_waypoint(),
);
}
let mut z = self.alt + ALT_OFFSET;
for floor in &self.floors {
z -= floor.total_depth();
@ -228,6 +201,7 @@ pub struct Floor {
hollow_depth: i32,
#[allow(dead_code)]
stair_tile: Vec2<i32>,
final_level: bool,
}
const FLOOR_SIZE: Vec2<i32> = Vec2::new(18, 18);
@ -260,6 +234,7 @@ impl Floor {
solid_depth: if level == 0 { 80 } else { 32 },
hollow_depth: 30,
stair_tile: new_stair_tile - tile_offset,
final_level,
};
const STAIR_ROOM_HEIGHT: i32 = 13;
@ -305,7 +280,7 @@ impl Floor {
this.create_rooms(ctx, level, 7);
// Create routes between all rooms
let room_areas = this.rooms.iter().map(|r| r.area).collect::<Vec<_>>();
let room_areas = this.rooms.values().map(|r| r.area).collect::<Vec<_>>();
for a in room_areas.iter() {
for b in room_areas.iter() {
this.create_route(ctx, a.center(), b.center());
@ -342,7 +317,7 @@ impl Floor {
// Ensure no overlap
if self
.rooms
.iter()
.values()
.any(|r| r.area.collides_with_rect(area_border))
{
return None;
@ -423,6 +398,24 @@ impl Floor {
origin: Vec3<i32>,
supplement: &mut ChunkSupplement,
) {
let stair_rcenter =
Vec3::from((self.stair_tile + self.tile_offset).map(|e| e * TILE_SIZE + TILE_SIZE / 2));
if area.contains_point(stair_rcenter.xy()) {
let offs = Vec2::new(rng.gen_range(-1.0, 1.0), rng.gen_range(-1.0, 1.0))
.try_normalized()
.unwrap_or_else(Vec2::unit_y)
* FLOOR_SIZE.x as f32
/ 2.0
- 8.0;
if !self.final_level {
supplement.add_entity(
EntityInfo::at((origin + stair_rcenter).map(|e| e as f32) + Vec3::from(offs))
.into_waypoint(),
);
}
}
for x in area.min.x..area.max.x {
for y in area.min.y..area.max.y {
let tile_pos = Vec2::new(x, y).map(|e| e.div_euclid(TILE_SIZE)) - self.tile_offset;
@ -478,7 +471,16 @@ impl Floor {
if room.boss {
let boss_spawn_tile = room.area.center();
// Don't spawn the boss in a pillar
let boss_spawn_tile = boss_spawn_tile + if tile_is_pillar { 1 } else { 0 };
let boss_tile_is_pillar = room
.pillars
.map(|pillar_space| {
boss_spawn_tile
.map(|e| e.rem_euclid(pillar_space) == 0)
.reduce_and()
})
.unwrap_or(false);
let boss_spawn_tile =
boss_spawn_tile + if boss_tile_is_pillar { 1 } else { 0 };
if tile_pos == boss_spawn_tile && tile_wcenter.xy() == wpos2d {
let entity = EntityInfo::at(tile_wcenter.map(|e| e as f32))
@ -635,10 +637,13 @@ impl Floor {
empty
};
let tunnel_height = if self.final_level { 16.0 } else { 8.0 };
move |z| match self.tiles.get(tile_pos) {
Some(Tile::Solid) => BlockMask::nothing(),
Some(Tile::Tunnel) => {
if dist_to_wall >= wall_thickness && (z as f32) < 8.0 - 8.0 * tunnel_dist.powf(4.0)
if dist_to_wall >= wall_thickness
&& (z as f32) < tunnel_height * (1.0 - tunnel_dist.powf(4.0))
{
if z == 0 { floor_sprite } else { empty }
} else {

155
world/src/site/economy.rs Normal file
View File

@ -0,0 +1,155 @@
use crate::util::{DHashMap, MapVec};
#[repr(u8)]
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum Good {
Wheat = 0,
Flour = 1,
Meat = 2,
Fish = 3,
Game = 4,
Food = 5,
Logs = 6,
Wood = 7,
Rock = 8,
Stone = 9,
}
use Good::*;
#[repr(u8)]
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum Labor {
Farmer = 0,
Lumberjack = 1,
Miner = 2,
Fisher = 3,
Hunter = 4,
Cook = 5,
}
use Labor::*;
pub struct Economy {
pub pop: f32,
pub stocks: MapVec<Good, f32>,
pub surplus: MapVec<Good, f32>,
pub marginal_surplus: MapVec<Good, f32>,
pub values: MapVec<Good, Option<f32>>,
pub labor_values: MapVec<Good, Option<f32>>,
pub material_costs: MapVec<Good, f32>,
pub labors: MapVec<Labor, f32>,
pub yields: MapVec<Labor, f32>,
pub productivity: MapVec<Labor, f32>,
}
impl Default for Economy {
fn default() -> Self {
Self {
pop: 32.0,
stocks: Default::default(),
surplus: Default::default(),
marginal_surplus: Default::default(),
values: Default::default(),
labor_values: Default::default(),
material_costs: Default::default(),
labors: Default::default(),
yields: Default::default(),
productivity: Default::default(),
}
}
}
impl Economy {
pub fn get_orders(&self) -> DHashMap<Option<Labor>, Vec<(Good, f32)>> {
vec![
(None, vec![(Food, 0.5)]),
(Some(Cook), vec![
(Flour, 12.0),
(Meat, 4.0),
(Wood, 1.5),
(Stone, 1.0),
]),
(Some(Lumberjack), vec![(Logs, 0.5)]),
(Some(Miner), vec![(Rock, 0.5)]),
(Some(Fisher), vec![(Fish, 4.0)]),
(Some(Hunter), vec![(Game, 1.0)]),
(Some(Farmer), vec![(Wheat, 2.0)]),
]
.into_iter()
.collect()
}
pub fn get_productivity(&self) -> MapVec<Labor, (Good, f32)> {
// Per labourer, per year
MapVec::from_list(
&[
(Farmer, (Flour, 2.0)),
(Lumberjack, (Wood, 0.5)),
(Miner, (Stone, 0.5)),
(Fisher, (Meat, 4.0)),
(Hunter, (Meat, 1.0)),
(Cook, (Food, 16.0)),
],
(Rock, 0.0),
)
.map(|l, (good, v)| (good, v * (1.0 + self.labors[l])))
}
pub fn replenish(&mut self, time: f32) {
//use rand::Rng;
for (i, (g, v)) in [
(Wheat, 50.0),
(Logs, 20.0),
(Rock, 120.0),
(Game, 12.0),
(Fish, 10.0),
]
.iter()
.enumerate()
{
self.stocks[*g] = (*v
* (1.25 + (((time * 0.0001 + i as f32).sin() + 1.0) % 1.0) * 0.5)
- self.stocks[*g])
* 0.075; //rand::thread_rng().gen_range(0.05, 0.1);
}
}
}
impl Default for Good {
fn default() -> Self {
Good::Rock // Arbitrary
}
}
impl Good {
pub fn list() -> &'static [Self] {
static GOODS: [Good; 10] = [
Wheat, Flour, Meat, Fish, Game, Food, Logs, Wood, Rock, Stone,
];
&GOODS
}
pub fn decay_rate(&self) -> f32 {
match self {
Food => 0.2,
Wheat => 0.1,
Meat => 0.25,
Fish => 0.2,
_ => 0.0,
}
}
}
impl Labor {
pub fn list() -> &'static [Self] {
static LABORS: [Labor; 6] = [Farmer, Lumberjack, Miner, Fisher, Hunter, Cook];
&LABORS
}
}

View File

@ -1,57 +1,24 @@
mod block_mask;
mod castle;
mod dungeon;
pub mod economy;
mod settlement;
// Reexports
pub use self::{dungeon::Dungeon, settlement::Settlement};
pub use self::{
block_mask::BlockMask, castle::Castle, dungeon::Dungeon, economy::Economy,
settlement::Settlement,
};
use crate::column::ColumnSample;
use common::{
generation::ChunkSupplement,
terrain::Block,
vol::{BaseVol, ReadVol, RectSizedVol, Vox, WriteVol},
vol::{BaseVol, ReadVol, RectSizedVol, WriteVol},
};
use rand::Rng;
use std::{fmt, sync::Arc};
use vek::*;
#[derive(Copy, Clone)]
pub struct BlockMask {
block: Block,
priority: i32,
}
impl BlockMask {
pub fn new(block: Block, priority: i32) -> Self { Self { block, priority } }
pub fn nothing() -> Self {
Self {
block: Block::empty(),
priority: 0,
}
}
pub fn with_priority(mut self, priority: i32) -> Self {
self.priority = priority;
self
}
pub fn resolve_with(self, other: Self) -> Self {
if self.priority >= other.priority {
self
} else {
other
}
}
pub fn finish(self) -> Option<Block> {
if self.priority > 0 {
Some(self.block)
} else {
None
}
}
}
pub struct SpawnRules {
pub trees: bool,
}
@ -60,31 +27,60 @@ impl Default for SpawnRules {
fn default() -> Self { Self { trees: true } }
}
#[derive(Clone)]
pub enum Site {
Settlement(Arc<Settlement>),
Dungeon(Arc<Dungeon>),
pub struct Site {
pub kind: SiteKind,
pub economy: Economy,
}
pub enum SiteKind {
Settlement(Settlement),
Dungeon(Dungeon),
Castle(Castle),
}
impl Site {
pub fn settlement(s: Settlement) -> Self {
Self {
kind: SiteKind::Settlement(s),
economy: Economy::default(),
}
}
pub fn dungeon(d: Dungeon) -> Self {
Self {
kind: SiteKind::Dungeon(d),
economy: Economy::default(),
}
}
pub fn castle(c: Castle) -> Self {
Self {
kind: SiteKind::Castle(c),
economy: Economy::default(),
}
}
pub fn radius(&self) -> f32 {
match self {
Site::Settlement(settlement) => settlement.radius(),
Site::Dungeon(dungeon) => dungeon.radius(),
match &self.kind {
SiteKind::Settlement(s) => s.radius(),
SiteKind::Dungeon(d) => d.radius(),
SiteKind::Castle(c) => c.radius(),
}
}
pub fn get_origin(&self) -> Vec2<i32> {
match self {
Site::Settlement(s) => s.get_origin(),
Site::Dungeon(d) => d.get_origin(),
match &self.kind {
SiteKind::Settlement(s) => s.get_origin(),
SiteKind::Dungeon(d) => d.get_origin(),
SiteKind::Castle(c) => c.get_origin(),
}
}
pub fn spawn_rules(&self, wpos: Vec2<i32>) -> SpawnRules {
match self {
Site::Settlement(s) => s.spawn_rules(wpos),
Site::Dungeon(d) => d.spawn_rules(wpos),
match &self.kind {
SiteKind::Settlement(s) => s.spawn_rules(wpos),
SiteKind::Dungeon(d) => d.spawn_rules(wpos),
SiteKind::Castle(c) => c.spawn_rules(wpos),
}
}
@ -94,9 +90,10 @@ impl Site {
get_column: impl FnMut(Vec2<i32>) -> Option<&'a ColumnSample<'a>>,
vol: &mut (impl BaseVol<Vox = Block> + RectSizedVol + ReadVol + WriteVol),
) {
match self {
Site::Settlement(settlement) => settlement.apply_to(wpos2d, get_column, vol),
Site::Dungeon(dungeon) => dungeon.apply_to(wpos2d, get_column, vol),
match &self.kind {
SiteKind::Settlement(s) => s.apply_to(wpos2d, get_column, vol),
SiteKind::Dungeon(d) => d.apply_to(wpos2d, get_column, vol),
SiteKind::Castle(c) => c.apply_to(wpos2d, get_column, vol),
}
}
@ -107,28 +104,10 @@ impl Site {
get_column: impl FnMut(Vec2<i32>) -> Option<&'a ColumnSample<'a>>,
supplement: &mut ChunkSupplement,
) {
match self {
Site::Settlement(settlement) => {
settlement.apply_supplement(rng, wpos2d, get_column, supplement)
},
Site::Dungeon(dungeon) => dungeon.apply_supplement(rng, wpos2d, get_column, supplement),
}
}
}
impl From<Settlement> for Site {
fn from(settlement: Settlement) -> Self { Site::Settlement(Arc::new(settlement)) }
}
impl From<Dungeon> for Site {
fn from(dungeon: Dungeon) -> Self { Site::Dungeon(Arc::new(dungeon)) }
}
impl fmt::Debug for Site {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Site::Settlement(_) => write!(f, "Settlement"),
Site::Dungeon(_) => write!(f, "Dungeon"),
match &self.kind {
SiteKind::Settlement(s) => s.apply_supplement(rng, wpos2d, get_column, supplement),
SiteKind::Dungeon(d) => d.apply_supplement(rng, wpos2d, get_column, supplement),
SiteKind::Castle(c) => c.apply_supplement(rng, wpos2d, get_column, supplement),
}
}
}

View File

@ -12,40 +12,81 @@ use common::{
use rand::prelude::*;
use vek::*;
const COLOR_THEMES: [Rgb<u8>; 11] = [
Rgb::new(0x1D, 0x4D, 0x45),
Rgb::new(0xB3, 0x7D, 0x60),
Rgb::new(0xAC, 0x5D, 0x26),
Rgb::new(0x32, 0x46, 0x6B),
Rgb::new(0x2B, 0x19, 0x0F),
Rgb::new(0x93, 0x78, 0x51),
Rgb::new(0x92, 0x57, 0x24),
Rgb::new(0x4A, 0x4E, 0x4E),
Rgb::new(0x2F, 0x32, 0x47),
Rgb::new(0x8F, 0x35, 0x43),
Rgb::new(0x6D, 0x1E, 0x3A),
pub struct ColorTheme {
roof: Rgb<u8>,
wall: Rgb<u8>,
support: Rgb<u8>,
}
const ROOF_COLORS: &[Rgb<u8>] = &[
// Rgb::new(0x1D, 0x4D, 0x45),
// Rgb::new(0xB3, 0x7D, 0x60),
// Rgb::new(0xAC, 0x5D, 0x26),
// Rgb::new(0x32, 0x46, 0x6B),
// Rgb::new(0x2B, 0x19, 0x0F),
// Rgb::new(0x93, 0x78, 0x51),
// Rgb::new(0x92, 0x57, 0x24),
// Rgb::new(0x4A, 0x4E, 0x4E),
// Rgb::new(0x2F, 0x32, 0x47),
// Rgb::new(0x8F, 0x35, 0x43),
// Rgb::new(0x6D, 0x1E, 0x3A),
// Rgb::new(0x6D, 0xA7, 0x80),
// Rgb::new(0x4F, 0xA0, 0x95),
// Rgb::new(0xE2, 0xB9, 0x99),
// Rgb::new(0x7A, 0x30, 0x22),
// Rgb::new(0x4A, 0x06, 0x08),
// Rgb::new(0x8E, 0xB4, 0x57),
Rgb::new(0x99, 0x5E, 0x54),
Rgb::new(0x43, 0x63, 0x64),
Rgb::new(0x76, 0x6D, 0x68),
Rgb::new(0x7B, 0x41, 0x61),
Rgb::new(0x52, 0x20, 0x20),
Rgb::new(0x1A, 0x4A, 0x59),
Rgb::new(0xCC, 0x76, 0x4E),
];
const WALL_COLORS: &[Rgb<u8>] = &[
Rgb::new(200, 180, 150),
Rgb::new(0xB8, 0xB4, 0xA4),
Rgb::new(0x76, 0x6D, 0x68),
Rgb::new(0xF3, 0xC9, 0x8F),
Rgb::new(0xD3, 0xB7, 0x99),
Rgb::new(0xE1, 0xAB, 0x91),
Rgb::new(0x82, 0x57, 0x4C),
Rgb::new(0xB9, 0x96, 0x77),
Rgb::new(0xAE, 0x8D, 0x9C),
];
const SUPPORT_COLORS: &[Rgb<u8>] = &[
Rgb::new(60, 45, 30),
Rgb::new(0x65, 0x55, 0x56),
Rgb::new(0x53, 0x33, 0x13),
Rgb::new(0x58, 0x42, 0x33),
];
pub struct House {
roof_color: Rgb<u8>,
noise: RandomField,
roof_ribbing: bool,
roof_ribbing_diagonal: bool,
pub colors: ColorTheme,
pub noise: RandomField,
pub roof_ribbing: bool,
pub roof_ribbing_diagonal: bool,
}
enum Pillar {
#[derive(Copy, Clone)]
pub enum Pillar {
None,
Chimney(i32),
Tower(i32),
}
enum RoofStyle {
#[derive(Copy, Clone)]
pub enum RoofStyle {
Hip,
Gable,
Rounded,
}
enum StoreyFill {
#[derive(Copy, Clone)]
pub enum StoreyFill {
None,
Upper,
All,
@ -69,16 +110,19 @@ impl StoreyFill {
}
}
#[derive(Copy, Clone)]
pub struct Attr {
central_supports: bool,
storey_fill: StoreyFill,
roof_style: RoofStyle,
mansard: i32,
pillar: Pillar,
pub central_supports: bool,
pub storey_fill: StoreyFill,
pub roof_style: RoofStyle,
pub mansard: i32,
pub pillar: Pillar,
pub levels: i32,
pub window: BlockKind,
}
impl Attr {
fn generate<R: Rng>(rng: &mut R, locus: i32) -> Self {
pub fn generate<R: Rng>(rng: &mut R, _locus: i32) -> Self {
Self {
central_supports: rng.gen(),
storey_fill: match rng.gen_range(0, 2) {
@ -93,9 +137,16 @@ impl Attr {
},
mansard: rng.gen_range(-7, 4).max(0),
pillar: match rng.gen_range(0, 4) {
0 => Pillar::Chimney(9 + locus + rng.gen_range(0, 4)),
0 => Pillar::Chimney(rng.gen_range(2, 6)),
_ => Pillar::None,
},
levels: rng.gen_range(1, 3),
window: match rng.gen_range(0, 4) {
0 => BlockKind::Window1,
1 => BlockKind::Window2,
2 => BlockKind::Window3,
_ => BlockKind::Window4,
},
}
}
}
@ -107,6 +158,7 @@ impl Archetype for House {
let len = rng.gen_range(-8, 24).clamped(0, 20);
let locus = 6 + rng.gen_range(0, 5);
let branches_per_side = 1 + len as usize / 20;
let levels = rng.gen_range(1, 3);
let skel = Skeleton {
offset: -rng.gen_range(0, len + 7).clamped(0, len),
ori: if rng.gen() { Ori::East } else { Ori::North },
@ -116,10 +168,11 @@ impl Archetype for House {
storey_fill: StoreyFill::All,
mansard: 0,
pillar: match rng.gen_range(0, 3) {
0 => Pillar::Chimney(10 + locus + rng.gen_range(0, 4)),
1 => Pillar::Tower(15 + locus + rng.gen_range(0, 4)),
0 => Pillar::Chimney(rng.gen_range(2, 6)),
1 => Pillar::Tower(5 + rng.gen_range(1, 5)),
_ => Pillar::None,
},
levels,
..Attr::generate(rng, locus)
},
locus,
@ -134,7 +187,10 @@ impl Archetype for House {
i as i32 * len / (branches_per_side - 1).max(1) as i32,
Branch {
len: rng.gen_range(8, 16) * flip,
attr: Attr::generate(rng, locus),
attr: Attr {
levels: rng.gen_range(1, 4).min(levels),
..Attr::generate(rng, locus)
},
locus: (6 + rng.gen_range(0, 3)).min(locus),
border: 4,
children: Vec::new(),
@ -149,10 +205,11 @@ impl Archetype for House {
};
let this = Self {
roof_color: COLOR_THEMES
.choose(rng)
.unwrap()
.map(|e| e.saturating_add(rng.gen_range(0, 20)) - 10),
colors: ColorTheme {
roof: *ROOF_COLORS.choose(rng).unwrap(),
wall: *WALL_COLORS.choose(rng).unwrap(),
support: *SUPPORT_COLORS.choose(rng).unwrap(),
},
noise: RandomField::new(rng.gen()),
roof_ribbing: rng.gen(),
roof_ribbing_diagonal: rng.gen(),
@ -165,12 +222,15 @@ impl Archetype for House {
#[allow(clippy::int_plus_one)] // TODO: Pending review in #587
fn draw(
&self,
_pos: Vec3<i32>,
dist: i32,
bound_offset: Vec2<i32>,
center_offset: Vec2<i32>,
z: i32,
ori: Ori,
branch: &Branch<Self::Attr>,
locus: i32,
_len: i32,
attr: &Self::Attr,
) -> BlockMask {
let profile = Vec2::new(bound_offset.x, z);
@ -185,7 +245,7 @@ impl Archetype for House {
)
};
let make_block = |r, g, b| {
let make_block = |(r, g, b)| {
let nz = self
.noise
.get(Vec3::new(center_offset.x, center_offset.y, z * 8));
@ -205,32 +265,48 @@ impl Archetype for House {
let foundation_layer = internal_layer + 1;
let floor_layer = foundation_layer + 1;
let foundation = make_block(100, 100, 100).with_priority(foundation_layer);
let log = make_block(60, 45, 30);
let floor = make_block(100, 75, 50);
let wall = make_block(200, 180, 150).with_priority(facade_layer);
let roof = make_block(self.roof_color.r, self.roof_color.g, self.roof_color.b)
.with_priority(facade_layer - 1);
let foundation = make_block((100, 100, 100)).with_priority(foundation_layer);
let log = make_block(self.colors.support.into_tuple());
let floor = make_block((100, 75, 50));
let wall = make_block(self.colors.wall.into_tuple()).with_priority(facade_layer);
let roof = make_block(self.colors.roof.into_tuple()).with_priority(facade_layer - 1);
let empty = BlockMask::nothing();
let internal = BlockMask::new(Block::empty(), internal_layer);
let end_window = BlockMask::new(
Block::new(BlockKind::Window1, make_meta(ori.flip())),
Block::new(attr.window, make_meta(ori.flip())),
structural_layer,
);
let fire = BlockMask::new(Block::new(BlockKind::Ember, Rgb::white()), foundation_layer);
let ceil_height = 6;
let lower_width = branch.locus - 1;
let upper_width = branch.locus;
let storey_height = 6;
let storey = ((z - 1) / storey_height).min(attr.levels - 1);
let floor_height = storey_height * storey;
let ceil_height = storey_height * (storey + 1);
let lower_width = locus - 1;
let upper_width = locus;
let width = if profile.y >= ceil_height {
upper_width
} else {
lower_width
};
let foundation_height = 0 - (dist - width - 1).max(0);
let roof_top = 8 + width;
let roof_top = storey_height * attr.levels + 2 + width;
if let Pillar::Chimney(chimney_top) = branch.attr.pillar {
let edge_ori = if bound_offset.x.abs() > bound_offset.y.abs() {
if center_offset.x > 0 { 6 } else { 2 }
} else if (center_offset.y > 0) ^ (ori == Ori::East) {
0
} else {
4
};
let edge_ori = if ori == Ori::East {
(edge_ori + 2) % 8
} else {
edge_ori
};
if let Pillar::Chimney(chimney_height) = attr.pillar {
let chimney_top = roof_top + chimney_height;
// Chimney shaft
if center_offset.map(|e| e.abs()).reduce_max() == 0
&& profile.y >= foundation_height + 1
@ -258,7 +334,7 @@ impl Archetype for House {
if profile.y <= foundation_height && dist < width + 3 {
// Foundations
if branch.attr.storey_fill.has_lower() {
if attr.storey_fill.has_lower() {
if dist == width - 1 {
// Floor lining
return log.with_priority(floor_layer);
@ -281,7 +357,7 @@ impl Archetype for House {
|profile: Vec2<i32>, width, dist, bound_offset: Vec2<i32>, roof_top, mansard| {
// Roof
let (roof_profile, roof_dist) = match &branch.attr.roof_style {
let (roof_profile, roof_dist) = match &attr.roof_style {
RoofStyle::Hip => (Vec2::new(dist, profile.y), dist),
RoofStyle::Gable => (profile, dist),
RoofStyle::Rounded => {
@ -320,13 +396,15 @@ impl Archetype for House {
&& bound_offset.x > 0
&& bound_offset.x < width
&& profile.y < ceil_height
&& branch.attr.storey_fill.has_lower()
&& attr.storey_fill.has_lower()
&& storey == 0
{
return Some(
if (bound_offset.x == (width - 1) / 2
|| bound_offset.x == (width - 1) / 2 + 1)
&& profile.y <= foundation_height + 3
{
// Doors on first floor only
if profile.y == foundation_height + 1 {
BlockMask::new(
Block::new(
@ -351,9 +429,9 @@ impl Archetype for House {
if bound_offset.x == bound_offset.y || profile.y == ceil_height {
// Support beams
return Some(log);
} else if !branch.attr.storey_fill.has_lower() && profile.y < ceil_height {
} else if !attr.storey_fill.has_lower() && profile.y < ceil_height {
return Some(empty);
} else if !branch.attr.storey_fill.has_upper() {
} else if !attr.storey_fill.has_upper() {
return Some(empty);
} else {
let (frame_bounds, frame_borders) = if profile.y >= ceil_height {
@ -367,7 +445,7 @@ impl Archetype for House {
} else {
(
Aabr {
min: Vec2::new(2, foundation_height + 2),
min: Vec2::new(2, floor_height + 2),
max: Vec2::new(width - 2, ceil_height - 2),
},
Vec2::new(1, 0),
@ -392,7 +470,7 @@ impl Archetype for House {
}
// Wall
return Some(if branch.attr.central_supports && profile.x == 0 {
return Some(if attr.central_supports && profile.x == 0 {
// Support beams
log.with_priority(structural_layer)
} else {
@ -407,36 +485,88 @@ impl Archetype for House {
if profile.x == 0 {
// Rafters
return Some(log);
} else if branch.attr.storey_fill.has_upper() {
} else if attr.storey_fill.has_upper() {
// Ceiling
return Some(floor);
}
} else if (!branch.attr.storey_fill.has_lower() && profile.y < ceil_height)
|| (!branch.attr.storey_fill.has_upper() && profile.y >= ceil_height)
} else if (!attr.storey_fill.has_lower() && profile.y < ceil_height)
|| (!attr.storey_fill.has_upper() && profile.y >= ceil_height)
{
return Some(empty);
// Furniture
} else if dist == width - 1
&& center_offset.sum() % 2 == 0
&& profile.y == floor_height + 1
&& self
.noise
.chance(Vec3::new(center_offset.x, center_offset.y, z), 0.2)
{
let furniture = match self.noise.get(Vec3::new(
center_offset.x,
center_offset.y,
z + 100,
)) % 11
{
0 => BlockKind::Planter,
1 => BlockKind::ChairSingle,
2 => BlockKind::ChairDouble,
3 => BlockKind::CoatRack,
4 => BlockKind::Crate,
6 => BlockKind::DrawerMedium,
7 => BlockKind::DrawerSmall,
8 => BlockKind::TableSide,
9 => BlockKind::WardrobeSingle,
_ => BlockKind::Pot,
};
return Some(BlockMask::new(
Block::new(furniture, Rgb::new(edge_ori, 0, 0)),
internal_layer,
));
} else {
return Some(internal);
}
}
None
// Wall ornaments
if dist == width + 1
&& center_offset.map(|e| e.abs()).reduce_min() == 0
&& profile.y == floor_height + 3
&& self
.noise
.chance(Vec3::new(center_offset.x, center_offset.y, z), 0.35)
&& attr.storey_fill.has_lower()
{
let ornament =
match self
.noise
.get(Vec3::new(center_offset.x, center_offset.y, z + 100))
% 4
{
0 => BlockKind::HangingSign,
1 | 2 | 3 => BlockKind::HangingBasket,
_ => BlockKind::DungeonWallDecor,
};
Some(BlockMask::new(
Block::new(ornament, Rgb::new((edge_ori + 4) % 8, 0, 0)),
internal_layer,
))
} else {
None
}
};
let mut cblock = empty;
if let Some(block) = do_roof_wall(
profile,
width,
dist,
bound_offset,
roof_top,
branch.attr.mansard,
) {
if let Some(block) =
do_roof_wall(profile, width, dist, bound_offset, roof_top, attr.mansard)
{
cblock = cblock.resolve_with(block);
}
if let Pillar::Tower(tower_top) = branch.attr.pillar {
if let Pillar::Tower(tower_height) = attr.pillar {
let tower_top = roof_top + tower_height;
let profile = Vec2::new(center_offset.x.abs(), profile.y);
let dist = center_offset.map(|e| e.abs()).reduce_max();
@ -446,7 +576,7 @@ impl Archetype for House {
dist,
center_offset.map(|e| e.abs()),
tower_top,
branch.attr.mansard,
attr.mansard,
) {
cblock = cblock.resolve_with(block);
}

View File

@ -1,5 +1,8 @@
use super::{super::skeleton::*, Archetype};
use crate::site::BlockMask;
use crate::{
site::BlockMask,
util::{RandomField, Sampler},
};
use common::{
terrain::{Block, BlockKind},
vol::Vox,
@ -7,29 +10,56 @@ use common::{
use rand::prelude::*;
use vek::*;
pub struct Keep;
pub struct Keep {
pub flag_color: Rgb<u8>,
pub stone_color: Rgb<u8>,
}
pub struct Attr {
pub storeys: i32,
pub is_tower: bool,
pub flag: bool,
pub ridged: bool,
pub rounded: bool,
pub has_doors: bool,
}
impl Archetype for Keep {
type Attr = ();
type Attr = Attr;
fn generate<R: Rng>(rng: &mut R) -> (Self, Skeleton<Self::Attr>) {
let len = rng.gen_range(-8, 12).max(0);
let len = rng.gen_range(-8, 24).max(0);
let storeys = rng.gen_range(1, 3);
let skel = Skeleton {
offset: -rng.gen_range(0, len + 7).clamped(0, len),
ori: if rng.gen() { Ori::East } else { Ori::North },
root: Branch {
len,
attr: Self::Attr::default(),
locus: 5 + rng.gen_range(0, 5),
attr: Attr {
storeys,
is_tower: false,
flag: false,
ridged: false,
rounded: true,
has_doors: true,
},
locus: 10 + rng.gen_range(0, 5),
border: 3,
children: (0..rng.gen_range(0, 4))
children: (0..1)
.map(|_| {
(
rng.gen_range(-5, len + 5).clamped(0, len.max(1) - 1),
Branch {
len: rng.gen_range(5, 12) * if rng.gen() { 1 } else { -1 },
attr: Self::Attr::default(),
locus: 5 + rng.gen_range(0, 3),
len: 0,
attr: Attr {
storeys: storeys + rng.gen_range(1, 3),
is_tower: true,
flag: true,
ridged: false,
rounded: true,
has_doors: false,
},
locus: 6 + rng.gen_range(0, 3),
border: 3,
children: Vec::new(),
},
@ -39,44 +69,188 @@ impl Archetype for Keep {
},
};
(Self, skel)
(
Self {
flag_color: Rgb::new(200, 80, 40),
stone_color: Rgb::new(100, 100, 110),
},
skel,
)
}
#[allow(clippy::if_same_then_else)] // TODO: Pending review in #587
fn draw(
&self,
dist: i32,
pos: Vec3<i32>,
_dist: i32,
bound_offset: Vec2<i32>,
_center_offset: Vec2<i32>,
center_offset: Vec2<i32>,
z: i32,
_ori: Ori,
branch: &Branch<Self::Attr>,
ori: Ori,
locus: i32,
_len: i32,
attr: &Self::Attr,
) -> BlockMask {
let profile = Vec2::new(bound_offset.x, z);
let make_block =
|r, g, b| BlockMask::new(Block::new(BlockKind::Normal, Rgb::new(r, g, b)), 2);
let weak_layer = 1;
let normal_layer = weak_layer + 1;
let important_layer = normal_layer + 1;
let internal_layer = important_layer + 1;
let foundation = make_block(100, 100, 100);
let wall = make_block(75, 100, 125);
let roof = make_block(150, 120, 50);
let empty = BlockMask::new(Block::empty(), 2);
let make_meta = |ori| {
Rgb::new(
match ori {
Ori::East => 0,
Ori::North => 2,
},
0,
0,
)
};
let width = branch.locus;
let rampart_width = 5 + branch.locus;
let ceil_height = 16;
let make_block = |r, g, b| {
BlockMask::new(
Block::new(BlockKind::Normal, Rgb::new(r, g, b)),
normal_layer,
)
};
if profile.y <= 1 - (dist - width - 1).max(0) && dist < width + 3 {
let brick_tex_pos = (pos + Vec3::new(pos.z, pos.z, 0)) / Vec3::new(2, 2, 1);
let brick_tex = RandomField::new(0).get(brick_tex_pos) as u8 % 24;
let foundation = make_block(80 + brick_tex, 80 + brick_tex, 80 + brick_tex);
let wall = make_block(
self.stone_color.r + brick_tex,
self.stone_color.g + brick_tex,
self.stone_color.b + brick_tex,
);
let window = BlockMask::new(
Block::new(BlockKind::Window1, make_meta(ori.flip())),
normal_layer,
);
let floor = make_block(
80 + (pos.y.abs() % 2) as u8 * 15,
60 + (pos.y.abs() % 2) as u8 * 15,
10 + (pos.y.abs() % 2) as u8 * 15,
)
.with_priority(important_layer);
let pole = make_block(90, 70, 50).with_priority(important_layer);
let flag = make_block(self.flag_color.r, self.flag_color.g, self.flag_color.b)
.with_priority(important_layer);
let internal = BlockMask::new(Block::empty(), internal_layer);
let empty = BlockMask::nothing();
let make_staircase = move |pos: Vec3<i32>, radius: f32, inner_radius: f32, stretch: f32| {
let stone = BlockMask::new(Block::new(BlockKind::Normal, Rgb::new(150, 150, 175)), 5);
if (pos.xy().magnitude_squared() as f32) < inner_radius.powf(2.0) {
stone
} else if (pos.xy().magnitude_squared() as f32) < radius.powf(2.0) {
if ((pos.x as f32).atan2(pos.y as f32) / (std::f32::consts::PI * 2.0) * stretch
+ pos.z as f32)
.rem_euclid(stretch)
< 1.5
{
stone
} else {
internal
}
} else {
BlockMask::nothing()
}
};
let ridge_x = (center_offset.map(|e| e.abs()).reduce_min() + 2) % 8;
let width = locus
+ if ridge_x < 4 && attr.ridged && !attr.rounded {
1
} else {
0
};
let rampart_width = 2 + width;
let storey_height = 9;
let roof_height = attr.storeys * storey_height;
let storey_y = profile.y % storey_height;
let door_height = 6;
let rampart_height = roof_height + if ridge_x % 2 == 0 { 3 } else { 4 };
let min_dist = if attr.rounded {
bound_offset.map(|e| e.pow(2) as f32).sum().powf(0.5) as i32
} else {
bound_offset.map(|e| e.abs()).reduce_max()
};
if profile.y <= 0 - (min_dist - width - 1).max(0) && min_dist < width + 3 {
// Foundations
foundation
} else if profile.y == ceil_height && dist < rampart_width {
roof
} else if dist == rampart_width && profile.y >= ceil_height && profile.y < ceil_height + 4 {
wall
} else if dist == width && profile.y <= ceil_height {
wall
} else if (0..=roof_height).contains(&profile.y) && storey_y == 0 && min_dist <= width + 1 {
if min_dist < width { floor } else { wall }
} else if bound_offset.x.abs() < 3
&& profile.y < door_height - bound_offset.x.abs()
&& profile.y > 0
&& min_dist >= width - 2
&& min_dist <= width + 1
&& attr.has_doors
{
internal
} else if (min_dist == width || (!attr.is_tower && min_dist == width + 1))
&& profile.y <= roof_height
{
if attr.is_tower
&& (3..7).contains(&storey_y)
&& bound_offset.x.abs() < width - 2
&& (5..7).contains(&ridge_x)
{
window
} else {
wall
}
} else if profile.y >= roof_height {
if profile.y > roof_height
&& (min_dist < rampart_width - 1 || (attr.is_tower && min_dist < rampart_width))
{
if attr.is_tower
&& attr.flag
&& center_offset == Vec2::zero()
&& profile.y < roof_height + 16
{
pole
} else if attr.is_tower
&& attr.flag
&& center_offset.x == 0
&& center_offset.y > 0
&& center_offset.y < 8
&& profile.y > roof_height + 8
&& profile.y < roof_height + 14
{
flag
} else {
empty
}
} else if min_dist <= rampart_width {
if profile.y < rampart_height {
wall
} else {
empty
}
} else {
empty
}
} else if profile.y < roof_height && min_dist < width {
internal
} else {
empty
}
.resolve_with(
if attr.is_tower && profile.y > 0 && profile.y <= roof_height {
make_staircase(
Vec3::new(center_offset.x, center_offset.y, pos.z),
7.0f32.min(width as f32 - 1.0),
0.5,
9.0,
)
} else {
BlockMask::nothing()
},
)
}
}

View File

@ -12,13 +12,18 @@ pub trait Archetype {
fn generate<R: Rng>(rng: &mut R) -> (Self, Skeleton<Self::Attr>)
where
Self: Sized;
#[allow(clippy::too_many_arguments)]
fn draw(
&self,
pos: Vec3<i32>,
dist: i32,
bound_offset: Vec2<i32>,
center_offset: Vec2<i32>,
z: i32,
ori: Ori,
branch: &Branch<Self::Attr>,
locus: i32,
len: i32,
attr: &Self::Attr,
) -> BlockMask;
}

View File

@ -1,16 +1,16 @@
mod archetype;
mod skeleton;
pub mod archetype;
pub mod skeleton;
// Reexports
pub use self::archetype::Archetype;
pub use self::{
archetype::{house::House, keep::Keep, Archetype},
skeleton::*,
};
use self::skeleton::*;
use common::terrain::Block;
use rand::prelude::*;
use vek::*;
pub type HouseBuilding = Building<archetype::house::House>;
pub struct Building<A: Archetype> {
skel: Skeleton<A::Attr>,
archetype: A,
@ -42,7 +42,7 @@ impl<A: Archetype> Building<A> {
let aabr = self.bounds_2d();
Aabb {
min: Vec3::from(aabr.min) + Vec3::unit_z() * (self.origin.z - 8),
max: Vec3::from(aabr.max) + Vec3::unit_z() * (self.origin.z + 32),
max: Vec3::from(aabr.max) + Vec3::unit_z() * (self.origin.z + 48),
}
}
@ -50,10 +50,19 @@ impl<A: Archetype> Building<A> {
let rpos = pos - self.origin;
self.skel
.sample_closest(
rpos.into(),
|dist, bound_offset, center_offset, ori, branch| {
self.archetype
.draw(dist, bound_offset, center_offset, rpos.z, ori, branch)
rpos,
|pos, dist, bound_offset, center_offset, ori, branch| {
self.archetype.draw(
pos,
dist,
bound_offset,
center_offset,
rpos.z,
ori,
branch.locus,
branch.len,
&branch.attr,
)
},
)
.finish()

View File

@ -76,8 +76,8 @@ impl<T> Skeleton<T> {
#[allow(clippy::or_fun_call)] // TODO: Pending review in #587
pub fn sample_closest(
&self,
pos: Vec2<i32>,
mut f: impl FnMut(i32, Vec2<i32>, Vec2<i32>, Ori, &Branch<T>) -> BlockMask,
pos: Vec3<i32>,
mut f: impl FnMut(Vec3<i32>, i32, Vec2<i32>, Vec2<i32>, Ori, &Branch<T>) -> BlockMask,
) -> BlockMask {
let mut min = None::<(_, BlockMask)>;
self.for_each(|node, ori, branch, is_child, parent_locus| {
@ -117,7 +117,7 @@ impl<T> Skeleton<T> {
}
|| true
{
let new_bm = f(dist, bound_offset, center_offset, ori, branch);
let new_bm = f(pos, dist, bound_offset, center_offset, ori, branch);
min = min
.map(|(_, bm)| (dist_locus, bm.resolve_with(new_bm)))
.or(Some((dist_locus, new_bm)));

View File

@ -1,6 +1,10 @@
mod building;
pub mod building;
mod town;
use self::building::HouseBuilding;
use self::{
building::{Building, House, Keep},
town::{District, Town},
};
use super::SpawnRules;
use crate::{
column::ColumnSample,
@ -82,7 +86,8 @@ const AREA_SIZE: u32 = 32;
fn to_tile(e: i32) -> i32 { ((e as f32).div_euclid(AREA_SIZE as f32)).floor() as i32 }
pub enum StructureKind {
House(HouseBuilding),
House(Building<House>),
Keep(Building<Keep>),
}
pub struct Structure {
@ -93,6 +98,21 @@ impl Structure {
pub fn bounds_2d(&self) -> Aabr<i32> {
match &self.kind {
StructureKind::House(house) => house.bounds_2d(),
StructureKind::Keep(keep) => keep.bounds_2d(),
}
}
pub fn bounds(&self) -> Aabb<i32> {
match &self.kind {
StructureKind::House(house) => house.bounds(),
StructureKind::Keep(keep) => keep.bounds(),
}
}
pub fn sample(&self, rpos: Vec3<i32>) -> Option<Block> {
match &self.kind {
StructureKind::House(house) => house.sample(rpos),
StructureKind::Keep(keep) => keep.sample(rpos),
}
}
}
@ -107,10 +127,6 @@ pub struct Settlement {
noise: RandomField,
}
pub struct Town {
base_tile: Vec2<i32>,
}
pub struct Farm {
#[allow(dead_code)]
base_tile: Vec2<i32>,
@ -216,7 +232,7 @@ impl Settlement {
for _ in 0..PATH_COUNT {
dir = (Vec2::new(rng.gen::<f32>() - 0.5, rng.gen::<f32>() - 0.5) * 2.0 - dir)
.try_normalized()
.unwrap_or(Vec2::zero());
.unwrap_or_else(Vec2::zero);
let origin = dir.map(|e| (e * 100.0) as i32);
let origin = self
.land
@ -260,12 +276,28 @@ impl Settlement {
Some(Plot::Dirt) => true,
_ => false,
}) {
self.land
.plot_at_mut(base_tile)
.map(|plot| *plot = Plot::Town);
// self.land
// .plot_at_mut(base_tile)
// .map(|plot| *plot = Plot::Town { district: None });
if i == 0 {
self.town = Some(Town { base_tile });
let town = Town::generate(self.origin, base_tile, ctx);
for (id, district) in town.districts().iter() {
let district_plot =
self.land.plots.insert(Plot::Town { district: Some(id) });
for x in district.aabr.min.x..district.aabr.max.x {
for y in district.aabr.min.y..district.aabr.max.y {
if !matches!(self.land.plot_at(Vec2::new(x, y)), Some(Plot::Hazard))
{
self.land.set(Vec2::new(x, y), district_plot);
}
}
}
}
self.town = Some(town);
origin = base_tile;
}
}
@ -331,42 +363,56 @@ impl Settlement {
.take(16usize.pow(2))
{
// This is a stupid way to decide how to place buildings
for _ in 0..ctx.rng.gen_range(2, 5) {
for i in 0..ctx.rng.gen_range(2, 5) {
for _ in 0..25 {
let house_pos = tile.map(|e| e * AREA_SIZE as i32 + AREA_SIZE as i32 / 2)
+ Vec2::<i32>::zero().map(|_| {
ctx.rng
.gen_range(-(AREA_SIZE as i32) / 2, AREA_SIZE as i32 / 2)
.gen_range(-(AREA_SIZE as i32) / 4, AREA_SIZE as i32 / 4)
});
let tile_pos = house_pos.map(|e| e.div_euclid(AREA_SIZE as i32));
if !matches!(self.land.plot_at(tile_pos), Some(Plot::Town))
|| self
.land
.tile_at(tile_pos)
.map(|t| t.contains(WayKind::Path))
.unwrap_or(true)
if self
.land
.tile_at(tile_pos)
.map(|t| t.contains(WayKind::Path))
.unwrap_or(true)
|| ctx
.sim
.and_then(|sim| sim.get_nearest_path(self.origin + house_pos))
.map(|(dist, _)| dist < 28.0)
.map(|(dist, _, _, _)| dist < 28.0)
.unwrap_or(false)
{
continue;
}
let structure = Structure {
kind: StructureKind::House(HouseBuilding::generate(
ctx.rng,
Vec3::new(
house_pos.x,
house_pos.y,
let alt = if let Some(Plot::Town { district }) = self.land.plot_at(tile_pos) {
district
.and_then(|d| self.town.as_ref().map(|t| t.districts().get(d)))
.map(|d| d.alt)
.filter(|_| false) // Temporary
.unwrap_or_else(|| {
ctx.sim
.and_then(|sim| sim.get_alt_approx(self.origin + house_pos))
.unwrap_or(0.0)
.ceil() as i32,
),
)),
.ceil() as i32
})
} else {
continue;
};
let structure = Structure {
kind: if tile == town_center && i == 0 {
StructureKind::Keep(Building::<Keep>::generate(
ctx.rng,
Vec3::new(house_pos.x, house_pos.y, alt),
))
} else {
StructureKind::House(Building::<House>::generate(
ctx.rng,
Vec3::new(house_pos.x, house_pos.y, alt),
))
},
};
let bounds = structure.bounds_2d();
@ -501,12 +547,13 @@ impl Settlement {
} else {
continue;
};
let surface_z = col_sample.riverless_alt.floor() as i32;
let land_surface_z = col_sample.riverless_alt.floor() as i32;
let mut surface_z = land_surface_z;
// Sample settlement
let sample = self.land.get_at_block(rpos);
let noisy_color = |col: Rgb<u8>, factor: u32| {
let noisy_color = move |col: Rgb<u8>, factor: u32| {
let nz = self.noise.get(Vec3::new(wpos2d.x, wpos2d.y, surface_z));
col.map(|e| {
(e as u32 + nz % (factor * 2))
@ -515,46 +562,75 @@ impl Settlement {
})
};
// District alt
if let Some(Plot::Town { district }) = sample.plot {
if let Some(d) = district
.and_then(|d| self.town.as_ref().map(|t| t.districts().get(d)))
.filter(|_| false)
// Temporary
{
let other = self
.land
.plot_at(sample.second_closest)
.and_then(|p| match p {
Plot::Town { district } => *district,
_ => None,
})
.and_then(|d| {
self.town.as_ref().map(|t| t.districts().get(d).alt as f32)
})
.filter(|_| false)
.unwrap_or(surface_z as f32);
surface_z = Lerp::lerp(
(other + d.alt as f32) / 2.0,
d.alt as f32,
(1.25 * sample.edge_dist / (d.alt as f32 - other).abs()).min(1.0),
) as i32;
}
}
// Paths
if let Some((WayKind::Path, dist, nearest)) = sample.way {
let inset = -1;
// if let Some((WayKind::Path, dist, nearest)) = sample.way {
// let inset = -1;
// Try to use the column at the centre of the path for sampling to make them
// flatter
let col = get_column(offs + (nearest.floor().map(|e| e as i32) - rpos))
.unwrap_or(col_sample);
let (bridge_offset, depth) = if let Some(water_dist) = col.water_dist {
(
((water_dist.max(0.0) * 0.2).min(f32::consts::PI).cos() + 1.0) * 5.0,
((1.0 - ((water_dist + 2.0) * 0.3).min(0.0).cos().abs())
* (col.riverless_alt + 5.0 - col.alt).max(0.0)
* 1.75
+ 3.0) as i32,
)
} else {
(0.0, 3)
};
let surface_z = (col.riverless_alt + bridge_offset).floor() as i32;
// // Try to use the column at the centre of the path for sampling to make
// them // flatter
// let col = get_column(offs + (nearest.floor().map(|e| e as i32) - rpos))
// .unwrap_or(col_sample);
// let (bridge_offset, depth) = if let Some(water_dist) = col.water_dist {
// (
// ((water_dist.max(0.0) * 0.2).min(f32::consts::PI).cos() + 1.0) *
// 5.0, ((1.0 - ((water_dist + 2.0) *
// 0.3).min(0.0).cos().abs())
// * (col.riverless_alt + 5.0 - col.alt).max(0.0)
// * 1.75
// + 3.0) as i32,
// )
// } else {
// (0.0, 3)
// };
// let surface_z = (col.riverless_alt + bridge_offset).floor() as i32;
for z in inset - depth..inset {
let _ = vol.set(
Vec3::new(offs.x, offs.y, surface_z + z),
if bridge_offset >= 2.0 && dist >= 3.0 || z < inset - 1 {
Block::new(BlockKind::Normal, noisy_color(Rgb::new(80, 80, 100), 8))
} else {
Block::new(BlockKind::Normal, noisy_color(Rgb::new(80, 50, 30), 8))
},
);
}
let head_space = (8 - (dist * 0.25).powf(6.0).round() as i32).max(1);
for z in inset..inset + head_space {
let pos = Vec3::new(offs.x, offs.y, surface_z + z);
if vol.get(pos).unwrap().kind() != BlockKind::Water {
let _ = vol.set(pos, Block::empty());
}
}
// Ground colour
} else {
// for z in inset - depth..inset {
// let _ = vol.set(
// Vec3::new(offs.x, offs.y, surface_z + z),
// if bridge_offset >= 2.0 && dist >= 3.0 || z < inset - 1 {
// Block::new(BlockKind::Normal, noisy_color(Rgb::new(80, 80,
// 100), 8)) } else {
// Block::new(BlockKind::Normal, noisy_color(Rgb::new(80, 50,
// 30), 8)) },
// );
// }
// let head_space = (8 - (dist * 0.25).powf(6.0).round() as i32).max(1);
// for z in inset..inset + head_space {
// let pos = Vec3::new(offs.x, offs.y, surface_z + z);
// if vol.get(pos).unwrap().kind() != BlockKind::Water {
// let _ = vol.set(pos, Block::empty());
// }
// }
// // Ground colour
// } else
{
let mut surface_block = None;
let roll =
@ -564,27 +640,29 @@ impl Settlement {
Some(Plot::Dirt) => Some(Rgb::new(90, 70, 50)),
Some(Plot::Grass) => Some(Rgb::new(100, 200, 0)),
Some(Plot::Water) => Some(Rgb::new(100, 150, 250)),
Some(Plot::Town) => {
if let Some((_, path_nearest)) = col_sample.path {
//Some(Plot::Town { district }) => None,
Some(Plot::Town { .. }) => {
if let Some((_, path_nearest, _, _)) = col_sample.path {
let path_dir = (path_nearest - wpos2d.map(|e| e as f32))
.rotated_z(f32::consts::PI / 2.0)
.normalized();
let is_lamp = if path_dir.x.abs() > path_dir.y.abs() {
wpos2d.x as f32 % 20.0 / path_dir.dot(Vec2::unit_y()).abs()
wpos2d.x as f32 % 30.0 / path_dir.dot(Vec2::unit_y()).abs()
<= 1.0
} else {
wpos2d.y as f32 % 20.0 / path_dir.dot(Vec2::unit_x()).abs()
(wpos2d.y as f32 + 10.0) % 30.0
/ path_dir.dot(Vec2::unit_x()).abs()
<= 1.0
};
if (col_sample.path.map(|(dist, _)| dist > 6.0 && dist < 7.0).unwrap_or(false) && is_lamp) //roll(0, 50) == 0)
|| roll(0, 2000) == 0
if (col_sample.path.map(|(dist, _, _, _)| dist > 6.0 && dist < 7.0).unwrap_or(false) && is_lamp) //roll(0, 50) == 0)
|| (roll(0, 2000) == 0 && col_sample.path.map(|(dist, _, _, _)| dist > 20.0).unwrap_or(true))
{
surface_block =
Some(Block::new(BlockKind::StreetLamp, Rgb::white()));
}
}
Some(Rgb::new(100, 90, 75).map2(Rgb::iota(), |e: u8, i: i32| {
Some(Rgb::new(100, 95, 65).map2(Rgb::iota(), |e: u8, i: i32| {
e.saturating_add(
(self.noise.get(Vec3::new(wpos2d.x, wpos2d.y, i * 5)) % 1)
as u8,
@ -657,14 +735,27 @@ impl Settlement {
};
if let Some(color) = color {
if col_sample.water_dist.map(|dist| dist > 2.0).unwrap_or(true) {
for z in -8..3 {
let is_path = col_sample
.path
.map(|(dist, _, path, _)| dist < path.width)
.unwrap_or(false);
if col_sample.water_dist.map(|dist| dist > 2.0).unwrap_or(true) && !is_path
{
let diff = (surface_z - land_surface_z).abs();
for z in -8 - diff..4 + diff {
let pos = Vec3::new(offs.x, offs.y, surface_z + z);
let block = vol.get(pos).ok().copied().unwrap_or_else(Block::empty);
if block.kind() == BlockKind::Air {
break;
}
if let (0, Some(block)) = (z, surface_block) {
let _ = vol.set(pos, block);
} else if z >= 0 {
if vol.get(pos).unwrap().kind() != BlockKind::Water {
if block.kind() != BlockKind::Water {
let _ = vol.set(pos, Block::empty());
}
} else {
@ -728,33 +819,27 @@ impl Settlement {
continue;
}
match &structure.kind {
StructureKind::House(b) => {
let bounds = b.bounds();
let bounds = structure.bounds();
for x in bounds.min.x..bounds.max.x + 1 {
for y in bounds.min.y..bounds.max.y + 1 {
let col = if let Some(col) =
get_column(self.origin + Vec2::new(x, y) - wpos2d)
{
col
} else {
continue;
};
for x in bounds.min.x..bounds.max.x + 1 {
for y in bounds.min.y..bounds.max.y + 1 {
let col = if let Some(col) = get_column(self.origin + Vec2::new(x, y) - wpos2d)
{
col
} else {
continue;
};
for z in bounds.min.z.min(col.alt.floor() as i32 - 1)..bounds.max.z + 1
{
let rpos = Vec3::new(x, y, z);
let wpos = Vec3::from(self.origin) + rpos;
let coffs = wpos - Vec3::from(wpos2d);
for z in bounds.min.z.min(col.alt.floor() as i32 - 1)..bounds.max.z + 1 {
let rpos = Vec3::new(x, y, z);
let wpos = Vec3::from(self.origin) + rpos;
let coffs = wpos - Vec3::from(wpos2d);
if let Some(block) = b.sample(rpos) {
let _ = vol.set(coffs, block);
}
}
if let Some(block) = structure.sample(rpos) {
let _ = vol.set(coffs, block);
}
}
},
}
}
}
}
@ -785,7 +870,7 @@ impl Settlement {
let entity_wpos = Vec3::new(wpos2d.x as f32, wpos2d.y as f32, col_sample.alt + 3.0);
if matches!(sample.plot, Some(Plot::Town))
if matches!(sample.plot, Some(Plot::Town { .. }))
&& RandomField::new(self.seed).chance(Vec3::from(wpos2d), 1.0 / (50.0 * 50.0))
{
let is_human: bool;
@ -875,7 +960,7 @@ impl Settlement {
Some(Plot::Dirt) => return Some(Rgb::new(90, 70, 50)),
Some(Plot::Grass) => return Some(Rgb::new(100, 200, 0)),
Some(Plot::Water) => return Some(Rgb::new(100, 150, 250)),
Some(Plot::Town) => {
Some(Plot::Town { .. }) => {
return Some(Rgb::new(150, 110, 60).map2(Rgb::iota(), |e: u8, i: i32| {
e.saturating_add((self.noise.get(Vec3::new(pos.x, pos.y, i * 5)) % 16) as u8)
.saturating_sub(8)
@ -927,7 +1012,9 @@ pub enum Plot {
Dirt,
Grass,
Water,
Town,
Town {
district: Option<Id<District>>,
},
Field {
farm: Id<Farm>,
seed: u32,
@ -987,6 +1074,8 @@ pub struct Sample<'a> {
plot: Option<&'a Plot>,
way: Option<(&'a WayKind, f32, Vec2<f32>)>,
tower: Option<(&'a Tower, Vec2<i32>)>,
edge_dist: f32,
second_closest: Vec2<i32>,
}
pub struct Land {
@ -1021,6 +1110,15 @@ impl Land {
.min_by_key(|(center, _)| center.distance_squared(pos))
.unwrap()
.0;
let second_closest = neighbors
.iter()
.filter(|(center, _)| *center != closest)
.min_by_key(|(center, _)| center.distance_squared(pos))
.unwrap()
.0;
sample.second_closest = second_closest.map(to_tile);
sample.edge_dist = (second_closest - pos).map(|e| e as f32).magnitude()
- (closest - pos).map(|e| e as f32).magnitude();
let center_tile = self.tile_at(neighbors[4].0.map(to_tile));
@ -1064,6 +1162,7 @@ impl Land {
self.tiles.get(&pos).map(|tile| self.plots.get(tile.plot))
}
#[allow(dead_code)]
pub fn plot_at_mut(&mut self, pos: Vec2<i32>) -> Option<&mut Plot> {
self.tiles
.get(&pos)

View File

@ -0,0 +1,82 @@
use super::{GenCtx, AREA_SIZE};
use common::store::Store;
use rand::prelude::*;
use vek::*;
pub struct Town {
pub base_tile: Vec2<i32>,
radius: i32,
districts: Store<District>,
}
impl Town {
pub fn districts(&self) -> &Store<District> { &self.districts }
pub fn generate(origin: Vec2<i32>, base_tile: Vec2<i32>, ctx: &mut GenCtx<impl Rng>) -> Self {
let mut this = Self {
base_tile,
radius: 4,
districts: Store::default(),
};
this.generate_districts(origin, ctx);
this
}
fn generate_districts(&mut self, origin: Vec2<i32>, ctx: &mut GenCtx<impl Rng>) {
let base_aabr = Aabr {
min: self.base_tile - self.radius,
max: self.base_tile + self.radius,
};
gen_plot(base_aabr, ctx).for_each(base_aabr, &mut |aabr| {
if aabr.center().distance_squared(self.base_tile) < self.radius.pow(2) {
self.districts.insert(District {
seed: ctx.rng.gen(),
aabr,
alt: ctx
.sim
.and_then(|sim| {
sim.get_alt_approx(
origin + aabr.center() * AREA_SIZE as i32 + AREA_SIZE as i32 / 2,
)
})
.unwrap_or(0.0) as i32,
});
}
});
}
}
pub struct District {
pub seed: u32,
pub aabr: Aabr<i32>,
pub alt: i32,
}
enum Plot {
District,
Parent(Vec<(Aabr<i32>, Plot)>),
}
impl Plot {
fn for_each(&self, aabr: Aabr<i32>, f: &mut impl FnMut(Aabr<i32>)) {
match self {
Plot::District => f(aabr),
Plot::Parent(children) => children.iter().for_each(|(aabr, p)| p.for_each(*aabr, f)),
}
}
}
fn gen_plot(aabr: Aabr<i32>, ctx: &mut GenCtx<impl Rng>) -> Plot {
if aabr.size().product() <= 9 {
Plot::District
} else if aabr.size().w < aabr.size().h {
let [a, b] = aabr.split_at_y(aabr.min.y + ctx.rng.gen_range(1, aabr.size().h));
Plot::Parent(vec![(a, gen_plot(a, ctx)), (b, gen_plot(b, ctx))])
} else {
let [a, b] = aabr.split_at_x(aabr.min.x + ctx.rng.gen_range(1, aabr.size().w));
Plot::Parent(vec![(a, gen_plot(a, ctx)), (b, gen_plot(b, ctx))])
}
}

79
world/src/util/map_vec.rs Normal file
View File

@ -0,0 +1,79 @@
use crate::util::DHashMap;
use std::hash::Hash;
#[derive(Clone, Debug)]
pub struct MapVec<K, T> {
/// We use this hasher (FxHasher32) because
/// (1) we don't care about DDOS attacks (ruling out SipHash);
/// (2) we care about determinism across computers (ruling out AAHash);
/// (3) we have 1-byte keys (for which FxHash is supposedly fastest).
entries: DHashMap<K, T>,
default: T,
}
/// Need manual implementation of Default since K doesn't need that bound.
impl<K, T: Default> Default for MapVec<K, T> {
fn default() -> Self {
Self {
entries: Default::default(),
default: Default::default(),
}
}
}
impl<K: Copy + Eq + Hash, T: Clone> MapVec<K, T> {
pub fn from_list<'a>(i: impl IntoIterator<Item = &'a (K, T)>, default: T) -> Self
where
K: 'a,
T: 'a,
{
Self {
entries: i.into_iter().cloned().collect(),
default,
}
}
pub fn from_default(default: T) -> Self {
Self {
entries: DHashMap::default(),
default,
}
}
pub fn get_mut(&mut self, entry: K) -> &mut T {
let default = &self.default;
self.entries.entry(entry).or_insert_with(|| default.clone())
}
pub fn get(&self, entry: K) -> &T { self.entries.get(&entry).unwrap_or(&self.default) }
#[allow(clippy::clone_on_copy)] // TODO: Pending review in #587
pub fn map<U: Default>(self, mut f: impl FnMut(K, T) -> U) -> MapVec<K, U> {
MapVec {
entries: self
.entries
.into_iter()
.map(|(s, v)| (s.clone(), f(s, v)))
.collect(),
default: U::default(),
}
}
pub fn iter(&self) -> impl Iterator<Item = (K, &T)> + '_ {
self.entries.iter().map(|(s, v)| (*s, v))
}
pub fn iter_mut(&mut self) -> impl Iterator<Item = (K, &mut T)> + '_ {
self.entries.iter_mut().map(|(s, v)| (*s, v))
}
}
impl<K: Copy + Eq + Hash, T: Clone> std::ops::Index<K> for MapVec<K, T> {
type Output = T;
fn index(&self, entry: K) -> &Self::Output { self.get(entry) }
}
impl<K: Copy + Eq + Hash, T: Clone> std::ops::IndexMut<K> for MapVec<K, T> {
fn index_mut(&mut self, entry: K) -> &mut Self::Output { self.get_mut(entry) }
}

View File

@ -1,5 +1,6 @@
pub mod fast_noise;
pub mod grid;
pub mod map_vec;
pub mod random;
pub mod sampler;
pub mod seed_expan;
@ -11,6 +12,7 @@ pub mod unit_chooser;
pub use self::{
fast_noise::FastNoise,
grid::Grid,
map_vec::MapVec,
random::{RandomField, RandomPerm},
sampler::{Sampler, SamplerMut},
small_cache::SmallCache,
@ -18,8 +20,15 @@ pub use self::{
unit_chooser::UnitChooser,
};
use fxhash::FxHasher32;
use hashbrown::{HashMap, HashSet};
use std::hash::BuildHasherDefault;
use vek::*;
// Deterministic HashMap and HashSet
pub type DHashMap<K, V> = HashMap<K, V, BuildHasherDefault<FxHasher32>>;
pub type DHashSet<T> = HashSet<T, BuildHasherDefault<FxHasher32>>;
pub fn attempt<T>(max_iters: usize, mut f: impl FnMut() -> Option<T>) -> Option<T> {
(0..max_iters).find_map(|_| f())
}

View File

@ -20,7 +20,22 @@ impl Sampler<'static> for RandomField {
fn get(&self, pos: Self::Index) -> Self::Sample {
let pos = pos.map(|e| u32::from_le_bytes(e.to_le_bytes()));
seed_expan::diffuse_mult(&[self.seed, pos.x, pos.y, pos.z])
let mut a = self.seed;
a = (a ^ 61) ^ (a >> 16);
a = a.wrapping_add(a << 3);
a ^= pos.x;
a ^= a >> 4;
a = a.wrapping_mul(0x27d4eb2d);
a ^= a >> 15;
a ^= pos.y;
a = (a ^ 61) ^ (a >> 16);
a = a.wrapping_add(a << 3);
a ^= a >> 4;
a ^= pos.z;
a = a.wrapping_mul(0x27d4eb2d);
a ^= a >> 15;
a
}
}

26
world/src/util/wgrid.rs Normal file
View File

@ -0,0 +1,26 @@
use super::Grid;
use vek::*;
pub struct WGrid<T> {
cell_size: u32,
grid: Grid<T>,
}
impl<T> WGrid<T> {
pub fn new(radius: u32, cell_size: u32, default_cell: T) -> Self
where T: Clone
{
Self {
cell_size,
grid: Grid::new(Vec2::broadcast(radius as i32 * 2 + 1), default_cell),
}
}
fn offset(&self) -> Vec2<i32> {
self.grid.size() / 2
}
pub fn get_local(&self, pos: Vec2<i32>) -> Option<&T> {
self.grid.get(pos + self.offset())
}
}