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"),

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9c336506cf0c529f31d0f09e9f5eef136607b08ea3f6f0eb859d46820646d921
size 1588

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e954d6f39d2eaede5fb4f44b900f9575e6cc6f33c5918032f1e17fb54168fbd5
size 1228

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d6d88a3846ec7643c9903c22f1bf0ac472126db60dea3bf1a46fdc985b88f50c
oid sha256:4b288a9470d9e3b89959035b855508d43454c1a57d2dd96deb6f71e087688629
size 10352

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4d20a65fb839dfe38bbab6f67b783b17b6ed78e6cd4714feec51bf1ce6132b84
oid sha256:2c79030afd2d1aa29424e7204a494ecfb5c8d4c45141ee4d0330d76b5ca019b1
size 10120

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:55a879347c38c7755b829aebb954d866cb6ba3427927f9953421fcd500b914c2
oid sha256:621d9e108e98a2e1364427bff831abf32080e96e0afe0666a6d8dc03d24aa613
size 2040

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ae7ac53ac0950b319d500046024e182d4a861a91d2a250af752935c55bf1547f
oid sha256:963f9ef3bfe6c265886f3dc49c7ac04efa3e996bd0a4fd5c7670a6011fea8831
size 23868

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:914db732f7cf6bda66b3a76e72b80adf0e1f75656f264ca36edbd8461c6864ce
oid sha256:a564f82caa29a9dd2e7d8a9345a5544825d5aa41fa3f9e4812b5b35166fb2411
size 24180

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:20a1e1584decf69caff7e9ae5ba281083dacbe95f9885acdc8c9fc4c68ab5fa9
oid sha256:931ae880000e0e775ceb5b8b00f54979f06ef94f2088f435681beb02292cd804
size 13328

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9867ff78442e130f90f0900bb8e6c9a2307b64e27b291855fbc935164d5e8fe0
oid sha256:9d6b5366dbb1c8c451c2e832ef6d61e9dbd2b6db84eb7e9e9760bc3624736466
size 13048

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:45ee10e5805fa443292b063410caa5f5a7b7b6a935731bc9079c4a4fa2693ce5
size 55651

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:89470bacf9a12f6f7473f7b61d4d0a8827d320f1b52d44d26fc199a554c7d53a
size 55663

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6c89055c2cd7f71975d0ddbfccbc3d5b96b15a158ba3f9f1000f36a4b7217ec3
size 55663

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:acc14c82052df18f96d002ad401722ad53ce4decdb36ad02045346638be2003b
size 55643

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2c1cbbd8090fa8e378055ebb191c8d528ab7e70215a3a171246cfa6332eb143c
size 55679

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e9bda5fd9430404feeb8b042e864528f9e9572bc00fb5d454bf546aadac509b5
size 55767

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f285aebc92333f7d3a0970191ee398d22c850372504b9d38061cd8e07f566171
size 55823

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:25b79468d0fb6080f41d98cc8db0e8a69aa5eb8fa2e74bdb31a1fa183d9a483b
size 55787

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:803febeaed019bc690af3906a1cbcbaf8409388d265a23ff41d62f074b167ea8
size 55791

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6ffb358971c47cfd6819ea7971117c051dfde09feab2cb59ddc079a3fe6f236b
size 55787

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())
}
}