Merge branch 'rtsim2' into 'master'

Initial implementation of rtsim2

Closes #1476

See merge request veloren/veloren!3517
This commit is contained in:
Joshua Barretto 2023-04-12 16:17:52 +00:00
commit 9e17042bf6
167 changed files with 8458 additions and 3406 deletions

View File

@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Added
- Command to toggle experimental shaders.
- Faster Energy Regeneration while sitting.
- Lantern glow for dropped lanterns.
@ -25,8 +26,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Setting in userdata/server/server_config/settings.ron that controls the length of each day/night cycle.
- Starting site can now be chosen during character creation
- Durability loss of equipped items on death
- Reputation system: crimes will be remembered and NPCs will tell each other about crimes they witness
- NPCs will now talk to players and to each other
- NPCs now have dedicated professions and will act accordingly
- NPCs other than merchants can be traded with
- NPCs will seek out a place to sleep when night comes
- Merchants now travel between towns
- Travellers and merchants will stay a while in each town they visit and converse with the locals
- Resource tracking: resources in the world can be temporarily exhausted, requiring time to replenish
- Airships now have pilot NPCs
- Simulated NPCs now have repopulation mechanics
- NPCs now have unique names
- A /scale command that can be used to change the in-game scale of players
- Merchants will flog their wares in towns, encouraging nearby character to buy goods from them
### Changed
- Bats move slower and use a simple proportional controller to maintain altitude
- Bats now have less health
- Climbing no longer requires having 10 energy
@ -34,11 +49,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Sword
- Rescaling of images for the UI is now done when sampling from them on the GPU. Improvements are
particularily noticeable when opening the map screen (which involves rescaling a few large
images) and also when using the voxel minimap view (where a medium size image is updated often).
images) and also when using the voxel minimap view (where a medium size image is updated often).
### Removed
### Fixed
- Doors
- Debug hitboxes now scale with the `Scale` component
- Potion quaffing no longer makes characters practically immortal.
@ -49,6 +65,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Fixed various issues with showing the correct text hint for interactable blocks.
- Intert entities like arrows no longer obstruct interacting with nearby entities/blocks.
- Underwater fall damage
- The scale component now behaves properly
## [0.14.0] - 2023-01-07

85
Cargo.lock generated
View File

@ -145,6 +145,12 @@ version = "1.0.65"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98161a4e3e2184da77bb14f02184cdd111e83bbbcc9979dfee3c44b9a85f5602"
[[package]]
name = "anymap2"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d301b3b94cb4b2f23d7917810addbbaff90738e0ca2be692bd027e70d7e0330c"
[[package]]
name = "app_dirs2"
version = "2.5.4"
@ -1839,6 +1845,27 @@ dependencies = [
"syn 1.0.100",
]
[[package]]
name = "enum-map"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f5a56d54c8dd9b3ad34752ed197a4eb2a6601bc010808eb097a04a58ae4c43e1"
dependencies = [
"enum-map-derive",
"serde",
]
[[package]]
name = "enum-map-derive"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9045e2676cd5af83c3b167d917b0a5c90a4d8e266e2683d6631b235c457fc27"
dependencies = [
"proc-macro2 1.0.43",
"quote 1.0.21",
"syn 1.0.100",
]
[[package]]
name = "enumset"
version = "1.0.11"
@ -4297,6 +4324,12 @@ dependencies = [
"regex",
]
[[package]]
name = "paste"
version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9423e2b32f7a043629287a536f21951e8c6a82482d0acb1eeebfc90bc2225b22"
[[package]]
name = "peeking_take_while"
version = "0.1.2"
@ -5075,6 +5108,28 @@ dependencies = [
"syn 1.0.100",
]
[[package]]
name = "rmp"
version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44519172358fd6d58656c86ab8e7fbc9e1490c3e8f14d35ed78ca0dd07403c9f"
dependencies = [
"byteorder",
"num-traits",
"paste",
]
[[package]]
name = "rmp-serde"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25786b0d276110195fa3d6f3f31299900cf71dfbd6c28450f3f58a0e7f7a347e"
dependencies = [
"byteorder",
"rmp",
"serde",
]
[[package]]
name = "rodio"
version = "0.15.0"
@ -6637,6 +6692,7 @@ dependencies = [
"serde",
"tracing",
"unic-langid",
"veloren-common",
"veloren-common-assets",
]
@ -6655,6 +6711,7 @@ dependencies = [
"crossbeam-utils 0.8.11",
"csv",
"dot_vox",
"enum-map",
"fxhash",
"hashbrown 0.12.3",
"indexmap",
@ -6884,6 +6941,29 @@ dependencies = [
"veloren-plugin-derive",
]
[[package]]
name = "veloren-rtsim"
version = "0.10.0"
dependencies = [
"anymap2",
"atomic_refcell",
"enum-map",
"fxhash",
"hashbrown 0.12.3",
"itertools",
"rand 0.8.5",
"rand_chacha 0.3.1",
"rayon",
"rmp-serde",
"ron 0.8.0",
"serde",
"slotmap 1.0.6",
"tracing",
"vek 0.15.8",
"veloren-common",
"veloren-world",
]
[[package]]
name = "veloren-server"
version = "0.14.0"
@ -6896,6 +6976,7 @@ dependencies = [
"chrono-tz",
"crossbeam-channel",
"drop_guard",
"enum-map",
"enumset",
"futures-util",
"hashbrown 0.12.3",
@ -6933,6 +7014,7 @@ dependencies = [
"veloren-common-systems",
"veloren-network",
"veloren-plugin-api",
"veloren-rtsim",
"veloren-server-agent",
"veloren-world",
]
@ -6951,6 +7033,8 @@ dependencies = [
"veloren-common-base",
"veloren-common-dynlib",
"veloren-common-ecs",
"veloren-common-net",
"veloren-rtsim",
]
[[package]]
@ -7108,6 +7192,7 @@ dependencies = [
"csv",
"deflate",
"enum-iterator 1.1.3",
"enum-map",
"fallible-iterator",
"flate2",
"fxhash",

View File

@ -16,6 +16,7 @@ members = [
"plugin/api",
"plugin/derive",
"plugin/rt",
"rtsim",
"server",
"server/agent",
"server-cli",

View File

@ -0,0 +1,21 @@
#![enable(implicit_some)]
(
name: Name("Captain"),
body: RandomWith("humanoid"),
alignment: Alignment(Npc),
loot: LootTable("common.loot_tables.creature.humanoid"),
inventory: (
loadout: Inline((
inherit: Asset("common.loadout.village.captain"),
active_hands: InHands((ModularWeapon(tool: Sword, material: Orichalcum, hands: Two), None)),
)),
items: [
(10, "common.items.food.cheese"),
(10, "common.items.food.plainsalad"),
(10, "common.items.consumable.potion_med"),
],
),
meta: [
SkillSetAsset("common.skillset.preset.rank5.fullskill"),
],
)

View File

@ -0,0 +1,23 @@
#![enable(implicit_some)]
(
name: Name("Farmer"),
body: RandomWith("humanoid"),
alignment: Alignment(Npc),
loot: LootTable("common.loot_tables.creature.humanoid"),
inventory: (
loadout: Inline((
inherit: Asset("common.loadout.village.farmer"),
active_hands: InHands((Choice([
(1, Item("common.items.weapons.tool.hoe")),
(1, Item("common.items.weapons.tool.rake")),
(1, Item("common.items.weapons.tool.shovel-0")),
(1, Item("common.items.weapons.tool.shovel-1")),
]), None)),
)),
items: [
(10, "common.items.food.cheese"),
(10, "common.items.food.plainsalad"),
],
),
meta: [],
)

View File

@ -0,0 +1,21 @@
#![enable(implicit_some)]
(
name: Name("Herbalist"),
body: RandomWith("humanoid"),
alignment: Alignment(Npc),
loot: LootTable("common.loot_tables.creature.humanoid"),
inventory: (
loadout: Inline((
inherit: Asset("common.loadout.village.herbalist"),
active_hands: InHands((Choice([
(1, Item("common.items.weapons.tool.hoe")),
(1, Item("common.items.weapons.tool.rake")),
]), None)),
)),
items: [
(10, "common.items.food.cheese"),
(10, "common.items.food.plainsalad"),
],
),
meta: [],
)

View File

@ -0,0 +1,23 @@
#![enable(implicit_some)]
(
name: Name("Hunter"),
body: RandomWith("humanoid"),
alignment: Alignment(Npc),
loot: LootTable("common.loot_tables.creature.humanoid"),
inventory: (
loadout: Inline((
inherit: Asset("common.loadout.village.hunter"),
active_hands: InHands((Choice([
(8, ModularWeapon(tool: Bow, material: Wood, hands: None)),
(4, ModularWeapon(tool: Bow, material: Bamboo, hands: None)),
(2, ModularWeapon(tool: Bow, material: Hardwood, hands: None)),
(2, ModularWeapon(tool: Bow, material: Ironwood, hands: None)),
(1, ModularWeapon(tool: Bow, material: Eldwood, hands: None)),
]), None)),
)),
items: [
(10, "common.items.consumable.potion_big"),
],
),
meta: [],
)

View File

@ -6,6 +6,7 @@
loot: LootTable("common.loot_tables.creature.humanoid"),
inventory: (
loadout: Inline((
inherit: Asset("common.loadout.village.merchant"),
active_hands: InHands((Choice([
(2, ModularWeapon(tool: Bow, material: Eldwood, hands: None)),
(1, ModularWeapon(tool: Sword, material: Steel, hands: None)),

View File

@ -0,0 +1,11 @@
#![enable(implicit_some)]
(
name: Automatic,
body: RandomWith("dog"),
alignment: Alignment(Wild),
loot: LootTable("common.loot_tables.creature.quad_small.generic"),
inventory: (
loadout: FromBody,
),
meta: [],
)

View File

@ -0,0 +1,26 @@
// Christmas event
//(1.0, Some(Item("common.items.calendar.christmas.armor.misc.head.woolly_wintercap"))),
#![enable(implicit_some)]
(
head: Item("common.items.armor.pirate.hat"),
shoulders: Item("common.items.armor.mail.orichalcum.shoulder"),
chest: Item("common.items.armor.mail.orichalcum.chest"),
gloves: Item("common.items.armor.mail.orichalcum.hand"),
back: Choice([
(1, Item("common.items.armor.misc.back.backpack")),
(1, Item("common.items.npc_armor.back.backpack_blue")),
(1, Item("common.items.armor.mail.orichalcum.back")),
(1, None),
]),
belt: Item("common.items.armor.mail.orichalcum.belt"),
legs: Item("common.items.armor.mail.orichalcum.pants"),
feet: Item("common.items.armor.mail.orichalcum.foot"),
lantern: Choice([
(1, Item("common.items.lantern.black_0")),
(1, Item("common.items.lantern.blue_0")),
(1, Item("common.items.lantern.green_0")),
(1, Item("common.items.lantern.red_0")),
(1, Item("common.items.lantern.geode_purp")),
(1, Item("common.items.boss_drops.lantern")),
]),
)

View File

@ -0,0 +1,30 @@
// Christmas event
//(1.0, Some(Item("common.items.calendar.christmas.armor.misc.head.woolly_wintercap"))),
#![enable(implicit_some)]
(
head: Choice([
(3, Item("common.items.armor.misc.head.straw")),
(3, Item("common.items.armor.misc.head.bamboo_twig")),
(2, None),
]),
chest: Choice([
(1, Item("common.items.armor.misc.chest.worker_green_0")),
(1, Item("common.items.armor.misc.chest.worker_green_1")),
(1, Item("common.items.armor.misc.chest.worker_red_0")),
(1, Item("common.items.armor.misc.chest.worker_red_1")),
(1, Item("common.items.armor.misc.chest.worker_purple_0")),
(1, Item("common.items.armor.misc.chest.worker_purple_1")),
(1, Item("common.items.armor.misc.chest.worker_yellow_0")),
(1, Item("common.items.armor.misc.chest.worker_yellow_1")),
(1, Item("common.items.armor.misc.chest.worker_orange_0")),
(1, Item("common.items.armor.misc.chest.worker_orange_1")),
]),
legs: Choice([
(1, Item("common.items.armor.misc.pants.worker_blue")),
(1, Item("common.items.armor.misc.pants.worker_brown")),
]),
feet: Choice([
(1, Item("common.items.armor.misc.foot.sandals")),
(1, Item("common.items.armor.cloth_blue.foot")),
]),
)

View File

@ -0,0 +1,26 @@
// Christmas event
//(1.0, Some(Item("common.items.calendar.christmas.armor.misc.head.woolly_wintercap"))),
#![enable(implicit_some)]
(
head: Choice([
(3, Item("common.items.armor.misc.head.straw")),
(3, Item("common.items.armor.misc.head.hood")),
(2, None),
]),
chest: Choice([
(1, Item("common.items.armor.twigs.chest")),
(1, Item("common.items.armor.twigsflowers.chest")),
(1, Item("common.items.armor.twigsleaves.chest")),
]),
legs: Choice([
(1, Item("common.items.armor.twigs.pants")),
(1, Item("common.items.armor.twigsflowers.pants")),
(1, Item("common.items.armor.twigsleaves.pants")),
]),
feet: Choice([
(1, Item("common.items.armor.twigs.foot")),
(1, Item("common.items.armor.twigsflowers.foot")),
(1, Item("common.items.armor.twigsleaves.foot")),
(1, Item("common.items.armor.misc.foot.sandals")),
]),
)

View File

@ -0,0 +1,28 @@
// Christmas event
//(1.0, Some(Item("common.items.calendar.christmas.armor.misc.head.woolly_wintercap"))),
#![enable(implicit_some)]
(
head: Choice([
(6, None),
(2, Item("common.items.armor.misc.head.straw")),
(3, Item("common.items.armor.misc.head.hood")),
(3, Item("common.items.armor.misc.head.hood_dark")),
]),
chest: Choice([
(1, Item("common.items.armor.hide.leather.chest")),
(1, Item("common.items.armor.hide.rawhide.chest")),
(1, Item("common.items.armor.hide.primal.chest")),
]),
legs: Choice([
(1, Item("common.items.armor.hide.leather.pants")),
(1, Item("common.items.armor.hide.rawhide.pants")),
(1, Item("common.items.armor.hide.primal.pants")),
]),
feet: Choice([
(1, None),
(2, Item("common.items.armor.misc.foot.sandals")),
(4, Item("common.items.armor.hide.leather.foot")),
(4, Item("common.items.armor.hide.rawhide.foot")),
(4, Item("common.items.armor.hide.primal.foot")),
]),
)

View File

@ -11,5 +11,4 @@
legs: Item("common.items.armor.merchant.pants"),
feet: Item("common.items.armor.merchant.foot"),
lantern: Item("common.items.lantern.black_0"),
tabard: Item("common.items.debug.admin"),
)
)

View File

@ -223,3 +223,35 @@ npc-speech-prisoner =
.a2 = That Cardinal can't be trusted.
.a3 = These Clerics are up to no good.
.a4 = I wish i still had my pick!
npc-speech-moving_on =
.a0 = I've spent enough time here, onward to { $site }!
npc-speech-night_time =
.a0 = It's dark, time to head home.
.a1 = I'm tired.
.a2 = My bed beckons!
npc-speech-day_time =
.a0 = A new day begins!
.a1 = I never liked waking up...
npc-speech-start_hunting =
.a0 = Time to go hunting!
npc-speech-guard_thought =
.a0 = My brother's out fighting ogres. What do I get? Guard duty...
.a1 = Just one more patrol, then I can head home.
.a2 = No bandits are going to get past me.
npc-speech-merchant_sell_undirected =
.a0 = All my goods are of the highest quality!
.a1 = Does anybody want to buy my wares?
.a2 = I've got the best offers in town.
.a3 = Looking for supplies? I've got you covered.
npc-speech-merchant_sell_directed =
.a0 = You there! Are you in need of a new thingamabob?
.a1 = Are you hungry? I'm sure I've got some cheese you can buy.
.a2 = You look like you could do with some new armour!
npc-speech-witness_murder =
.a0 = Murderer!
.a1 = How could you do this?
.a2 = Aaargh!
npc-speech-witness_death =
.a0 = No!
.a1 = This is terrible!
.a2 = Oh my goodness!

View File

@ -22,7 +22,10 @@ layout(location = 0) in vec3 f_pos;
layout(location = 1) in vec3 f_norm;
layout(location = 2) in vec4 f_col;
layout(location = 3) in vec3 model_pos;
layout(location = 4) in float snow_cover;
layout(location = 4) flat in uint f_flags;
const uint FLAG_SNOW_COVERED = 1;
const uint FLAG_IS_BUILDING = 2;
layout(location = 0) out vec4 tgt_color;
layout(location = 1) out uvec4 tgt_mat;
@ -117,11 +120,25 @@ void main() {
emitted_light *= f_ao;
reflected_light *= f_ao;
vec3 side_color = mix(surf_color, vec3(0.5, 0.6, 1.0), snow_cover);
vec3 top_color = mix(surf_color, surf_color * 0.3, 0.5 + snow_cover * 0.5);
vec3 glow = vec3(0);
if ((f_flags & FLAG_IS_BUILDING) > 0u && abs(f_norm.z) < 0.1) {
ivec3 wpos = ivec3((f_pos.xyz + focus_off.xyz) * 0.2);
if (((wpos.x & wpos.y & wpos.z) & 1) == 1) {
glow += vec3(1, 0.7, 0.3) * 2;
} else {
reflected_light += vec3(1, 0.7, 0.3) * 0.9;
}
}
vec3 side_color = surf_color;
vec3 top_color = surf_color;
if ((f_flags & FLAG_SNOW_COVERED) > 0u && f_norm.z > 0.0) {
side_color = mix(side_color, vec3(0.5, 0.6, 1.0), f_norm.z);
top_color = mix(top_color, surf_color * 0.3, 0.5 + f_norm.z * 0.5);
}
surf_color = mix(side_color, top_color, pow(fract(model_pos.z * 0.1), 2.0));
surf_color = illuminate(max_light, view_dir, surf_color * emitted_light, surf_color * reflected_light);
surf_color = illuminate(max_light, view_dir, surf_color * emitted_light + glow, surf_color * reflected_light);
tgt_color = vec4(surf_color, 1.0);
tgt_mat = uvec4(uvec3((f_norm + 1.0) * 127.0), MAT_LOD);

View File

@ -24,13 +24,11 @@ layout(location = 3) in vec3 inst_pos;
layout(location = 4) in uvec3 inst_col;
layout(location = 5) in uint inst_flags;
const uint FLAG_SNOW_COVERED = 1;
layout(location = 0) out vec3 f_pos;
layout(location = 1) out vec3 f_norm;
layout(location = 2) out vec4 f_col;
layout(location = 3) out vec3 model_pos;
layout(location = 4) out float snow_cover;
layout(location = 4) flat out uint f_flags;
void main() {
vec3 obj_pos = inst_pos - focus_off.xyz;
@ -50,12 +48,7 @@ void main() {
f_norm = v_norm;
f_col = vec4(vec3(inst_col) * (1.0 / 255.0) * v_col * (hash(inst_pos.xyxy) * 0.35 + 0.65), 1.0);
if ((inst_flags & FLAG_SNOW_COVERED) > 0u && f_norm.z > 0.0) {
snow_cover = 1.0;
} else {
snow_cover = 0.0;
}
f_flags = inst_flags;
gl_Position =
all_mat *

View File

@ -103,7 +103,7 @@ fn main() {
&localisation.read(),
SHOW_NAME,
)
.message
.1
),
Event::Disconnect => {}, // TODO
Event::DisconnectionNotification(time) => {

View File

@ -7,6 +7,7 @@ version = "0.13.0"
[dependencies]
# Assets
common = {package = "veloren-common", path = "../../common"}
common-assets = {package = "veloren-common-assets", path = "../../common/assets"}
ron = "0.8"
serde = { version = "1.0", features = ["derive"] }

View File

@ -19,10 +19,11 @@ use std::{borrow::Cow, io};
use assets::{source::DirEntry, AssetExt, AssetGuard, AssetHandle, ReloadWatcher, SharedString};
use tracing::warn;
// Re-export because I don't like prefix
use common::comp::{Content, LocalizationArg};
use common_assets as assets;
// Re-export for argument creation
pub use fluent::fluent_args;
pub use fluent::{fluent_args, FluentValue};
pub use fluent_bundle::FluentArgs;
/// The reference language, aka the more up-to-date localization data.
@ -116,7 +117,9 @@ impl Language {
let msg = bundle.get_message(key)?;
let mut attrs = msg.attributes();
if attrs.len() != 0 {
let mut errs = Vec::new();
let msg = if attrs.len() != 0 {
let idx = usize::from(seed) % attrs.len();
// unwrap is ok here, because idx is bound to attrs.len()
// by using modulo operator.
@ -134,16 +137,17 @@ impl Language {
// * len = 0
// * no matter what seed is, we return None in code above
let variation = attrs.nth(idx).unwrap();
let mut errs = Vec::new();
let msg = bundle.format_pattern(variation.value(), args, &mut errs);
for err in errs {
tracing::error!("err: {err} for {key}");
}
Some(msg)
bundle.format_pattern(variation.value(), args, &mut errs)
} else {
None
// Fall back to single message if there are no attributes
bundle.format_pattern(msg.value()?, args, &mut errs)
};
for err in errs {
tracing::error!("err: {err} for {key}");
}
Some(msg)
}
}
@ -328,6 +332,47 @@ impl LocalizationGuard {
})
}
/// Localize the given content.
pub fn get_content(&self, content: &Content) -> String {
// On error, produces the localisation but with the missing key inline
fn get_content_inner(lang: &Language, content: &Content) -> Result<String, String> {
match content {
Content::Plain(text) => Ok(text.clone()),
Content::Localized { key, seed, args } => {
let mut is_err = false;
let mut fargs = FluentArgs::new();
for (k, arg) in args {
fargs.set(k, match arg {
LocalizationArg::Content(content) => FluentValue::String(
get_content_inner(lang, content)
.unwrap_or_else(|broken_text| {
is_err = true;
broken_text
})
.into(),
),
LocalizationArg::Nat(n) => FluentValue::from(n),
});
}
lang.try_variation(key, *seed, Some(&fargs))
.map(Cow::into_owned)
.ok_or_else(|| key.clone())
.and_then(|text| if is_err { Err(text) } else { Ok(text) })
},
}
}
match get_content_inner(&self.active, content) {
Ok(text) => text,
// If part of the localisation failed, use the fallback language
Err(broken_text) => self.fallback.as_ref()
.and_then(|fb| get_content_inner(fb, content).ok())
// If all else fails, localise with the active language, but with the missing key included inline
.unwrap_or(broken_text),
}
}
/// Get a localized text from the variation of given key with given
/// arguments
///

View File

@ -30,7 +30,7 @@ use common::{
slot::{EquipSlot, InvSlotId, Slot},
CharacterState, ChatMode, ControlAction, ControlEvent, Controller, ControllerInputs,
GroupManip, InputKind, InventoryAction, InventoryEvent, InventoryUpdateEvent,
MapMarkerChange, UtteranceKind,
MapMarkerChange, PresenceKind, UtteranceKind,
},
event::{EventBus, LocalEvent, UpdateCharacterMetadata},
grid::Grid,
@ -59,8 +59,8 @@ use common_net::{
self,
world_msg::{EconomyInfo, PoiInfo, SiteId, SiteInfo},
ChatTypeContext, ClientGeneral, ClientMsg, ClientRegister, ClientType, DisconnectReason,
InviteAnswer, Notification, PingMsg, PlayerInfo, PlayerListUpdate, PresenceKind,
RegisterError, ServerGeneral, ServerInit, ServerRegisterAnswer,
InviteAnswer, Notification, PingMsg, PlayerInfo, PlayerListUpdate, RegisterError,
ServerGeneral, ServerInit, ServerRegisterAnswer,
},
sync::WorldSyncExt,
};
@ -1855,6 +1855,7 @@ impl Client {
true,
None,
&self.connected_server_constants,
|_, _| {},
);
// TODO: avoid emitting these in the first place
let _ = self
@ -2279,13 +2280,15 @@ impl Client {
.any(|r| !matches!(r, group::Role::Pet))
{
frontend_events
.push(Event::Chat(comp::ChatType::Meta.chat_msg(
// TODO: localise
.push(Event::Chat(comp::ChatType::Meta.into_plain_msg(
"Type /g or /group to chat with your group members",
)));
}
if let Some(player_info) = self.player_list.get(&uid) {
frontend_events.push(Event::Chat(
comp::ChatType::GroupMeta("Group".into()).chat_msg(format!(
// TODO: localise
comp::ChatType::GroupMeta("Group".into()).into_plain_msg(format!(
"[{}] joined group",
self.personalize_alias(uid, player_info.player_alias.clone())
)),
@ -2302,7 +2305,8 @@ impl Client {
Removed(uid) => {
if let Some(player_info) = self.player_list.get(&uid) {
frontend_events.push(Event::Chat(
comp::ChatType::GroupMeta("Group".into()).chat_msg(format!(
// TODO: localise
comp::ChatType::GroupMeta("Group".into()).into_plain_msg(format!(
"[{}] left group",
self.personalize_alias(uid, player_info.player_alias.clone())
)),
@ -2789,20 +2793,20 @@ impl Client {
KillSource::Other => (),
};
},
comp::ChatType::Tell(from, to) | comp::ChatType::NpcTell(from, to, _) => {
comp::ChatType::Tell(from, to) | comp::ChatType::NpcTell(from, to) => {
alias_of_uid(from);
alias_of_uid(to);
},
comp::ChatType::Say(uid)
| comp::ChatType::Region(uid)
| comp::ChatType::World(uid)
| comp::ChatType::NpcSay(uid, _) => {
| comp::ChatType::NpcSay(uid) => {
alias_of_uid(uid);
},
comp::ChatType::Group(uid, _) | comp::ChatType::Faction(uid, _) => {
alias_of_uid(uid);
},
comp::ChatType::Npc(uid, _) => alias_of_uid(uid),
comp::ChatType::Npc(uid) => alias_of_uid(uid),
comp::ChatType::Meta => (),
};
result
@ -2963,7 +2967,7 @@ mod tests {
&localisation.read(),
true,
)
.message;
.1;
},
Event::Disconnect => {},
Event::DisconnectionNotification(_) => {

View File

@ -26,6 +26,7 @@ common-base = { package = "veloren-common-base", path = "base" }
serde = { version = "1.0.110", features = ["derive", "rc"] }
# Util
enum-map = "2.4"
vek = { version = "0.15.8", features = ["serde"] }
cfg-if = "1.0.0"
chrono = "0.4.22"

View File

@ -101,7 +101,7 @@ impl ClientMsg {
&self,
c_type: ClientType,
registered: bool,
presence: Option<super::PresenceKind>,
presence: Option<comp::PresenceKind>,
) -> bool {
match self {
ClientMsg::Type(t) => c_type == *t,

View File

@ -19,23 +19,8 @@ pub use self::{
},
world_msg::WorldMapMsg,
};
use common::character::CharacterId;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum PresenceKind {
Spectator,
Character(CharacterId),
Possessor,
}
impl PresenceKind {
/// Check if the presence represents a control of a character, and thus
/// certain in-game messages from the client such as control inputs
/// should be handled.
pub fn controlling_char(&self) -> bool { matches!(self, Self::Character(_) | Self::Possessor) }
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum PingMsg {
Ping,

View File

@ -6,7 +6,7 @@ use crate::sync;
use common::{
calendar::Calendar,
character::{self, CharacterItem},
comp::{self, invite::InviteKind, item::MaterialStatManifest},
comp::{self, invite::InviteKind, item::MaterialStatManifest, Content},
event::UpdateCharacterMetadata,
lod,
outcome::Outcome,
@ -215,11 +215,10 @@ pub enum ServerGeneral {
}
impl ServerGeneral {
pub fn server_msg<S>(chat_type: comp::ChatType<String>, msg: S) -> Self
where
S: Into<String>,
{
ServerGeneral::ChatMsg(chat_type.chat_msg(msg))
// TODO: Don't use `Into<Content>` since this treats all strings as plaintext,
// properly localise server messages
pub fn server_msg(chat_type: comp::ChatType<String>, content: impl Into<Content>) -> Self {
ServerGeneral::ChatMsg(chat_type.into_msg(content.into()))
}
}
@ -296,7 +295,7 @@ impl ServerMsg {
&self,
c_type: ClientType,
registered: bool,
presence: Option<super::PresenceKind>,
presence: Option<comp::PresenceKind>,
) -> bool {
match self {
ServerMsg::Info(_) | ServerMsg::Init(_) | ServerMsg::RegisterAnswer(_) => {

View File

@ -40,6 +40,7 @@ macro_rules! synced_components {
sticky: Sticky,
immovable: Immovable,
character_state: CharacterState,
character_activity: CharacterActivity,
shockwave: Shockwave,
beam_segment: BeamSegment,
alignment: Alignment,
@ -201,6 +202,10 @@ impl NetSync for CharacterState {
const SYNC_FROM: SyncFrom = SyncFrom::AnyEntity;
}
impl NetSync for CharacterActivity {
const SYNC_FROM: SyncFrom = SyncFrom::AnyEntity;
}
impl NetSync for Shockwave {
const SYNC_FROM: SyncFrom = SyncFrom::AnyEntity;
}

View File

@ -45,6 +45,15 @@ impl<T> PathResult<T> {
_ => None,
}
}
pub fn map<U>(self, f: impl FnOnce(Path<T>) -> Path<U>) -> PathResult<U> {
match self {
PathResult::None(p) => PathResult::None(f(p)),
PathResult::Exhausted(p) => PathResult::Exhausted(f(p)),
PathResult::Path(p) => PathResult::Path(f(p)),
PathResult::Pending => PathResult::Pending,
}
}
}
#[derive(Clone)]
@ -78,7 +87,7 @@ impl<S: Clone + Eq + Hash + fmt::Debug, H: BuildHasher> fmt::Debug for Astar<S,
}
impl<S: Clone + Eq + Hash, H: BuildHasher + Clone> Astar<S, H> {
pub fn new(max_iters: usize, start: S, heuristic: impl FnOnce(&S) -> f32, hasher: H) -> Self {
pub fn new(max_iters: usize, start: S, hasher: H) -> Self {
Self {
max_iters,
iter: 0,
@ -95,7 +104,7 @@ impl<S: Clone + Eq + Hash, H: BuildHasher + Clone> Astar<S, H> {
},
final_scores: {
let mut h = HashMap::with_capacity_and_hasher(1, hasher.clone());
h.extend(core::iter::once((start.clone(), heuristic(&start))));
h.extend(core::iter::once((start.clone(), 0.0)));
h
},
visited: {
@ -111,7 +120,7 @@ impl<S: Clone + Eq + Hash, H: BuildHasher + Clone> Astar<S, H> {
pub fn poll<I>(
&mut self,
iters: usize,
mut heuristic: impl FnMut(&S) -> f32,
mut heuristic: impl FnMut(&S, &S) -> f32,
mut neighbors: impl FnMut(&S) -> I,
mut transition: impl FnMut(&S, &S) -> f32,
mut satisfied: impl FnMut(&S) -> bool,
@ -134,7 +143,7 @@ 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 h = heuristic(&neighbor);
let h = heuristic(&neighbor, &node);
let neighbor_cost = cost + h;
self.final_scores.insert(neighbor.clone(), neighbor_cost);

View File

@ -5,7 +5,9 @@ use serde::{Deserialize, Serialize};
/// The limit on how many characters that a player can have
pub const MAX_CHARACTERS_PER_PLAYER: usize = 8;
pub type CharacterId = i64;
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[serde(transparent)]
pub struct CharacterId(pub i64);
pub const MAX_NAME_LENGTH: usize = 20;

View File

@ -296,8 +296,14 @@ pub enum ServerChatCommand {
Respawn,
RevokeBuild,
RevokeBuildAll,
RtsimChunk,
RtsimInfo,
RtsimNpc,
RtsimPurge,
RtsimTp,
Safezone,
Say,
Scale,
ServerPhysics,
SetMotd,
Ship,
@ -653,6 +659,7 @@ impl ServerChatCommand {
Enum("entity", ENTITIES.clone(), Required),
Integer("amount", 1, Optional),
Boolean("ai", "true".to_string(), Optional),
Float("scale", 1.0, Optional),
],
"Spawn a test entity",
Some(Admin),
@ -677,6 +684,36 @@ impl ServerChatCommand {
"Teleport to another player",
Some(Moderator),
),
ServerChatCommand::RtsimTp => cmd(
vec![Integer("npc index", 0, Required)],
"Teleport to an rtsim npc",
Some(Moderator),
),
ServerChatCommand::RtsimInfo => cmd(
vec![Integer("npc index", 0, Required)],
"Display information about an rtsim NPC",
Some(Moderator),
),
ServerChatCommand::RtsimNpc => cmd(
vec![Any("query", Required), Integer("max number", 20, Optional)],
"List rtsim NPCs that fit a given query (e.g: simulated,merchant) in order of \
distance",
Some(Moderator),
),
ServerChatCommand::RtsimPurge => cmd(
vec![Boolean(
"whether purging of rtsim data should occur on next startup",
true.to_string(),
Required,
)],
"Purge rtsim data on next startup",
Some(Admin),
),
ServerChatCommand::RtsimChunk => cmd(
vec![],
"Display information about the current chunk from rtsim",
Some(Moderator),
),
ServerChatCommand::Unban => cmd(
vec![PlayerName(Required)],
"Remove the ban for the given username",
@ -727,6 +764,14 @@ impl ServerChatCommand {
ServerChatCommand::Lightning => {
cmd(vec![], "Lightning strike at current position", Some(Admin))
},
ServerChatCommand::Scale => cmd(
vec![
Float("factor", 1.0, Required),
Boolean("reset_mass", true.to_string(), Optional),
],
"Scale your character",
Some(Admin),
),
}
}
@ -796,6 +841,11 @@ impl ServerChatCommand {
ServerChatCommand::Tell => "tell",
ServerChatCommand::Time => "time",
ServerChatCommand::Tp => "tp",
ServerChatCommand::RtsimTp => "rtsim_tp",
ServerChatCommand::RtsimInfo => "rtsim_info",
ServerChatCommand::RtsimNpc => "rtsim_npc",
ServerChatCommand::RtsimPurge => "rtsim_purge",
ServerChatCommand::RtsimChunk => "rtsim_chunk",
ServerChatCommand::Unban => "unban",
ServerChatCommand::Version => "version",
ServerChatCommand::Waypoint => "waypoint",
@ -808,6 +858,7 @@ impl ServerChatCommand {
ServerChatCommand::DeleteLocation => "delete_location",
ServerChatCommand::WeatherZone => "weather_zone",
ServerChatCommand::Lightning => "lightning",
ServerChatCommand::Scale => "scale",
}
}

View File

@ -4,7 +4,7 @@ use crate::{
quadruped_small, ship, Body, UtteranceKind,
},
path::Chaser,
rtsim::{Memory, MemoryItem, RtSimController, RtSimEvent},
rtsim::RtSimController,
trade::{PendingTrade, ReducedInventory, SiteId, SitePrices, TradeId, TradeResult},
uid::Uid,
};
@ -83,7 +83,7 @@ impl Alignment {
}
}
// Never attacks
// Usually never attacks
pub fn passive_towards(self, other: Alignment) -> bool {
match (self, other) {
(Alignment::Enemy, Alignment::Enemy) => true,
@ -98,6 +98,20 @@ impl Alignment {
_ => false,
}
}
// Never attacks
pub fn friendly_towards(self, other: Alignment) -> bool {
match (self, other) {
(Alignment::Enemy, Alignment::Enemy) => true,
(Alignment::Owned(a), Alignment::Owned(b)) if a == b => true,
(Alignment::Npc, Alignment::Npc) => true,
(Alignment::Npc, Alignment::Tame) => true,
(Alignment::Tame, Alignment::Npc) => true,
(Alignment::Tame, Alignment::Tame) => true,
(_, Alignment::Passive) => true,
_ => false,
}
}
}
impl Component for Alignment {
@ -611,6 +625,11 @@ impl Awareness {
self.reached = false;
}
}
pub fn set_maximally_aware(&mut self) {
self.reached = true;
self.level = Self::ALERT;
}
}
#[derive(Clone, Debug, PartialOrd, PartialEq, Eq)]
@ -713,23 +732,6 @@ impl Agent {
}
pub fn allowed_to_speak(&self) -> bool { self.behavior.can(BehaviorCapability::SPEAK) }
pub fn forget_enemy(&mut self, target_name: &str) {
self.rtsim_controller
.events
.push(RtSimEvent::ForgetEnemy(target_name.to_owned()));
}
pub fn add_fight_to_memory(&mut self, target_name: &str, time: f64) {
self.rtsim_controller
.events
.push(RtSimEvent::AddMemory(Memory {
item: MemoryItem::CharacterFight {
name: target_name.to_owned(),
},
time_to_forget: time + 300.0,
}));
}
}
impl Component for Agent {
@ -904,21 +906,21 @@ impl<F: Fn(Vec3<f32>, Vec3<f32>) -> f32, const NUM_SAMPLES: usize> PidController
/// Get the PID coefficients associated with some Body, since it will likely
/// need to be tuned differently for each body type
pub fn pid_coefficients(body: &Body) -> (f32, f32, f32) {
pub fn pid_coefficients(body: &Body) -> Option<(f32, f32, f32)> {
// A pure-proportional controller is { kp: 1.0, ki: 0.0, kd: 0.0 }
match body {
Body::Ship(ship::Body::DefaultAirship) => {
let kp = 1.0;
let ki = 0.1;
let kd = 1.2;
(kp, ki, kd)
Some((kp, ki, kd))
},
Body::Ship(ship::Body::AirBalloon) => {
let kp = 1.0;
let ki = 0.1;
let kd = 0.8;
(kp, ki, kd)
Some((kp, ki, kd))
},
// default to a pure-proportional controller, which is the first step when tuning
_ => (1.0, 0.0, 0.0),
_ => None,
}
}

View File

@ -983,7 +983,7 @@ impl Body {
}
/// Returns the eye height for this creature.
pub fn eye_height(&self) -> f32 { self.height() * 0.9 }
pub fn eye_height(&self, scale: f32) -> f32 { self.height() * 0.9 * scale }
pub fn default_light_offset(&self) -> Vec3<f32> {
// TODO: Make this a manifest

View File

@ -12,6 +12,7 @@ use crate::{
utils::{AbilityInfo, StageSection},
*,
},
util::Dir,
};
use serde::{Deserialize, Serialize};
use specs::{Component, DerefFlaggedStorage};
@ -30,6 +31,7 @@ pub struct StateUpdate {
pub should_strafe: bool,
pub queued_inputs: BTreeMap<InputKind, InputAttr>,
pub removed_inputs: Vec<InputKind>,
pub character_activity: CharacterActivity,
}
pub struct OutputEvents<'a> {
@ -60,6 +62,7 @@ impl From<&JoinData<'_>> for StateUpdate {
character: data.character.clone(),
queued_inputs: BTreeMap::new(),
removed_inputs: Vec::new(),
character_activity: *data.character_activity,
}
}
}
@ -257,7 +260,6 @@ impl CharacterState {
| CharacterState::Shockwave(_)
| CharacterState::BasicBeam(_)
| CharacterState::Stunned(_)
| CharacterState::UseItem(_)
| CharacterState::Wielding(_)
| CharacterState::Talk
| CharacterState::FinisherMelee(_)
@ -980,3 +982,20 @@ impl Default for CharacterState {
impl Component for CharacterState {
type Storage = DerefFlaggedStorage<Self, specs::VecStorage<Self>>;
}
/// Contains information about the visual activity of a character.
///
/// For now this only includes the direction they're looking in, but later it
/// might include markers indicating that they're available for
/// trade/interaction, more details about their stance or appearance, facial
/// expression, etc.
#[derive(Copy, Clone, Default, Debug, PartialEq, Serialize, Deserialize)]
pub struct CharacterActivity {
/// `None` means that the look direction should be derived from the
/// orientation
pub look_dir: Option<Dir>,
}
impl Component for CharacterActivity {
type Storage = DerefFlaggedStorage<Self, specs::VecStorage<Self>>;
}

View File

@ -2,6 +2,7 @@ use crate::{
comp::{group::Group, BuffKind},
uid::Uid,
};
use hashbrown::HashMap;
use serde::{Deserialize, Serialize};
use specs::{Component, DenseVecStorage};
use std::time::{Duration, Instant};
@ -29,8 +30,8 @@ impl Component for ChatMode {
}
impl ChatMode {
/// Create a message from your current chat mode and uuid.
pub fn new_message(&self, from: Uid, message: String) -> UnresolvedChatMsg {
/// Create a plain message from your current chat mode and uuid.
pub fn to_plain_msg(&self, from: Uid, text: impl ToString) -> UnresolvedChatMsg {
let chat_type = match self {
ChatMode::Tell(to) => ChatType::Tell(from, *to),
ChatMode::Say => ChatType::Say(from),
@ -39,7 +40,10 @@ impl ChatMode {
ChatMode::Faction(faction) => ChatType::Faction(from, faction.clone()),
ChatMode::World => ChatType::World(from),
};
UnresolvedChatMsg { chat_type, message }
UnresolvedChatMsg {
chat_type,
content: Content::Plain(text.to_string()),
}
}
}
@ -102,26 +106,28 @@ pub enum ChatType<G> {
/// World chat
World(Uid),
/// Messages sent from NPCs (Not shown in chat but as speech bubbles)
///
/// The u16 field is a random number for selecting localization variants.
Npc(Uid, u16),
Npc(Uid),
/// From NPCs but in the chat for clients in the near vicinity
NpcSay(Uid, u16),
NpcSay(Uid),
/// From NPCs but in the chat for a specific client. Shows a chat bubble.
/// (from, to, localization variant)
NpcTell(Uid, Uid, u16),
NpcTell(Uid, Uid),
/// Anything else
Meta,
}
impl<G> ChatType<G> {
pub fn chat_msg<S>(self, msg: S) -> GenericChatMsg<G>
where
S: Into<String>,
{
pub fn into_plain_msg(self, text: impl ToString) -> GenericChatMsg<G> {
GenericChatMsg {
chat_type: self,
message: msg.into(),
content: Content::Plain(text.to_string()),
}
}
pub fn into_msg(self, content: Content) -> GenericChatMsg<G> {
GenericChatMsg {
chat_type: self,
content,
}
}
@ -140,9 +146,9 @@ impl<G> ChatType<G> {
ChatType::Faction(u, _s) => Some(*u),
ChatType::Region(u) => Some(*u),
ChatType::World(u) => Some(*u),
ChatType::Npc(u, _r) => Some(*u),
ChatType::NpcSay(u, _r) => Some(*u),
ChatType::NpcTell(u, _t, _r) => Some(*u),
ChatType::Npc(u) => Some(*u),
ChatType::NpcSay(u) => Some(*u),
ChatType::NpcTell(u, _t) => Some(*u),
ChatType::Meta => None,
}
}
@ -156,9 +162,9 @@ impl<G> ChatType<G> {
| ChatType::CommandError
| ChatType::FactionMeta(_)
| ChatType::GroupMeta(_)
| ChatType::Npc(_, _)
| ChatType::NpcSay(_, _)
| ChatType::NpcTell(_, _, _)
| ChatType::Npc(_)
| ChatType::NpcSay(_)
| ChatType::NpcTell(_, _)
| ChatType::Meta
| ChatType::Kill(_, _) => None,
ChatType::Tell(_, _) | ChatType::Group(_, _) | ChatType::Faction(_, _) => Some(true),
@ -167,11 +173,118 @@ impl<G> ChatType<G> {
}
}
/// The content of a chat message.
// TODO: This could be generalised to *any* in-game text, not just chat messages (hence it not being
// called `ChatContent`). A few examples:
//
// - Signposts, both those appearing as overhead messages and those displayed 'in-world' on a shop
// sign
// - UI elements
// - In-game notes/books (we could add a variant that allows structuring complex, novel textual
// information as a syntax tree or some other intermediate format that can be localised by the
// client)
// TODO: We probably want to have this type be able to represent similar things to
// `fluent::FluentValue`, such as numeric values, so that they can be properly localised in whatever
// manner is required.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum Content {
/// The content is a plaintext string that should be shown to the user
/// verbatim.
Plain(String),
/// The content is a localizable message with the given arguments.
Localized {
/// i18n key
key: String,
/// Pseudorandom seed value that allows frontends to select a
/// deterministic (but pseudorandom) localised output
seed: u16,
/// i18n arguments
args: HashMap<String, LocalizationArg>,
},
}
// TODO: Remove impl and make use of `Plain(...)` explicit (to discourage it)
impl From<String> for Content {
fn from(text: String) -> Self { Self::Plain(text) }
}
// TODO: Remove impl and make use of `Plain(...)` explicit (to discourage it)
impl<'a> From<&'a str> for Content {
fn from(text: &'a str) -> Self { Self::Plain(text.to_string()) }
}
/// A localisation argument for localised content (see [`Content::Localized`]).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum LocalizationArg {
/// The localisation argument is itself a section of content.
///
/// Note that this allows [`Content`] to recursively refer to itself. It may
/// be tempting to decide to parameterise everything, having dialogue
/// generated with a compact tree. "It's simpler!", you might say. False.
/// Over-parameterisation is an anti-pattern that hurts translators. Where
/// possible, prefer fewer levels of nesting unless doing so would
/// result in an intractably larger number of combinations. See [here](https://github.com/projectfluent/fluent/wiki/Good-Practices-for-Developers#prefer-wet-over-dry) for the
/// guidance provided by the docs for `fluent`, the localisation library
/// used by clients.
Content(Content),
/// The localisation argument is a natural number
Nat(u64),
}
// TODO: Remove impl and make use of `Content(Plain(...))` explicit (to
// discourage it)
impl From<String> for LocalizationArg {
fn from(text: String) -> Self { Self::Content(Content::Plain(text)) }
}
// TODO: Remove impl and make use of `Content(Plain(...))` explicit (to
// discourage it)
impl<'a> From<&'a str> for LocalizationArg {
fn from(text: &'a str) -> Self { Self::Content(Content::Plain(text.to_string())) }
}
// TODO: Remove impl and make use of `Content(Plain(...))` explicit (to
// discourage it)
impl From<u64> for LocalizationArg {
fn from(n: u64) -> Self { Self::Nat(n) }
}
impl Content {
pub fn localized(key: impl ToString) -> Self {
Self::Localized {
key: key.to_string(),
seed: rand::random(),
args: HashMap::default(),
}
}
pub fn localized_with_args<'a, A: Into<LocalizationArg>>(
key: impl ToString,
args: impl IntoIterator<Item = (&'a str, A)>,
) -> Self {
Self::Localized {
key: key.to_string(),
seed: rand::random(),
args: args
.into_iter()
.map(|(k, v)| (k.to_string(), v.into()))
.collect(),
}
}
pub fn as_plain(&self) -> Option<&str> {
match self {
Self::Plain(text) => Some(text.as_str()),
Self::Localized { .. } => None,
}
}
}
// Stores chat text, type
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GenericChatMsg<G> {
pub chat_type: ChatType<G>,
pub message: String,
content: Content,
}
pub type ChatMsg = GenericChatMsg<String>;
@ -183,19 +296,19 @@ impl<G> GenericChatMsg<G> {
pub const REGION_DISTANCE: f32 = 1000.0;
pub const SAY_DISTANCE: f32 = 100.0;
pub fn npc(uid: Uid, message: String) -> Self {
let chat_type = ChatType::Npc(uid, rand::random());
Self { chat_type, message }
pub fn npc(uid: Uid, content: Content) -> Self {
let chat_type = ChatType::Npc(uid);
Self { chat_type, content }
}
pub fn npc_say(uid: Uid, message: String) -> Self {
let chat_type = ChatType::NpcSay(uid, rand::random());
Self { chat_type, message }
pub fn npc_say(uid: Uid, content: Content) -> Self {
let chat_type = ChatType::NpcSay(uid);
Self { chat_type, content }
}
pub fn npc_tell(from: Uid, to: Uid, message: String) -> Self {
let chat_type = ChatType::NpcTell(from, to, rand::random());
Self { chat_type, message }
pub fn npc_tell(from: Uid, to: Uid, content: Content) -> Self {
let chat_type = ChatType::NpcTell(from, to);
Self { chat_type, content }
}
pub fn map_group<T>(self, mut f: impl FnMut(G) -> T) -> GenericChatMsg<T> {
@ -213,15 +326,15 @@ impl<G> GenericChatMsg<G> {
ChatType::Faction(a, b) => ChatType::Faction(a, b),
ChatType::Region(a) => ChatType::Region(a),
ChatType::World(a) => ChatType::World(a),
ChatType::Npc(a, b) => ChatType::Npc(a, b),
ChatType::NpcSay(a, b) => ChatType::NpcSay(a, b),
ChatType::NpcTell(a, b, c) => ChatType::NpcTell(a, b, c),
ChatType::Npc(a) => ChatType::Npc(a),
ChatType::NpcSay(a) => ChatType::NpcSay(a),
ChatType::NpcTell(a, b) => ChatType::NpcTell(a, b),
ChatType::Meta => ChatType::Meta,
};
GenericChatMsg {
chat_type,
message: self.message,
content: self.content,
}
}
@ -234,15 +347,8 @@ impl<G> GenericChatMsg<G> {
}
pub fn to_bubble(&self) -> Option<(SpeechBubble, Uid)> {
let icon = self.icon();
if let ChatType::Npc(from, r) | ChatType::NpcSay(from, r) | ChatType::NpcTell(from, _, r) =
self.chat_type
{
Some((SpeechBubble::npc_new(&self.message, r, icon), from))
} else {
self.uid()
.map(|from| (SpeechBubble::player_new(&self.message, icon), from))
}
self.uid()
.map(|from| (SpeechBubble::new(self.content.clone(), self.icon()), from))
}
pub fn icon(&self) -> SpeechBubbleType {
@ -260,14 +366,20 @@ impl<G> GenericChatMsg<G> {
ChatType::Faction(_u, _s) => SpeechBubbleType::Faction,
ChatType::Region(_u) => SpeechBubbleType::Region,
ChatType::World(_u) => SpeechBubbleType::World,
ChatType::Npc(_u, _r) => SpeechBubbleType::None,
ChatType::NpcSay(_u, _r) => SpeechBubbleType::Say,
ChatType::NpcTell(_f, _t, _) => SpeechBubbleType::Say,
ChatType::Npc(_u) => SpeechBubbleType::None,
ChatType::NpcSay(_u) => SpeechBubbleType::Say,
ChatType::NpcTell(_f, _t) => SpeechBubbleType::Say,
ChatType::Meta => SpeechBubbleType::None,
}
}
pub fn uid(&self) -> Option<Uid> { self.chat_type.uid() }
pub fn content(&self) -> &Content { &self.content }
pub fn into_content(self) -> Content { self.content }
pub fn set_content(&mut self, content: Content) { self.content = content; }
}
/// Player factions are used to coordinate pvp vs hostile factions or segment
@ -283,15 +395,6 @@ impl From<String> for Faction {
fn from(s: String) -> Self { Faction(s) }
}
/// The contents of a speech bubble
pub enum SpeechBubbleMessage {
/// This message was said by a player and needs no translation
Plain(String),
/// This message was said by an NPC. The fields are a i18n key and a random
/// u16 index
Localized(String, u16),
}
/// List of chat types for players and NPCs. Each one has its own icon.
///
/// This is a subset of `ChatType`, and a superset of `ChatMode`
@ -311,7 +414,7 @@ pub enum SpeechBubbleType {
/// Adds a speech bubble above the character
pub struct SpeechBubble {
pub message: SpeechBubbleMessage,
pub content: Content,
pub icon: SpeechBubbleType,
pub timeout: Instant,
}
@ -320,33 +423,14 @@ impl SpeechBubble {
/// Default duration in seconds of speech bubbles
pub const DEFAULT_DURATION: f64 = 5.0;
pub fn npc_new(i18n_key: &str, r: u16, icon: SpeechBubbleType) -> Self {
let message = SpeechBubbleMessage::Localized(i18n_key.to_string(), r);
pub fn new(content: Content, icon: SpeechBubbleType) -> Self {
let timeout = Instant::now() + Duration::from_secs_f64(SpeechBubble::DEFAULT_DURATION);
Self {
message,
content,
icon,
timeout,
}
}
pub fn player_new(message: &str, icon: SpeechBubbleType) -> Self {
let message = SpeechBubbleMessage::Plain(message.to_string());
let timeout = Instant::now() + Duration::from_secs_f64(SpeechBubble::DEFAULT_DURATION);
Self {
message,
icon,
timeout,
}
}
pub fn message<F>(&self, i18n_variation: F) -> String
where
F: Fn(&str, u16) -> String,
{
match &self.message {
SpeechBubbleMessage::Plain(m) => m.to_string(),
SpeechBubbleMessage::Localized(k, i) => i18n_variation(k, *i),
}
}
pub fn content(&self) -> &Content { &self.content }
}

View File

@ -1,4 +1,5 @@
use vek::Vec2;
// TODO: Move this to common/src/, it's not a component
/// Cardinal directions
pub enum Direction {

View File

@ -135,6 +135,7 @@ impl Body {
rel_flow: &Vel,
fluid_density: f32,
wings: Option<&Wings>,
scale: f32,
) -> Vec3<f32> {
let v_sq = rel_flow.0.magnitude_squared();
if v_sq < 0.25 {
@ -201,11 +202,11 @@ impl Body {
debug_assert!(c_d.is_sign_positive());
debug_assert!(c_l.is_sign_positive() || aoa.is_sign_negative());
planform_area * (c_l * *lift_dir + c_d * *rel_flow_dir)
+ self.parasite_drag() * *rel_flow_dir
planform_area * scale.powf(2.0) * (c_l * *lift_dir + c_d * *rel_flow_dir)
+ self.parasite_drag(scale) * *rel_flow_dir
},
_ => self.parasite_drag() * *rel_flow_dir,
_ => self.parasite_drag(scale) * *rel_flow_dir,
}
}
}
@ -214,13 +215,13 @@ impl Body {
/// Skin friction is the drag arising from the shear forces between a fluid
/// and a surface, while pressure drag is due to flow separation. Both are
/// viscous effects.
fn parasite_drag(&self) -> f32 {
fn parasite_drag(&self, scale: f32) -> f32 {
// Reference area and drag coefficient assumes best-case scenario of the
// orientation producing least amount of drag
match self {
// Cross-section, head/feet first
Body::BipedLarge(_) | Body::BipedSmall(_) | Body::Golem(_) | Body::Humanoid(_) => {
let dim = self.dimensions().xy().map(|a| a * 0.5);
let dim = self.dimensions().xy().map(|a| a * 0.5 * scale);
const CD: f32 = 0.7;
CD * PI * dim.x * dim.y
},
@ -231,7 +232,7 @@ impl Body {
| Body::QuadrupedSmall(_)
| Body::QuadrupedLow(_)
| Body::Arthropod(_) => {
let dim = self.dimensions().map(|a| a * 0.5);
let dim = self.dimensions().map(|a| a * 0.5 * scale);
let cd: f32 = if matches!(self, Body::QuadrupedLow(_)) {
0.7
} else {
@ -242,7 +243,7 @@ impl Body {
// Cross-section, zero-lift angle; exclude the wings (width * 0.2)
Body::BirdMedium(_) | Body::BirdLarge(_) | Body::Dragon(_) => {
let dim = self.dimensions().map(|a| a * 0.5);
let dim = self.dimensions().map(|a| a * 0.5 * scale);
let cd: f32 = match self {
// "Field Estimates of Body Drag Coefficient
// on the Basis of Dives in Passerine Birds",
@ -256,7 +257,7 @@ impl Body {
// Cross-section, zero-lift angle; exclude the fins (width * 0.2)
Body::FishMedium(_) | Body::FishSmall(_) => {
let dim = self.dimensions().map(|a| a * 0.5);
let dim = self.dimensions().map(|a| a * 0.5 * scale);
// "A Simple Method to Determine Drag Coefficients in Aquatic Animals",
// D. Bilo and W. Nachtigall, 1980
const CD: f32 = 0.031;
@ -276,7 +277,7 @@ impl Body {
| object::Body::FireworkYellow
| object::Body::MultiArrow
| object::Body::Dart => {
let dim = self.dimensions().map(|a| a * 0.5);
let dim = self.dimensions().map(|a| a * 0.5 * scale);
const CD: f32 = 0.02;
CD * PI * dim.x * dim.z
},
@ -295,20 +296,20 @@ impl Body {
| object::Body::Pumpkin3
| object::Body::Pumpkin4
| object::Body::Pumpkin5 => {
let dim = self.dimensions().map(|a| a * 0.5);
let dim = self.dimensions().map(|a| a * 0.5 * scale);
const CD: f32 = 0.5;
CD * PI * dim.x * dim.z
},
_ => {
let dim = self.dimensions();
let dim = self.dimensions().map(|a| a * scale);
const CD: f32 = 2.0;
CD * (PI / 6.0 * dim.x * dim.y * dim.z).powf(2.0 / 3.0)
},
},
Body::ItemDrop(_) => {
let dim = self.dimensions();
let dim = self.dimensions().map(|a| a * scale);
const CD: f32 = 2.0;
CD * (PI / 6.0 * dim.x * dim.y * dim.z).powf(2.0 / 3.0)
},
@ -316,7 +317,7 @@ impl Body {
Body::Ship(_) => {
// Airships tend to use the square of the cube root of its volume for
// reference area
let dim = self.dimensions();
let dim = self.dimensions().map(|a| a * scale);
(PI / 6.0 * dim.x * dim.y * dim.z).powf(2.0 / 3.0)
},
}

View File

@ -38,6 +38,8 @@ pub mod loot_owner;
#[cfg(not(target_arch = "wasm32"))] mod player;
#[cfg(not(target_arch = "wasm32"))] pub mod poise;
#[cfg(not(target_arch = "wasm32"))]
pub mod presence;
#[cfg(not(target_arch = "wasm32"))]
pub mod projectile;
#[cfg(not(target_arch = "wasm32"))]
pub mod shockwave;
@ -71,9 +73,10 @@ pub use self::{
Buff, BuffCategory, BuffChange, BuffData, BuffEffect, BuffId, BuffKind, BuffSource, Buffs,
ModifierKind,
},
character_state::{CharacterState, StateUpdate},
character_state::{CharacterActivity, CharacterState, StateUpdate},
chat::{
ChatMode, ChatMsg, ChatType, Faction, SpeechBubble, SpeechBubbleType, UnresolvedChatMsg,
ChatMode, ChatMsg, ChatType, Content, Faction, LocalizationArg, SpeechBubble,
SpeechBubbleType, UnresolvedChatMsg,
},
combo::Combo,
controller::{
@ -107,6 +110,7 @@ pub use self::{
player::DisconnectReason,
player::{AliasError, Player, MAX_ALIAS_LEN},
poise::{Poise, PoiseChange, PoiseState},
presence::{Presence, PresenceKind},
projectile::{Projectile, ProjectileConstructor},
shockwave::{Shockwave, ShockwaveHitEntities},
skillset::{

View File

@ -94,6 +94,7 @@ pub fn is_mountable(mount: &Body, rider: Option<&Body>) -> bool {
| quadruped_low::Species::Elbst
| quadruped_low::Species::Tortoise
),
Body::Ship(_) => true,
_ => false,
}
}

128
common/src/comp/presence.rs Normal file
View File

@ -0,0 +1,128 @@
use crate::{character::CharacterId, ViewDistances};
use serde::{Deserialize, Serialize};
use specs::Component;
use std::time::{Duration, Instant};
use vek::*;
#[derive(Debug)]
pub struct Presence {
pub terrain_view_distance: ViewDistance,
pub entity_view_distance: ViewDistance,
pub kind: PresenceKind,
pub lossy_terrain_compression: bool,
}
impl Presence {
pub fn new(view_distances: ViewDistances, kind: PresenceKind) -> Self {
let now = Instant::now();
Self {
terrain_view_distance: ViewDistance::new(view_distances.terrain, now),
entity_view_distance: ViewDistance::new(view_distances.entity, now),
kind,
lossy_terrain_compression: false,
}
}
}
impl Component for Presence {
type Storage = specs::DenseVecStorage<Self>;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum PresenceKind {
Spectator,
Character(CharacterId),
Possessor,
}
impl PresenceKind {
/// Check if the presence represents a control of a character, and thus
/// certain in-game messages from the client such as control inputs
/// should be handled.
pub fn controlling_char(&self) -> bool { matches!(self, Self::Character(_) | Self::Possessor) }
}
#[derive(PartialEq, Debug, Clone, Copy)]
enum Direction {
Up,
Down,
}
/// Distance from the [Presence] from which the world is loaded and information
/// is synced to clients.
///
/// We limit the frequency that changes in the view distance change direction
/// (e.g. shifting from increasing the value to decreasing it). This is useful
/// since we want to avoid rapid cycles of shrinking and expanding of the view
/// distance.
#[derive(Debug)]
pub struct ViewDistance {
direction: Direction,
last_direction_change_time: Instant,
target: Option<u32>,
current: u32,
}
impl ViewDistance {
/// Minimum time allowed between changes in direction of value adjustments.
const TIME_PER_DIR_CHANGE: Duration = Duration::from_millis(300);
pub fn new(start_value: u32, now: Instant) -> Self {
Self {
direction: Direction::Up,
last_direction_change_time: now.checked_sub(Self::TIME_PER_DIR_CHANGE).unwrap_or(now),
target: None,
current: start_value,
}
}
/// Returns the current value.
pub fn current(&self) -> u32 { self.current }
/// Applies deferred change based on the whether the time to apply it has
/// been reached.
pub fn update(&mut self, now: Instant) {
if let Some(target_val) = self.target {
if now.saturating_duration_since(self.last_direction_change_time)
> Self::TIME_PER_DIR_CHANGE
{
self.last_direction_change_time = now;
self.current = target_val;
self.target = None;
}
}
}
/// Sets the target value.
///
/// If this hasn't been changed recently or it is in the same direction as
/// the previous change it will be applied immediately. Otherwise, it
/// will be deferred to a later time (limiting the frequency of changes
/// in the change direction).
pub fn set_target(&mut self, new_target: u32, now: Instant) {
use core::cmp::Ordering;
let new_direction = match new_target.cmp(&self.current) {
Ordering::Equal => return, // No change needed.
Ordering::Less => Direction::Down,
Ordering::Greater => Direction::Up,
};
// Change is in the same direction as before so we can just apply it.
if new_direction == self.direction {
self.current = new_target;
self.target = None;
// If it has already been a while since the last direction change we can
// directly apply the request and switch the direction.
} else if now.saturating_duration_since(self.last_direction_change_time)
> Self::TIME_PER_DIR_CHANGE
{
self.direction = new_direction;
self.last_direction_change_time = now;
self.current = new_target;
self.target = None;
// Otherwise, we need to defer the request.
} else {
self.target = Some(new_target);
}
}
}

View File

@ -8,7 +8,7 @@ use crate::{
},
lottery::LootSpec,
outcome::Outcome,
rtsim::RtSimEntity,
rtsim::{RtSimEntity, RtSimVehicle},
terrain::SpriteKind,
trade::{TradeAction, TradeId},
uid::Uid,
@ -42,6 +42,92 @@ pub struct UpdateCharacterMetadata {
pub skill_set_persistence_load_error: Option<comp::skillset::SkillsPersistenceError>,
}
pub struct NpcBuilder {
pub stats: comp::Stats,
pub skill_set: comp::SkillSet,
pub health: Option<comp::Health>,
pub poise: comp::Poise,
pub inventory: comp::inventory::Inventory,
pub body: comp::Body,
pub agent: Option<comp::Agent>,
pub alignment: comp::Alignment,
pub scale: comp::Scale,
pub anchor: Option<comp::Anchor>,
pub loot: LootSpec<String>,
pub rtsim_entity: Option<RtSimEntity>,
pub projectile: Option<comp::Projectile>,
}
impl NpcBuilder {
pub fn new(stats: comp::Stats, body: comp::Body, alignment: comp::Alignment) -> Self {
Self {
stats,
skill_set: comp::SkillSet::default(),
health: None,
poise: comp::Poise::new(body),
inventory: comp::Inventory::with_empty(),
body,
agent: None,
alignment,
scale: comp::Scale(1.0),
anchor: None,
loot: LootSpec::Nothing,
rtsim_entity: None,
projectile: None,
}
}
pub fn with_health(mut self, health: impl Into<Option<comp::Health>>) -> Self {
self.health = health.into();
self
}
pub fn with_poise(mut self, poise: comp::Poise) -> Self {
self.poise = poise;
self
}
pub fn with_agent(mut self, agent: impl Into<Option<comp::Agent>>) -> Self {
self.agent = agent.into();
self
}
pub fn with_anchor(mut self, anchor: comp::Anchor) -> Self {
self.anchor = Some(anchor);
self
}
pub fn with_rtsim(mut self, rtsim: RtSimEntity) -> Self {
self.rtsim_entity = Some(rtsim);
self
}
pub fn with_projectile(mut self, projectile: impl Into<Option<comp::Projectile>>) -> Self {
self.projectile = projectile.into();
self
}
pub fn with_scale(mut self, scale: comp::Scale) -> Self {
self.scale = scale;
self
}
pub fn with_inventory(mut self, inventory: comp::Inventory) -> Self {
self.inventory = inventory;
self
}
pub fn with_skill_set(mut self, skill_set: comp::SkillSet) -> Self {
self.skill_set = skill_set;
self
}
pub fn with_loot(mut self, loot: LootSpec<String>) -> Self {
self.loot = loot;
self
}
}
#[allow(clippy::large_enum_variant)] // TODO: Pending review in #587
#[derive(strum::EnumDiscriminants)]
#[strum_discriminants(repr(usize))]
@ -137,26 +223,13 @@ pub enum ServerEvent {
// TODO: to avoid breakage when adding new fields, perhaps have an `NpcBuilder` type?
CreateNpc {
pos: Pos,
stats: comp::Stats,
skill_set: comp::SkillSet,
health: Option<comp::Health>,
poise: comp::Poise,
inventory: comp::inventory::Inventory,
body: comp::Body,
agent: Option<comp::Agent>,
alignment: comp::Alignment,
scale: comp::Scale,
anchor: Option<comp::Anchor>,
loot: LootSpec<String>,
rtsim_entity: Option<RtSimEntity>,
projectile: Option<comp::Projectile>,
npc: NpcBuilder,
},
CreateShip {
pos: Pos,
ship: comp::ship::Body,
mountable: bool,
agent: Option<comp::Agent>,
rtsim_entity: Option<RtSimEntity>,
rtsim_entity: Option<RtSimVehicle>,
driver: Option<NpcBuilder>,
},
CreateWaypoint(Vec3<f32>),
ClientDisconnect(EcsEntity, DisconnectReason),

View File

@ -7,8 +7,10 @@ use crate::{
},
lottery::LootSpec,
npc::{self, NPC_NAMES},
rtsim,
trade::SiteInformation,
};
use enum_map::EnumMap;
use serde::Deserialize;
use vek::*;
@ -254,7 +256,7 @@ impl EntityInfo {
self = self.with_name(name);
},
NameKind::Automatic => {
self = self.with_automatic_name();
self = self.with_automatic_name(None);
},
NameKind::Uninit => {},
}
@ -373,8 +375,8 @@ impl EntityInfo {
}
#[must_use]
pub fn with_agent_mark(mut self, agent_mark: agent::Mark) -> Self {
self.agent_mark = Some(agent_mark);
pub fn with_agent_mark(mut self, agent_mark: impl Into<Option<agent::Mark>>) -> Self {
self.agent_mark = agent_mark.into();
self
}
@ -406,7 +408,7 @@ impl EntityInfo {
}
#[must_use]
pub fn with_automatic_name(mut self) -> Self {
pub fn with_automatic_name(mut self, alias: Option<String>) -> Self {
let npc_names = NPC_NAMES.read();
let name = match &self.body {
Body::Humanoid(body) => Some(get_npc_name(&npc_names.humanoid, body.species)),
@ -428,14 +430,30 @@ impl EntityInfo {
Body::Arthropod(body) => Some(get_npc_name(&npc_names.arthropod, body.species)),
_ => None,
};
self.name = name.map(str::to_owned);
self.name = name.map(|name| {
if let Some(alias) = alias {
format!("{alias} ({name})")
} else {
name.to_string()
}
});
self
}
#[must_use]
pub fn with_alias(mut self, alias: String) -> Self {
self.name = Some(if let Some(name) = self.name {
format!("{alias} ({name})")
} else {
alias
});
self
}
/// map contains price+amount
#[must_use]
pub fn with_economy(mut self, e: &SiteInformation) -> Self {
self.trading_information = Some(e.clone());
pub fn with_economy<'a>(mut self, e: impl Into<Option<&'a SiteInformation>>) -> Self {
self.trading_information = e.into().cloned();
self
}
@ -444,11 +462,18 @@ impl EntityInfo {
self.no_flee = true;
self
}
#[must_use]
pub fn with_loadout(mut self, loadout: LoadoutBuilder) -> Self {
self.loadout = loadout;
self
}
}
#[derive(Default)]
pub struct ChunkSupplement {
pub entities: Vec<EntityInfo>,
pub rtsim_max_resources: EnumMap<rtsim::ChunkResource, usize>,
}
impl ChunkSupplement {

View File

@ -10,6 +10,7 @@ bitflags::bitflags! {
#[derive(Serialize, Deserialize)]
pub struct Flags: u8 {
const SNOW_COVERED = 0b00000001;
const IS_BUILDING = 0b00000010;
}
}

View File

@ -1,6 +1,5 @@
use crate::{
comp,
comp::{pet::is_mountable, Body},
link::{Is, Link, LinkHandle, Role},
terrain::TerrainGrid,
uid::{Uid, UidAllocator},
@ -29,6 +28,7 @@ pub struct Mounting {
pub rider: Uid,
}
#[derive(Debug)]
pub enum MountingError {
NoSuchEntity,
NotMountable,
@ -39,7 +39,6 @@ impl Link for Mounting {
Read<'a, UidAllocator>,
WriteStorage<'a, Is<Mount>>,
WriteStorage<'a, Is<Rider>>,
WriteStorage<'a, Body>,
);
type DeleteData<'a> = (
Read<'a, UidAllocator>,
@ -60,7 +59,7 @@ impl Link for Mounting {
fn create(
this: &LinkHandle<Self>,
(uid_allocator, mut is_mounts, mut is_riders, body): Self::CreateData<'_>,
(uid_allocator, mut is_mounts, mut is_riders): Self::CreateData<'_>,
) -> Result<(), Self::Error> {
let entity = |uid: Uid| uid_allocator.retrieve_entity_internal(uid.into());
@ -68,23 +67,15 @@ impl Link for Mounting {
// Forbid self-mounting
Err(MountingError::NotMountable)
} else if let Some((mount, rider)) = entity(this.mount).zip(entity(this.rider)) {
if let Some(mount_body) = body.get(mount) {
if is_mountable(mount_body, body.get(rider)) {
let can_mount_with =
|entity| is_mounts.get(entity).is_none() && is_riders.get(entity).is_none();
let can_mount_with =
|entity| is_mounts.get(entity).is_none() && is_riders.get(entity).is_none();
// Ensure that neither mount or rider are already part of a mounting
// relationship
if can_mount_with(mount) && can_mount_with(rider) {
let _ = is_mounts.insert(mount, this.make_role());
let _ = is_riders.insert(rider, this.make_role());
Ok(())
} else {
Err(MountingError::NotMountable)
}
} else {
Err(MountingError::NotMountable)
}
// Ensure that neither mount or rider are already part of a mounting
// relationship
if can_mount_with(mount) && can_mount_with(rider) {
let _ = is_mounts.insert(mount, this.make_role());
let _ = is_riders.insert(rider, this.make_role());
Ok(())
} else {
Err(MountingError::NotMountable)
}
@ -146,7 +137,7 @@ impl Link for Mounting {
let old_pos = pos.0.map(|e| e.floor() as i32);
pos.0 = safe_pos
.map(|p| p.0.map(|e| e.floor()))
.unwrap_or_else(|| terrain.find_space(old_pos).map(|e| e as f32))
.unwrap_or_else(|| terrain.find_ground(old_pos).map(|e| e as f32))
+ Vec3::new(0.5, 0.5, 0.0);
if let Some(force_update) = force_update.get_mut(rider) {
force_update.update();

View File

@ -19,7 +19,7 @@ use vek::*;
#[derive(Clone, Debug)]
pub struct Path<T> {
nodes: Vec<T>,
pub nodes: Vec<T>,
}
impl<T> Default for Path<T> {
@ -534,7 +534,7 @@ where
_ => return (None, false),
};
let heuristic = |pos: &Vec3<i32>| (pos.distance_squared(end) as f32).sqrt();
let heuristic = |pos: &Vec3<i32>, _: &Vec3<i32>| (pos.distance_squared(end) as f32).sqrt();
let neighbors = |pos: &Vec3<i32>| {
let pos = *pos;
const DIRS: [Vec3<i32>; 17] = [
@ -639,7 +639,7 @@ where
let satisfied = |pos: &Vec3<i32>| pos == &end;
let mut new_astar = match astar.take() {
None => Astar::new(25_000, start, heuristic, DefaultHashBuilder::default()),
None => Astar::new(25_000, start, DefaultHashBuilder::default()),
Some(astar) => astar,
};

View File

@ -3,41 +3,211 @@
// `Agent`). When possible, this should be moved to the `rtsim`
// module in `server`.
use crate::{character::CharacterId, comp::Content};
use rand::{seq::IteratorRandom, Rng};
use serde::{Deserialize, Serialize};
use specs::Component;
use std::collections::VecDeque;
use strum::{EnumIter, IntoEnumIterator};
use vek::*;
use crate::comp::dialogue::MoodState;
slotmap::new_key_type! { pub struct NpcId; }
pub type RtSimId = usize;
slotmap::new_key_type! { pub struct VehicleId; }
slotmap::new_key_type! { pub struct SiteId; }
slotmap::new_key_type! { pub struct FactionId; }
#[derive(Copy, Clone, Debug)]
pub struct RtSimEntity(pub RtSimId);
pub struct RtSimEntity(pub NpcId);
impl Component for RtSimEntity {
type Storage = specs::VecStorage<Self>;
}
#[derive(Clone, Debug)]
pub enum RtSimEvent {
AddMemory(Memory),
SetMood(Memory),
ForgetEnemy(String),
PrintMemories,
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub enum Actor {
Npc(NpcId),
Character(CharacterId),
}
#[derive(Clone, Debug)]
pub struct Memory {
pub item: MemoryItem,
pub time_to_forget: f64,
impl Actor {
pub fn npc(&self) -> Option<NpcId> {
match self {
Actor::Npc(id) => Some(*id),
Actor::Character(_) => None,
}
}
}
#[derive(Clone, Debug)]
pub enum MemoryItem {
// These are structs to allow more data beyond name to be stored
// such as clothing worn, weapon used, etc.
CharacterInteraction { name: String },
CharacterFight { name: String },
Mood { state: MoodState },
#[derive(Copy, Clone, Debug)]
pub struct RtSimVehicle(pub VehicleId);
impl Component for RtSimVehicle {
type Storage = specs::VecStorage<Self>;
}
#[derive(EnumIter, Clone, Copy)]
pub enum PersonalityTrait {
Open,
Adventurous,
Closed,
Conscientious,
Busybody,
Unconscientious,
Extroverted,
Introverted,
Agreeable,
Sociable,
Disagreeable,
Neurotic,
Seeker,
Worried,
SadLoner,
Stable,
}
#[derive(Serialize, Deserialize, Clone, Copy, Debug)]
pub struct Personality {
openness: u8,
conscientiousness: u8,
extraversion: u8,
agreeableness: u8,
neuroticism: u8,
}
fn distributed(min: u8, max: u8, rng: &mut impl Rng) -> u8 {
let l = max - min;
min + rng.gen_range(0..=l / 3)
+ rng.gen_range(0..=l / 3 + l % 3 % 2)
+ rng.gen_range(0..=l / 3 + l % 3 / 2)
}
impl Personality {
pub const HIGH_THRESHOLD: u8 = Self::MAX - Self::LOW_THRESHOLD;
pub const LITTLE_HIGH: u8 = Self::MID + (Self::MAX - Self::MIN) / 20;
pub const LITTLE_LOW: u8 = Self::MID - (Self::MAX - Self::MIN) / 20;
pub const LOW_THRESHOLD: u8 = (Self::MAX - Self::MIN) / 5 * 2 + Self::MIN;
const MAX: u8 = 255;
pub const MID: u8 = (Self::MAX - Self::MIN) / 2;
const MIN: u8 = 0;
fn distributed_value(rng: &mut impl Rng) -> u8 { distributed(Self::MIN, Self::MAX, rng) }
pub fn random(rng: &mut impl Rng) -> Self {
Self {
openness: Self::distributed_value(rng),
conscientiousness: Self::distributed_value(rng),
extraversion: Self::distributed_value(rng),
agreeableness: Self::distributed_value(rng),
neuroticism: Self::distributed_value(rng),
}
}
pub fn random_evil(rng: &mut impl Rng) -> Self {
Self {
openness: Self::distributed_value(rng),
extraversion: Self::distributed_value(rng),
neuroticism: Self::distributed_value(rng),
agreeableness: distributed(0, Self::LOW_THRESHOLD - 1, rng),
conscientiousness: distributed(0, Self::LOW_THRESHOLD - 1, rng),
}
}
pub fn random_good(rng: &mut impl Rng) -> Self {
Self {
openness: Self::distributed_value(rng),
extraversion: Self::distributed_value(rng),
neuroticism: Self::distributed_value(rng),
agreeableness: Self::distributed_value(rng),
conscientiousness: distributed(Self::LOW_THRESHOLD, Self::MAX, rng),
}
}
pub fn is(&self, trait_: PersonalityTrait) -> bool {
match trait_ {
PersonalityTrait::Open => self.openness > Personality::HIGH_THRESHOLD,
PersonalityTrait::Adventurous => {
self.openness > Personality::HIGH_THRESHOLD && self.neuroticism < Personality::MID
},
PersonalityTrait::Closed => self.openness < Personality::LOW_THRESHOLD,
PersonalityTrait::Conscientious => self.conscientiousness > Personality::HIGH_THRESHOLD,
PersonalityTrait::Busybody => self.agreeableness < Personality::LOW_THRESHOLD,
PersonalityTrait::Unconscientious => {
self.conscientiousness < Personality::LOW_THRESHOLD
},
PersonalityTrait::Extroverted => self.extraversion > Personality::HIGH_THRESHOLD,
PersonalityTrait::Introverted => self.extraversion < Personality::LOW_THRESHOLD,
PersonalityTrait::Agreeable => self.agreeableness > Personality::HIGH_THRESHOLD,
PersonalityTrait::Sociable => {
self.agreeableness > Personality::HIGH_THRESHOLD
&& self.extraversion > Personality::MID
},
PersonalityTrait::Disagreeable => self.agreeableness < Personality::LOW_THRESHOLD,
PersonalityTrait::Neurotic => self.neuroticism > Personality::HIGH_THRESHOLD,
PersonalityTrait::Seeker => {
self.neuroticism > Personality::HIGH_THRESHOLD
&& self.openness > Personality::LITTLE_HIGH
},
PersonalityTrait::Worried => {
self.neuroticism > Personality::HIGH_THRESHOLD
&& self.agreeableness > Personality::LITTLE_HIGH
},
PersonalityTrait::SadLoner => {
self.neuroticism > Personality::HIGH_THRESHOLD
&& self.extraversion < Personality::LITTLE_LOW
},
PersonalityTrait::Stable => self.neuroticism < Personality::LOW_THRESHOLD,
}
}
pub fn chat_trait(&self, rng: &mut impl Rng) -> Option<PersonalityTrait> {
PersonalityTrait::iter().filter(|t| self.is(*t)).choose(rng)
}
pub fn will_ambush(&self) -> bool {
self.agreeableness < Self::LOW_THRESHOLD && self.conscientiousness < Self::LOW_THRESHOLD
}
pub fn get_generic_comment(&self, rng: &mut impl Rng) -> Content {
let i18n_key = if let Some(extreme_trait) = self.chat_trait(rng) {
match extreme_trait {
PersonalityTrait::Open => "npc-speech-villager_open",
PersonalityTrait::Adventurous => "npc-speech-villager_adventurous",
PersonalityTrait::Closed => "npc-speech-villager_closed",
PersonalityTrait::Conscientious => "npc-speech-villager_conscientious",
PersonalityTrait::Busybody => "npc-speech-villager_busybody",
PersonalityTrait::Unconscientious => "npc-speech-villager_unconscientious",
PersonalityTrait::Extroverted => "npc-speech-villager_extroverted",
PersonalityTrait::Introverted => "npc-speech-villager_introverted",
PersonalityTrait::Agreeable => "npc-speech-villager_agreeable",
PersonalityTrait::Sociable => "npc-speech-villager_sociable",
PersonalityTrait::Disagreeable => "npc-speech-villager_disagreeable",
PersonalityTrait::Neurotic => "npc-speech-villager_neurotic",
PersonalityTrait::Seeker => "npc-speech-villager_seeker",
PersonalityTrait::SadLoner => "npc-speech-villager_sad_loner",
PersonalityTrait::Worried => "npc-speech-villager_worried",
PersonalityTrait::Stable => "npc-speech-villager_stable",
}
} else {
"npc-speech-villager"
};
Content::localized(i18n_key)
}
}
impl Default for Personality {
fn default() -> Self {
Self {
openness: Personality::MID,
conscientiousness: Personality::MID,
extraversion: Personality::MID,
agreeableness: Personality::MID,
neuroticism: Personality::MID,
}
}
}
/// This type is the map route through which the rtsim (real-time simulation)
@ -49,36 +219,112 @@ pub enum MemoryItem {
/// into the game as a physical entity or not). Agent code should attempt to act
/// upon its instructions where reasonable although deviations for various
/// reasons (obstacle avoidance, counter-attacking, etc.) are expected.
#[derive(Clone, Debug)]
#[derive(Clone, Debug, Default)]
pub struct RtSimController {
/// When this field is `Some(..)`, the agent should attempt to make progress
/// toward the given location, accounting for obstacles and other
/// high-priority situations like being attacked.
pub travel_to: Option<(Vec3<f32>, String)>,
/// Proportion of full speed to move
pub speed_factor: f32,
/// Events
pub events: Vec<RtSimEvent>,
}
impl Default for RtSimController {
fn default() -> Self {
Self {
travel_to: None,
speed_factor: 1.0,
events: Vec::new(),
}
}
pub activity: Option<NpcActivity>,
pub actions: VecDeque<NpcAction>,
pub personality: Personality,
pub heading_to: Option<String>,
}
impl RtSimController {
pub fn reset(&mut self) { *self = Self::default(); }
pub fn with_destination(pos: Vec3<f32>) -> Self {
Self {
travel_to: Some((pos, format!("{:0.1?}", pos))),
speed_factor: 0.25,
events: Vec::new(),
activity: Some(NpcActivity::Goto(pos, 0.5)),
..Default::default()
}
}
}
#[derive(Clone, Copy, Debug)]
pub enum NpcActivity {
/// (travel_to, speed_factor)
Goto(Vec3<f32>, f32),
Gather(&'static [ChunkResource]),
// TODO: Generalise to other entities? What kinds of animals?
HuntAnimals,
Dance,
}
/// Represents event-like actions that rtsim NPCs can perform to interact with
/// the world
#[derive(Clone, Debug)]
pub enum NpcAction {
/// Speak the given message, with an optional target for that speech.
// TODO: Use some sort of structured, language-independent value that frontends can translate
// instead
Say(Option<Actor>, Content),
/// Attack the given target
Attack(Actor),
}
// Note: the `serde(name = "...")` is to minimise the length of field
// identifiers for the sake of rtsim persistence
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, enum_map::Enum)]
pub enum ChunkResource {
#[serde(rename = "0")]
Grass,
#[serde(rename = "1")]
Flower,
#[serde(rename = "2")]
Fruit,
#[serde(rename = "3")]
Vegetable,
#[serde(rename = "4")]
Mushroom,
#[serde(rename = "5")]
Loot, // Chests, boxes, potions, etc.
#[serde(rename = "6")]
Plant, // Flax, cotton, wheat, corn, etc.
#[serde(rename = "7")]
Stone,
#[serde(rename = "8")]
Wood, // Twigs, logs, bamboo, etc.
#[serde(rename = "9")]
Gem, // Amethyst, diamond, etc.
#[serde(rename = "a")]
Ore, // Iron, copper, etc.
}
// Note: the `serde(name = "...")` is to minimise the length of field
// identifiers for the sake of rtsim persistence
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum Profession {
#[serde(rename = "0")]
Farmer,
#[serde(rename = "1")]
Hunter,
#[serde(rename = "2")]
Merchant,
#[serde(rename = "3")]
Guard,
#[serde(rename = "4")]
Adventurer(u32),
#[serde(rename = "5")]
Blacksmith,
#[serde(rename = "6")]
Chef,
#[serde(rename = "7")]
Alchemist,
#[serde(rename = "8")]
Pirate,
#[serde(rename = "9")]
Cultist,
#[serde(rename = "10")]
Herbalist,
#[serde(rename = "11")]
Captain,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct WorldSettings {
pub start_time: f64,
}
impl Default for WorldSettings {
fn default() -> Self {
Self {
start_time: 9.0 * 3600.0, // 9am
}
}
}

View File

@ -91,7 +91,10 @@ impl CharacterBehavior for Data {
// Shoots all projectiles simultaneously
for i in 0..self.static_data.num_projectiles {
// Gets offsets
let body_offsets = data.body.projectile_offsets(update.ori.look_vec());
let body_offsets = data.body.projectile_offsets(
update.ori.look_vec(),
data.scale.map_or(1.0, |s| s.0),
);
let pos = Pos(data.pos.0 + body_offsets);
// Adds a slight spread to the projectiles. First projectile has no spread,
// and spread increases linearly with number of projectiles created.

View File

@ -6,7 +6,7 @@ use crate::{
skillset::skills,
Behavior, BehaviorCapability, CharacterState, Projectile, StateUpdate,
},
event::{LocalEvent, ServerEvent},
event::{LocalEvent, NpcBuilder, ServerEvent},
outcome::Outcome,
skillset_builder::{self, SkillSetBuilder},
states::{
@ -149,7 +149,7 @@ impl CharacterBehavior for Data {
let collision_vector = Vec3::new(
data.pos.0.x + (summon_frac * 2.0 * PI).sin() * obstacle_xy,
data.pos.0.y + (summon_frac * 2.0 * PI).cos() * obstacle_xy,
data.pos.0.z + data.body.eye_height(),
data.pos.0.z + data.body.eye_height(data.scale.map_or(1.0, |s| s.0)),
);
// Check for collision in z up to 50 blocks
@ -174,27 +174,22 @@ impl CharacterBehavior for Data {
// Send server event to create npc
output_events.emit_server(ServerEvent::CreateNpc {
pos: comp::Pos(collision_vector - Vec3::unit_z() * obstacle_z),
stats,
skill_set,
health,
poise: comp::Poise::new(body),
inventory: comp::Inventory::with_loadout(loadout, body),
body,
agent: Some(
comp::Agent::from_body(&body)
.with_behavior(Behavior::from(BehaviorCapability::SPEAK))
.with_no_flee_if(true),
),
alignment: comp::Alignment::Owned(*data.uid),
scale: self
.static_data
.summon_info
.scale
.unwrap_or(comp::Scale(1.0)),
anchor: None,
loot: crate::lottery::LootSpec::Nothing,
rtsim_entity: None,
projectile,
npc: NpcBuilder::new(stats, body, comp::Alignment::Owned(*data.uid))
.with_skill_set(skill_set)
.with_health(health)
.with_inventory(comp::Inventory::with_loadout(loadout, body))
.with_agent(
comp::Agent::from_body(&body)
.with_behavior(Behavior::from(BehaviorCapability::SPEAK))
.with_no_flee_if(true),
)
.with_scale(
self.static_data
.summon_info
.scale
.unwrap_or(comp::Scale(1.0)),
)
.with_projectile(projectile),
});
// Send local event used for frontend shenanigans

View File

@ -3,10 +3,10 @@ use crate::{
self,
character_state::OutputEvents,
item::{tool::AbilityMap, MaterialStatManifest},
ActiveAbilities, Beam, Body, CharacterState, Combo, ControlAction, Controller,
ControllerInputs, Density, Energy, Health, InputAttr, InputKind, Inventory,
InventoryAction, Mass, Melee, Ori, PhysicsState, Pos, SkillSet, Stance, StateUpdate, Stats,
Vel,
ActiveAbilities, Beam, Body, CharacterActivity, CharacterState, Combo, ControlAction,
Controller, ControllerInputs, Density, Energy, Health, InputAttr, InputKind, Inventory,
InventoryAction, Mass, Melee, Ori, PhysicsState, Pos, Scale, SkillSet, Stance, StateUpdate,
Stats, Vel,
},
link::Is,
mounting::Rider,
@ -120,9 +120,11 @@ pub struct JoinData<'a> {
pub entity: Entity,
pub uid: &'a Uid,
pub character: &'a CharacterState,
pub character_activity: &'a CharacterActivity,
pub pos: &'a Pos,
pub vel: &'a Vel,
pub ori: &'a Ori,
pub scale: Option<&'a Scale>,
pub mass: &'a Mass,
pub density: &'a Density,
pub dt: &'a DeltaTime,
@ -152,9 +154,11 @@ pub struct JoinStruct<'a> {
pub entity: Entity,
pub uid: &'a Uid,
pub char_state: FlaggedAccessMut<'a, &'a mut CharacterState, CharacterState>,
pub character_activity: FlaggedAccessMut<'a, &'a mut CharacterActivity, CharacterActivity>,
pub pos: &'a mut Pos,
pub vel: &'a mut Vel,
pub ori: &'a mut Ori,
pub scale: Option<&'a Scale>,
pub mass: &'a Mass,
pub density: FlaggedAccessMut<'a, &'a mut Density, Density>,
pub energy: FlaggedAccessMut<'a, &'a mut Energy, Energy>,
@ -188,9 +192,11 @@ impl<'a> JoinData<'a> {
entity: j.entity,
uid: j.uid,
character: &j.char_state,
character_activity: &j.character_activity,
pos: j.pos,
vel: j.vel,
ori: j.ori,
scale: j.scale,
mass: j.mass,
density: &j.density,
energy: &j.energy,

View File

@ -114,7 +114,9 @@ impl CharacterBehavior for Data {
get_crit_data(data, self.static_data.ability_info);
let tool_stats = get_tool_stats(data, self.static_data.ability_info);
// Gets offsets
let body_offsets = data.body.projectile_offsets(update.ori.look_vec());
let body_offsets = data
.body
.projectile_offsets(update.ori.look_vec(), data.scale.map_or(1.0, |s| s.0));
let pos = Pos(data.pos.0 + body_offsets);
let projectile = arrow.create_projectile(
Some(*data.uid),

View File

@ -76,7 +76,8 @@ impl CharacterBehavior for Data {
// They've climbed atop something, give them a boost
output_events.emit_local(LocalEvent::Jump(
data.entity,
CLIMB_BOOST_JUMP_FACTOR * impulse / data.mass.0,
CLIMB_BOOST_JUMP_FACTOR * impulse / data.mass.0
* data.scale.map_or(1.0, |s| s.0.powf(13.0).powf(0.25)),
));
};
update.character = CharacterState::Idle(idle::Data::default());
@ -122,10 +123,14 @@ impl CharacterBehavior for Data {
// Apply Vertical Climbing Movement
match climb {
Climb::Down => {
update.vel.0.z += data.dt.0 * (GRAVITY - self.static_data.movement_speed.powi(2))
update.vel.0.z += data.dt.0
* (GRAVITY
- self.static_data.movement_speed.powi(2) * data.scale.map_or(1.0, |s| s.0))
},
Climb::Up => {
update.vel.0.z += data.dt.0 * (GRAVITY + self.static_data.movement_speed.powi(2))
update.vel.0.z += data.dt.0
* (GRAVITY
+ self.static_data.movement_speed.powi(2) * data.scale.map_or(1.0, |s| s.0))
},
Climb::Hold => update.vel.0.z += data.dt.0 * GRAVITY,
}

View File

@ -95,7 +95,9 @@ impl CharacterBehavior for Data {
get_crit_data(data, self.static_data.ability_info);
let tool_stats = get_tool_stats(data, self.static_data.ability_info);
// Gets offsets
let body_offsets = data.body.projectile_offsets(update.ori.look_vec());
let body_offsets = data
.body
.projectile_offsets(update.ori.look_vec(), data.scale.map_or(1.0, |s| s.0));
let pos = Pos(data.pos.0 + body_offsets);
let projectile = self.static_data.projectile.create_projectile(
Some(*data.uid),

View File

@ -220,7 +220,7 @@ impl Body {
_ => 2.0,
},
Body::Ship(ship) if ship.has_water_thrust() => 0.1,
Body::Ship(_) => 0.035,
Body::Ship(_) => 0.12,
Body::Arthropod(_) => 3.5,
}
}
@ -298,10 +298,10 @@ impl Body {
/// Returns the position where a projectile should be fired relative to this
/// body
pub fn projectile_offsets(&self, ori: Vec3<f32>) -> Vec3<f32> {
pub fn projectile_offsets(&self, ori: Vec3<f32>, scale: f32) -> Vec3<f32> {
let body_offsets_z = match self {
Body::Golem(_) => self.height() * 0.4,
_ => self.eye_height(),
_ => self.eye_height(scale),
};
let dim = self.dimensions();
@ -385,7 +385,11 @@ fn basic_move(data: &JoinData<'_>, update: &mut StateUpdate, efficiency: f32) {
let accel = if let Some(block) = data.physics.on_ground {
// FRIC_GROUND temporarily used to normalize things around expected values
data.body.base_accel() * block.get_traction() * block.get_friction() / FRIC_GROUND
data.body.base_accel()
* data.scale.map_or(1.0, |s| s.0.sqrt())
* block.get_traction()
* block.get_friction()
/ FRIC_GROUND
} else {
data.body.air_accel()
} * efficiency;
@ -434,8 +438,11 @@ pub fn handle_forced_movement(
// FRIC_GROUND temporarily used to normalize things around expected values
data.body.base_accel() * block.get_traction() * block.get_friction() / FRIC_GROUND
}) {
update.vel.0 +=
Vec2::broadcast(data.dt.0) * accel * Vec2::from(*data.ori) * strength;
update.vel.0 += Vec2::broadcast(data.dt.0)
* accel
* data.scale.map_or(1.0, |s| s.0.sqrt())
* Vec2::from(*data.ori)
* strength;
}
},
ForcedMovement::Reverse(strength) => {
@ -444,8 +451,11 @@ pub fn handle_forced_movement(
// FRIC_GROUND temporarily used to normalize things around expected values
data.body.base_accel() * block.get_traction() * block.get_friction() / FRIC_GROUND
}) {
update.vel.0 +=
Vec2::broadcast(data.dt.0) * accel * -Vec2::from(*data.ori) * strength;
update.vel.0 += Vec2::broadcast(data.dt.0)
* accel
* data.scale.map_or(1.0, |s| s.0.sqrt())
* -Vec2::from(*data.ori)
* strength;
}
},
ForcedMovement::Sideways(strength) => {
@ -467,7 +477,11 @@ pub fn handle_forced_movement(
}
};
update.vel.0 += Vec2::broadcast(data.dt.0) * accel * direction * strength;
update.vel.0 += Vec2::broadcast(data.dt.0)
* accel
* data.scale.map_or(1.0, |s| s.0.sqrt())
* direction
* strength;
}
},
ForcedMovement::DirectedReverse(strength) => {
@ -516,6 +530,7 @@ pub fn handle_forced_movement(
dir.y,
vertical,
)
* data.scale.map_or(1.0, |s| s.0.sqrt())
// Multiply decreasing amount linearly over time (with average of 1)
* 2.0 * progress
// Apply direction
@ -529,7 +544,9 @@ pub fn handle_forced_movement(
},
ForcedMovement::Hover { move_input } => {
update.vel.0 = Vec3::new(data.vel.0.x, data.vel.0.y, 0.0)
+ move_input * data.inputs.move_dir.try_normalized().unwrap_or_default();
+ move_input
* data.scale.map_or(1.0, |s| s.0.sqrt())
* data.inputs.move_dir.try_normalized().unwrap_or_default();
},
}
}
@ -569,7 +586,7 @@ pub fn handle_orientation(
.map_or_else(|| to_horizontal_fast(data.ori), |dir| dir.into())
};
// unit is multiples of 180°
let half_turns_per_tick = data.body.base_ori_rate()
let half_turns_per_tick = data.body.base_ori_rate() / data.scale.map_or(1.0, |s| s.0.sqrt())
* efficiency
* if data.physics.on_ground.is_some() {
1.0
@ -594,6 +611,9 @@ pub fn handle_orientation(
.ori
.slerped_towards(target_ori, target_fraction.min(1.0))
};
// Look at things
update.character_activity.look_dir = Some(data.controller.inputs.look_dir);
}
/// Updates components to move player as if theyre swimming
@ -605,7 +625,7 @@ fn swim_move(
) -> bool {
let efficiency = efficiency * data.stats.move_speed_modifier * data.stats.friction_modifier;
if let Some(force) = data.body.swim_thrust() {
let force = efficiency * force;
let force = efficiency * force * data.scale.map_or(1.0, |s| s.0);
let mut water_accel = force / data.mass.0;
if let Ok(level) = data.skill_set.skill_level(Skill::Swim(SwimSkill::Speed)) {
@ -912,13 +932,13 @@ pub fn handle_manipulate_loadout(
let iters =
(3.0 * (sprite_pos_f32 - data.pos.0).map(|x| x.abs()).sum()) as usize;
// Heuristic compares manhattan distance of start and end pos
let heuristic =
move |pos: &Vec3<i32>| (sprite_pos - pos).map(|x| x.abs()).sum() as f32;
let heuristic = move |pos: &Vec3<i32>, _: &Vec3<i32>| {
(sprite_pos - pos).map(|x| x.abs()).sum() as f32
};
let mut astar = Astar::new(
iters,
data.pos.0.map(|x| x.floor() as i32),
heuristic,
BuildHasherDefault::<FxHasher64>::default(),
);
@ -1068,7 +1088,9 @@ pub fn handle_jump(
.map(|impulse| {
output_events.emit_local(LocalEvent::Jump(
data.entity,
strength * impulse / data.mass.0 * data.stats.move_speed_modifier,
strength * impulse / data.mass.0
* data.scale.map_or(1.0, |s| s.0.powf(13.0).powf(0.25))
* data.stats.move_speed_modifier,
));
})
.is_some()

View File

@ -3,7 +3,7 @@ use crate::{
comp::{fluid_dynamics::LiquidKind, tool::ToolKind},
consts::FRIC_GROUND,
lottery::LootSpec,
make_case_elim,
make_case_elim, rtsim,
};
use num_derive::FromPrimitive;
use num_traits::FromPrimitive;
@ -195,6 +195,98 @@ impl Block {
}
}
/// Returns the rtsim resource, if any, that this block corresponds to. If
/// you want the scarcity of a block to change with rtsim's resource
/// depletion tracking, you can do so by editing this function.
#[inline]
pub fn get_rtsim_resource(&self) -> Option<rtsim::ChunkResource> {
match self.get_sprite()? {
SpriteKind::Stones => Some(rtsim::ChunkResource::Stone),
SpriteKind::Twigs
| SpriteKind::Wood
| SpriteKind::Bamboo
| SpriteKind::Hardwood
| SpriteKind::Ironwood
| SpriteKind::Frostwood
| SpriteKind::Eldwood => Some(rtsim::ChunkResource::Wood),
SpriteKind::Amethyst
| SpriteKind::Ruby
| SpriteKind::Sapphire
| SpriteKind::Emerald
| SpriteKind::Topaz
| SpriteKind::Diamond
| SpriteKind::AmethystSmall
| SpriteKind::TopazSmall
| SpriteKind::DiamondSmall
| SpriteKind::RubySmall
| SpriteKind::EmeraldSmall
| SpriteKind::SapphireSmall
| SpriteKind::CrystalHigh
| SpriteKind::CrystalLow => Some(rtsim::ChunkResource::Gem),
SpriteKind::Bloodstone
| SpriteKind::Coal
| SpriteKind::Cobalt
| SpriteKind::Copper
| SpriteKind::Iron
| SpriteKind::Tin
| SpriteKind::Silver
| SpriteKind::Gold => Some(rtsim::ChunkResource::Ore),
SpriteKind::LongGrass
| SpriteKind::MediumGrass
| SpriteKind::ShortGrass
| SpriteKind::LargeGrass
| SpriteKind::GrassSnow
| SpriteKind::GrassBlue
| SpriteKind::SavannaGrass
| SpriteKind::TallSavannaGrass
| SpriteKind::RedSavannaGrass
| SpriteKind::JungleRedGrass
| SpriteKind::Fern => Some(rtsim::ChunkResource::Grass),
SpriteKind::BlueFlower
| SpriteKind::PinkFlower
| SpriteKind::PurpleFlower
| SpriteKind::RedFlower
| SpriteKind::WhiteFlower
| SpriteKind::YellowFlower
| SpriteKind::Sunflower
| SpriteKind::Moonbell
| SpriteKind::Pyrebloom => Some(rtsim::ChunkResource::Flower),
SpriteKind::Reed
| SpriteKind::Flax
| SpriteKind::WildFlax
| SpriteKind::Cotton
| SpriteKind::Corn
| SpriteKind::WheatYellow
| SpriteKind::WheatGreen => Some(rtsim::ChunkResource::Plant),
SpriteKind::Apple
| SpriteKind::Pumpkin
| SpriteKind::Beehive // TODO: Not a fruit, but kind of acts like one
| SpriteKind::Coconut => Some(rtsim::ChunkResource::Fruit),
SpriteKind::Cabbage
| SpriteKind::Carrot
| SpriteKind::Tomato
| SpriteKind::Radish
| SpriteKind::Turnip => Some(rtsim::ChunkResource::Vegetable),
SpriteKind::Mushroom
| SpriteKind::CaveMushroom
| SpriteKind::CeilingMushroom => Some(rtsim::ChunkResource::Mushroom),
SpriteKind::Chest
| SpriteKind::ChestBuried
| SpriteKind::PotionMinor
| SpriteKind::DungeonChest0
| SpriteKind::DungeonChest1
| SpriteKind::DungeonChest2
| SpriteKind::DungeonChest3
| SpriteKind::DungeonChest4
| SpriteKind::DungeonChest5
| SpriteKind::CoralChest
| SpriteKind::Crate => Some(rtsim::ChunkResource::Loot),
_ => None,
}
}
#[inline]
pub fn get_glow(&self) -> Option<u8> {
match self.kind() {

View File

@ -84,7 +84,9 @@ pub trait CoordinateConversions {
impl CoordinateConversions for Vec2<i32> {
#[inline]
fn wpos_to_cpos(&self) -> Self { self.map2(TerrainChunkSize::RECT_SIZE, |e, sz| e / sz as i32) }
fn wpos_to_cpos(&self) -> Self {
self.map2(TerrainChunkSize::RECT_SIZE, |e, sz| e.div_euclid(sz as i32))
}
#[inline]
fn cpos_to_wpos(&self) -> Self { self.map2(TerrainChunkSize::RECT_SIZE, |e, sz| e * sz as i32) }
@ -222,11 +224,13 @@ impl TerrainChunkMeta {
pub type TerrainChunk = chonk::Chonk<Block, TerrainChunkSize, TerrainChunkMeta>;
pub type TerrainGrid = VolGrid2d<TerrainChunk>;
const TERRAIN_GRID_SEARCH_DIST: i32 = 63;
impl TerrainGrid {
/// Find a location suitable for spawning an entity near the given
/// position (but in the same chunk).
pub fn find_space(&self, pos: Vec3<i32>) -> Vec3<i32> {
self.try_find_space(pos).unwrap_or(pos)
pub fn find_ground(&self, pos: Vec3<i32>) -> Vec3<i32> {
self.try_find_ground(pos).unwrap_or(pos)
}
pub fn is_space(&self, pos: Vec3<i32>) -> bool {
@ -237,8 +241,14 @@ impl TerrainGrid {
}
pub fn try_find_space(&self, pos: Vec3<i32>) -> Option<Vec3<i32>> {
const SEARCH_DIST: i32 = 63;
(0..SEARCH_DIST * 2 + 1)
(0..TERRAIN_GRID_SEARCH_DIST * 2 + 1)
.map(|i| if i % 2 == 0 { i } else { -i } / 2)
.map(|z_diff| pos + Vec3::unit_z() * z_diff)
.find(|pos| self.is_space(*pos))
}
pub fn try_find_ground(&self, pos: Vec3<i32>) -> Option<Vec3<i32>> {
(0..TERRAIN_GRID_SEARCH_DIST * 2 + 1)
.map(|i| if i % 2 == 0 { i } else { -i } / 2)
.map(|z_diff| pos + Vec3::unit_z() * z_diff)
.find(|pos| {

View File

@ -381,31 +381,29 @@ impl SitePrices {
inventories: &[Option<ReducedInventory>; 2],
who: usize,
reduce: bool,
) -> f32 {
) -> Option<f32> {
offers[who]
.iter()
.map(|(slot, amount)| {
inventories[who]
.as_ref()
.and_then(|ri| {
ri.inventory.get(slot).map(|item| {
if let Some(vec) = TradePricing::get_materials(&item.name.as_ref()) {
vec.iter()
.map(|(amount2, material)| {
self.values.get(material).copied().unwrap_or_default()
* *amount2
* (if reduce { material.trade_margin() } else { 1.0 })
})
.sum::<f32>()
* (*amount as f32)
} else {
0.0
}
})
.map(|ri| {
let item = ri.inventory.get(slot)?;
let vec = TradePricing::get_materials(&item.name.as_ref())?;
Some(
vec.iter()
.map(|(amount2, material)| {
self.values.get(material).copied().unwrap_or_default()
* *amount2
* (if reduce { material.trade_margin() } else { 1.0 })
})
.sum::<f32>()
* (*amount as f32),
)
})
.unwrap_or_default()
.unwrap_or(Some(0.0))
})
.sum()
.try_fold(0.0, |a, p| Some(a + p?))
}
}

View File

@ -6,4 +6,4 @@ mod build_areas;
mod state;
// TODO: breakup state module and remove glob
pub use build_areas::{BuildAreaError, BuildAreas};
pub use state::{BlockChange, State, TerrainChanges};
pub use state::{BlockChange, BlockDiff, State, TerrainChanges};

View File

@ -113,6 +113,13 @@ impl TerrainChanges {
}
}
#[derive(Clone)]
pub struct BlockDiff {
pub wpos: Vec3<i32>,
pub old: Block,
pub new: Block,
}
/// A type used to represent game state stored on both the client and the
/// server. This includes things like entity components, terrain data, and
/// global states like weather, time of day, etc.
@ -199,6 +206,7 @@ impl State {
ecs.register::<comp::Sticky>();
ecs.register::<comp::Immovable>();
ecs.register::<comp::CharacterState>();
ecs.register::<comp::CharacterActivity>();
ecs.register::<comp::Object>();
ecs.register::<comp::Group>();
ecs.register::<comp::Shockwave>();
@ -524,7 +532,9 @@ impl State {
}
// Apply terrain changes
pub fn apply_terrain_changes(&self) { self.apply_terrain_changes_internal(false); }
pub fn apply_terrain_changes(&self, block_update: impl FnMut(&specs::World, Vec<BlockDiff>)) {
self.apply_terrain_changes_internal(false, block_update);
}
/// `during_tick` is true if and only if this is called from within
/// [State::tick].
@ -534,7 +544,11 @@ impl State {
/// from within both the client and the server ticks, right after
/// handling terrain messages; currently, client sets it to true and
/// server to false.
fn apply_terrain_changes_internal(&self, during_tick: bool) {
fn apply_terrain_changes_internal(
&self,
during_tick: bool,
mut block_update: impl FnMut(&specs::World, Vec<BlockDiff>),
) {
span!(
_guard,
"apply_terrain_changes",
@ -575,17 +589,30 @@ impl State {
}
// Apply block modifications
// Only include in `TerrainChanges` if successful
modified_blocks.retain(|pos, block| {
let res = terrain.set(*pos, *block);
if let (&Ok(old_block), true) = (&res, during_tick) {
let mut updated_blocks = Vec::with_capacity(modified_blocks.len());
modified_blocks.retain(|wpos, new| {
let res = terrain.map(*wpos, |old| {
updated_blocks.push(BlockDiff {
wpos: *wpos,
old,
new: *new,
});
*new
});
if let (&Ok(old), true) = (&res, during_tick) {
// NOTE: If the changes are applied during the tick, we push the *old* value as
// the modified block (since it otherwise can't be recovered after the tick).
// Otherwise, the changes will be applied after the tick, so we push the *new*
// value.
*block = old_block;
*new = old;
}
res.is_ok()
});
if !updated_blocks.is_empty() {
block_update(&self.ecs, updated_blocks);
}
self.ecs.write_resource::<TerrainChanges>().modified_blocks = modified_blocks;
}
@ -597,6 +624,7 @@ impl State {
update_terrain_and_regions: bool,
mut metrics: Option<&mut StateTickMetrics>,
server_constants: &ServerConstants,
block_update: impl FnMut(&specs::World, Vec<BlockDiff>),
) {
span!(_guard, "tick", "State::tick");
@ -643,7 +671,7 @@ impl State {
drop(guard);
if update_terrain_and_regions {
self.apply_terrain_changes_internal(true);
self.apply_terrain_changes_internal(true, block_update);
}
// Process local events

View File

@ -8,9 +8,9 @@ use common::{
self,
character_state::OutputEvents,
inventory::item::{tool::AbilityMap, MaterialStatManifest},
ActiveAbilities, Beam, Body, CharacterState, Combo, Controller, Density, Energy, Health,
Inventory, InventoryManip, Mass, Melee, Ori, PhysicsState, Poise, Pos, SkillSet, Stance,
StateUpdate, Stats, Vel,
ActiveAbilities, Beam, Body, CharacterActivity, CharacterState, Combo, Controller, Density,
Energy, Health, Inventory, InventoryManip, Mass, Melee, Ori, PhysicsState, Poise, Pos,
Scale, SkillSet, Stance, StateUpdate, Stats, Vel,
},
event::{EventBus, LocalEvent, ServerEvent},
link::Is,
@ -37,6 +37,7 @@ pub struct ReadData<'a> {
healths: ReadStorage<'a, Health>,
bodies: ReadStorage<'a, Body>,
masses: ReadStorage<'a, Mass>,
scales: ReadStorage<'a, Scale>,
physics_states: ReadStorage<'a, PhysicsState>,
melee_attacks: ReadStorage<'a, Melee>,
beams: ReadStorage<'a, Beam>,
@ -64,6 +65,7 @@ impl<'a> System<'a> for Sys {
type SystemData = (
ReadData<'a>,
WriteStorage<'a, CharacterState>,
WriteStorage<'a, CharacterActivity>,
WriteStorage<'a, Pos>,
WriteStorage<'a, Vel>,
WriteStorage<'a, Ori>,
@ -83,6 +85,7 @@ impl<'a> System<'a> for Sys {
(
read_data,
mut character_states,
mut character_activities,
mut positions,
mut velocities,
mut orientations,
@ -105,6 +108,7 @@ impl<'a> System<'a> for Sys {
entity,
uid,
mut char_state,
character_activity,
pos,
vel,
ori,
@ -115,13 +119,13 @@ impl<'a> System<'a> for Sys {
controller,
health,
body,
physics,
(stat, skill_set, active_abilities, is_rider),
(physics, scale, stat, skill_set, active_abilities, is_rider),
combo,
) in (
&read_data.entities,
&read_data.uids,
&mut character_states,
&mut character_activities,
&mut positions,
&mut velocities,
&mut orientations,
@ -132,8 +136,9 @@ impl<'a> System<'a> for Sys {
&mut controllers,
read_data.healths.maybe(),
&read_data.bodies,
&read_data.physics_states,
(
&read_data.physics_states,
read_data.scales.maybe(),
&read_data.stats,
&read_data.skill_sets,
read_data.active_abilities.maybe(),
@ -180,9 +185,11 @@ impl<'a> System<'a> for Sys {
entity,
uid,
char_state,
character_activity,
pos,
vel,
ori,
scale,
mass,
density,
energy,
@ -258,6 +265,9 @@ impl Sys {
if *join.char_state != state_update.character {
*join.char_state = state_update.character
}
if *join.character_activity != state_update.character_activity {
*join.character_activity = state_update.character_activity
}
if *join.density != state_update.density {
*join.density = state_update.density
}

View File

@ -2,7 +2,7 @@ use common::{
comp::{
ability::Stance,
agent::{Sound, SoundKind},
Body, BuffChange, ControlEvent, Controller, Pos,
Body, BuffChange, ControlEvent, Controller, Pos, Scale,
},
event::{EventBus, ServerEvent},
uid::UidAllocator,
@ -22,6 +22,7 @@ pub struct ReadData<'a> {
server_bus: Read<'a, EventBus<ServerEvent>>,
positions: ReadStorage<'a, Pos>,
bodies: ReadStorage<'a, Body>,
scales: ReadStorage<'a, Scale>,
}
#[derive(Default)]
@ -91,13 +92,15 @@ impl<'a> System<'a> for Sys {
},
ControlEvent::Respawn => server_emitter.emit(ServerEvent::Respawn(entity)),
ControlEvent::Utterance(kind) => {
if let (Some(pos), Some(body)) = (
if let (Some(pos), Some(body), scale) = (
read_data.positions.get(entity),
read_data.bodies.get(entity),
read_data.scales.get(entity),
) {
let sound = Sound::new(
SoundKind::Utterance(kind, *body),
pos.0 + Vec3::unit_z() * body.eye_height(),
pos.0
+ Vec3::unit_z() * body.eye_height(scale.map_or(1.0, |s| s.0)),
8.0, // TODO: Come up with a better way of determining this
1.0,
);

View File

@ -1,4 +1,4 @@
#![feature(btree_drain_filter)]
#![feature(drain_filter)]
#![allow(clippy::option_map_unit_fn)]
mod aura;

View File

@ -69,13 +69,14 @@ impl<'a> System<'a> for Sys {
let mut rng = rand::thread_rng();
// Attacks
for (attacker, uid, pos, ori, melee_attack, body) in (
for (attacker, uid, pos, ori, melee_attack, body, scale) in (
&read_data.entities,
&read_data.uids,
&read_data.positions,
&read_data.orientations,
&mut melee_attacks,
&read_data.bodies,
read_data.scales.maybe(),
)
.join()
{
@ -88,7 +89,7 @@ impl<'a> System<'a> for Sys {
melee_attack.applied = true;
// Scales
let eye_pos = pos.0 + Vec3::unit_z() * body.eye_height();
let eye_pos = pos.0 + Vec3::unit_z() * body.eye_height(scale.map_or(1.0, |s| s.0));
let scale = read_data.scales.get(attacker).map_or(1.0, |s| s.0);
let height = body.height() * scale;
// TODO: use Capsule Prisms instead of Cylinders

View File

@ -1,5 +1,5 @@
use common::{
comp::{Body, Controller, InputKind, Ori, Pos, Vel},
comp::{Body, ControlAction, Controller, InputKind, Ori, Pos, Scale, Vel},
link::Is,
mounting::Mount,
uid::UidAllocator,
@ -24,6 +24,7 @@ impl<'a> System<'a> for Sys {
WriteStorage<'a, Vel>,
WriteStorage<'a, Ori>,
ReadStorage<'a, Body>,
ReadStorage<'a, Scale>,
);
const NAME: &'static str = "mount";
@ -41,22 +42,24 @@ impl<'a> System<'a> for Sys {
mut velocities,
mut orientations,
bodies,
scales,
): Self::SystemData,
) {
// For each mount...
for (entity, is_mount, body) in (&entities, &is_mounts, bodies.maybe()).join() {
// ...find the rider...
let Some((inputs, queued_inputs, rider)) = uid_allocator
let Some((inputs, actions, rider)) = uid_allocator
.retrieve_entity_internal(is_mount.rider.id())
.and_then(|rider| {
controllers
.get_mut(rider)
.map(|c| {
let queued_inputs = c.queued_inputs
// TODO: Formalise ways to pass inputs to mounts
.drain_filter(|i, _| matches!(i, InputKind::Jump | InputKind::Fly | InputKind::Roll))
.collect();
(c.inputs.clone(), queued_inputs, rider)
let actions = c.actions.drain_filter(|action| match action {
ControlAction::StartInput { input: i, .. }
| ControlAction::CancelInput(i) => matches!(i, InputKind::Jump | InputKind::Fly | InputKind::Roll),
_ => false
}).collect();
(c.inputs.clone(), actions, rider)
})
})
else { continue };
@ -68,18 +71,17 @@ impl<'a> System<'a> for Sys {
if let (Some(pos), Some(ori), Some(vel)) = (pos, ori, vel) {
let mounter_body = bodies.get(rider);
let mounting_offset = body.map_or(Vec3::unit_z(), Body::mount_offset)
+ mounter_body.map_or(Vec3::zero(), Body::rider_offset);
* scales.get(entity).map_or(1.0, |s| s.0)
+ mounter_body.map_or(Vec3::zero(), Body::rider_offset)
* scales.get(rider).map_or(1.0, |s| s.0);
let _ = positions.insert(rider, Pos(pos.0 + ori.to_quat() * mounting_offset));
let _ = orientations.insert(rider, ori);
let _ = velocities.insert(rider, vel);
}
// ...and apply the rider's inputs to the mount's controller.
if let Some(controller) = controllers.get_mut(entity) {
*controller = Controller {
inputs,
queued_inputs,
..Default::default()
}
controller.inputs = inputs;
controller.actions = actions;
}
}
}

View File

@ -50,6 +50,7 @@ fn integrate_forces(
mass: &Mass,
fluid: &Fluid,
gravity: f32,
scale: Option<Scale>,
) -> Vel {
let dim = body.dimensions();
let height = dim.z;
@ -61,7 +62,13 @@ fn integrate_forces(
// Aerodynamic/hydrodynamic forces
if !rel_flow.0.is_approx_zero() {
debug_assert!(!rel_flow.0.map(|a| a.is_nan()).reduce_or());
let impulse = dt.0 * body.aerodynamic_forces(&rel_flow, fluid_density.0, wings);
let impulse = dt.0
* body.aerodynamic_forces(
&rel_flow,
fluid_density.0,
wings,
scale.map_or(1.0, |s| s.0),
);
debug_assert!(!impulse.map(|a| a.is_nan()).reduce_or());
if !impulse.is_approx_zero() {
let new_v = vel.0 + impulse / mass.0;
@ -610,6 +617,7 @@ impl<'a> PhysicsData<'a> {
&write.physics_states,
&read.masses,
&read.densities,
read.scales.maybe(),
!&read.is_ridings,
)
.par_join()
@ -628,6 +636,7 @@ impl<'a> PhysicsData<'a> {
physics_state,
mass,
density,
scale,
_,
)| {
let in_loaded_chunk = read
@ -672,6 +681,7 @@ impl<'a> PhysicsData<'a> {
mass,
&fluid,
GRAVITY,
scale.copied(),
)
.0
},
@ -1092,19 +1102,21 @@ impl<'a> PhysicsData<'a> {
// TODO: Cache the matrices here to avoid recomputing
let transform_last_from = Mat4::<f32>::translation_3d(
previous_cache_other.pos.unwrap_or(*pos_other).0
- previous_cache.pos.unwrap_or(Pos(wpos)).0,
) * Mat4::from(
previous_cache_other.ori,
) * Mat4::<f32>::translation_3d(
voxel_collider.translation,
);
let transform_last_from =
Mat4::<f32>::translation_3d(
previous_cache_other.pos.unwrap_or(*pos_other).0
- previous_cache.pos.unwrap_or(Pos(wpos)).0,
) * Mat4::from(previous_cache_other.ori)
* Mat4::<f32>::scaling_3d(previous_cache_other.scale)
* Mat4::<f32>::translation_3d(
voxel_collider.translation,
);
let transform_last_to = transform_last_from.inverted();
let transform_from =
Mat4::<f32>::translation_3d(pos_other.0 - wpos)
* Mat4::from(ori_other.to_quat())
* Mat4::<f32>::scaling_3d(previous_cache_other.scale)
* Mat4::<f32>::translation_3d(
voxel_collider.translation,
);
@ -1350,12 +1362,9 @@ fn box_voxel_collision<T: BaseVol<Vox = Block> + ReadVol>(
read: &PhysicsRead,
ori: &Ori,
) {
// FIXME: Review these
#![allow(
clippy::cast_precision_loss,
clippy::cast_possible_truncation,
clippy::cast_sign_loss
)]
// We cap out scale at 10.0 to prevent an enormous amount of lag
let scale = read.scales.get(entity).map_or(1.0, |s| s.0.min(10.0));
//prof_span!("box_voxel_collision");
// Convience function to compute the player aabb
@ -1410,7 +1419,7 @@ fn box_voxel_collision<T: BaseVol<Vox = Block> + ReadVol>(
#[allow(clippy::trivially_copy_pass_by_ref)]
fn always_hits(_: &Block) -> bool { true }
let (radius, z_min, z_max) = cylinder;
let (radius, z_min, z_max) = (Vec3::from(cylinder) * scale).into_tuple();
// Probe distances
let hdist = radius.ceil() as i32;
@ -1440,7 +1449,8 @@ fn box_voxel_collision<T: BaseVol<Vox = Block> + ReadVol>(
// Don't jump too far at once
const MAX_INCREMENTS: usize = 100; // The maximum number of collision tests per tick
let increments = ((pos_delta.map(|e| e.abs()).reduce_partial_max() / 0.3).ceil() as usize)
let min_step = (radius / 2.0).min(z_max - z_min).clamped(0.01, 0.3);
let increments = ((pos_delta.map(|e| e.abs()).reduce_partial_max() / min_step).ceil() as usize)
.clamped(1, MAX_INCREMENTS);
let old_pos = pos.0;
for _ in 0..increments {

View File

@ -2,8 +2,8 @@
mod tests {
use common::{
comp::{
item::MaterialStatManifest, skills::GeneralSkill, tool::AbilityMap, CharacterState,
Controller, Energy, Ori, PhysicsState, Poise, Pos, Skill, Stats, Vel,
item::MaterialStatManifest, skills::GeneralSkill, tool::AbilityMap, CharacterActivity,
CharacterState, Controller, Energy, Ori, PhysicsState, Poise, Pos, Skill, Stats, Vel,
},
resources::{DeltaTime, GameMode, Time},
shared_server_config::ServerConstants,
@ -53,6 +53,7 @@ mod tests {
.ecs_mut()
.create_entity()
.with(CharacterState::Idle(common::states::idle::Data::default()))
.with(CharacterActivity::default())
.with(Pos(Vec3::zero()))
.with(Vel::default())
.with(ori)
@ -84,6 +85,7 @@ mod tests {
None,
// Dummy ServerConstants
&ServerConstants::default(),
|_, _| {},
);
}
@ -115,8 +117,12 @@ mod tests {
for i in 0..TESTCASES {
if let Some(e) = entities[i] {
let result = Dir::from(*results.get(e).expect("Ori missing"));
assert!(result.abs_diff_eq(&testcases[i].1, 0.0005));
// println!("{:?}", result);
assert!(
result.abs_diff_eq(&testcases[i].1, 0.0005),
"{:?} != {:?}",
result,
testcases[i].1
);
}
}
}

View File

@ -19,6 +19,7 @@ fn simple_run() {
false,
None,
&ServerConstants::default(),
|_, _| {},
);
}
@ -127,11 +128,11 @@ fn fall_dt_speed_diff() -> Result<(), Box<dyn Error>> {
assert_relative_eq!(svel.0.z, -4.9847627, epsilon = EPSILON);
assert_relative_eq!(fpos.0.x, 16.0);
assert_relative_eq!(fpos.0.y, 16.0);
assert_relative_eq!(fpos.0.z, 264.25073, epsilon = EPSILON);
assert_relative_eq!(fpos.0.z, 264.25067, epsilon = EPSILON);
assert_relative_eq!(fvel.0.z, -4.9930925, epsilon = EPSILON);
// Diff after 200ms
assert_relative_eq!((spos.0.z - fpos.0.z).abs(), 0.2253418, epsilon = EPSILON);
assert_relative_eq!((spos.0.z - fpos.0.z).abs(), 0.22540283, epsilon = EPSILON);
assert_relative_eq!((svel.0.z - fvel.0.z).abs(), 0.008329868, epsilon = EPSILON);
Ok(())

View File

@ -3,8 +3,8 @@ use common::{
inventory::item::MaterialStatManifest,
skills::{GeneralSkill, Skill},
tool::AbilityMap,
Auras, Buffs, CharacterState, Collider, Combo, Controller, Energy, Health, Ori, Pos, Stats,
Vel,
Auras, Buffs, CharacterActivity, CharacterState, Collider, Combo, Controller, Energy,
Health, Ori, Pos, Stats, Vel,
},
resources::{DeltaTime, GameMode, Time},
shared_server_config::ServerConstants,
@ -66,6 +66,7 @@ pub fn tick(state: &mut State, dt: Duration) {
false,
None,
&ServerConstants::default(),
|_, _| {},
);
}
@ -122,6 +123,7 @@ pub fn create_player(state: &mut State) -> Entity {
.with(body)
.with(Controller::default())
.with(CharacterState::default())
.with(CharacterActivity::default())
.with(Buffs::default())
.with(Combo::default())
.with(Auras::default())

23
rtsim/Cargo.toml Normal file
View File

@ -0,0 +1,23 @@
[package]
name = "veloren-rtsim"
version = "0.10.0"
edition = "2021"
[dependencies]
common = { package = "veloren-common", path = "../common" }
world = { package = "veloren-world", path = "../world" }
ron = "0.8"
serde = { version = "1.0.110", features = ["derive"] }
hashbrown = { version = "0.12", features = ["rayon", "serde", "nightly"] }
enum-map = { version = "2.4", features = ["serde"] }
vek = { version = "0.15.8", features = ["serde"] }
rmp-serde = "1.1.0"
anymap2 = "0.13"
tracing = "0.1"
atomic_refcell = "0.1"
slotmap = { version = "1.0.6", features = ["serde"] }
rand = { version = "0.8", features = ["small_rng"] }
rand_chacha = "0.3"
fxhash = "0.2.1"
itertools = "0.10.3"
rayon = "1.5"

878
rtsim/src/ai/mod.rs Normal file
View File

@ -0,0 +1,878 @@
use crate::{
data::{
npc::{Controller, Npc, NpcId},
ReportId, Sentiments,
},
RtState,
};
use common::resources::{Time, TimeOfDay};
use hashbrown::HashSet;
use rand_chacha::ChaChaRng;
use std::{any::Any, collections::VecDeque, marker::PhantomData, ops::ControlFlow};
use world::{IndexRef, World};
/// The context provided to an [`Action`] while it is being performed. It should
/// be possible to access any and all important information about the game world
/// through this struct.
pub struct NpcCtx<'a> {
pub state: &'a RtState,
pub world: &'a World,
pub index: IndexRef<'a>,
pub time_of_day: TimeOfDay,
pub time: Time,
pub npc_id: NpcId,
pub npc: &'a Npc,
pub controller: &'a mut Controller,
pub inbox: &'a mut VecDeque<ReportId>, // TODO: Allow more inbox items
pub sentiments: &'a mut Sentiments,
pub known_reports: &'a mut HashSet<ReportId>,
pub rng: ChaChaRng,
}
/// A trait that describes 'actions': long-running tasks performed by rtsim
/// NPCs. These can be as simple as walking in a straight line between two
/// locations or as complex as taking part in an adventure with players or
/// performing an entire daily work schedule.
///
/// Actions are built up from smaller sub-actions via the combinator methods
/// defined on this trait, and with the standalone functions in this module.
/// Using these combinators, in a similar manner to using the [`Iterator`] API,
/// it is possible to construct arbitrarily complex actions including behaviour
/// trees (see [`choose`] and [`watch`]) and other forms of moment-by-moment
/// decision-making.
///
/// On completion, actions may produce a value, denoted by the type parameter
/// `R`. For example, an action may communicate whether it was successful or
/// unsuccessful through this completion value.
///
/// You should not need to implement this trait yourself when writing AI code.
/// If you find yourself wanting to implement it, please discuss with the core
/// dev team first.
pub trait Action<R = ()>: Any + Send + Sync {
/// Returns `true` if the action should be considered the 'same' (i.e:
/// achieving the same objective) as another. In general, the AI system
/// will try to avoid switching (and therefore restarting) tasks when the
/// new task is the 'same' as the old one.
// TODO: Figure out a way to compare actions based on their 'intention': i.e:
// two pathing actions should be considered equivalent if their destination
// is the same regardless of the progress they've each made.
fn is_same(&self, other: &Self) -> bool
where
Self: Sized;
/// Like [`Action::is_same`], but allows for dynamic dispatch.
fn dyn_is_same_sized(&self, other: &dyn Action<R>) -> bool
where
Self: Sized,
{
match (other as &dyn Any).downcast_ref::<Self>() {
Some(other) => self.is_same(other),
None => false,
}
}
/// Like [`Action::is_same`], but allows for dynamic dispatch.
fn dyn_is_same(&self, other: &dyn Action<R>) -> bool;
/// Generate a backtrace for the action. The action should recursively push
/// all of the tasks it is currently performing.
fn backtrace(&self, bt: &mut Vec<String>);
/// Reset the action to its initial state such that it can be repeated.
fn reset(&mut self);
/// Perform the action for the current tick.
fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow<R>;
/// Create an action that chains together two sub-actions, one after the
/// other.
///
/// # Example
///
/// ```ignore
/// // Walk toward an enemy NPC and, once done, attack the enemy NPC
/// goto(enemy_npc).then(attack(enemy_npc))
/// ```
#[must_use]
fn then<A1: Action<R1>, R1>(self, other: A1) -> Then<Self, A1, R>
where
Self: Sized,
{
Then {
a0: self,
a0_finished: false,
a1: other,
phantom: PhantomData,
}
}
/// Create an action that repeats a sub-action indefinitely.
///
/// # Example
///
/// ```ignore
/// // Endlessly collect flax from the environment
/// find_and_collect(ChunkResource::Flax).repeat()
/// ```
#[must_use]
fn repeat<R1>(self) -> Repeat<Self, R1>
where
Self: Sized,
{
Repeat(self, PhantomData)
}
/// Stop the sub-action suddenly if a condition is reached.
///
/// # Example
///
/// ```ignore
/// // Keep going on adventures until your 111th birthday
/// go_on_an_adventure().repeat().stop_if(|ctx| ctx.npc.age > 111.0)
/// ```
#[must_use]
fn stop_if<F: FnMut(&mut NpcCtx) -> bool + Clone>(self, f: F) -> StopIf<Self, F>
where
Self: Sized,
{
StopIf(self, f.clone(), f)
}
/// Pause an action to possibly perform another action.
///
/// # Example
///
/// ```ignore
/// // Keep going on adventures until your 111th birthday
/// walk_to_the_shops()
/// .interrupt_with(|ctx| if ctx.npc.is_hungry() {
/// Some(eat_food())
/// } else {
/// None
/// })
/// ```
#[must_use]
fn interrupt_with<A1: Action<R1>, R1, F: FnMut(&mut NpcCtx) -> Option<A1> + Clone>(
self,
f: F,
) -> InterruptWith<Self, F, A1, R1>
where
Self: Sized,
{
InterruptWith {
a0: self,
f: f.clone(),
f2: f,
a1: None,
phantom: PhantomData,
}
}
/// Map the completion value of this action to something else.
#[must_use]
fn map<F: FnMut(R) -> R1, R1>(self, f: F) -> Map<Self, F, R>
where
Self: Sized,
{
Map(self, f, PhantomData)
}
/// Box the action. Often used to perform type erasure, such as when you
/// want to return one of many actions (each with different types) from
/// the same function.
///
/// Note that [`Either`] can often be used to unify mismatched types without
/// the need for boxing.
///
/// # Example
///
/// ```ignore
/// // Error! Type mismatch between branches
/// if npc.is_too_tired() {
/// goto(npc.home)
/// } else {
/// go_on_an_adventure()
/// }
///
/// // All fine
/// if npc.is_too_tired() {
/// goto(npc.home).boxed()
/// } else {
/// go_on_an_adventure().boxed()
/// }
/// ```
#[must_use]
fn boxed(self) -> Box<dyn Action<R>>
where
Self: Sized,
{
Box::new(self)
}
/// Add debugging information to the action that will be visible when using
/// the `/npc_info` command.
///
/// # Example
///
/// ```ignore
/// goto(npc.home).debug(|| "Going home")
/// ```
#[must_use]
fn debug<F, T>(self, mk_info: F) -> Debug<Self, F, T>
where
Self: Sized,
{
Debug(self, mk_info, PhantomData)
}
}
impl<R: 'static> Action<R> for Box<dyn Action<R>> {
fn is_same(&self, other: &Self) -> bool { (**self).dyn_is_same(other) }
fn dyn_is_same(&self, other: &dyn Action<R>) -> bool {
match (other as &dyn Any).downcast_ref::<Self>() {
Some(other) => self.is_same(other),
None => false,
}
}
fn backtrace(&self, bt: &mut Vec<String>) { (**self).backtrace(bt) }
fn reset(&mut self) { (**self).reset(); }
fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow<R> { (**self).tick(ctx) }
}
impl<R: 'static, A: Action<R>, B: Action<R>> Action<R> for itertools::Either<A, B> {
fn is_same(&self, other: &Self) -> bool {
match (self, other) {
(itertools::Either::Left(x), itertools::Either::Left(y)) => x.is_same(y),
(itertools::Either::Right(x), itertools::Either::Right(y)) => x.is_same(y),
_ => false,
}
}
fn dyn_is_same(&self, other: &dyn Action<R>) -> bool { self.dyn_is_same_sized(other) }
fn backtrace(&self, bt: &mut Vec<String>) {
match self {
itertools::Either::Left(x) => x.backtrace(bt),
itertools::Either::Right(x) => x.backtrace(bt),
}
}
fn reset(&mut self) {
match self {
itertools::Either::Left(x) => x.reset(),
itertools::Either::Right(x) => x.reset(),
}
}
fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow<R> {
match self {
itertools::Either::Left(x) => x.tick(ctx),
itertools::Either::Right(x) => x.tick(ctx),
}
}
}
// Now
/// See [`now`].
#[derive(Copy, Clone)]
pub struct Now<F, A>(F, Option<A>);
impl<R: Send + Sync + 'static, F: FnMut(&mut NpcCtx) -> A + Send + Sync + 'static, A: Action<R>>
Action<R> for Now<F, A>
{
// TODO: This doesn't compare?!
fn is_same(&self, _other: &Self) -> bool { true }
fn dyn_is_same(&self, other: &dyn Action<R>) -> bool { self.dyn_is_same_sized(other) }
fn backtrace(&self, bt: &mut Vec<String>) {
if let Some(action) = &self.1 {
action.backtrace(bt);
} else {
bt.push("<thinking>".to_string());
}
}
// TODO: Reset closure?
fn reset(&mut self) { self.1 = None; }
// TODO: Reset closure state?
fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow<R> {
(self.1.get_or_insert_with(|| (self.0)(ctx))).tick(ctx)
}
}
/// Start a new action based on the state of the world (`ctx`) at the moment the
/// action is started.
///
/// If you're in a situation where you suddenly find yourself needing `ctx`, you
/// probably want to use this.
///
/// # Example
///
/// ```ignore
/// // An action that makes an NPC immediately travel to its *current* home
/// now(|ctx| goto(ctx.npc.home))
/// ```
pub fn now<F, A>(f: F) -> Now<F, A>
where
F: FnMut(&mut NpcCtx) -> A,
{
Now(f, None)
}
// Until
/// See [`now`].
#[derive(Copy, Clone)]
pub struct Until<F, A, R>(F, Option<A>, PhantomData<R>);
impl<
R: Send + Sync + 'static,
F: FnMut(&mut NpcCtx) -> Option<A> + Send + Sync + 'static,
A: Action<R>,
> Action<()> for Until<F, A, R>
{
// TODO: This doesn't compare?!
fn is_same(&self, _other: &Self) -> bool { true }
fn dyn_is_same(&self, other: &dyn Action<()>) -> bool { self.dyn_is_same_sized(other) }
fn backtrace(&self, bt: &mut Vec<String>) {
if let Some(action) = &self.1 {
action.backtrace(bt);
} else {
bt.push("<thinking>".to_string());
}
}
// TODO: Reset closure?
fn reset(&mut self) { self.1 = None; }
// TODO: Reset closure state?
fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow<()> {
match &mut self.1 {
Some(x) => match x.tick(ctx) {
ControlFlow::Continue(()) => ControlFlow::Continue(()),
ControlFlow::Break(_) => {
self.1 = None;
ControlFlow::Continue(())
},
},
None => match (self.0)(ctx) {
Some(x) => {
self.1 = Some(x);
ControlFlow::Continue(())
},
None => ControlFlow::Break(()),
},
}
}
}
pub fn until<F, A, R>(f: F) -> Until<F, A, R>
where
F: FnMut(&mut NpcCtx) -> Option<A>,
{
Until(f, None, PhantomData)
}
// Just
/// See [`just`].
#[derive(Copy, Clone)]
pub struct Just<F, R = ()>(F, PhantomData<R>);
impl<R: Send + Sync + 'static, F: FnMut(&mut NpcCtx) -> R + Send + Sync + 'static> Action<R>
for Just<F, R>
{
fn is_same(&self, _other: &Self) -> bool { true }
fn dyn_is_same(&self, other: &dyn Action<R>) -> bool { self.dyn_is_same_sized(other) }
fn backtrace(&self, _bt: &mut Vec<String>) {}
// TODO: Reset closure?
fn reset(&mut self) {}
// TODO: Reset closure state?
fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow<R> { ControlFlow::Break((self.0)(ctx)) }
}
/// An action that executes some code just once when performed.
///
/// If you want to execute this code on every tick, consider combining it with
/// [`Action::repeat`].
///
/// # Example
///
/// ```ignore
/// // Make the current NPC say 'Hello, world!' exactly once
/// just(|ctx| ctx.controller.say("Hello, world!"))
/// ```
pub fn just<F, R: Send + Sync + 'static>(f: F) -> Just<F, R>
where
F: FnMut(&mut NpcCtx) -> R + Send + Sync + 'static,
{
Just(f, PhantomData)
}
// Finish
/// See [`finish`].
#[derive(Copy, Clone)]
pub struct Finish;
impl Action<()> for Finish {
fn is_same(&self, _other: &Self) -> bool { true }
fn dyn_is_same(&self, other: &dyn Action<()>) -> bool { self.dyn_is_same_sized(other) }
fn backtrace(&self, _bt: &mut Vec<String>) {}
fn reset(&mut self) {}
fn tick(&mut self, _ctx: &mut NpcCtx) -> ControlFlow<()> { ControlFlow::Break(()) }
}
/// An action that immediately finishes without doing anything.
///
/// This action is useless by itself, but becomes useful when combined with
/// actions that make decisions.
///
/// # Example
///
/// ```ignore
/// now(|ctx| {
/// if ctx.npc.is_tired() {
/// sleep().boxed() // If we're tired, sleep
/// } else if ctx.npc.is_hungry() {
/// eat().boxed() // If we're hungry, eat
/// } else {
/// finish().boxed() // Otherwise, do nothing
/// }
/// })
/// ```
#[must_use]
pub fn finish() -> Finish { Finish }
// Tree
pub type Priority = usize;
pub const URGENT: Priority = 0;
pub const IMPORTANT: Priority = 1;
pub const CASUAL: Priority = 2;
pub struct Node<R>(Box<dyn Action<R>>, Priority);
/// Perform an action with [`URGENT`] priority (see [`choose`]).
#[must_use]
pub fn urgent<A: Action<R>, R>(a: A) -> Node<R> { Node(Box::new(a), URGENT) }
/// Perform an action with [`IMPORTANT`] priority (see [`choose`]).
#[must_use]
pub fn important<A: Action<R>, R>(a: A) -> Node<R> { Node(Box::new(a), IMPORTANT) }
/// Perform an action with [`CASUAL`] priority (see [`choose`]).
#[must_use]
pub fn casual<A: Action<R>, R>(a: A) -> Node<R> { Node(Box::new(a), CASUAL) }
/// See [`choose`] and [`watch`].
pub struct Tree<F, R> {
next: F,
prev: Option<Node<R>>,
interrupt: bool,
}
impl<F: FnMut(&mut NpcCtx) -> Node<R> + Send + Sync + 'static, R: 'static> Action<R>
for Tree<F, R>
{
fn is_same(&self, _other: &Self) -> bool { true }
fn dyn_is_same(&self, other: &dyn Action<R>) -> bool { self.dyn_is_same_sized(other) }
fn backtrace(&self, bt: &mut Vec<String>) {
if let Some(prev) = &self.prev {
prev.0.backtrace(bt);
} else {
bt.push("<thinking>".to_string());
}
}
fn reset(&mut self) { self.prev = None; }
// TODO: Reset `next` too?
fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow<R> {
let new = (self.next)(ctx);
let prev = match &mut self.prev {
Some(prev) if prev.1 <= new.1 && (prev.0.dyn_is_same(&*new.0) || !self.interrupt) => {
prev
},
_ => self.prev.insert(new),
};
match prev.0.tick(ctx) {
ControlFlow::Continue(()) => ControlFlow::Continue(()),
ControlFlow::Break(r) => {
self.prev = None;
ControlFlow::Break(r)
},
}
}
}
/// An action that allows implementing a decision tree, with action
/// prioritisation.
///
/// The inner function will be run every tick to decide on an action. When an
/// action is chosen, it will be performed until completed *UNLESS* an action
/// with a more urgent priority is chosen in a subsequent tick. [`choose`] tries
/// to commit to actions when it can: only more urgent actions will interrupt an
/// action that's currently being performed. If you want something that's more
/// eager to switch actions, see [`watch`].
///
/// # Example
///
/// ```ignore
/// choose(|ctx| {
/// if ctx.npc.is_being_attacked() {
/// urgent(combat()) // If we're in danger, do something!
/// } else if ctx.npc.is_hungry() {
/// important(eat()) // If we're hungry, eat
/// } else {
/// casual(idle()) // Otherwise, do nothing
/// }
/// })
/// ```
#[must_use]
pub fn choose<R: 'static, F>(f: F) -> impl Action<R>
where
F: FnMut(&mut NpcCtx) -> Node<R> + Send + Sync + 'static,
{
Tree {
next: f,
prev: None,
interrupt: false,
}
}
/// An action that allows implementing a decision tree, with action
/// prioritisation.
///
/// The inner function will be run every tick to decide on an action. When an
/// action is chosen, it will be performed until completed unless a different
/// action of the same or higher priority is chosen in a subsequent tick.
/// [`watch`] is very unfocused and will happily switch between actions
/// rapidly between ticks if conditions change. If you want something that
/// tends to commit to actions until they are completed, see [`choose`].
///
/// # Example
///
/// ```ignore
/// watch(|ctx| {
/// if ctx.npc.is_being_attacked() {
/// urgent(combat()) // If we're in danger, do something!
/// } else if ctx.npc.is_hungry() {
/// important(eat()) // If we're hungry, eat
/// } else {
/// casual(idle()) // Otherwise, do nothing
/// }
/// })
/// ```
#[must_use]
pub fn watch<R: 'static, F>(f: F) -> impl Action<R>
where
F: FnMut(&mut NpcCtx) -> Node<R> + Send + Sync + 'static,
{
Tree {
next: f,
prev: None,
interrupt: true,
}
}
// Then
/// See [`Action::then`].
#[derive(Copy, Clone)]
pub struct Then<A0, A1, R0> {
a0: A0,
a0_finished: bool,
a1: A1,
phantom: PhantomData<R0>,
}
impl<A0: Action<R0>, A1: Action<R1>, R0: Send + Sync + 'static, R1: Send + Sync + 'static>
Action<R1> for Then<A0, A1, R0>
{
fn is_same(&self, other: &Self) -> bool {
self.a0.is_same(&other.a0) && self.a1.is_same(&other.a1)
}
fn dyn_is_same(&self, other: &dyn Action<R1>) -> bool { self.dyn_is_same_sized(other) }
fn backtrace(&self, bt: &mut Vec<String>) {
if self.a0_finished {
self.a1.backtrace(bt);
} else {
self.a0.backtrace(bt);
}
}
fn reset(&mut self) {
self.a0.reset();
self.a0_finished = false;
self.a1.reset();
}
fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow<R1> {
if !self.a0_finished {
match self.a0.tick(ctx) {
ControlFlow::Continue(()) => return ControlFlow::Continue(()),
ControlFlow::Break(_) => self.a0_finished = true,
}
}
self.a1.tick(ctx)
}
}
// InterruptWith
/// See [`Action::then`].
#[derive(Copy, Clone)]
pub struct InterruptWith<A0, F, A1, R1> {
a0: A0,
f: F,
f2: F,
a1: Option<A1>,
phantom: PhantomData<R1>,
}
impl<
A0: Action<R0>,
A1: Action<R1>,
F: FnMut(&mut NpcCtx) -> Option<A1> + Clone + Send + Sync + 'static,
R0: Send + Sync + 'static,
R1: Send + Sync + 'static,
> Action<R0> for InterruptWith<A0, F, A1, R1>
{
fn is_same(&self, other: &Self) -> bool { self.a0.is_same(&other.a0) }
fn dyn_is_same(&self, other: &dyn Action<R0>) -> bool { self.dyn_is_same_sized(other) }
fn backtrace(&self, bt: &mut Vec<String>) {
if let Some(a1) = &self.a1 {
// TODO: Find a way to represent interrupts in backtraces
bt.push("<interrupted>".to_string());
a1.backtrace(bt);
} else {
self.a0.backtrace(bt);
}
}
fn reset(&mut self) {
self.a0.reset();
self.f = self.f2.clone();
self.a1 = None;
}
fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow<R0> {
if let Some(new_a1) = (self.f)(ctx) {
self.a1 = Some(new_a1);
}
if let Some(a1) = &mut self.a1 {
match a1.tick(ctx) {
ControlFlow::Continue(()) => return ControlFlow::Continue(()),
ControlFlow::Break(_) => self.a1 = None,
}
}
self.a0.tick(ctx)
}
}
// Repeat
/// See [`Action::repeat`].
#[derive(Copy, Clone)]
pub struct Repeat<A, R = ()>(A, PhantomData<R>);
impl<R: Send + Sync + 'static, A: Action<R>> Action<!> for Repeat<A, R> {
fn is_same(&self, other: &Self) -> bool { self.0.is_same(&other.0) }
fn dyn_is_same(&self, other: &dyn Action<!>) -> bool { self.dyn_is_same_sized(other) }
fn backtrace(&self, bt: &mut Vec<String>) { self.0.backtrace(bt); }
fn reset(&mut self) { self.0.reset(); }
fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow<!> {
match self.0.tick(ctx) {
ControlFlow::Continue(()) => ControlFlow::Continue(()),
ControlFlow::Break(_) => {
self.0.reset();
ControlFlow::Continue(())
},
}
}
}
// Sequence
/// See [`seq`].
#[derive(Copy, Clone)]
pub struct Sequence<I, A, R = ()>(I, I, Option<A>, PhantomData<R>);
impl<R: Send + Sync + 'static, I: Iterator<Item = A> + Clone + Send + Sync + 'static, A: Action<R>>
Action<()> for Sequence<I, A, R>
{
fn is_same(&self, _other: &Self) -> bool { true }
fn dyn_is_same(&self, other: &dyn Action<()>) -> bool { self.dyn_is_same_sized(other) }
fn backtrace(&self, bt: &mut Vec<String>) {
if let Some(action) = &self.2 {
action.backtrace(bt);
} else {
bt.push("<thinking>".to_string());
}
}
fn reset(&mut self) {
self.0 = self.1.clone();
self.2 = None;
}
fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow<()> {
let item = if let Some(prev) = &mut self.2 {
prev
} else {
match self.0.next() {
Some(next) => self.2.insert(next),
None => return ControlFlow::Break(()),
}
};
if let ControlFlow::Break(_) = item.tick(ctx) {
self.2 = None;
}
ControlFlow::Continue(())
}
}
/// An action that consumes and performs an iterator of actions in sequence, one
/// after another.
///
/// # Example
///
/// ```ignore
/// // A list of enemies we should attack in turn
/// let enemies = vec![
/// ugly_goblin,
/// stinky_troll,
/// rude_dwarf,
/// ];
///
/// // Attack each enemy, one after another
/// seq(enemies
/// .into_iter()
/// .map(|enemy| attack(enemy)))
/// ```
#[must_use]
pub fn seq<I, A, R>(iter: I) -> Sequence<I, A, R>
where
I: Iterator<Item = A> + Clone,
A: Action<R>,
{
Sequence(iter.clone(), iter, None, PhantomData)
}
// StopIf
/// See [`Action::stop_if`].
#[derive(Copy, Clone)]
pub struct StopIf<A, F>(A, F, F);
impl<A: Action<R>, F: FnMut(&mut NpcCtx) -> bool + Clone + Send + Sync + 'static, R>
Action<Option<R>> for StopIf<A, F>
{
fn is_same(&self, other: &Self) -> bool { self.0.is_same(&other.0) }
fn dyn_is_same(&self, other: &dyn Action<Option<R>>) -> bool { self.dyn_is_same_sized(other) }
fn backtrace(&self, bt: &mut Vec<String>) { self.0.backtrace(bt); }
fn reset(&mut self) {
self.0.reset();
self.1 = self.2.clone();
}
fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow<Option<R>> {
if (self.1)(ctx) {
ControlFlow::Break(None)
} else {
self.0.tick(ctx).map_break(Some)
}
}
}
// Map
/// See [`Action::map`].
#[derive(Copy, Clone)]
pub struct Map<A, F, R>(A, F, PhantomData<R>);
impl<A: Action<R>, F: FnMut(R) -> R1 + Send + Sync + 'static, R: Send + Sync + 'static, R1>
Action<R1> for Map<A, F, R>
{
fn is_same(&self, other: &Self) -> bool { self.0.is_same(&other.0) }
fn dyn_is_same(&self, other: &dyn Action<R1>) -> bool { self.dyn_is_same_sized(other) }
fn backtrace(&self, bt: &mut Vec<String>) { self.0.backtrace(bt); }
fn reset(&mut self) { self.0.reset(); }
fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow<R1> {
self.0.tick(ctx).map_break(&mut self.1)
}
}
// Debug
/// See [`Action::debug`].
#[derive(Copy, Clone)]
pub struct Debug<A, F, T>(A, F, PhantomData<T>);
impl<
A: Action<R>,
F: Fn() -> T + Send + Sync + 'static,
R: Send + Sync + 'static,
T: Send + Sync + std::fmt::Display + 'static,
> Action<R> for Debug<A, F, T>
{
fn is_same(&self, other: &Self) -> bool { self.0.is_same(&other.0) }
fn dyn_is_same(&self, other: &dyn Action<R>) -> bool { self.dyn_is_same_sized(other) }
fn backtrace(&self, bt: &mut Vec<String>) {
bt.push((self.1)().to_string());
self.0.backtrace(bt);
}
fn reset(&mut self) { self.0.reset(); }
fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow<R> { self.0.tick(ctx) }
}

43
rtsim/src/data/faction.rs Normal file
View File

@ -0,0 +1,43 @@
use crate::data::Sentiments;
use common::rtsim::Actor;
pub use common::rtsim::FactionId;
use serde::{Deserialize, Serialize};
use slotmap::HopSlotMap;
use std::ops::{Deref, DerefMut};
use vek::*;
#[derive(Clone, Serialize, Deserialize)]
pub struct Faction {
pub seed: u32,
pub leader: Option<Actor>,
pub good_or_evil: bool, // TODO: Very stupid, get rid of this
#[serde(default)]
pub sentiments: Sentiments,
}
impl Faction {
pub fn cleanup(&mut self) {
self.sentiments
.cleanup(crate::data::sentiment::FACTION_MAX_SENTIMENTS);
}
}
#[derive(Clone, Default, Serialize, Deserialize)]
pub struct Factions {
pub factions: HopSlotMap<FactionId, Faction>,
}
impl Factions {
pub fn create(&mut self, faction: Faction) -> FactionId { self.factions.insert(faction) }
}
impl Deref for Factions {
type Target = HopSlotMap<FactionId, Faction>;
fn deref(&self) -> &Self::Target { &self.factions }
}
impl DerefMut for Factions {
fn deref_mut(&mut self) -> &mut Self::Target { &mut self.factions }
}

152
rtsim/src/data/mod.rs Normal file
View File

@ -0,0 +1,152 @@
pub mod faction;
pub mod nature;
pub mod npc;
pub mod report;
pub mod sentiment;
pub mod site;
pub use self::{
faction::{Faction, FactionId, Factions},
nature::Nature,
npc::{Npc, NpcId, Npcs},
report::{Report, ReportId, ReportKind, Reports},
sentiment::{Sentiment, Sentiments},
site::{Site, SiteId, Sites},
};
use common::resources::TimeOfDay;
use enum_map::{enum_map, EnumArray, EnumMap};
use serde::{de, ser, Deserialize, Serialize};
use std::{
cmp::PartialEq,
fmt,
io::{Read, Write},
marker::PhantomData,
};
/// The current version of rtsim data.
///
/// Note that this number does *not* need incrementing on every change: most
/// field removals/additions are fine. This number should only be incremented
/// when we wish to perform a *hard purge* of rtsim data.
pub const CURRENT_VERSION: u32 = 0;
#[derive(Clone, Serialize, Deserialize)]
pub struct Data {
// Absence of field just implied version = 0
#[serde(default)]
pub version: u32,
pub nature: Nature,
#[serde(default)]
pub npcs: Npcs,
#[serde(default)]
pub sites: Sites,
#[serde(default)]
pub factions: Factions,
#[serde(default)]
pub reports: Reports,
#[serde(default)]
pub tick: u64,
#[serde(default)]
pub time_of_day: TimeOfDay,
// If true, rtsim data will be ignored (and, hence, overwritten on next save) on load.
#[serde(default)]
pub should_purge: bool,
}
pub enum ReadError {
Load(rmp_serde::decode::Error),
// Preserve old data
VersionMismatch(Box<Data>),
}
impl fmt::Debug for ReadError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::Load(err) => err.fmt(f),
Self::VersionMismatch(_) => write!(f, "VersionMismatch"),
}
}
}
pub type WriteError = rmp_serde::encode::Error;
impl Data {
pub fn spawn_npc(&mut self, npc: Npc) -> NpcId {
let home = npc.home;
let id = self.npcs.create_npc(npc);
if let Some(home) = home.and_then(|home| self.sites.get_mut(home)) {
home.population.insert(id);
}
id
}
pub fn from_reader<R: Read>(reader: R) -> Result<Box<Self>, ReadError> {
rmp_serde::decode::from_read(reader)
.map_err(ReadError::Load)
.and_then(|data: Data| {
if data.version == CURRENT_VERSION {
Ok(Box::new(data))
} else {
Err(ReadError::VersionMismatch(Box::new(data)))
}
})
}
pub fn write_to<W: Write>(&self, mut writer: W) -> Result<(), WriteError> {
rmp_serde::encode::write_named(&mut writer, self)
}
}
fn rugged_ser_enum_map<
K: EnumArray<V> + Serialize,
V: From<i16> + PartialEq + Serialize,
S: ser::Serializer,
const DEFAULT: i16,
>(
map: &EnumMap<K, V>,
ser: S,
) -> Result<S::Ok, S::Error> {
ser.collect_map(
map.iter()
.filter(|(_, v)| v != &&V::from(DEFAULT))
.map(|(k, v)| (k, v)),
)
}
fn rugged_de_enum_map<
'a,
K: EnumArray<V> + EnumArray<Option<V>> + Deserialize<'a>,
V: From<i16> + Deserialize<'a>,
D: de::Deserializer<'a>,
const DEFAULT: i16,
>(
de: D,
) -> Result<EnumMap<K, V>, D::Error> {
struct Visitor<K, V, const DEFAULT: i16>(PhantomData<(K, V)>);
impl<'de, K, V, const DEFAULT: i16> de::Visitor<'de> for Visitor<K, V, DEFAULT>
where
K: EnumArray<V> + EnumArray<Option<V>> + Deserialize<'de>,
V: From<i16> + Deserialize<'de>,
{
type Value = EnumMap<K, V>;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
write!(formatter, "a map")
}
fn visit_map<M: de::MapAccess<'de>>(self, mut access: M) -> Result<Self::Value, M::Error> {
let mut entries = EnumMap::default();
while let Some((key, value)) = access.next_entry()? {
entries[key] = Some(value);
}
Ok(enum_map! { key => entries[key].take().unwrap_or_else(|| V::from(DEFAULT)) })
}
}
de.deserialize_map(Visitor::<_, _, DEFAULT>(PhantomData))
}

64
rtsim/src/data/nature.rs Normal file
View File

@ -0,0 +1,64 @@
use common::{grid::Grid, rtsim::ChunkResource};
use enum_map::EnumMap;
use serde::{Deserialize, Serialize};
use vek::*;
use world::World;
/// Represents the state of 'natural' elements of the world such as
/// plant/animal/resource populations, weather systems, etc.
///
/// Where possible, this data does not define the state of natural aspects of
/// the world, but instead defines 'modifications' that sit on top of the world
/// data generated by initial generation.
#[derive(Clone, Serialize, Deserialize)]
pub struct Nature {
chunks: Grid<Chunk>,
}
impl Nature {
pub fn generate(world: &World) -> Self {
Self {
chunks: Grid::populate_from(world.sim().get_size().map(|e| e as i32), |_| Chunk {
res: EnumMap::<_, f32>::default().map(|_, _| 1.0),
}),
}
}
// TODO: Clean up this API a bit
pub fn get_chunk_resources(&self, key: Vec2<i32>) -> EnumMap<ChunkResource, f32> {
self.chunks.get(key).map(|c| c.res).unwrap_or_default()
}
pub fn set_chunk_resources(&mut self, key: Vec2<i32>, res: EnumMap<ChunkResource, f32>) {
if let Some(chunk) = self.chunks.get_mut(key) {
chunk.res = res;
}
}
}
#[derive(Clone, Serialize, Deserialize)]
pub struct Chunk {
/// Represent the 'naturally occurring' resource proportion that exists in
/// this chunk.
///
/// 0.0 => None of the resources generated by terrain generation should be
/// present
///
/// 1.0 => All of the resources generated by terrain generation should be
/// present
///
/// It's important to understand this this number does not represent the
/// total amount of a resource present in a chunk, nor is it even
/// proportional to the amount of the resource present. To get the total
/// amount of the resource in a chunk, one must first multiply this
/// factor by the amount of 'natural' resources given by terrain
/// generation. This value represents only the variable 'depletion' factor
/// of that resource, which shall change over time as the world evolves
/// and players interact with it.
// TODO: Consider whether we can use `i16` or similar here instead: `f32` has more resolution
// than we might need.
#[serde(rename = "r")]
#[serde(serialize_with = "crate::data::rugged_ser_enum_map::<_, _, _, 1>")]
#[serde(deserialize_with = "crate::data::rugged_de_enum_map::<_, _, _, 1>")]
res: EnumMap<ChunkResource, f32>,
}

396
rtsim/src/data/npc.rs Normal file
View File

@ -0,0 +1,396 @@
use crate::{
ai::Action,
data::{ReportId, Reports, Sentiments},
gen::name,
};
pub use common::rtsim::{NpcId, Profession};
use common::{
character::CharacterId,
comp,
grid::Grid,
rtsim::{
Actor, ChunkResource, FactionId, NpcAction, NpcActivity, Personality, SiteId, VehicleId,
},
store::Id,
terrain::CoordinateConversions,
};
use hashbrown::{HashMap, HashSet};
use rand::prelude::*;
use serde::{Deserialize, Serialize};
use slotmap::HopSlotMap;
use std::{
collections::VecDeque,
ops::{Deref, DerefMut},
};
use vek::*;
use world::{
civ::Track,
site::Site as WorldSite,
util::{RandomPerm, LOCALITY},
};
#[derive(Copy, Clone, Debug, Default)]
pub enum SimulationMode {
/// The NPC is unloaded and is being simulated via rtsim.
#[default]
Simulated,
/// The NPC has been loaded into the game world as an ECS entity.
Loaded,
}
#[derive(Clone)]
pub struct PathData<P, N> {
pub end: N,
pub path: VecDeque<P>,
pub repoll: bool,
}
#[derive(Clone, Default)]
pub struct PathingMemory {
pub intrasite_path: Option<(PathData<Vec2<i32>, Vec2<i32>>, Id<WorldSite>)>,
pub intersite_path: Option<(PathData<(Id<Track>, bool), SiteId>, usize)>,
}
#[derive(Default)]
pub struct Controller {
pub actions: Vec<NpcAction>,
pub activity: Option<NpcActivity>,
}
impl Controller {
pub fn do_idle(&mut self) { self.activity = None; }
pub fn do_goto(&mut self, wpos: Vec3<f32>, speed_factor: f32) {
self.activity = Some(NpcActivity::Goto(wpos, speed_factor));
}
pub fn do_gather(&mut self, resources: &'static [ChunkResource]) {
self.activity = Some(NpcActivity::Gather(resources));
}
pub fn do_hunt_animals(&mut self) { self.activity = Some(NpcActivity::HuntAnimals); }
pub fn do_dance(&mut self) { self.activity = Some(NpcActivity::Dance); }
pub fn say(&mut self, target: impl Into<Option<Actor>>, content: comp::Content) {
self.actions.push(NpcAction::Say(target.into(), content));
}
pub fn attack(&mut self, target: impl Into<Actor>) {
self.actions.push(NpcAction::Attack(target.into()));
}
}
pub struct Brain {
pub action: Box<dyn Action<!>>,
}
#[derive(Serialize, Deserialize)]
pub struct Npc {
// Persisted state
pub seed: u32,
/// Represents the location of the NPC.
pub wpos: Vec3<f32>,
pub body: comp::Body,
pub profession: Option<Profession>,
pub home: Option<SiteId>,
pub faction: Option<FactionId>,
pub riding: Option<Riding>,
pub is_dead: bool,
/// The [`Report`]s that the NPC is aware of.
pub known_reports: HashSet<ReportId>,
#[serde(default)]
pub personality: Personality,
#[serde(default)]
pub sentiments: Sentiments,
// Unpersisted state
#[serde(skip)]
pub chunk_pos: Option<Vec2<i32>>,
#[serde(skip)]
pub current_site: Option<SiteId>,
#[serde(skip)]
pub controller: Controller,
#[serde(skip)]
pub inbox: VecDeque<ReportId>,
/// Whether the NPC is in simulated or loaded mode (when rtsim is run on the
/// server, loaded corresponds to being within a loaded chunk). When in
/// loaded mode, the interactions of the NPC should not be simulated but
/// should instead be derived from the game.
#[serde(skip)]
pub mode: SimulationMode,
#[serde(skip)]
pub brain: Option<Brain>,
}
impl Clone for Npc {
fn clone(&self) -> Self {
Self {
seed: self.seed,
wpos: self.wpos,
profession: self.profession.clone(),
home: self.home,
faction: self.faction,
riding: self.riding.clone(),
is_dead: self.is_dead,
known_reports: self.known_reports.clone(),
body: self.body,
personality: self.personality,
sentiments: self.sentiments.clone(),
// Not persisted
chunk_pos: None,
current_site: Default::default(),
controller: Default::default(),
inbox: Default::default(),
mode: Default::default(),
brain: Default::default(),
}
}
}
impl Npc {
pub const PERM_ENTITY_CONFIG: u32 = 1;
const PERM_NAME: u32 = 0;
pub fn new(seed: u32, wpos: Vec3<f32>, body: comp::Body) -> Self {
Self {
seed,
wpos,
body,
personality: Default::default(),
sentiments: Default::default(),
profession: None,
home: None,
faction: None,
riding: None,
is_dead: false,
known_reports: Default::default(),
chunk_pos: None,
current_site: None,
controller: Default::default(),
inbox: Default::default(),
mode: SimulationMode::Simulated,
brain: None,
}
}
// TODO: have a dedicated `NpcBuilder` type for this.
pub fn with_personality(mut self, personality: Personality) -> Self {
self.personality = personality;
self
}
// TODO: have a dedicated `NpcBuilder` type for this.
pub fn with_profession(mut self, profession: impl Into<Option<Profession>>) -> Self {
self.profession = profession.into();
self
}
// TODO: have a dedicated `NpcBuilder` type for this.
pub fn with_home(mut self, home: impl Into<Option<SiteId>>) -> Self {
self.home = home.into();
self
}
// TODO: have a dedicated `NpcBuilder` type for this.
pub fn steering(mut self, vehicle: impl Into<Option<VehicleId>>) -> Self {
self.riding = vehicle.into().map(|vehicle| Riding {
vehicle,
steering: true,
});
self
}
// TODO: have a dedicated `NpcBuilder` type for this.
pub fn riding(mut self, vehicle: impl Into<Option<VehicleId>>) -> Self {
self.riding = vehicle.into().map(|vehicle| Riding {
vehicle,
steering: false,
});
self
}
// TODO: have a dedicated `NpcBuilder` type for this.
pub fn with_faction(mut self, faction: impl Into<Option<FactionId>>) -> Self {
self.faction = faction.into();
self
}
pub fn rng(&self, perm: u32) -> impl Rng { RandomPerm::new(self.seed.wrapping_add(perm)) }
// TODO: Don't make this depend on deterministic RNG, actually persist names
// once we've decided that we want to
pub fn get_name(&self) -> String { name::generate(&mut self.rng(Self::PERM_NAME)) }
pub fn cleanup(&mut self, reports: &Reports) {
// Clear old or superfluous sentiments
// TODO: It might be worth giving more important NPCs a higher sentiment
// 'budget' than less important ones.
self.sentiments
.cleanup(crate::data::sentiment::NPC_MAX_SENTIMENTS);
// Clear reports that have been forgotten
self.known_reports
.retain(|report| reports.contains_key(*report));
// TODO: Limit number of reports
// TODO: Clear old inbox items
}
}
#[derive(Clone, Serialize, Deserialize)]
pub struct Riding {
pub vehicle: VehicleId,
pub steering: bool,
}
#[derive(Clone, Serialize, Deserialize)]
pub enum VehicleKind {
Airship,
Boat,
}
// TODO: Merge into `Npc`?
#[derive(Clone, Serialize, Deserialize)]
pub struct Vehicle {
pub wpos: Vec3<f32>,
pub body: comp::ship::Body,
#[serde(skip)]
pub chunk_pos: Option<Vec2<i32>>,
#[serde(skip)]
pub driver: Option<Actor>,
/// Whether the Vehicle is in simulated or loaded mode (when rtsim is run on
/// the server, loaded corresponds to being within a loaded chunk). When
/// in loaded mode, the interactions of the Vehicle should not be
/// simulated but should instead be derived from the game.
#[serde(skip)]
pub mode: SimulationMode,
}
impl Vehicle {
pub fn new(wpos: Vec3<f32>, body: comp::ship::Body) -> Self {
Self {
wpos,
body,
chunk_pos: None,
driver: None,
mode: SimulationMode::Simulated,
}
}
pub fn get_body(&self) -> comp::Body { comp::Body::Ship(self.body) }
/// Max speed in block/s
pub fn get_speed(&self) -> f32 {
match self.body {
comp::ship::Body::DefaultAirship => 15.0,
comp::ship::Body::AirBalloon => 16.0,
comp::ship::Body::SailBoat => 12.0,
comp::ship::Body::Galleon => 13.0,
_ => 10.0,
}
}
}
#[derive(Default, Clone, Serialize, Deserialize)]
pub struct GridCell {
pub npcs: Vec<NpcId>,
pub vehicles: Vec<VehicleId>,
}
#[derive(Clone, Serialize, Deserialize)]
pub struct Npcs {
pub npcs: HopSlotMap<NpcId, Npc>,
pub vehicles: HopSlotMap<VehicleId, Vehicle>,
// TODO: This feels like it should be its own rtsim resource
// TODO: Consider switching to `common::util::SpatialGrid` instead
#[serde(skip, default = "construct_npc_grid")]
pub npc_grid: Grid<GridCell>,
#[serde(skip)]
pub character_map: HashMap<Vec2<i32>, Vec<(CharacterId, Vec3<f32>)>>,
}
impl Default for Npcs {
fn default() -> Self {
Self {
npcs: Default::default(),
vehicles: Default::default(),
npc_grid: construct_npc_grid(),
character_map: Default::default(),
}
}
}
fn construct_npc_grid() -> Grid<GridCell> { Grid::new(Vec2::zero(), Default::default()) }
impl Npcs {
pub fn create_npc(&mut self, npc: Npc) -> NpcId { self.npcs.insert(npc) }
pub fn create_vehicle(&mut self, vehicle: Vehicle) -> VehicleId {
self.vehicles.insert(vehicle)
}
/// Queries nearby npcs, not garantueed to work if radius > 32.0
// TODO: Find a more efficient way to implement this, it's currently
// (theoretically) O(n^2).
pub fn nearby(
&self,
this_npc: Option<NpcId>,
wpos: Vec3<f32>,
radius: f32,
) -> impl Iterator<Item = Actor> + '_ {
let chunk_pos = wpos.xy().as_().wpos_to_cpos();
let r_sqr = radius * radius;
LOCALITY
.into_iter()
.flat_map(move |neighbor| {
self.npc_grid.get(chunk_pos + neighbor).map(move |cell| {
cell.npcs
.iter()
.copied()
.filter(move |npc| {
self.npcs
.get(*npc)
.map_or(false, |npc| npc.wpos.distance_squared(wpos) < r_sqr)
&& Some(*npc) != this_npc
})
.map(Actor::Npc)
})
})
.flatten()
.chain(
self.character_map
.get(&chunk_pos)
.map(|characters| {
characters.iter().filter_map(move |(character, c_wpos)| {
if c_wpos.distance_squared(wpos) < r_sqr {
Some(Actor::Character(*character))
} else {
None
}
})
})
.into_iter()
.flatten(),
)
}
}
impl Deref for Npcs {
type Target = HopSlotMap<NpcId, Npc>;
fn deref(&self) -> &Self::Target { &self.npcs }
}
impl DerefMut for Npcs {
fn deref_mut(&mut self) -> &mut Self::Target { &mut self.npcs }
}

69
rtsim/src/data/report.rs Normal file
View File

@ -0,0 +1,69 @@
use common::{resources::TimeOfDay, rtsim::Actor};
use serde::{Deserialize, Serialize};
use slotmap::HopSlotMap;
use std::ops::Deref;
use vek::*;
slotmap::new_key_type! { pub struct ReportId; }
/// Represents a single piece of information known by an rtsim entity.
///
/// Reports are the medium through which rtsim represents information sharing
/// between NPCs, factions, and sites. They can represent deaths, attacks,
/// changes in diplomacy, or any other piece of information representing a
/// singular event that might be communicated.
///
/// Note that they should not be used to communicate sentiments like 'this actor
/// is friendly': the [`crate::data::Sentiment`] system should be used for that.
/// Some events might generate both a report and a change in sentiment. For
/// example, the murder of an NPC might generate both a murder report and highly
/// negative sentiments.
#[derive(Clone, Serialize, Deserialize)]
pub struct Report {
pub kind: ReportKind,
pub at: TimeOfDay,
}
impl Report {
/// The time, in in-game seconds, for which the report will be remembered
fn remember_for(&self) -> f64 {
const DAYS: f64 = 60.0 * 60.0 * 24.0;
match &self.kind {
ReportKind::Death { killer, .. } => {
if killer.is_some() {
// Murder is less easy to forget
DAYS * 15.0
} else {
DAYS * 5.0
}
},
}
}
}
#[derive(Copy, Clone, Serialize, Deserialize)]
pub enum ReportKind {
Death { actor: Actor, killer: Option<Actor> },
}
#[derive(Clone, Default, Serialize, Deserialize)]
pub struct Reports {
pub reports: HopSlotMap<ReportId, Report>,
}
impl Reports {
pub fn create(&mut self, report: Report) -> ReportId { self.reports.insert(report) }
pub fn cleanup(&mut self, current_time: TimeOfDay) {
// Forget reports that are too old
self.reports
.retain(|_, report| (current_time.0 - report.at.0).max(0.0) < report.remember_for());
// TODO: Limit global number of reports
}
}
impl Deref for Reports {
type Target = HopSlotMap<ReportId, Report>;
fn deref(&self) -> &Self::Target { &self.reports }
}

204
rtsim/src/data/sentiment.rs Normal file
View File

@ -0,0 +1,204 @@
use common::{
character::CharacterId,
rtsim::{Actor, FactionId, NpcId},
};
use hashbrown::HashMap;
use rand::prelude::*;
use serde::{Deserialize, Serialize};
use std::collections::BinaryHeap;
// Factions have a larger 'social memory' than individual NPCs and so we allow
// them to have more sentiments
pub const FACTION_MAX_SENTIMENTS: usize = 1024;
pub const NPC_MAX_SENTIMENTS: usize = 128;
/// Magic factor used to control sentiment decay speed (note: higher = slower
/// decay, for implementation reasons).
const DECAY_TIME_FACTOR: f32 = 1.0; //6.0; TODO: Use this value when we're happy that everything is working as intended
/// The target that a sentiment is felt toward.
// NOTE: More could be added to this! For example:
// - Animal species (dislikes spiders?)
// - Kind of food (likes meat?)
// - Occupations (hatred of hunters or chefs?)
// - Ideologies (dislikes democracy, likes monarchy?)
// - etc.
#[derive(Copy, Clone, Hash, PartialEq, Eq, Serialize, Deserialize, PartialOrd, Ord)]
pub enum Target {
Character(CharacterId),
Npc(NpcId),
Faction(FactionId),
}
impl From<NpcId> for Target {
fn from(npc: NpcId) -> Self { Self::Npc(npc) }
}
impl From<FactionId> for Target {
fn from(faction: FactionId) -> Self { Self::Faction(faction) }
}
impl From<CharacterId> for Target {
fn from(character: CharacterId) -> Self { Self::Character(character) }
}
impl From<Actor> for Target {
fn from(actor: Actor) -> Self {
match actor {
Actor::Character(character) => Self::Character(character),
Actor::Npc(npc) => Self::Npc(npc),
}
}
}
#[derive(Clone, Default, Serialize, Deserialize)]
pub struct Sentiments {
#[serde(rename = "m")]
map: HashMap<Target, Sentiment>,
}
impl Sentiments {
/// Return the sentiment that is felt toward the given target.
pub fn toward(&self, target: impl Into<Target>) -> Sentiment {
self.map.get(&target.into()).copied().unwrap_or_default()
}
/// Change the sentiment toward the given target by the given amount,
/// capping out at the given value.
pub fn change_by(&mut self, target: impl Into<Target>, change: f32, cap: f32) {
let target = target.into();
self.map.entry(target).or_default().change_by(change, cap);
}
/// Progressively decay the sentiment back to a neutral sentiment.
///
/// Note that sentiment get decay gets slower the harsher the sentiment is.
/// You can calculate the **average** number of seconds required for a
/// sentiment to neutral decay with the following formula:
///
/// ```ignore
/// seconds_until_neutrality = ((sentiment_value * 127 * DECAY_TIME_FACTOR) ^ 2) / 2
/// ```
///
/// For example, a positive (see [`Sentiment::POSITIVE`]) sentiment has a
/// value of `0.2`, so we get
///
/// ```ignore
/// seconds_until_neutrality = ((0.1 * 127 * DECAY_TIME_FACTOR) ^ 2) / 2 = ~2,903 seconds, or 48 minutes
/// ```
///
/// Some 'common' sentiment decay times are as follows:
///
/// - `POSITIVE`/`NEGATIVE`: ~48 minutes
/// - `ALLY`/`RIVAL`: ~7 hours
/// - `FRIEND`/`ENEMY`: ~29 hours
/// - `HERO`/`VILLAIN`: ~65 hours
pub fn decay(&mut self, rng: &mut impl Rng, dt: f32) {
self.map.retain(|_, sentiment| {
sentiment.decay(rng, dt);
// We can eliminate redundant sentiments that don't need remembering
!sentiment.is_redundant()
});
}
/// Clean up sentiments to avoid them growing too large
pub fn cleanup(&mut self, max_sentiments: usize) {
if self.map.len() > max_sentiments {
let mut sentiments = self.map
.iter()
// For each sentiment, calculate how valuable it is for us to remember.
// For now, we just use the absolute value of the sentiment but later on we might want to favour
// sentiments toward factions and other 'larger' groups over, say, sentiments toward players/other NPCs
.map(|(tgt, sentiment)| (sentiment.positivity.unsigned_abs(), *tgt))
.collect::<BinaryHeap<_>>();
// Remove the superfluous sentiments
for (_, tgt) in sentiments
.drain_sorted()
.take(self.map.len() - max_sentiments)
{
self.map.remove(&tgt);
}
}
}
}
#[derive(Copy, Clone, Default, Serialize, Deserialize)]
pub struct Sentiment {
/// How positive the sentiment is.
///
/// Using i8 to reduce on-disk memory footprint.
/// Semantically, this value is -1 <= x <= 1.
#[serde(rename = "p")]
positivity: i8,
}
impl Sentiment {
/// Substantial positive sentiments: NPC may go out of their way to help
/// actors associated with the target, greet them, etc.
pub const ALLY: f32 = 0.3;
/// Very negative sentiments: NPC may confront the actor, get aggressive
/// with them, or even use force against them.
pub const ENEMY: f32 = -0.6;
/// Very positive sentiments: NPC may join the actor as a companion,
/// encourage them to join their faction, etc.
pub const FRIEND: f32 = 0.6;
/// Extremely positive sentiments: NPC may switch sides to join the actor's
/// faction, protect them at all costs, turn against friends for them,
/// etc. Verging on cult-like behaviour.
pub const HERO: f32 = 0.8;
/// Minor negative sentiments: NPC might be less willing to provide
/// information, give worse trade deals, etc.
pub const NEGATIVE: f32 = -0.1;
/// Minor positive sentiments: NPC might be more willing to provide
/// information, give better trade deals, etc.
pub const POSITIVE: f32 = 0.1;
/// Substantial positive sentiments: NPC may reject attempts to trade or
/// avoid actors associated with the target, insult them, but will not
/// use physical force.
pub const RIVAL: f32 = -0.3;
/// Extremely negative sentiments: NPC may aggressively persue or hunt down
/// the actor, organise others around them to do the same, and will
/// generally try to harm the actor in any way they can.
pub const VILLAIN: f32 = -0.8;
fn value(&self) -> f32 { self.positivity as f32 * (1.0 / 126.0) }
fn change_by(&mut self, change: f32, cap: f32) {
// There's a bit of ceremony here for two reasons:
// 1) Very small changes should not be rounded to 0
// 2) Sentiment should never (over/under)flow
if change != 0.0 {
let abs = (change * 126.0).abs().clamp(1.0, 126.0) as i8;
let cap = (cap.abs().min(1.0) * 126.0) as i8;
self.positivity = if change > 0.0 {
self.positivity.saturating_add(abs).min(cap)
} else {
self.positivity.saturating_sub(abs).max(-cap)
};
}
}
fn decay(&mut self, rng: &mut impl Rng, dt: f32) {
if self.positivity != 0 {
// TODO: Find a slightly nicer way to have sentiment decay, perhaps even by
// remembering the last interaction instead of constant updates.
if rng.gen_bool(
(1.0 / (self.positivity.unsigned_abs() as f32 * DECAY_TIME_FACTOR.powi(2) * dt))
as f64,
) {
self.positivity -= self.positivity.signum();
}
}
}
/// Return `true` if the sentiment can be forgotten without changing
/// anything (i.e: is entirely neutral, the default stance).
fn is_redundant(&self) -> bool { self.positivity == 0 }
/// Returns `true` if the sentiment has reached the given threshold.
pub fn is(&self, val: f32) -> bool {
if val > 0.0 {
self.value() >= val
} else {
self.value() <= val
}
}
}

86
rtsim/src/data/site.rs Normal file
View File

@ -0,0 +1,86 @@
use crate::data::{ReportId, Reports};
pub use common::rtsim::SiteId;
use common::{
rtsim::{FactionId, NpcId},
store::Id,
};
use hashbrown::{HashMap, HashSet};
use serde::{Deserialize, Serialize};
use slotmap::HopSlotMap;
use std::ops::{Deref, DerefMut};
use vek::*;
use world::site::Site as WorldSite;
#[derive(Clone, Serialize, Deserialize)]
pub struct Site {
pub seed: u32,
pub wpos: Vec2<i32>,
pub faction: Option<FactionId>,
/// The [`Report`]s that the site tracks (you can imagine them being on a
/// noticeboard or something).
pub known_reports: HashSet<ReportId>,
/// The site generated during initial worldgen that this site corresponds
/// to.
///
/// Eventually, rtsim should replace initial worldgen's site system and this
/// will not be necessary.
///
/// When setting up rtsim state, we try to 'link' these two definitions of a
/// site: but if initial worldgen has changed, this might not be
/// possible. We try to delete sites that no longer exist during setup, but
/// this is an inherent fallible process. If linking fails, we try to
/// delete the site in rtsim2 in order to avoid an 'orphaned' site.
/// (TODO: create new sites for new initial worldgen sites that come into
/// being too).
#[serde(skip_serializing, skip_deserializing)]
pub world_site: Option<Id<WorldSite>>,
// Note: there's currently no guarantee that site populations are non-intersecting
#[serde(skip_serializing, skip_deserializing)]
pub population: HashSet<NpcId>,
}
impl Site {
pub fn with_faction(mut self, faction: impl Into<Option<FactionId>>) -> Self {
self.faction = faction.into();
self
}
pub fn cleanup(&mut self, reports: &Reports) {
// Clear reports that have been forgotten
self.known_reports
.retain(|report| reports.contains_key(*report));
// TODO: Limit number of reports
}
}
#[derive(Clone, Default, Serialize, Deserialize)]
pub struct Sites {
pub sites: HopSlotMap<SiteId, Site>,
#[serde(skip_serializing, skip_deserializing)]
pub world_site_map: HashMap<Id<WorldSite>, SiteId>,
}
impl Sites {
pub fn create(&mut self, site: Site) -> SiteId {
let world_site = site.world_site;
let key = self.sites.insert(site);
if let Some(world_site) = world_site {
self.world_site_map.insert(world_site, key);
}
key
}
}
impl Deref for Sites {
type Target = HopSlotMap<SiteId, Site>;
fn deref(&self) -> &Self::Target { &self.sites }
}
impl DerefMut for Sites {
fn deref_mut(&mut self) -> &mut Self::Target { &mut self.sites }
}

38
rtsim/src/event.rs Normal file
View File

@ -0,0 +1,38 @@
use crate::{RtState, Rule};
use common::{
resources::{Time, TimeOfDay},
rtsim::Actor,
};
use vek::*;
use world::{IndexRef, World};
pub trait Event: Clone + 'static {}
pub struct EventCtx<'a, R: Rule, E: Event> {
pub state: &'a RtState,
pub rule: &'a mut R,
pub event: &'a E,
pub world: &'a World,
pub index: IndexRef<'a>,
}
#[derive(Clone)]
pub struct OnSetup;
impl Event for OnSetup {}
#[derive(Clone)]
pub struct OnTick {
pub time_of_day: TimeOfDay,
pub time: Time,
pub tick: u64,
pub dt: f32,
}
impl Event for OnTick {}
#[derive(Clone)]
pub struct OnDeath {
pub actor: Actor,
pub wpos: Option<Vec3<f32>>,
pub killer: Option<Actor>,
}
impl Event for OnDeath {}

14
rtsim/src/gen/faction.rs Normal file
View File

@ -0,0 +1,14 @@
use crate::data::Faction;
use rand::prelude::*;
use world::{IndexRef, World};
impl Faction {
pub fn generate(_world: &World, _index: IndexRef, rng: &mut impl Rng) -> Self {
Self {
seed: rng.gen(),
leader: None,
good_or_evil: rng.gen(),
sentiments: Default::default(),
}
}
}

222
rtsim/src/gen/mod.rs Normal file
View File

@ -0,0 +1,222 @@
pub mod faction;
pub mod name;
pub mod site;
use crate::data::{
faction::Faction,
npc::{Npc, Npcs, Profession, Vehicle},
site::Site,
Data, Nature, CURRENT_VERSION,
};
use common::{
comp::{self, Body},
grid::Grid,
resources::TimeOfDay,
rtsim::{Personality, WorldSettings},
terrain::TerrainChunkSize,
vol::RectVolSize,
};
use rand::prelude::*;
use tracing::info;
use vek::*;
use world::{site::SiteKind, site2::PlotKind, IndexRef, World};
impl Data {
pub fn generate(settings: &WorldSettings, world: &World, index: IndexRef) -> Self {
let mut seed = [0; 32];
seed.iter_mut()
.zip(&mut index.seed.to_le_bytes())
.for_each(|(dst, src)| *dst = *src);
let mut rng = SmallRng::from_seed(seed);
let mut this = Self {
version: CURRENT_VERSION,
nature: Nature::generate(world),
npcs: Npcs {
npcs: Default::default(),
vehicles: Default::default(),
npc_grid: Grid::new(Vec2::zero(), Default::default()),
character_map: Default::default(),
},
sites: Default::default(),
factions: Default::default(),
reports: Default::default(),
tick: 0,
time_of_day: TimeOfDay(settings.start_time),
should_purge: false,
};
let initial_factions = (0..16)
.map(|_| {
let faction = Faction::generate(world, index, &mut rng);
let wpos = world
.sim()
.get_size()
.map2(TerrainChunkSize::RECT_SIZE, |e, sz| {
rng.gen_range(0..(e * sz) as i32)
});
(wpos, this.factions.create(faction))
})
.collect::<Vec<_>>();
info!("Generated {} rtsim factions.", this.factions.len());
// Register sites with rtsim
for (world_site_id, _) in index.sites.iter() {
let site = Site::generate(
world_site_id,
world,
index,
&initial_factions,
&this.factions,
&mut rng,
);
this.sites.create(site);
}
info!(
"Registering {} rtsim sites from world sites.",
this.sites.len()
);
// Spawn some test entities at the sites
for (site_id, site, site2) in this.sites.iter()
// TODO: Stupid. Only find site2 towns
.filter_map(|(site_id, site)| Some((site_id, site, site.world_site
.and_then(|ws| match &index.sites.get(ws).kind {
SiteKind::Refactor(site2)
| SiteKind::CliffTown(site2)
| SiteKind::SavannahPit(site2)
| SiteKind::DesertCity(site2) => Some(site2),
_ => None,
})?)))
{
let Some(good_or_evil) = site
.faction
.and_then(|f| this.factions.get(f))
.map(|f| f.good_or_evil)
else { continue };
let rand_wpos = |rng: &mut SmallRng, matches_plot: fn(&PlotKind) -> bool| {
let wpos2d = site2
.plots()
.filter(|plot| matches_plot(plot.kind()))
.choose(&mut thread_rng())
.map(|plot| site2.tile_center_wpos(plot.root_tile()))
.unwrap_or_else(|| site.wpos.map(|e| e + rng.gen_range(-10..10)));
wpos2d
.map(|e| e as f32 + 0.5)
.with_z(world.sim().get_alt_approx(wpos2d).unwrap_or(0.0))
};
let random_humanoid = |rng: &mut SmallRng| {
let species = comp::humanoid::ALL_SPECIES.choose(&mut *rng).unwrap();
Body::Humanoid(comp::humanoid::Body::random_with(rng, species))
};
let matches_buildings = (|kind: &PlotKind| {
matches!(
kind,
PlotKind::House(_) | PlotKind::Workshop(_) | PlotKind::Plaza
)
}) as _;
let matches_plazas = (|kind: &PlotKind| matches!(kind, PlotKind::Plaza)) as _;
if good_or_evil {
for _ in 0..site2.plots().len() {
this.npcs.create_npc(
Npc::new(
rng.gen(),
rand_wpos(&mut rng, matches_buildings),
random_humanoid(&mut rng),
)
.with_faction(site.faction)
.with_home(site_id)
.with_personality(Personality::random(&mut rng))
.with_profession(match rng.gen_range(0..20) {
0 => Profession::Hunter,
1 => Profession::Blacksmith,
2 => Profession::Chef,
3 => Profession::Alchemist,
5..=8 => Profession::Farmer,
9..=10 => Profession::Herbalist,
11..=16 => Profession::Guard,
_ => Profession::Adventurer(rng.gen_range(0..=3)),
}),
);
}
} else {
for _ in 0..15 {
this.npcs.create_npc(
Npc::new(
rng.gen(),
rand_wpos(&mut rng, matches_buildings),
random_humanoid(&mut rng),
)
.with_personality(Personality::random_evil(&mut rng))
.with_faction(site.faction)
.with_home(site_id)
.with_profession(Profession::Cultist),
);
}
}
// Merchants
if good_or_evil {
for _ in 0..(site2.plots().len() / 6) + 1 {
this.npcs.create_npc(
Npc::new(
rng.gen(),
rand_wpos(&mut rng, matches_plazas),
random_humanoid(&mut rng),
)
.with_home(site_id)
.with_personality(Personality::random_good(&mut rng))
.with_profession(Profession::Merchant),
);
}
}
if rng.gen_bool(0.4) {
let wpos = rand_wpos(&mut rng, matches_plazas) + Vec3::unit_z() * 50.0;
let vehicle_id = this
.npcs
.create_vehicle(Vehicle::new(wpos, comp::body::ship::Body::DefaultAirship));
this.npcs.create_npc(
Npc::new(rng.gen(), wpos, random_humanoid(&mut rng))
.with_home(site_id)
.with_profession(Profession::Captain)
.with_personality(Personality::random_good(&mut rng))
.steering(vehicle_id),
);
}
}
for (site_id, site) in this.sites.iter()
// TODO: Stupid
.filter(|(_, site)| site.world_site.map_or(false, |ws|
matches!(&index.sites.get(ws).kind, SiteKind::Dungeon(_))))
{
let rand_wpos = |rng: &mut SmallRng| {
let wpos2d = site.wpos.map(|e| e + rng.gen_range(-10..10));
wpos2d
.map(|e| e as f32 + 0.5)
.with_z(world.sim().get_alt_approx(wpos2d).unwrap_or(0.0))
};
let species = [
comp::body::bird_large::Species::Phoenix,
comp::body::bird_large::Species::Cockatrice,
comp::body::bird_large::Species::Roc,
]
.choose(&mut rng)
.unwrap();
this.npcs.create_npc(
Npc::new(
rng.gen(),
rand_wpos(&mut rng),
Body::BirdLarge(comp::body::bird_large::Body::random_with(&mut rng, species)),
)
.with_home(site_id),
);
}
info!("Generated {} rtsim NPCs.", this.npcs.len());
this
}
}

22
rtsim/src/gen/name.rs Normal file
View File

@ -0,0 +1,22 @@
use rand::prelude::*;
pub fn generate(rng: &mut impl Rng) -> String {
let starts = ["ad", "tr", "b", "l", "p", "d", "r", "w", "t", "fr", "s"];
let vowels = ["o", "e", "a", "i"];
let cons = ["m", "d", "st", "n", "y", "gh", "s"];
let mut name = String::new();
name += starts.choose(rng).unwrap();
for _ in 0..rng.gen_range(1..=3) {
name += vowels.choose(rng).unwrap();
name += cons.choose(rng).unwrap();
}
// Make the first letter uppercase (hacky)
name.chars()
.enumerate()
.map(|(i, c)| if i == 0 { c.to_ascii_uppercase() } else { c })
.collect()
}

58
rtsim/src/gen/site.rs Normal file
View File

@ -0,0 +1,58 @@
use crate::data::{FactionId, Factions, Site};
use common::store::Id;
use rand::prelude::*;
use vek::*;
use world::{
site::{Site as WorldSite, SiteKind},
IndexRef, World,
};
impl Site {
pub fn generate(
world_site_id: Id<WorldSite>,
_world: &World,
index: IndexRef,
nearby_factions: &[(Vec2<i32>, FactionId)],
factions: &Factions,
rng: &mut impl Rng,
) -> Self {
let world_site = index.sites.get(world_site_id);
let wpos = world_site.get_origin();
// TODO: This is stupid, do better
let good_or_evil = match &world_site.kind {
// Good
SiteKind::Refactor(_)
| SiteKind::CliffTown(_)
| SiteKind::DesertCity(_)
| SiteKind::SavannahPit(_) => Some(true),
// Evil
SiteKind::Dungeon(_) | SiteKind::ChapelSite(_) | SiteKind::Gnarling(_) => Some(false),
// Neutral
SiteKind::Settlement(_)
| SiteKind::Castle(_)
| SiteKind::Tree(_)
| SiteKind::GiantTree(_)
| SiteKind::Bridge(_) => None,
};
Self {
seed: rng.gen(),
wpos,
world_site: Some(world_site_id),
faction: good_or_evil.and_then(|good_or_evil| {
nearby_factions
.iter()
.filter(|(_, faction)| {
factions
.get(*faction)
.map_or(false, |f| f.good_or_evil == good_or_evil)
})
.min_by_key(|(faction_wpos, _)| faction_wpos.distance_squared(wpos))
.map(|(_, faction)| *faction)
}),
population: Default::default(),
known_reports: Default::default(),
}
}
}

189
rtsim/src/lib.rs Normal file
View File

@ -0,0 +1,189 @@
#![feature(
never_type,
try_blocks,
generator_trait,
generators,
trait_alias,
trait_upcasting,
control_flow_enum,
let_chains,
binary_heap_drain_sorted
)]
pub mod ai;
pub mod data;
pub mod event;
pub mod gen;
pub mod rule;
pub use self::{
data::Data,
event::{Event, EventCtx, OnTick},
rule::{Rule, RuleError},
};
use anymap2::SendSyncAnyMap;
use atomic_refcell::AtomicRefCell;
use common::resources::{Time, TimeOfDay};
use std::{
any::type_name,
ops::{Deref, DerefMut},
};
use tracing::{error, info};
use world::{IndexRef, World};
pub struct RtState {
resources: SendSyncAnyMap,
rules: SendSyncAnyMap,
event_handlers: SendSyncAnyMap,
}
type RuleState<R> = AtomicRefCell<R>;
type EventHandlersOf<E> = Vec<Box<dyn Fn(&RtState, &World, IndexRef, &E) + Send + Sync + 'static>>;
impl RtState {
pub fn new(data: Data) -> Self {
let mut this = Self {
resources: SendSyncAnyMap::new(),
rules: SendSyncAnyMap::new(),
event_handlers: SendSyncAnyMap::new(),
}
.with_resource(data);
this.start_default_rules();
this
}
pub fn with_resource<R: Send + Sync + 'static>(mut self, r: R) -> Self {
self.resources.insert(AtomicRefCell::new(r));
self
}
fn start_default_rules(&mut self) {
info!("Starting default rtsim rules...");
self.start_rule::<rule::migrate::Migrate>();
self.start_rule::<rule::replenish_resources::ReplenishResources>();
self.start_rule::<rule::report::ReportEvents>();
self.start_rule::<rule::sync_npcs::SyncNpcs>();
self.start_rule::<rule::simulate_npcs::SimulateNpcs>();
self.start_rule::<rule::npc_ai::NpcAi>();
self.start_rule::<rule::cleanup::CleanUp>();
}
pub fn start_rule<R: Rule>(&mut self) {
info!("Initiating '{}' rule...", type_name::<R>());
match R::start(self) {
Ok(rule) => {
self.rules.insert::<RuleState<R>>(AtomicRefCell::new(rule));
},
Err(e) => error!("Error when initiating '{}' rule: {}", type_name::<R>(), e),
}
}
fn rule_mut<R: Rule>(&self) -> impl DerefMut<Target = R> + '_ {
self.rules
.get::<RuleState<R>>()
.unwrap_or_else(|| {
panic!(
"Tried to access rule '{}' but it does not exist",
type_name::<R>()
)
})
.borrow_mut()
}
// TODO: Consider whether it's worth explicitly calling rule event handlers
// instead of allowing them to bind event handlers. Less modular, but
// potentially easier to deal with data dependencies?
pub fn bind<R: Rule, E: Event>(
&mut self,
f: impl FnMut(EventCtx<R, E>) + Send + Sync + 'static,
) {
let f = AtomicRefCell::new(f);
self.event_handlers
.entry::<EventHandlersOf<E>>()
.or_default()
.push(Box::new(move |state, world, index, event| {
(f.borrow_mut())(EventCtx {
state,
rule: &mut state.rule_mut(),
event,
world,
index,
})
}));
}
pub fn data(&self) -> impl Deref<Target = Data> + '_ { self.resource() }
pub fn data_mut(&self) -> impl DerefMut<Target = Data> + '_ { self.resource_mut() }
pub fn get_data_mut(&mut self) -> &mut Data { self.get_resource_mut() }
pub fn resource<R: Send + Sync + 'static>(&self) -> impl Deref<Target = R> + '_ {
self.resources
.get::<AtomicRefCell<R>>()
.unwrap_or_else(|| {
panic!(
"Tried to access resource '{}' but it does not exist",
type_name::<R>()
)
})
.borrow()
}
pub fn get_resource_mut<R: Send + Sync + 'static>(&mut self) -> &mut R {
self.resources
.get_mut::<AtomicRefCell<R>>()
.unwrap_or_else(|| {
panic!(
"Tried to access resource '{}' but it does not exist",
type_name::<R>()
)
})
.get_mut()
}
pub fn resource_mut<R: Send + Sync + 'static>(&self) -> impl DerefMut<Target = R> + '_ {
self.resources
.get::<AtomicRefCell<R>>()
.unwrap_or_else(|| {
panic!(
"Tried to access resource '{}' but it does not exist",
type_name::<R>()
)
})
.borrow_mut()
}
pub fn emit<E: Event>(&mut self, e: E, world: &World, index: IndexRef) {
// TODO: Queue these events up and handle them on a regular rtsim tick instead
// of executing their handlers immediately.
if let Some(handlers) = self.event_handlers.get::<EventHandlersOf<E>>() {
handlers.iter().for_each(|f| f(self, world, index, &e));
}
}
pub fn tick(
&mut self,
world: &World,
index: IndexRef,
time_of_day: TimeOfDay,
time: Time,
dt: f32,
) {
let tick = {
let mut data = self.data_mut();
data.time_of_day = time_of_day;
data.tick += 1;
data.tick
};
let event = OnTick {
time_of_day,
tick,
time,
dt,
};
self.emit(event, world, index);
}
}

29
rtsim/src/rule.rs Normal file
View File

@ -0,0 +1,29 @@
pub mod cleanup;
pub mod migrate;
pub mod npc_ai;
pub mod replenish_resources;
pub mod report;
pub mod simulate_npcs;
pub mod sync_npcs;
use super::RtState;
use std::fmt;
#[derive(Debug)]
pub enum RuleError {
NoSuchRule(&'static str),
}
impl fmt::Display for RuleError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::NoSuchRule(r) => {
write!(f, "tried to fetch rule state '{}' but it does not exist", r)
},
}
}
}
pub trait Rule: Sized + Send + Sync + 'static {
fn start(rtstate: &mut RtState) -> Result<Self, RuleError>;
}

70
rtsim/src/rule/cleanup.rs Normal file
View File

@ -0,0 +1,70 @@
use crate::{event::OnTick, RtState, Rule, RuleError};
use rand::prelude::*;
use rand_chacha::ChaChaRng;
/// Prevent performing cleanup for every NPC every tick
const NPC_SENTIMENT_TICK_SKIP: u64 = 30;
const NPC_CLEANUP_TICK_SKIP: u64 = 100;
const FACTION_CLEANUP_TICK_SKIP: u64 = 30;
const SITE_CLEANUP_TICK_SKIP: u64 = 30;
/// A rule that cleans up data structures in rtsim: removing old reports,
/// irrelevant sentiments, etc.
///
/// Also performs sentiment decay (although this should be moved elsewhere)
pub struct CleanUp;
impl Rule for CleanUp {
fn start(rtstate: &mut RtState) -> Result<Self, RuleError> {
rtstate.bind::<Self, OnTick>(|ctx| {
let data = &mut *ctx.state.data_mut();
let mut rng = ChaChaRng::from_seed(thread_rng().gen::<[u8; 32]>());
// TODO: Use `.into_par_iter()` for these by implementing rayon traits in upstream slotmap.
// Decay NPC sentiments
data.npcs
.iter_mut()
// Only cleanup NPCs every few ticks
.filter(|(_, npc)| (npc.seed as u64 + ctx.event.tick) % NPC_SENTIMENT_TICK_SKIP == 0)
.for_each(|(_, npc)| npc.sentiments.decay(&mut rng, ctx.event.dt * NPC_SENTIMENT_TICK_SKIP as f32));
// Remove dead NPCs
// TODO: Don't do this every tick, find a sensible way to gradually remove dead NPCs after they've been
// forgotten
data.npcs
.retain(|npc_id, npc| if npc.is_dead {
// Remove NPC from home population
if let Some(home) = npc.home.and_then(|home| data.sites.get_mut(home)) {
home.population.remove(&npc_id);
}
false
} else {
true
});
// Clean up entities
data.npcs
.iter_mut()
.filter(|(_, npc)| (npc.seed as u64 + ctx.event.tick) % NPC_CLEANUP_TICK_SKIP == 0)
.for_each(|(_, npc)| npc.cleanup(&data.reports));
// Clean up factions
data.factions
.iter_mut()
.filter(|(_, faction)| (faction.seed as u64 + ctx.event.tick) % FACTION_CLEANUP_TICK_SKIP == 0)
.for_each(|(_, faction)| faction.cleanup());
// Clean up sites
data.sites
.iter_mut()
.filter(|(_, site)| (site.seed as u64 + ctx.event.tick) % SITE_CLEANUP_TICK_SKIP == 0)
.for_each(|(_, site)| site.cleanup(&data.reports));
// Clean up old reports
data.reports.cleanup(data.time_of_day);
});
Ok(Self)
}
}

87
rtsim/src/rule/migrate.rs Normal file
View File

@ -0,0 +1,87 @@
use crate::{data::Site, event::OnSetup, RtState, Rule, RuleError};
use rand::prelude::*;
use rand_chacha::ChaChaRng;
use tracing::warn;
use world::site::SiteKind;
/// This rule runs at rtsim startup and broadly acts to perform some primitive
/// migration/sanitisation in order to ensure that the state of rtsim is mostly
/// sensible.
pub struct Migrate;
impl Rule for Migrate {
fn start(rtstate: &mut RtState) -> Result<Self, RuleError> {
rtstate.bind::<Self, OnSetup>(|ctx| {
let data = &mut *ctx.state.data_mut();
let mut rng = ChaChaRng::from_seed(thread_rng().gen::<[u8; 32]>());
// Delete rtsim sites that don't correspond to a world site
data.sites.sites.retain(|site_id, site| {
if let Some((world_site_id, _)) = ctx
.index
.sites
.iter()
.find(|(_, world_site)| world_site.get_origin() == site.wpos)
{
site.world_site = Some(world_site_id);
data.sites.world_site_map.insert(world_site_id, site_id);
true
} else {
warn!(
"{:?} is no longer valid because the site it was derived from no longer \
exists. It will now be deleted.",
site_id
);
false
}
});
// Generate rtsim sites for world sites that don't have a corresponding rtsim
// site yet
for (world_site_id, _) in ctx.index.sites.iter() {
if !data.sites.values().any(|site| {
site.world_site
.expect("Rtsim site not assigned to world site")
== world_site_id
}) {
warn!(
"{:?} is new and does not have a corresponding rtsim site. One will now \
be generated afresh.",
world_site_id
);
data.sites.create(Site::generate(
world_site_id,
ctx.world,
ctx.index,
&[],
&data.factions,
&mut rng,
));
}
}
// Reassign NPCs to sites if their old one was deleted. If they were already homeless, no need to do anything.
for npc in data.npcs.values_mut() {
if let Some(home) = npc.home
&& !data.sites.contains_key(home)
{
// Choose the closest habitable site as the new home for the NPC
npc.home = data.sites.sites
.iter()
.filter(|(_, site)| {
// TODO: This is a bit silly, but needs to wait on the removal of site1
site.world_site.map_or(false, |ws| matches!(&ctx.index.sites.get(ws).kind, SiteKind::Refactor(_)
| SiteKind::CliffTown(_)
| SiteKind::SavannahPit(_)
| SiteKind::DesertCity(_)))
})
.min_by_key(|(_, site)| site.wpos.as_().distance_squared(npc.wpos.xy()) as i32)
.map(|(site_id, _)| site_id);
}
}
});
Ok(Self)
}
}

1034
rtsim/src/rule/npc_ai.rs Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,42 @@
use crate::{event::OnTick, RtState, Rule, RuleError};
use rand::prelude::*;
pub struct ReplenishResources;
/// Take 1 hour to replenish resources entirely. Makes farming unviable, but
/// probably still poorly balanced.
// TODO: Different rates for different resources?
// TODO: Non-renewable resources?
pub const REPLENISH_TIME: f32 = 60.0 * 60.0;
/// How many chunks should be replenished per tick?
// TODO: It should be possible to optimise this be remembering the last
// modification time for each chunk, then lazily projecting forward using a
// closed-form solution to the replenishment to calculate resources in a lazy
// manner.
pub const REPLENISH_PER_TICK: usize = 8192;
impl Rule for ReplenishResources {
fn start(rtstate: &mut RtState) -> Result<Self, RuleError> {
rtstate.bind::<Self, OnTick>(|ctx| {
let world_size = ctx.world.sim().get_size();
let mut data = ctx.state.data_mut();
// How much should be replenished for each chosen chunk to hit our target
// replenishment rate?
let replenish_amount = world_size.product() as f32
* ctx.event.dt
* (1.0 / REPLENISH_TIME / REPLENISH_PER_TICK as f32);
for _ in 0..REPLENISH_PER_TICK {
let key = world_size.map(|e| thread_rng().gen_range(0..e as i32));
let mut res = data.nature.get_chunk_resources(key);
for (_, res) in &mut res {
*res = (*res + replenish_amount).clamp(0.0, 1.0);
}
data.nature.set_chunk_resources(key, res);
}
});
Ok(Self)
}
}

46
rtsim/src/rule/report.rs Normal file
View File

@ -0,0 +1,46 @@
use crate::{
data::{report::ReportKind, Report},
event::{EventCtx, OnDeath},
RtState, Rule, RuleError,
};
pub struct ReportEvents;
impl Rule for ReportEvents {
fn start(rtstate: &mut RtState) -> Result<Self, RuleError> {
rtstate.bind::<Self, OnDeath>(on_death);
Ok(Self)
}
}
fn on_death(ctx: EventCtx<ReportEvents, OnDeath>) {
let data = &mut *ctx.state.data_mut();
if let Some(wpos) = ctx.event.wpos {
let nearby = data
.npcs
.nearby(None, wpos, 32.0)
.filter_map(|actor| actor.npc())
.collect::<Vec<_>>();
if !nearby.is_empty() {
let report = data.reports.create(Report {
kind: ReportKind::Death {
actor: ctx.event.actor,
killer: ctx.event.killer,
},
at: data.time_of_day,
});
// TODO: Don't push report to NPC inboxes, have a dedicated data structure that
// tracks reports by chunks and then have NPCs decide to query this
// data structure in their own time.
for npc_id in nearby {
if let Some(npc) = data.npcs.get_mut(npc_id) {
npc.inbox.push_back(report);
}
}
}
}
}

View File

@ -0,0 +1,275 @@
use crate::{
data::{npc::SimulationMode, Npc},
event::{EventCtx, OnDeath, OnSetup, OnTick},
RtState, Rule, RuleError,
};
use common::{
comp::{self, Body},
rtsim::{Actor, NpcAction, NpcActivity, Personality},
terrain::CoordinateConversions,
};
use rand::prelude::*;
use rand_chacha::ChaChaRng;
use tracing::{error, warn};
use world::site::SiteKind;
pub struct SimulateNpcs;
impl Rule for SimulateNpcs {
fn start(rtstate: &mut RtState) -> Result<Self, RuleError> {
rtstate.bind::<Self, OnSetup>(on_setup);
rtstate.bind::<Self, OnDeath>(on_death);
rtstate.bind::<Self, OnTick>(on_tick);
Ok(Self)
}
}
fn on_setup(ctx: EventCtx<SimulateNpcs, OnSetup>) {
let data = &mut *ctx.state.data_mut();
// Add riders to vehicles
for (npc_id, npc) in data.npcs.npcs.iter_mut() {
if let Some(ride) = &npc.riding {
if let Some(vehicle) = data.npcs.vehicles.get_mut(ride.vehicle) {
let actor = Actor::Npc(npc_id);
if ride.steering && vehicle.driver.replace(actor).is_some() {
error!("Replaced driver");
npc.riding = None;
}
}
}
}
}
fn on_death(ctx: EventCtx<SimulateNpcs, OnDeath>) {
let data = &mut *ctx.state.data_mut();
if let Actor::Npc(npc_id) = ctx.event.actor
&& let Some(npc) = data.npcs.get(npc_id)
{
let mut rng = ChaChaRng::from_seed(thread_rng().gen::<[u8; 32]>());
// Respawn dead NPCs
let details = match npc.body {
Body::Humanoid(_) => {
if let Some((site_id, site)) = data
.sites
.iter()
.filter(|(id, site)| {
Some(*id) != npc.home
&& (npc.faction.is_none() || site.faction == npc.faction)
&& site.world_site.map_or(false, |s| {
matches!(ctx.index.sites.get(s).kind, SiteKind::Refactor(_)
| SiteKind::CliffTown(_)
| SiteKind::SavannahPit(_)
| SiteKind::DesertCity(_))
})
})
.min_by_key(|(_, site)| site.population.len())
{
let rand_wpos = |rng: &mut ChaChaRng| {
let wpos2d = site.wpos.map(|e| e + rng.gen_range(-10..10));
wpos2d
.map(|e| e as f32 + 0.5)
.with_z(ctx.world.sim().get_alt_approx(wpos2d).unwrap_or(0.0))
};
let random_humanoid = |rng: &mut ChaChaRng| {
let species = comp::humanoid::ALL_SPECIES.choose(&mut *rng).unwrap();
Body::Humanoid(comp::humanoid::Body::random_with(rng, species))
};
let npc_id = data.spawn_npc(
Npc::new(rng.gen(), rand_wpos(&mut rng), random_humanoid(&mut rng))
.with_personality(Personality::random(&mut rng))
.with_home(site_id)
.with_faction(npc.faction)
.with_profession(npc.profession.clone()),
);
Some((npc_id, site_id))
} else {
warn!("No site found for respawning humanoid");
None
}
},
Body::BirdLarge(_) => {
if let Some((site_id, site)) = data
.sites
.iter()
.filter(|(id, site)| {
Some(*id) != npc.home
&& site.world_site.map_or(false, |s| {
matches!(ctx.index.sites.get(s).kind, SiteKind::Dungeon(_))
})
})
.min_by_key(|(_, site)| site.population.len())
{
let rand_wpos = |rng: &mut ChaChaRng| {
let wpos2d = site.wpos.map(|e| e + rng.gen_range(-10..10));
wpos2d
.map(|e| e as f32 + 0.5)
.with_z(ctx.world.sim().get_alt_approx(wpos2d).unwrap_or(0.0))
};
let species = [
comp::body::bird_large::Species::Phoenix,
comp::body::bird_large::Species::Cockatrice,
comp::body::bird_large::Species::Roc,
]
.choose(&mut rng)
.unwrap();
let npc_id = data.npcs.create_npc(
Npc::new(
rng.gen(),
rand_wpos(&mut rng),
Body::BirdLarge(comp::body::bird_large::Body::random_with(
&mut rng, species,
)),
)
.with_home(site_id),
);
Some((npc_id, site_id))
} else {
warn!("No site found for respawning bird");
None
}
},
body => {
error!("Tried to respawn rtsim NPC with invalid body: {:?}", body);
None
},
};
// Add the NPC to their home site
if let Some((npc_id, home_site)) = details {
if let Some(home) = data.sites.get_mut(home_site) {
home.population.insert(npc_id);
}
}
} else {
error!("Trying to respawn non-existent NPC");
}
}
fn on_tick(ctx: EventCtx<SimulateNpcs, OnTick>) {
let data = &mut *ctx.state.data_mut();
for npc in data
.npcs
.npcs
.values_mut()
.filter(|npc| matches!(npc.mode, SimulationMode::Simulated) && !npc.is_dead)
{
// Simulate NPC movement when riding
if let Some(riding) = &npc.riding {
if let Some(vehicle) = data.npcs.vehicles.get_mut(riding.vehicle) {
match npc.controller.activity {
// If steering, the NPC controls the vehicle's motion
Some(NpcActivity::Goto(target, speed_factor)) if riding.steering => {
let diff = target.xy() - vehicle.wpos.xy();
let dist2 = diff.magnitude_squared();
if dist2 > 0.5f32.powi(2) {
let mut wpos = vehicle.wpos
+ (diff
* (vehicle.get_speed() * speed_factor * ctx.event.dt
/ dist2.sqrt())
.min(1.0))
.with_z(0.0);
let is_valid = match vehicle.body {
common::comp::ship::Body::DefaultAirship
| common::comp::ship::Body::AirBalloon => true,
common::comp::ship::Body::SailBoat
| common::comp::ship::Body::Galleon => {
let chunk_pos = wpos.xy().as_().wpos_to_cpos();
ctx.world
.sim()
.get(chunk_pos)
.map_or(true, |f| f.river.river_kind.is_some())
},
_ => false,
};
if is_valid {
match vehicle.body {
common::comp::ship::Body::DefaultAirship
| common::comp::ship::Body::AirBalloon => {
if let Some(alt) = ctx
.world
.sim()
.get_alt_approx(wpos.xy().as_())
.filter(|alt| wpos.z < *alt)
{
wpos.z = alt;
}
},
common::comp::ship::Body::SailBoat
| common::comp::ship::Body::Galleon => {
wpos.z = ctx
.world
.sim()
.get_interpolated(
wpos.xy().map(|e| e as i32),
|chunk| chunk.water_alt,
)
.unwrap_or(0.0);
},
_ => {},
}
vehicle.wpos = wpos;
}
}
},
// When riding, other actions are disabled
Some(
NpcActivity::Goto(_, _)
| NpcActivity::Gather(_)
| NpcActivity::HuntAnimals
| NpcActivity::Dance,
) => {},
None => {},
}
npc.wpos = vehicle.wpos;
} else {
// Vehicle doens't exist anymore
npc.riding = None;
}
// If not riding, we assume they're just walking
} else {
match npc.controller.activity {
// Move NPCs if they have a target destination
Some(NpcActivity::Goto(target, speed_factor)) => {
let diff = target.xy() - npc.wpos.xy();
let dist2 = diff.magnitude_squared();
if dist2 > 0.5f32.powi(2) {
npc.wpos += (diff
* (npc.body.max_speed_approx() * speed_factor * ctx.event.dt
/ dist2.sqrt())
.min(1.0))
.with_z(0.0);
}
},
Some(NpcActivity::Gather(_) | NpcActivity::HuntAnimals | NpcActivity::Dance) => {
// TODO: Maybe they should walk around randomly
// when gathering resources?
},
None => {},
}
}
// Consume NPC actions
for action in std::mem::take(&mut npc.controller.actions) {
match action {
NpcAction::Say(_, _) => {}, // Currently, just swallow interactions
NpcAction::Attack(_) => {}, // TODO: Implement simulated combat
}
}
// Make sure NPCs remain on the surface
npc.wpos.z = ctx
.world
.sim()
.get_surface_alt_approx(npc.wpos.xy().map(|e| e as i32))
.unwrap_or(0.0)
+ npc.body.flying_height();
}
}

111
rtsim/src/rule/sync_npcs.rs Normal file
View File

@ -0,0 +1,111 @@
use crate::{
event::{EventCtx, OnDeath, OnSetup, OnTick},
RtState, Rule, RuleError,
};
use common::{grid::Grid, rtsim::Actor, terrain::CoordinateConversions};
pub struct SyncNpcs;
impl Rule for SyncNpcs {
fn start(rtstate: &mut RtState) -> Result<Self, RuleError> {
rtstate.bind::<Self, OnSetup>(on_setup);
rtstate.bind::<Self, OnDeath>(on_death);
rtstate.bind::<Self, OnTick>(on_tick);
Ok(Self)
}
}
fn on_setup(ctx: EventCtx<SyncNpcs, OnSetup>) {
let data = &mut *ctx.state.data_mut();
// Create NPC grid
data.npcs.npc_grid = Grid::new(ctx.world.sim().get_size().as_(), Default::default());
// Add NPCs to home population
for (npc_id, npc) in data.npcs.npcs.iter() {
if let Some(home) = npc.home.and_then(|home| data.sites.get_mut(home)) {
home.population.insert(npc_id);
}
}
}
fn on_death(ctx: EventCtx<SyncNpcs, OnDeath>) {
let data = &mut *ctx.state.data_mut();
if let Actor::Npc(npc_id) = ctx.event.actor {
if let Some(npc) = data.npcs.get_mut(npc_id) {
// Mark the NPC as dead, allowing us to clear them up later
npc.is_dead = true;
}
}
}
fn on_tick(ctx: EventCtx<SyncNpcs, OnTick>) {
let data = &mut *ctx.state.data_mut();
// Update vehicle grid cells
for (vehicle_id, vehicle) in data.npcs.vehicles.iter_mut() {
let chunk_pos = vehicle.wpos.xy().as_().wpos_to_cpos();
if vehicle.chunk_pos != Some(chunk_pos) {
if let Some(cell) = vehicle
.chunk_pos
.and_then(|chunk_pos| data.npcs.npc_grid.get_mut(chunk_pos))
{
if let Some(index) = cell.vehicles.iter().position(|id| *id == vehicle_id) {
cell.vehicles.swap_remove(index);
}
}
vehicle.chunk_pos = Some(chunk_pos);
if let Some(cell) = data.npcs.npc_grid.get_mut(chunk_pos) {
cell.vehicles.push(vehicle_id);
}
}
}
for (npc_id, npc) in data.npcs.npcs.iter_mut() {
// Update the NPC's current site, if any
npc.current_site = ctx
.world
.sim()
.get(npc.wpos.xy().as_().wpos_to_cpos())
.and_then(|chunk| {
chunk
.sites
.iter()
.find_map(|site| data.sites.world_site_map.get(site).copied())
});
// Share known reports with current site, if it's our home
// TODO: Only share new reports
if let Some(current_site) = npc.current_site
&& Some(current_site) == npc.home
{
if let Some(site) = data.sites.get_mut(current_site) {
// TODO: Sites should have an inbox and their own AI code
site.known_reports.extend(npc.known_reports
.iter()
.copied());
npc.inbox.extend(site.known_reports
.iter()
.copied()
.filter(|report| !npc.known_reports.contains(report)));
}
}
// Update the NPC's grid cell
let chunk_pos = npc.wpos.xy().as_().wpos_to_cpos();
if npc.chunk_pos != Some(chunk_pos) {
if let Some(cell) = npc
.chunk_pos
.and_then(|chunk_pos| data.npcs.npc_grid.get_mut(chunk_pos))
{
if let Some(index) = cell.npcs.iter().position(|id| *id == npc_id) {
cell.npcs.swap_remove(index);
}
}
npc.chunk_pos = Some(chunk_pos);
if let Some(cell) = data.npcs.npc_grid.get_mut(chunk_pos) {
cell.npcs.push(npc_id);
}
}
}
}

View File

@ -23,6 +23,7 @@ common-state = { package = "veloren-common-state", path = "../common/state" }
common-systems = { package = "veloren-common-systems", path = "../common/systems" }
common-net = { package = "veloren-common-net", path = "../common/net" }
world = { package = "veloren-world", path = "../world" }
rtsim = { package = "veloren-rtsim", path = "../rtsim" }
network = { package = "veloren-network", path = "../network", features = ["metrics", "compression", "quic"], default-features = false }
server-agent = {package = "veloren-server-agent", path = "agent"}
@ -63,6 +64,7 @@ authc = { git = "https://gitlab.com/veloren/auth.git", rev = "fb3dcbc4962b367253
slab = "0.4"
rand_distr = "0.4.0"
enumset = "1.0.8"
enum-map = "2.4"
noise = { version = "0.7", default-features = false }
censor = "0.2"

View File

@ -9,10 +9,12 @@ use-dyn-lib = ["common-dynlib"]
be-dyn-lib = []
[dependencies]
common = {package = "veloren-common", path = "../../common"}
common = { package = "veloren-common", path = "../../common"}
common-base = { package = "veloren-common-base", path = "../../common/base" }
common-net = { package = "veloren-common-net", path = "../../common/net" }
common-ecs = { package = "veloren-common-ecs", path = "../../common/ecs" }
common-dynlib = {package = "veloren-common-dynlib", path = "../../common/dynlib", optional = true}
common-dynlib = { package = "veloren-common-dynlib", path = "../../common/dynlib", optional = true}
rtsim = { package = "veloren-rtsim", path = "../../rtsim" }
specs = { version = "0.18", features = ["shred-derive"] }
vek = { version = "0.15.8", features = ["serde"] }

View File

@ -22,12 +22,13 @@ use common::{
},
item_drop,
projectile::ProjectileConstructor,
Agent, Alignment, Body, CharacterState, ControlAction, ControlEvent, Controller,
HealthChange, InputKind, InventoryAction, Pos, UnresolvedChatMsg, UtteranceKind,
Agent, Alignment, Body, CharacterState, Content, ControlAction, ControlEvent, Controller,
HealthChange, InputKind, InventoryAction, Pos, Scale, UnresolvedChatMsg, UtteranceKind,
},
effect::{BuffEffect, Effect},
event::{Emitter, ServerEvent},
path::TraversalConfig,
rtsim::NpcActivity,
states::basic_beam,
terrain::{Block, TerrainGrid},
time::DayPeriod,
@ -160,6 +161,7 @@ impl<'a> AgentData<'a> {
agent: &mut Agent,
controller: &mut Controller,
read_data: &ReadData,
event_emitter: &mut Emitter<ServerEvent>,
rng: &mut impl Rng,
) {
enum ActionTimers {
@ -212,123 +214,161 @@ impl<'a> AgentData<'a> {
}
agent.action_state.timers[ActionTimers::TimerIdle as usize] = 0.0;
if let Some((travel_to, _destination)) = &agent.rtsim_controller.travel_to {
// If it has an rtsim destination and can fly, then it should.
// If it is flying and bumps something above it, then it should move down.
if self.traversal_config.can_fly
&& !read_data
.terrain
.ray(self.pos.0, self.pos.0 + (Vec3::unit_z() * 3.0))
.until(Block::is_solid)
.cast()
.1
.map_or(true, |b| b.is_some())
{
controller.push_basic_input(InputKind::Fly);
} else {
controller.push_cancel_input(InputKind::Fly)
}
if let Some((bearing, speed)) = agent.chaser.chase(
&*read_data.terrain,
self.pos.0,
self.vel.0,
*travel_to,
TraversalConfig {
min_tgt_dist: 1.25,
..self.traversal_config
},
) {
controller.inputs.move_dir =
bearing.xy().try_normalized().unwrap_or_else(Vec2::zero)
* speed.min(agent.rtsim_controller.speed_factor);
self.jump_if(bearing.z > 1.5 || self.traversal_config.can_fly, controller);
controller.inputs.climb = Some(comp::Climb::Up);
//.filter(|_| bearing.z > 0.1 || self.physics_state.in_liquid().is_some());
let height_offset = bearing.z
+ if self.traversal_config.can_fly {
// NOTE: costs 4 us (imbris)
let obstacle_ahead = read_data
'activity: {
match agent.rtsim_controller.activity {
Some(NpcActivity::Goto(travel_to, speed_factor)) => {
// If it has an rtsim destination and can fly, then it should.
// If it is flying and bumps something above it, then it should move down.
if self.traversal_config.can_fly
&& !read_data
.terrain
.ray(
self.pos.0 + Vec3::unit_z(),
self.pos.0
+ bearing.try_normalized().unwrap_or_else(Vec3::unit_y) * 80.0
+ Vec3::unit_z(),
)
.ray(self.pos.0, self.pos.0 + (Vec3::unit_z() * 3.0))
.until(Block::is_solid)
.cast()
.1
.map_or(true, |b| b.is_some());
.map_or(true, |b| b.is_some())
{
controller.push_basic_input(InputKind::Fly);
} else {
controller.push_cancel_input(InputKind::Fly)
}
let mut ground_too_close = self
.body
.map(|body| {
#[cfg(feature = "worldgen")]
let height_approx = self.pos.0.z
- read_data
.world
.sim()
.get_alt_approx(self.pos.0.xy().map(|x: f32| x as i32))
.unwrap_or(0.0);
#[cfg(not(feature = "worldgen"))]
let height_approx = self.pos.0.z;
let chase_tgt = if self.traversal_config.can_fly {
read_data.terrain.try_find_space(travel_to.as_())
} else {
read_data.terrain.try_find_ground(travel_to.as_())
}
.map(|pos| pos.as_())
.unwrap_or(travel_to);
height_approx < body.flying_height()
})
.unwrap_or(false);
if let Some((bearing, speed)) = agent.chaser.chase(
&*read_data.terrain,
self.pos.0,
self.vel.0,
chase_tgt,
TraversalConfig {
min_tgt_dist: 1.25,
..self.traversal_config
},
) {
controller.inputs.move_dir =
bearing.xy().try_normalized().unwrap_or_else(Vec2::zero)
* speed.min(speed_factor);
self.jump_if(bearing.z > 1.5 || self.traversal_config.can_fly, controller);
controller.inputs.climb = Some(comp::Climb::Up);
//.filter(|_| bearing.z > 0.1 || self.physics_state.in_liquid().is_some());
const NUM_RAYS: usize = 5;
// NOTE: costs 15-20 us (imbris)
for i in 0..=NUM_RAYS {
let magnitude = self.body.map_or(20.0, |b| b.flying_height());
// Lerp between a line straight ahead and straight down to detect a
// wedge of obstacles we might fly into (inclusive so that both vectors
// are sampled)
if let Some(dir) = Lerp::lerp(
-Vec3::unit_z(),
Vec3::new(bearing.x, bearing.y, 0.0),
i as f32 / NUM_RAYS as f32,
)
.try_normalized()
{
ground_too_close |= read_data
let height_offset = bearing.z
+ if self.traversal_config.can_fly {
// NOTE: costs 4 us (imbris)
let obstacle_ahead = read_data
.terrain
.ray(self.pos.0, self.pos.0 + magnitude * dir)
.until(|b: &Block| b.is_solid() || b.is_liquid())
.ray(
self.pos.0 + Vec3::unit_z(),
self.pos.0
+ bearing.try_normalized().unwrap_or_else(Vec3::unit_y)
* 80.0
+ Vec3::unit_z(),
)
.until(Block::is_solid)
.cast()
.1
.map_or(false, |b| b.is_some())
}
}
.map_or(true, |b| b.is_some());
if obstacle_ahead || ground_too_close {
5.0 //fly up when approaching obstacles
let mut ground_too_close = self
.body
.map(|body| {
#[cfg(feature = "worldgen")]
let height_approx = self.pos.0.z
- read_data
.world
.sim()
.get_alt_approx(
self.pos.0.xy().map(|x: f32| x as i32),
)
.unwrap_or(0.0);
#[cfg(not(feature = "worldgen"))]
let height_approx = self.pos.0.z;
height_approx < body.flying_height()
})
.unwrap_or(false);
const NUM_RAYS: usize = 5;
// NOTE: costs 15-20 us (imbris)
for i in 0..=NUM_RAYS {
let magnitude = self.body.map_or(20.0, |b| b.flying_height());
// Lerp between a line straight ahead and straight down to
// detect a
// wedge of obstacles we might fly into (inclusive so that both
// vectors are sampled)
if let Some(dir) = Lerp::lerp(
-Vec3::unit_z(),
Vec3::new(bearing.x, bearing.y, 0.0),
i as f32 / NUM_RAYS as f32,
)
.try_normalized()
{
ground_too_close |= read_data
.terrain
.ray(self.pos.0, self.pos.0 + magnitude * dir)
.until(|b: &Block| b.is_solid() || b.is_liquid())
.cast()
.1
.map_or(false, |b| b.is_some())
}
}
if obstacle_ahead || ground_too_close {
5.0 //fly up when approaching obstacles
} else {
-2.0
} //flying things should slowly come down from the stratosphere
} else {
0.05 //normal land traveller offset
};
if let Some(pid) = agent.position_pid_controller.as_mut() {
pid.sp = self.pos.0.z + height_offset * Vec3::unit_z();
controller.inputs.move_z = pid.calc_err();
} else {
-2.0
} //flying things should slowly come down from the stratosphere
} else {
0.05 //normal land traveller offset
};
if let Some(pid) = agent.position_pid_controller.as_mut() {
pid.sp = self.pos.0.z + height_offset * Vec3::unit_z();
controller.inputs.move_z = pid.calc_err();
} else {
controller.inputs.move_z = height_offset;
}
// Put away weapon
if rng.gen_bool(0.1)
&& matches!(
read_data.char_states.get(*self.entity),
Some(CharacterState::Wielding(_))
)
{
controller.push_action(ControlAction::Unwield);
}
controller.inputs.move_z = height_offset;
}
// Put away weapon
if rng.gen_bool(0.1)
&& matches!(
read_data.char_states.get(*self.entity),
Some(CharacterState::Wielding(_))
)
{
controller.push_action(ControlAction::Unwield);
}
}
break 'activity; // Don't fall through to idle wandering
},
Some(NpcActivity::Gather(_resources)) => {
// TODO: Implement
controller.push_action(ControlAction::Dance);
break 'activity; // Don't fall through to idle wandering
},
Some(NpcActivity::Dance) => {
controller.push_action(ControlAction::Dance);
break 'activity; // Don't fall through to idle wandering
},
Some(NpcActivity::HuntAnimals) => {
if rng.gen::<f32>() < 0.1 {
self.choose_target(
agent,
controller,
read_data,
event_emitter,
AgentData::is_hunting_animal,
);
}
},
None => {},
}
} else {
// Bats should fly
// Use a proportional controller as the bouncing effect mimics bat flight
if self.traversal_config.can_fly
@ -476,8 +516,10 @@ impl<'a> AgentData<'a> {
target: EcsEntity,
) -> bool {
if let Some(tgt_pos) = read_data.positions.get(target) {
let eye_offset = self.body.map_or(0.0, |b| b.eye_height());
let tgt_eye_offset = read_data.bodies.get(target).map_or(0.0, |b| b.eye_height());
let eye_offset = self.body.map_or(0.0, |b| b.eye_height(self.scale));
let tgt_eye_offset = read_data.bodies.get(target).map_or(0.0, |b| {
b.eye_height(read_data.scales.get(target).map_or(1.0, |s| s.0))
});
if let Some(dir) = Dir::from_unnormalized(
Vec3::new(tgt_pos.0.x, tgt_pos.0.y, tgt_pos.0.z + tgt_eye_offset)
- Vec3::new(self.pos.0.x, self.pos.0.y, self.pos.0.z + eye_offset),
@ -645,7 +687,7 @@ impl<'a> AgentData<'a> {
controller: &mut Controller,
read_data: &ReadData,
event_emitter: &mut Emitter<ServerEvent>,
will_ambush: bool,
is_enemy: fn(&Self, EcsEntity, &ReadData) -> bool,
) {
enum ActionStateTimers {
TimerChooseTarget = 0,
@ -668,7 +710,7 @@ impl<'a> AgentData<'a> {
.get(entity)
.map_or(false, |eu| eu != self.uid)
};
if will_ambush
if agent.rtsim_controller.personality.will_ambush()
&& self_different_from_entity()
&& !self.passive_towards(entity, read_data)
{
@ -686,12 +728,12 @@ impl<'a> AgentData<'a> {
let get_pos = |entity| read_data.positions.get(entity);
let get_enemy = |(entity, attack_target): (EcsEntity, bool)| {
if attack_target {
if self.is_enemy(entity, read_data) {
if is_enemy(self, entity, read_data) {
Some((entity, true))
} else if can_ambush(entity, read_data) {
controller.clone().push_utterance(UtteranceKind::Ambush);
self.chat_npc_if_allowed_to_speak(
"npc-speech-ambush".to_string(),
Content::localized("npc-speech-ambush"),
agent,
event_emitter,
);
@ -756,8 +798,8 @@ impl<'a> AgentData<'a> {
},
};
let is_detected = |entity: &EcsEntity, e_pos: &Pos| {
self.detects_other(agent, controller, entity, e_pos, read_data)
let is_detected = |entity: &EcsEntity, e_pos: &Pos, e_scale: Option<&Scale>| {
self.detects_other(agent, controller, entity, e_pos, e_scale, read_data)
};
let target = entities_nearby
@ -767,7 +809,7 @@ impl<'a> AgentData<'a> {
.filter_map(|(entity, attack_target)| {
get_pos(entity).map(|pos| (entity, pos, attack_target))
})
.filter(|(entity, e_pos, _)| is_detected(entity, e_pos))
.filter(|(entity, e_pos, _)| is_detected(entity, e_pos, read_data.scales.get(*entity)))
.min_by_key(|(_, e_pos, attack_target)| {
(
*attack_target,
@ -959,9 +1001,11 @@ impl<'a> AgentData<'a> {
.angle_between((tgt_data.pos.0 - self.pos.0).xy())
.to_degrees();
let eye_offset = self.body.map_or(0.0, |b| b.eye_height());
let eye_offset = self.body.map_or(0.0, |b| b.eye_height(self.scale));
let tgt_eye_height = tgt_data.body.map_or(0.0, |b| b.eye_height());
let tgt_eye_height = tgt_data
.body
.map_or(0.0, |b| b.eye_height(tgt_data.scale.map_or(1.0, |s| s.0)));
let tgt_eye_offset = tgt_eye_height +
// Special case for jumping attacks to jump at the body
// of the target and not the ground around the target
@ -999,7 +1043,7 @@ impl<'a> AgentData<'a> {
projectile_speed,
self.pos.0
+ self.body.map_or(Vec3::zero(), |body| {
body.projectile_offsets(self.ori.look_vec())
body.projectile_offsets(self.ori.look_vec(), self.scale)
}),
Vec3::new(
tgt_data.pos.0.x,
@ -1024,7 +1068,7 @@ impl<'a> AgentData<'a> {
projectile_speed,
self.pos.0
+ self.body.map_or(Vec3::zero(), |body| {
body.projectile_offsets(self.ori.look_vec())
body.projectile_offsets(self.ori.look_vec(), self.scale)
}),
Vec3::new(
tgt_data.pos.0.x,
@ -1039,7 +1083,7 @@ impl<'a> AgentData<'a> {
projectile_speed,
self.pos.0
+ self.body.map_or(Vec3::zero(), |body| {
body.projectile_offsets(self.ori.look_vec())
body.projectile_offsets(self.ori.look_vec(), self.scale)
}),
Vec3::new(
tgt_data.pos.0.x,
@ -1371,12 +1415,13 @@ impl<'a> AgentData<'a> {
agent: &mut Agent,
controller: &mut Controller,
read_data: &ReadData,
event_emitter: &mut Emitter<ServerEvent>,
rng: &mut impl Rng,
) {
agent.forget_old_sounds(read_data.time.0);
if is_invulnerable(*self.entity, read_data) {
self.idle(agent, controller, read_data, rng);
self.idle(agent, controller, read_data, event_emitter, rng);
return;
}
@ -1409,13 +1454,13 @@ impl<'a> AgentData<'a> {
} else if self.below_flee_health(agent) || !follows_threatening_sounds {
self.flee(agent, controller, &sound_pos, &read_data.terrain);
} else {
self.idle(agent, controller, read_data, rng);
self.idle(agent, controller, read_data, event_emitter, rng);
}
} else {
self.idle(agent, controller, read_data, rng);
self.idle(agent, controller, read_data, event_emitter, rng);
}
} else {
self.idle(agent, controller, read_data, rng);
self.idle(agent, controller, read_data, event_emitter, rng);
}
}
@ -1424,6 +1469,7 @@ impl<'a> AgentData<'a> {
agent: &mut Agent,
read_data: &ReadData,
controller: &mut Controller,
event_emitter: &mut Emitter<ServerEvent>,
rng: &mut impl Rng,
) {
if let Some(Target { target, .. }) = agent.target {
@ -1453,14 +1499,15 @@ impl<'a> AgentData<'a> {
Some(tgt_pos.0),
));
self.idle(agent, controller, read_data, rng);
self.idle(agent, controller, read_data, event_emitter, rng);
} else {
let target_data = TargetData::new(tgt_pos, target, read_data);
if let Some(tgt_name) =
read_data.stats.get(target).map(|stats| stats.name.clone())
{
agent.add_fight_to_memory(&tgt_name, read_data.time.0)
}
// TODO: Reimplement this in rtsim
// if let Some(tgt_name) =
// read_data.stats.get(target).map(|stats| stats.name.clone())
// {
// agent.add_fight_to_memory(&tgt_name, read_data.time.0)
// }
self.attack(agent, controller, &target_data, read_data, rng);
}
}
@ -1470,9 +1517,11 @@ impl<'a> AgentData<'a> {
}
}
// TODO: Pass a localisation key instead of `Content` to avoid allocating if
// we're not permitted to speak.
pub fn chat_npc_if_allowed_to_speak(
&self,
msg: impl ToString,
msg: Content,
agent: &Agent,
event_emitter: &mut Emitter<'_, ServerEvent>,
) -> bool {
@ -1484,10 +1533,9 @@ impl<'a> AgentData<'a> {
}
}
pub fn chat_npc(&self, msg: impl ToString, event_emitter: &mut Emitter<'_, ServerEvent>) {
pub fn chat_npc(&self, content: Content, event_emitter: &mut Emitter<'_, ServerEvent>) {
event_emitter.emit(ServerEvent::Chat(UnresolvedChatMsg::npc(
*self.uid,
msg.to_string(),
*self.uid, content,
)));
}
@ -1516,13 +1564,13 @@ impl<'a> AgentData<'a> {
// FIXME: If going to use "cultist + low health + fleeing" string, make sure
// they are each true.
self.chat_npc_if_allowed_to_speak(
"npc-speech-cultist_low_health_fleeing",
Content::localized("npc-speech-cultist_low_health_fleeing"),
agent,
event_emitter,
);
} else if is_villager(self.alignment) {
self.chat_npc_if_allowed_to_speak(
"npc-speech-villager_under_attack",
Content::localized("npc-speech-villager_under_attack"),
agent,
event_emitter,
);
@ -1537,7 +1585,7 @@ impl<'a> AgentData<'a> {
) {
if is_villager(self.alignment) {
self.chat_npc_if_allowed_to_speak(
"npc-speech-villager_enemy_killed",
Content::localized("npc-speech-villager_enemy_killed"),
agent,
event_emitter,
);
@ -1580,7 +1628,7 @@ impl<'a> AgentData<'a> {
})
}
fn is_enemy(&self, entity: EcsEntity, read_data: &ReadData) -> bool {
pub fn is_enemy(&self, entity: EcsEntity, read_data: &ReadData) -> bool {
let other_alignment = read_data.alignments.get(entity);
(entity != *self.entity)
@ -1589,6 +1637,12 @@ impl<'a> AgentData<'a> {
|| (is_villager(self.alignment) && is_dressed_as_cultist(entity, read_data)))
}
pub fn is_hunting_animal(&self, entity: EcsEntity, read_data: &ReadData) -> bool {
(entity != *self.entity)
&& !self.friendly_towards(entity, read_data)
&& matches!(read_data.bodies.get(entity), Some(Body::QuadrupedSmall(_)))
}
fn should_defend(&self, entity: EcsEntity, read_data: &ReadData) -> bool {
let entity_alignment = read_data.alignments.get(entity);
@ -1621,12 +1675,23 @@ impl<'a> AgentData<'a> {
}
}
fn friendly_towards(&self, entity: EcsEntity, read_data: &ReadData) -> bool {
if let (Some(self_alignment), Some(other_alignment)) =
(self.alignment, read_data.alignments.get(entity))
{
self_alignment.friendly_towards(*other_alignment)
} else {
false
}
}
pub fn can_see_entity(
&self,
agent: &Agent,
controller: &Controller,
other: EcsEntity,
other_pos: &Pos,
other_scale: Option<&Scale>,
read_data: &ReadData,
) -> bool {
let other_stealth_multiplier = {
@ -1651,7 +1716,15 @@ impl<'a> AgentData<'a> {
(within_sight_dist)
&& within_fov
&& entities_have_line_of_sight(self.pos, self.body, other_pos, other_body, read_data)
&& entities_have_line_of_sight(
self.pos,
self.body,
self.scale,
other_pos,
other_body,
other_scale,
read_data,
)
}
pub fn detects_other(
@ -1660,10 +1733,11 @@ impl<'a> AgentData<'a> {
controller: &Controller,
other: &EcsEntity,
other_pos: &Pos,
other_scale: Option<&Scale>,
read_data: &ReadData,
) -> bool {
self.can_sense_directly_near(other_pos)
|| self.can_see_entity(agent, controller, *other, other_pos, read_data)
|| self.can_see_entity(agent, controller, *other, other_pos, other_scale, read_data)
}
pub fn can_sense_directly_near(&self, e_pos: &Pos) -> bool {
@ -1685,16 +1759,22 @@ impl<'a> AgentData<'a> {
let move_dir = controller.inputs.move_dir;
let move_dir_mag = move_dir.magnitude();
let small_chance = rng.gen::<f32>() < read_data.dt.0 * 0.25;
let mut chat = |msg: &str| {
self.chat_npc_if_allowed_to_speak(msg.to_string(), agent, event_emitter);
let mut chat = |content: Content| {
self.chat_npc_if_allowed_to_speak(content, agent, event_emitter);
};
let mut chat_villager_remembers_fighting = || {
let tgt_name = read_data.stats.get(target).map(|stats| stats.name.clone());
// TODO: Localise
if let Some(tgt_name) = tgt_name {
chat(format!("{}! How dare you cross me again!", &tgt_name).as_str());
chat(Content::Plain(format!(
"{}! How dare you cross me again!",
&tgt_name
)));
} else {
chat("You! How dare you cross me again!");
chat(Content::Plain(
"You! How dare you cross me again!".to_string(),
));
}
};
@ -1711,12 +1791,12 @@ impl<'a> AgentData<'a> {
if remembers_fight_with_target {
chat_villager_remembers_fighting();
} else if is_dressed_as_cultist(target, read_data) {
chat("npc-speech-villager_cultist_alarm");
chat(Content::localized("npc-speech-villager_cultist_alarm"));
} else {
chat("npc-speech-menacing");
chat(Content::localized("npc-speech-menacing"));
}
} else {
chat("npc-speech-menacing");
chat(Content::localized("npc-speech-menacing"));
}
}
}

View File

@ -247,7 +247,15 @@ impl<'a> AgentData<'a> {
read_data: &ReadData,
) {
let line_of_sight_with_target = || {
entities_have_line_of_sight(self.pos, self.body, tgt_data.pos, tgt_data.body, read_data)
entities_have_line_of_sight(
self.pos,
self.body,
self.scale,
tgt_data.pos,
tgt_data.body,
tgt_data.scale,
read_data,
)
};
let elevation = self.pos.0.z - tgt_data.pos.0.z;
@ -374,8 +382,10 @@ impl<'a> AgentData<'a> {
&& entities_have_line_of_sight(
self.pos,
self.body,
self.scale,
tgt_data.pos,
tgt_data.body,
tgt_data.scale,
read_data,
)
{
@ -451,8 +461,10 @@ impl<'a> AgentData<'a> {
&& entities_have_line_of_sight(
self.pos,
self.body,
self.scale,
tgt_data.pos,
tgt_data.body,
tgt_data.scale,
read_data,
)
{
@ -1269,7 +1281,15 @@ impl<'a> AgentData<'a> {
const DESIRED_ENERGY_LEVEL: f32 = 50.0;
let line_of_sight_with_target = || {
entities_have_line_of_sight(self.pos, self.body, tgt_data.pos, tgt_data.body, read_data)
entities_have_line_of_sight(
self.pos,
self.body,
self.scale,
tgt_data.pos,
tgt_data.body,
tgt_data.scale,
read_data,
)
};
// Logic to use abilities
@ -1521,8 +1541,10 @@ impl<'a> AgentData<'a> {
if entities_have_line_of_sight(
self.pos,
self.body,
self.scale,
tgt_data.pos,
tgt_data.body,
tgt_data.scale,
read_data,
) && attack_data.angle < 45.0
{
@ -1574,7 +1596,15 @@ impl<'a> AgentData<'a> {
const DESIRED_COMBO_LEVEL: u32 = 8;
let line_of_sight_with_target = || {
entities_have_line_of_sight(self.pos, self.body, tgt_data.pos, tgt_data.body, read_data)
entities_have_line_of_sight(
self.pos,
self.body,
self.scale,
tgt_data.pos,
tgt_data.body,
tgt_data.scale,
read_data,
)
};
// Logic to use abilities
@ -1724,8 +1754,10 @@ impl<'a> AgentData<'a> {
) && entities_have_line_of_sight(
self.pos,
self.body,
self.scale,
tgt_data.pos,
tgt_data.body,
tgt_data.scale,
read_data,
) && attack_data.angle < 90.0
{
@ -1907,8 +1939,10 @@ impl<'a> AgentData<'a> {
&& entities_have_line_of_sight(
self.pos,
self.body,
self.scale,
tgt_data.pos,
tgt_data.body,
tgt_data.scale,
read_data,
)
{
@ -2130,8 +2164,10 @@ impl<'a> AgentData<'a> {
&& entities_have_line_of_sight(
self.pos,
self.body,
self.scale,
tgt_data.pos,
tgt_data.body,
tgt_data.scale,
read_data,
)
{
@ -2313,9 +2349,7 @@ impl<'a> AgentData<'a> {
{
agent.action_state.timers[ActionStateTimers::TimerOrganAura as usize] = 0.0;
} else if agent.action_state.timers[ActionStateTimers::TimerOrganAura as usize] < 1.0 {
controller
.actions
.push(ControlAction::basic_input(InputKind::Primary));
controller.push_basic_input(InputKind::Primary);
agent.action_state.timers[ActionStateTimers::TimerOrganAura as usize] +=
read_data.dt.0;
} else {
@ -2367,8 +2401,15 @@ impl<'a> AgentData<'a> {
tgt_data: &TargetData,
read_data: &ReadData,
) {
if entities_have_line_of_sight(self.pos, self.body, tgt_data.pos, tgt_data.body, read_data)
&& attack_data.angle < 15.0
if entities_have_line_of_sight(
self.pos,
self.body,
self.scale,
tgt_data.pos,
tgt_data.body,
tgt_data.scale,
read_data,
) && attack_data.angle < 15.0
{
controller.push_basic_input(InputKind::Primary);
} else {
@ -2385,8 +2426,15 @@ impl<'a> AgentData<'a> {
read_data: &ReadData,
) {
controller.inputs.look_dir = self.ori.look_dir();
if entities_have_line_of_sight(self.pos, self.body, tgt_data.pos, tgt_data.body, read_data)
&& attack_data.angle < 15.0
if entities_have_line_of_sight(
self.pos,
self.body,
self.scale,
tgt_data.pos,
tgt_data.body,
tgt_data.scale,
read_data,
) && attack_data.angle < 15.0
{
controller.push_basic_input(InputKind::Primary);
} else {
@ -2408,8 +2456,15 @@ impl<'a> AgentData<'a> {
.try_normalized()
.unwrap_or_default(),
);
if entities_have_line_of_sight(self.pos, self.body, tgt_data.pos, tgt_data.body, read_data)
{
if entities_have_line_of_sight(
self.pos,
self.body,
self.scale,
tgt_data.pos,
tgt_data.body,
tgt_data.scale,
read_data,
) {
controller.push_basic_input(InputKind::Primary);
} else {
agent.target = None;
@ -2469,8 +2524,10 @@ impl<'a> AgentData<'a> {
if entities_have_line_of_sight(
self.pos,
self.body,
self.scale,
tgt_data.pos,
tgt_data.body,
tgt_data.scale,
read_data,
) {
// If close to target, use either primary or secondary ability
@ -2566,8 +2623,10 @@ impl<'a> AgentData<'a> {
&& entities_have_line_of_sight(
self.pos,
self.body,
self.scale,
tgt_data.pos,
tgt_data.body,
tgt_data.scale,
read_data,
)
&& attack_data.angle < 15.0
@ -2697,8 +2756,10 @@ impl<'a> AgentData<'a> {
&& entities_have_line_of_sight(
self.pos,
self.body,
self.scale,
tgt_data.pos,
tgt_data.body,
tgt_data.scale,
read_data,
)
&& attack_data.angle < 15.0
@ -2933,8 +2994,10 @@ impl<'a> AgentData<'a> {
&& entities_have_line_of_sight(
self.pos,
self.body,
self.scale,
tgt_data.pos,
tgt_data.body,
tgt_data.scale,
read_data,
)
{
@ -3188,7 +3251,15 @@ impl<'a> AgentData<'a> {
.and_then(|e| read_data.velocities.get(e))
.map_or(0.0, |v| v.0.cross(self.ori.look_vec()).magnitude_squared());
let line_of_sight_with_target = || {
entities_have_line_of_sight(self.pos, self.body, tgt_data.pos, tgt_data.body, read_data)
entities_have_line_of_sight(
self.pos,
self.body,
self.scale,
tgt_data.pos,
tgt_data.body,
tgt_data.scale,
read_data,
)
};
if attack_data.dist_sqrd < golem_melee_range.powi(2) {
@ -3265,7 +3336,15 @@ impl<'a> AgentData<'a> {
let health_fraction = self.health.map_or(0.5, |h| h.fraction());
let line_of_sight_with_target = || {
entities_have_line_of_sight(self.pos, self.body, tgt_data.pos, tgt_data.body, read_data)
entities_have_line_of_sight(
self.pos,
self.body,
self.scale,
tgt_data.pos,
tgt_data.body,
tgt_data.scale,
read_data,
)
};
// Sets counter at start of combat, using `condition` to keep track of whether
@ -3463,7 +3542,15 @@ impl<'a> AgentData<'a> {
let health_fraction = self.health.map_or(0.5, |h| h.fraction());
let line_of_sight_with_target = || {
entities_have_line_of_sight(self.pos, self.body, tgt_data.pos, tgt_data.body, read_data)
entities_have_line_of_sight(
self.pos,
self.body,
self.scale,
tgt_data.pos,
tgt_data.body,
tgt_data.scale,
read_data,
)
};
if health_fraction < VINE_CREATION_THRESHOLD
@ -3532,7 +3619,15 @@ impl<'a> AgentData<'a> {
}
let line_of_sight_with_target = || {
entities_have_line_of_sight(self.pos, self.body, tgt_data.pos, tgt_data.body, read_data)
entities_have_line_of_sight(
self.pos,
self.body,
self.scale,
tgt_data.pos,
tgt_data.body,
tgt_data.scale,
read_data,
)
};
let health_fraction = self.health.map_or(0.5, |h| h.fraction());
// Sets counter at start of combat, using `condition` to keep track of whether
@ -3673,8 +3768,10 @@ impl<'a> AgentData<'a> {
&& entities_have_line_of_sight(
self.pos,
self.body,
self.scale,
tgt_data.pos,
tgt_data.body,
tgt_data.scale,
read_data,
)
{
@ -3740,8 +3837,10 @@ impl<'a> AgentData<'a> {
&& entities_have_line_of_sight(
self.pos,
self.body,
self.scale,
tgt_data.pos,
tgt_data.body,
tgt_data.scale,
read_data,
)
{
@ -3824,8 +3923,10 @@ impl<'a> AgentData<'a> {
if entities_have_line_of_sight(
self.pos,
self.body,
self.scale,
tgt_data.pos,
tgt_data.body,
tgt_data.scale,
read_data,
) && attack_data.angle < 45.0
{
@ -3903,8 +4004,10 @@ impl<'a> AgentData<'a> {
} else if entities_have_line_of_sight(
self.pos,
self.body,
self.scale,
tgt_data.pos,
tgt_data.body,
tgt_data.scale,
read_data,
) {
// if enemy in mid range shoot dagon bombs and steamwave
@ -4018,8 +4121,10 @@ impl<'a> AgentData<'a> {
&& entities_have_line_of_sight(
self.pos,
self.body,
self.scale,
tgt_data.pos,
tgt_data.body,
tgt_data.scale,
read_data,
)
{
@ -4179,8 +4284,10 @@ impl<'a> AgentData<'a> {
} else if entities_have_line_of_sight(
self.pos,
self.body,
self.scale,
tgt_data.pos,
tgt_data.body,
tgt_data.scale,
read_data,
) {
// Else if in sight, barrage
@ -4243,8 +4350,10 @@ impl<'a> AgentData<'a> {
&& entities_have_line_of_sight(
self.pos,
self.body,
self.scale,
tgt_data.pos,
tgt_data.body,
tgt_data.scale,
read_data,
)
&& agent.action_state.timers[DASH_TIMER] > 4.0

View File

@ -6,21 +6,21 @@ use common::{
group,
item::MaterialStatManifest,
ActiveAbilities, Alignment, Body, CharacterState, Combo, Energy, Health, Inventory,
LightEmitter, LootOwner, Ori, PhysicsState, Poise, Pos, Scale, SkillSet, Stance, Stats,
Vel,
LightEmitter, LootOwner, Ori, PhysicsState, Poise, Pos, Presence, PresenceKind, Scale,
SkillSet, Stance, Stats, Vel,
},
link::Is,
mounting::Mount,
mounting::{Mount, Rider},
path::TraversalConfig,
resources::{DeltaTime, Time, TimeOfDay},
rtsim::RtSimEntity,
rtsim::{Actor, RtSimEntity},
states::utils::{ForcedMovement, StageSection},
terrain::TerrainGrid,
uid::{Uid, UidAllocator},
};
use specs::{
shred::ResourceId, Entities, Entity as EcsEntity, Read, ReadExpect, ReadStorage, SystemData,
World,
shred::ResourceId, Entities, Entity as EcsEntity, Join, Read, ReadExpect, ReadStorage,
SystemData, World,
};
// TODO: Move rtsim back into AgentData after rtsim2 when it has a separate
@ -54,6 +54,7 @@ pub struct AgentData<'a> {
pub stance: Option<&'a Stance>,
pub cached_spatial_grid: &'a common::CachedSpatialGrid,
pub msm: &'a MaterialStatManifest,
pub rtsim_entity: Option<&'a RtSimEntity>,
}
pub struct TargetData<'a> {
@ -232,6 +233,7 @@ pub struct ReadData<'a> {
pub alignments: ReadStorage<'a, Alignment>,
pub bodies: ReadStorage<'a, Body>,
pub is_mounts: ReadStorage<'a, Is<Mount>>,
pub is_riders: ReadStorage<'a, Is<Rider>>,
pub time_of_day: Read<'a, TimeOfDay>,
pub light_emitter: ReadStorage<'a, LightEmitter>,
#[cfg(feature = "worldgen")]
@ -244,6 +246,25 @@ pub struct ReadData<'a> {
pub msm: ReadExpect<'a, MaterialStatManifest>,
pub poises: ReadStorage<'a, Poise>,
pub stances: ReadStorage<'a, Stance>,
pub presences: ReadStorage<'a, Presence>,
}
impl<'a> ReadData<'a> {
pub fn lookup_actor(&self, actor: Actor) -> Option<EcsEntity> {
// TODO: We really shouldn't be doing a linear search here. The only saving
// grace is that the set of entities that fit each case should be
// *relatively* small.
match actor {
Actor::Character(character_id) => (&self.entities, &self.presences)
.join()
.find(|(_, p)| p.kind == PresenceKind::Character(character_id))
.map(|(entity, _)| entity),
Actor::Npc(npc_id) => (&self.entities, &self.rtsim_entities)
.join()
.find(|(_, e)| e.0 == npc_id)
.map(|(entity, _)| entity),
}
}
}
pub enum Path {

View File

@ -2,7 +2,7 @@ use crate::data::{ActionMode, AgentData, AttackData, Path, ReadData, TargetData}
use common::{
comp::{
agent::Psyche, buff::BuffKind, inventory::item::ItemTag, item::ItemDesc, Agent, Alignment,
Body, Controller, InputKind, Pos,
Body, Controller, InputKind, Pos, Scale,
},
consts::GRAVITY,
terrain::Block,
@ -146,17 +146,19 @@ pub fn are_our_owners_hostile(
pub fn entities_have_line_of_sight(
pos: &Pos,
body: Option<&Body>,
scale: f32,
other_pos: &Pos,
other_body: Option<&Body>,
other_scale: Option<&Scale>,
read_data: &ReadData,
) -> bool {
let get_eye_pos = |pos: &Pos, body: Option<&Body>| {
let eye_offset = body.map_or(0.0, |b| b.eye_height());
let get_eye_pos = |pos: &Pos, body: Option<&Body>, scale: f32| {
let eye_offset = body.map_or(0.0, |b| b.eye_height(scale));
Pos(pos.0.with_z(pos.0.z + eye_offset))
};
let eye_pos = get_eye_pos(pos, body);
let other_eye_pos = get_eye_pos(other_pos, other_body);
let eye_pos = get_eye_pos(pos, body, scale);
let other_eye_pos = get_eye_pos(other_pos, other_body, other_scale.map_or(1.0, |s| s.0));
positions_have_line_of_sight(&eye_pos, &other_eye_pos, read_data)
}

View File

@ -1,6 +1,6 @@
use crate::metrics::ChunkGenMetrics;
#[cfg(not(feature = "worldgen"))]
use crate::test_world::{IndexOwned, World};
use crate::{metrics::ChunkGenMetrics, rtsim::RtSim};
use common::{
calendar::Calendar, generation::ChunkSupplement, resources::TimeOfDay, slowjob::SlowJobPool,
terrain::TerrainChunk,
@ -44,6 +44,8 @@ impl ChunkGenerator {
key: Vec2<i32>,
slowjob_pool: &SlowJobPool,
world: Arc<World>,
#[cfg(feature = "worldgen")] rtsim: &RtSim,
#[cfg(not(feature = "worldgen"))] rtsim: &(),
index: IndexOwned,
time: (TimeOfDay, Calendar),
) {
@ -56,10 +58,17 @@ impl ChunkGenerator {
v.insert(Arc::clone(&cancel));
let chunk_tx = self.chunk_tx.clone();
self.metrics.chunks_requested.inc();
// Get state for this chunk from rtsim
#[cfg(feature = "worldgen")]
let rtsim_resources = Some(rtsim.get_chunk_resources(key));
#[cfg(not(feature = "worldgen"))]
let rtsim_resources = None;
slowjob_pool.spawn("CHUNK_GENERATOR", move || {
let index = index.as_index_ref();
let payload = world
.generate_chunk(index, key, || cancel.load(Ordering::Relaxed), Some(time))
.generate_chunk(index, key, rtsim_resources, || cancel.load(Ordering::Relaxed), Some(time))
// FIXME: Since only the first entity who cancels a chunk is notified, we end up
// delaying chunk re-requests for up to 3 seconds for other clients, which isn't
// great. We *could* store all the other requesting clients here, but it could

View File

@ -5,7 +5,6 @@ use crate::{
client::Client,
location::Locations,
login_provider::LoginProvider,
presence::Presence,
settings::{
Ban, BanAction, BanInfo, EditableSetting, SettingError, WhitelistInfo, WhitelistRecord,
},
@ -31,7 +30,7 @@ use common::{
buff::{Buff, BuffCategory, BuffData, BuffKind, BuffSource},
inventory::item::{tool::AbilityMap, MaterialStatManifest, Quality},
invite::InviteKind,
AdminRole, ChatType, Inventory, Item, LightEmitter, WaypointArea,
AdminRole, ChatType, Inventory, Item, LightEmitter, Presence, PresenceKind, WaypointArea,
},
depot,
effect::Effect,
@ -43,13 +42,14 @@ use common::{
outcome::Outcome,
parse_cmd_args,
resources::{BattleMode, PlayerPhysicsSettings, Secs, Time, TimeOfDay},
rtsim::Actor,
terrain::{Block, BlockKind, CoordinateConversions, SpriteKind, TerrainChunkSize},
uid::{Uid, UidAllocator},
vol::ReadVol,
weather, Damage, DamageKind, DamageSource, Explosion, LoadoutBuilder, RadiusEffect,
};
use common_net::{
msg::{DisconnectReason, Notification, PlayerListUpdate, PresenceKind, ServerGeneral},
msg::{DisconnectReason, Notification, PlayerListUpdate, ServerGeneral},
sync::WorldSyncExt,
};
use common_state::{BuildAreaError, BuildAreas};
@ -184,6 +184,11 @@ fn do_command(
ServerChatCommand::Tell => handle_tell,
ServerChatCommand::Time => handle_time,
ServerChatCommand::Tp => handle_tp,
ServerChatCommand::RtsimTp => handle_rtsim_tp,
ServerChatCommand::RtsimInfo => handle_rtsim_info,
ServerChatCommand::RtsimNpc => handle_rtsim_npc,
ServerChatCommand::RtsimPurge => handle_rtsim_purge,
ServerChatCommand::RtsimChunk => handle_rtsim_chunk,
ServerChatCommand::Unban => handle_unban,
ServerChatCommand::Version => handle_version,
ServerChatCommand::Waypoint => handle_waypoint,
@ -196,6 +201,7 @@ fn do_command(
ServerChatCommand::DeleteLocation => handle_delete_location,
ServerChatCommand::WeatherZone => handle_weather_zone,
ServerChatCommand::Lightning => handle_lightning,
ServerChatCommand::Scale => handle_scale,
};
handler(server, client, target, args, cmd)
@ -1181,6 +1187,241 @@ fn handle_tp(
})
}
fn handle_rtsim_tp(
server: &mut Server,
_client: EcsEntity,
target: EcsEntity,
args: Vec<String>,
action: &ServerChatCommand,
) -> CmdResult<()> {
use crate::rtsim::RtSim;
let pos = if let Some(id) = parse_cmd_args!(args, u32) {
// TODO: Take some other identifier than an integer to this command.
server
.state
.ecs()
.read_resource::<RtSim>()
.state()
.data()
.npcs
.values()
.nth(id as usize)
.ok_or(action.help_string())?
.wpos
} else {
return Err(action.help_string());
};
position_mut(server, target, "target", |target_pos| {
target_pos.0 = pos;
})
}
fn handle_rtsim_info(
server: &mut Server,
client: EcsEntity,
_target: EcsEntity,
args: Vec<String>,
action: &ServerChatCommand,
) -> CmdResult<()> {
use crate::rtsim::RtSim;
if let Some(id) = parse_cmd_args!(args, u32) {
// TODO: Take some other identifier than an integer to this command.
let rtsim = server.state.ecs().read_resource::<RtSim>();
let data = rtsim.state().data();
let npc = data
.npcs
.values()
.nth(id as usize)
.ok_or_else(|| format!("No NPC has index {}", id))?;
let mut info = String::new();
let _ = writeln!(&mut info, "-- General Information --");
let _ = writeln!(&mut info, "Seed: {}", npc.seed);
let _ = writeln!(&mut info, "Profession: {:?}", npc.profession);
let _ = writeln!(&mut info, "Home: {:?}", npc.home);
let _ = writeln!(&mut info, "-- Status --");
let _ = writeln!(&mut info, "Current site: {:?}", npc.current_site);
let _ = writeln!(&mut info, "Current mode: {:?}", npc.mode);
let _ = writeln!(&mut info, "-- Action State --");
if let Some(brain) = &npc.brain {
let mut bt = Vec::new();
brain.action.backtrace(&mut bt);
for (i, action) in bt.into_iter().enumerate() {
let _ = writeln!(&mut info, "[{}] {}", i, action);
}
} else {
let _ = writeln!(&mut info, "<NPC has no brain>");
}
server.notify_client(
client,
ServerGeneral::server_msg(ChatType::CommandInfo, info),
);
Ok(())
} else {
Err(action.help_string())
}
}
fn handle_rtsim_npc(
server: &mut Server,
client: EcsEntity,
target: EcsEntity,
args: Vec<String>,
action: &ServerChatCommand,
) -> CmdResult<()> {
use crate::rtsim::RtSim;
if let (Some(query), count) = parse_cmd_args!(args, String, u32) {
let terms = query
.split(',')
.filter(|s| !s.is_empty())
.map(|s| s.trim().to_lowercase())
.collect::<Vec<_>>();
let rtsim = server.state.ecs().read_resource::<RtSim>();
let data = rtsim.state().data();
let mut npcs = data
.npcs
.values()
.enumerate()
.filter(|(idx, npc)| {
let tags = [
npc.profession
.as_ref()
.map(|p| format!("{:?}", p))
.unwrap_or_default(),
format!("{:?}", npc.mode),
format!("{}", idx),
];
terms
.iter()
.all(|term| tags.iter().any(|tag| term.eq_ignore_ascii_case(tag.trim())))
})
.collect::<Vec<_>>();
if let Ok(pos) = position(server, target, "target") {
npcs.sort_by_key(|(_, npc)| (npc.wpos.distance_squared(pos.0) * 10.0) as u64);
}
let mut info = String::new();
let _ = writeln!(&mut info, "-- NPCs matching [{}] --", terms.join(", "));
for (idx, _) in npcs.iter().take(count.unwrap_or(!0) as usize) {
let _ = write!(&mut info, "{}, ", idx);
}
let _ = writeln!(&mut info);
let _ = writeln!(
&mut info,
"Showing {}/{} matching NPCs.",
count.unwrap_or(npcs.len() as u32),
npcs.len()
);
server.notify_client(
client,
ServerGeneral::server_msg(ChatType::CommandInfo, info),
);
Ok(())
} else {
Err(action.help_string())
}
}
// TODO: Remove this command when rtsim becomes more mature and we're sure we
// don't need purges to fix broken state.
fn handle_rtsim_purge(
server: &mut Server,
client: EcsEntity,
_target: EcsEntity,
args: Vec<String>,
action: &ServerChatCommand,
) -> CmdResult<()> {
use crate::rtsim::RtSim;
let client_uuid = uuid(server, client, "client")?;
if !matches!(real_role(server, client_uuid, "client")?, AdminRole::Admin) {
return Err(
"You must be a real admin (not just a temporary admin) to purge rtsim data."
.to_string(),
);
}
if let Some(should_purge) = parse_cmd_args!(args, bool) {
server
.state
.ecs()
.write_resource::<RtSim>()
.set_should_purge(should_purge);
server.notify_client(
client,
ServerGeneral::server_msg(
ChatType::CommandInfo,
format!(
"Rtsim data {} be purged on next startup",
if should_purge { "WILL" } else { "will NOT" },
),
),
);
Ok(())
} else {
Err(action.help_string())
}
}
fn handle_rtsim_chunk(
server: &mut Server,
client: EcsEntity,
target: EcsEntity,
_args: Vec<String>,
_action: &ServerChatCommand,
) -> CmdResult<()> {
use crate::rtsim::{ChunkStates, RtSim};
let pos = position(server, target, "target")?;
let chunk_key = pos.0.xy().as_::<i32>().wpos_to_cpos();
let rtsim = server.state.ecs().read_resource::<RtSim>();
let data = rtsim.state().data();
let chunk_states = rtsim.state().resource::<ChunkStates>();
let chunk_state = match chunk_states.0.get(chunk_key) {
Some(Some(chunk_state)) => chunk_state,
Some(None) => return Err(format!("Chunk {}, {} not loaded", chunk_key.x, chunk_key.y)),
None => {
return Err(format!(
"Chunk {}, {} not within map bounds",
chunk_key.x, chunk_key.y
));
},
};
let mut info = String::new();
let _ = writeln!(
&mut info,
"-- Chunk {}, {} Resources --",
chunk_key.x, chunk_key.y
);
for (res, frac) in data.nature.get_chunk_resources(chunk_key) {
let total = chunk_state.max_res[res];
let _ = writeln!(
&mut info,
"{:?}: {} / {} ({}%)",
res,
frac * total as f32,
total,
frac * 100.0
);
}
server.notify_client(
client,
ServerGeneral::server_msg(ChatType::CommandInfo, info),
);
Ok(())
}
fn handle_spawn(
server: &mut Server,
client: EcsEntity,
@ -1188,8 +1429,8 @@ fn handle_spawn(
args: Vec<String>,
action: &ServerChatCommand,
) -> CmdResult<()> {
match parse_cmd_args!(args, String, npc::NpcBody, u32, bool) {
(Some(opt_align), Some(npc::NpcBody(id, mut body)), opt_amount, opt_ai) => {
match parse_cmd_args!(args, String, npc::NpcBody, u32, bool, f32) {
(Some(opt_align), Some(npc::NpcBody(id, mut body)), opt_amount, opt_ai, opt_scale) => {
let uid = uid(server, target, "target")?;
let alignment = parse_alignment(uid, &opt_align)?;
let amount = opt_amount.filter(|x| *x > 0).unwrap_or(1).min(50);
@ -1226,7 +1467,8 @@ fn handle_spawn(
body,
)
.with(comp::Vel(vel))
.with(body.scale())
.with(opt_scale.map(comp::Scale).unwrap_or(body.scale()))
.maybe_with(opt_scale.map(|s| comp::Mass(body.mass().0 * s.powi(3))))
.with(alignment);
if ai {
@ -1342,7 +1584,7 @@ fn handle_spawn_airship(
let ship = comp::ship::Body::random_airship_with(&mut rng);
let mut builder = server
.state
.create_ship(pos, ship, |ship| ship.make_collider(), true)
.create_ship(pos, ship, |ship| ship.make_collider())
.with(LightEmitter {
col: Rgb::new(1.0, 0.65, 0.2),
strength: 2.0,
@ -1350,7 +1592,8 @@ fn handle_spawn_airship(
animated: true,
});
if let Some(pos) = destination {
let (kp, ki, kd) = comp::agent::pid_coefficients(&comp::Body::Ship(ship));
let (kp, ki, kd) =
comp::agent::pid_coefficients(&comp::Body::Ship(ship)).unwrap_or((1.0, 0.0, 0.0));
fn pure_z(sp: Vec3<f32>, pv: Vec3<f32>) -> f32 { (sp - pv).z }
let agent = comp::Agent::from_body(&comp::Body::Ship(ship))
.with_destination(pos)
@ -1390,7 +1633,7 @@ fn handle_spawn_ship(
let ship = comp::ship::Body::random_ship_with(&mut rng);
let mut builder = server
.state
.create_ship(pos, ship, |ship| ship.make_collider(), true)
.create_ship(pos, ship, |ship| ship.make_collider())
.with(LightEmitter {
col: Rgb::new(1.0, 0.65, 0.2),
strength: 2.0,
@ -1398,7 +1641,8 @@ fn handle_spawn_ship(
animated: true,
});
if let Some(pos) = destination {
let (kp, ki, kd) = comp::agent::pid_coefficients(&comp::Body::Ship(ship));
let (kp, ki, kd) =
comp::agent::pid_coefficients(&comp::Body::Ship(ship)).unwrap_or((1.0, 0.0, 0.0));
fn pure_z(sp: Vec3<f32>, pv: Vec3<f32>) -> f32 { (sp - pv).z }
let agent = comp::Agent::from_body(&comp::Body::Ship(ship))
.with_destination(pos)
@ -1439,12 +1683,9 @@ fn handle_make_volume(
};
server
.state
.create_ship(
comp::Pos(pos.0 + Vec3::unit_z() * 50.0),
ship,
move |_| collider,
true,
)
.create_ship(comp::Pos(pos.0 + Vec3::unit_z() * 50.0), ship, move |_| {
collider
})
.build();
server.notify_client(
@ -1847,13 +2088,22 @@ fn handle_kill_npcs(
let to_kill = {
let ecs = server.state.ecs();
let entities = ecs.entities();
let positions = ecs.write_storage::<comp::Pos>();
let healths = ecs.write_storage::<comp::Health>();
let players = ecs.read_storage::<comp::Player>();
let alignments = ecs.read_storage::<Alignment>();
let rtsim_entities = ecs.read_storage::<common::rtsim::RtSimEntity>();
let mut rtsim = ecs.write_resource::<crate::rtsim::RtSim>();
(&entities, &healths, !&players, alignments.maybe())
(
&entities,
&healths,
!&players,
alignments.maybe(),
&positions,
)
.join()
.filter_map(|(entity, _health, (), alignment)| {
.filter_map(|(entity, _health, (), alignment, pos)| {
let should_kill = kill_pets
|| if let Some(Alignment::Owned(owned)) = alignment {
ecs.entity_from_uid(owned.0)
@ -1862,7 +2112,20 @@ fn handle_kill_npcs(
true
};
should_kill.then_some(entity)
if should_kill {
if let Some(rtsim_entity) = rtsim_entities.get(entity).copied() {
rtsim.hook_rtsim_actor_death(
&ecs.read_resource::<Arc<world::World>>(),
ecs.read_resource::<world::IndexOwned>().as_index_ref(),
Actor::Npc(rtsim_entity.0),
Some(pos.0),
None,
);
}
Some(entity)
} else {
None
}
})
.collect::<Vec<_>>()
};
@ -2482,7 +2745,7 @@ fn handle_tell(
} else {
message_opt.join(" ")
};
server.state.send_chat(mode.new_message(target_uid, msg));
server.state.send_chat(mode.to_plain_msg(target_uid, msg));
server.notify_client(target, ServerGeneral::ChatMode(mode));
Ok(())
} else {
@ -2507,7 +2770,7 @@ fn handle_faction(
let msg = args.join(" ");
if !msg.is_empty() {
if let Some(uid) = server.state.ecs().read_storage().get(target) {
server.state.send_chat(mode.new_message(*uid, msg));
server.state.send_chat(mode.to_plain_msg(*uid, msg));
}
}
server.notify_client(target, ServerGeneral::ChatMode(mode));
@ -2534,7 +2797,7 @@ fn handle_group(
let msg = args.join(" ");
if !msg.is_empty() {
if let Some(uid) = server.state.ecs().read_storage().get(target) {
server.state.send_chat(mode.new_message(*uid, msg));
server.state.send_chat(mode.to_plain_msg(*uid, msg));
}
}
server.notify_client(target, ServerGeneral::ChatMode(mode));
@ -2658,7 +2921,7 @@ fn handle_region(
let msg = args.join(" ");
if !msg.is_empty() {
if let Some(uid) = server.state.ecs().read_storage().get(target) {
server.state.send_chat(mode.new_message(*uid, msg));
server.state.send_chat(mode.to_plain_msg(*uid, msg));
}
}
server.notify_client(target, ServerGeneral::ChatMode(mode));
@ -2679,7 +2942,7 @@ fn handle_say(
let msg = args.join(" ");
if !msg.is_empty() {
if let Some(uid) = server.state.ecs().read_storage().get(target) {
server.state.send_chat(mode.new_message(*uid, msg));
server.state.send_chat(mode.to_plain_msg(*uid, msg));
}
}
server.notify_client(target, ServerGeneral::ChatMode(mode));
@ -2700,7 +2963,7 @@ fn handle_world(
let msg = args.join(" ");
if !msg.is_empty() {
if let Some(uid) = server.state.ecs().read_storage().get(target) {
server.state.send_chat(mode.new_message(*uid, msg));
server.state.send_chat(mode.to_plain_msg(*uid, msg));
}
}
server.notify_client(target, ServerGeneral::ChatMode(mode));
@ -2729,8 +2992,9 @@ fn handle_join_faction(
.flatten()
.map(|f| f.0);
server.state.send_chat(
// TODO: Localise
ChatType::FactionMeta(faction.clone())
.chat_msg(format!("[{}] joined faction ({})", alias, faction)),
.into_plain_msg(format!("[{}] joined faction ({})", alias, faction)),
);
(faction_join, mode)
} else {
@ -2746,8 +3010,9 @@ fn handle_join_faction(
};
if let Some(faction) = faction_leave {
server.state.send_chat(
// TODO: Localise
ChatType::FactionMeta(faction.clone())
.chat_msg(format!("[{}] left faction ({})", alias, faction)),
.into_plain_msg(format!("[{}] left faction ({})", alias, faction)),
);
}
server.notify_client(target, ServerGeneral::ChatMode(mode));
@ -3835,3 +4100,33 @@ fn handle_body(
Err(action.help_string())
}
}
fn handle_scale(
server: &mut Server,
client: EcsEntity,
target: EcsEntity,
args: Vec<String>,
action: &ServerChatCommand,
) -> CmdResult<()> {
if let (Some(scale), reset_mass) = parse_cmd_args!(args, f32, bool) {
let scale = scale.clamped(0.025, 1000.0);
insert_or_replace_component(server, target, comp::Scale(scale), "target")?;
if reset_mass.unwrap_or(true) {
let mass = server.state.ecs()
.read_storage::<comp::Body>()
.get(target)
// Mass is derived from volume, which changes with the third power of scale
.map(|body| body.mass().0 * scale.powi(3));
if let Some(mass) = mass {
insert_or_replace_component(server, target, comp::Mass(mass), "target")?;
}
}
server.notify_client(
client,
ServerGeneral::server_msg(ChatType::CommandInfo, format!("Set scale to {}", scale)),
);
Ok(())
} else {
Err(action.help_string())
}
}

View File

@ -9,6 +9,7 @@ pub enum Error {
StreamErr(StreamError),
DatabaseErr(rusqlite::Error),
PersistenceErr(PersistenceError),
RtsimError(ron::Error),
Other(String),
}
@ -41,6 +42,7 @@ impl Display for Error {
Self::StreamErr(err) => write!(f, "Stream Error: {}", err),
Self::DatabaseErr(err) => write!(f, "Database Error: {}", err),
Self::PersistenceErr(err) => write!(f, "Persistence Error: {}", err),
Self::RtsimError(err) => write!(f, "Rtsim Error: {}", err),
Self::Other(err) => write!(f, "Error: {}", err),
}
}

View File

@ -1,24 +1,22 @@
use crate::{
client::Client, events::player::handle_exit_ingame, persistence::PersistedComponents, sys,
CharacterUpdater, Server, StateExt,
client::Client, events::player::handle_exit_ingame, persistence::PersistedComponents,
presence::RepositionOnChunkLoad, sys, CharacterUpdater, Server, StateExt,
};
use common::{
character::CharacterId,
comp::{
self,
agent::pid_coefficients,
aura::{Aura, AuraKind, AuraTarget},
beam,
buff::{BuffCategory, BuffData, BuffKind, BuffSource},
shockwave, Agent, Alignment, Anchor, BehaviorCapability, Body, Health, Inventory, ItemDrop,
LightEmitter, Object, Ori, PidController, Poise, Pos, Projectile, Scale, SkillSet, Stats,
TradingBehavior, Vel, WaypointArea,
shockwave, Alignment, BehaviorCapability, Body, ItemDrop, LightEmitter, Object, Ori, Pos,
Projectile, TradingBehavior, Vel, WaypointArea,
},
event::{EventBus, UpdateCharacterMetadata},
lottery::LootSpec,
event::{EventBus, NpcBuilder, UpdateCharacterMetadata},
mounting::Mounting,
outcome::Outcome,
resources::{Secs, Time},
rtsim::RtSimEntity,
rtsim::RtSimVehicle,
uid::Uid,
util::Dir,
ViewDistances,
@ -91,63 +89,56 @@ pub fn handle_loaded_character_data(
server.notify_client(entity, ServerGeneral::CharacterDataLoadResult(Ok(metadata)));
}
pub fn handle_create_npc(
server: &mut Server,
pos: Pos,
stats: Stats,
skill_set: SkillSet,
health: Option<Health>,
poise: Poise,
inventory: Inventory,
body: Body,
agent: impl Into<Option<Agent>>,
alignment: Alignment,
scale: Scale,
loot: LootSpec<String>,
home_chunk: Option<Anchor>,
rtsim_entity: Option<RtSimEntity>,
projectile: Option<Projectile>,
) {
pub fn handle_create_npc(server: &mut Server, pos: Pos, mut npc: NpcBuilder) -> EcsEntity {
let entity = server
.state
.create_npc(pos, stats, skill_set, health, poise, inventory, body)
.with(scale);
.create_npc(
pos,
npc.stats,
npc.skill_set,
npc.health,
npc.poise,
npc.inventory,
npc.body,
)
.with(npc.scale);
let mut agent = agent.into();
if let Some(agent) = &mut agent {
if let Alignment::Owned(_) = &alignment {
if let Some(agent) = &mut npc.agent {
if let Alignment::Owned(_) = &npc.alignment {
agent.behavior.allow(BehaviorCapability::TRADE);
agent.behavior.trading_behavior = TradingBehavior::AcceptFood;
}
}
let entity = entity.with(alignment);
let entity = entity.with(npc.alignment);
let entity = if let Some(agent) = agent {
let entity = if let Some(agent) = npc.agent {
entity.with(agent)
} else {
entity
};
let entity = if let Some(drop_item) = loot.to_item() {
let entity = if let Some(drop_item) = npc.loot.to_item() {
entity.with(ItemDrop(drop_item))
} else {
entity
};
let entity = if let Some(home_chunk) = home_chunk {
let entity = if let Some(home_chunk) = npc.anchor {
entity.with(home_chunk)
} else {
entity
};
let entity = if let Some(rtsim_entity) = rtsim_entity {
entity.with(rtsim_entity)
let entity = if let Some(rtsim_entity) = npc.rtsim_entity {
entity.with(rtsim_entity).with(RepositionOnChunkLoad {
needs_ground: false,
})
} else {
entity
};
let entity = if let Some(projectile) = projectile {
let entity = if let Some(projectile) = npc.projectile {
entity.with(projectile)
} else {
entity
@ -156,7 +147,7 @@ pub fn handle_create_npc(
let new_entity = entity.build();
// Add to group system if a pet
if let comp::Alignment::Owned(owner_uid) = alignment {
if let comp::Alignment::Owned(owner_uid) = npc.alignment {
let state = server.state();
let clients = state.ecs().read_storage::<Client>();
let uids = state.ecs().read_storage::<Uid>();
@ -187,7 +178,7 @@ pub fn handle_create_npc(
},
);
}
} else if let Some(group) = match alignment {
} else if let Some(group) = match npc.alignment {
Alignment::Wild => None,
Alignment::Passive => None,
Alignment::Enemy => Some(comp::group::ENEMY),
@ -196,19 +187,22 @@ pub fn handle_create_npc(
} {
let _ = server.state.ecs().write_storage().insert(new_entity, group);
}
new_entity
}
pub fn handle_create_ship(
server: &mut Server,
pos: Pos,
ship: comp::ship::Body,
mountable: bool,
agent: Option<Agent>,
rtsim_entity: Option<RtSimEntity>,
rtsim_vehicle: Option<RtSimVehicle>,
driver: Option<NpcBuilder>,
passengers: Vec<NpcBuilder>,
) {
let mut entity = server
.state
.create_ship(pos, ship, |ship| ship.make_collider(), mountable);
.create_ship(pos, ship, |ship| ship.make_collider());
/*
if let Some(mut agent) = agent {
let (kp, ki, kd) = pid_coefficients(&Body::Ship(ship));
fn pure_z(sp: Vec3<f32>, pv: Vec3<f32>) -> f32 { (sp - pv).z }
@ -216,10 +210,35 @@ pub fn handle_create_ship(
agent.with_position_pid_controller(PidController::new(kp, ki, kd, pos.0, 0.0, pure_z));
entity = entity.with(agent);
}
if let Some(rtsim_entity) = rtsim_entity {
entity = entity.with(rtsim_entity);
*/
if let Some(rtsim_vehicle) = rtsim_vehicle {
entity = entity.with(rtsim_vehicle);
}
let entity = entity.build();
if let Some(driver) = driver {
let npc_entity = handle_create_npc(server, pos, driver);
let uids = server.state.ecs().read_storage::<Uid>();
if let (Some(rider_uid), Some(mount_uid)) =
(uids.get(npc_entity).copied(), uids.get(entity).copied())
{
drop(uids);
server
.state
.link(Mounting {
mount: mount_uid,
rider: rider_uid,
})
.expect("Failed to link driver to ship");
} else {
panic!("Couldn't get Uid from newly created ship and npc");
}
}
for passenger in passengers {
handle_create_npc(server, Pos(pos.0 + Vec3::unit_z() * 5.0), passenger);
}
entity.build();
}
pub fn handle_shoot(

View File

@ -7,7 +7,7 @@ use crate::{
skillset::SkillGroupKind,
BuffKind, BuffSource, PhysicsState,
},
rtsim::RtSim,
rtsim,
sys::terrain::SAFE_ZONE_RADIUS,
Server, SpawnPoint, StateExt,
};
@ -26,7 +26,6 @@ use common::{
event::{EventBus, ServerEvent},
outcome::{HealthChangeInfo, Outcome},
resources::{Secs, Time},
rtsim::RtSimEntity,
states::utils::StageSection,
terrain::{Block, BlockKind, TerrainGrid},
uid::{Uid, UidAllocator},
@ -36,14 +35,13 @@ use common::{
};
use common_net::{msg::ServerGeneral, sync::WorldSyncExt};
use common_state::BlockChange;
use comp::chat::GenericChatMsg;
use hashbrown::HashSet;
use rand::{distributions::WeightedIndex, Rng};
use rand_distr::Distribution;
use specs::{
join::Join, saveload::MarkerAllocator, Builder, Entity as EcsEntity, Entity, WorldExt,
};
use std::{collections::HashMap, iter, time::Duration};
use std::{collections::HashMap, iter, sync::Arc, time::Duration};
use tracing::{debug, error};
use vek::{Vec2, Vec3};
@ -199,10 +197,7 @@ pub fn handle_destroy(server: &mut Server, entity: EcsEntity, last_change: Healt
_ => KillSource::Other,
};
state.send_chat(GenericChatMsg {
chat_type: comp::ChatType::Kill(kill_source, *uid),
message: "".to_string(),
});
state.send_chat(comp::ChatType::Kill(kill_source, *uid).into_plain_msg(""));
}
}
@ -519,16 +514,32 @@ pub fn handle_destroy(server: &mut Server, entity: EcsEntity, last_change: Healt
}
if should_delete {
if let Some(rtsim_entity) = state
.ecs()
.read_storage::<RtSimEntity>()
.get(entity)
.copied()
{
if let Some(actor) = state.entity_as_actor(entity) {
state
.ecs()
.write_resource::<RtSim>()
.destroy_entity(rtsim_entity.0);
.write_resource::<rtsim::RtSim>()
.hook_rtsim_actor_death(
&state.ecs().read_resource::<Arc<world::World>>(),
state
.ecs()
.read_resource::<world::IndexOwned>()
.as_index_ref(),
actor,
state.ecs().read_storage::<Pos>().get(entity).map(|p| p.0),
last_change
.by
.as_ref()
.and_then(
|(DamageContributor::Solo(entity_uid)
| DamageContributor::Group { entity_uid, .. })| {
state
.ecs()
.read_resource::<UidAllocator>()
.retrieve_entity_internal((*entity_uid).into())
},
)
.and_then(|killer| state.entity_as_actor(killer)),
);
}
if let Err(e) = state.delete_entity_recorded(entity) {

View File

@ -9,6 +9,7 @@ use common::{
dialogue::Subject,
inventory::slot::EquipSlot,
loot_owner::LootOwnerKind,
pet::is_mountable,
tool::ToolKind,
Inventory, LootOwner, Pos, SkillGroupKind,
},
@ -119,7 +120,15 @@ pub fn handle_mount(server: &mut Server, rider: EcsEntity, mount: EcsEntity) {
Some(comp::Alignment::Owned(owner)) if *owner == rider_uid,
);
if is_pet {
let can_ride = state
.ecs()
.read_storage()
.get(mount)
.map_or(false, |mount_body| {
is_mountable(mount_body, state.ecs().read_storage().get(rider))
});
if is_pet && can_ride {
drop(uids);
drop(healths);
let _ = state.link(Mounting {

Some files were not shown because too many files have changed in this diff Show More