mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
Merge branch 'rtsim2' into 'master'
Initial implementation of rtsim2 Closes #1476 See merge request veloren/veloren!3517
This commit is contained in:
commit
9e17042bf6
19
CHANGELOG.md
19
CHANGELOG.md
@ -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
85
Cargo.lock
generated
@ -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",
|
||||
|
@ -16,6 +16,7 @@ members = [
|
||||
"plugin/api",
|
||||
"plugin/derive",
|
||||
"plugin/rt",
|
||||
"rtsim",
|
||||
"server",
|
||||
"server/agent",
|
||||
"server-cli",
|
||||
|
21
assets/common/entity/village/captain.ron
Normal file
21
assets/common/entity/village/captain.ron
Normal 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"),
|
||||
],
|
||||
)
|
23
assets/common/entity/village/farmer.ron
Normal file
23
assets/common/entity/village/farmer.ron
Normal 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: [],
|
||||
)
|
21
assets/common/entity/village/herbalist.ron
Normal file
21
assets/common/entity/village/herbalist.ron
Normal 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: [],
|
||||
)
|
23
assets/common/entity/village/hunter.ron
Normal file
23
assets/common/entity/village/hunter.ron
Normal 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: [],
|
||||
)
|
@ -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)),
|
||||
|
11
assets/common/entity/wild/peaceful/dog.ron
Normal file
11
assets/common/entity/wild/peaceful/dog.ron
Normal 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: [],
|
||||
)
|
26
assets/common/loadout/village/captain.ron
Normal file
26
assets/common/loadout/village/captain.ron
Normal 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")),
|
||||
]),
|
||||
)
|
30
assets/common/loadout/village/farmer.ron
Normal file
30
assets/common/loadout/village/farmer.ron
Normal 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")),
|
||||
]),
|
||||
)
|
26
assets/common/loadout/village/herbalist.ron
Normal file
26
assets/common/loadout/village/herbalist.ron
Normal 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")),
|
||||
]),
|
||||
)
|
28
assets/common/loadout/village/hunter.ron
Normal file
28
assets/common/loadout/village/hunter.ron
Normal 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")),
|
||||
]),
|
||||
)
|
@ -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"),
|
||||
)
|
||||
)
|
||||
|
@ -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!
|
||||
|
@ -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);
|
||||
|
@ -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 *
|
||||
|
@ -103,7 +103,7 @@ fn main() {
|
||||
&localisation.read(),
|
||||
SHOW_NAME,
|
||||
)
|
||||
.message
|
||||
.1
|
||||
),
|
||||
Event::Disconnect => {}, // TODO
|
||||
Event::DisconnectionNotification(time) => {
|
||||
|
@ -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"] }
|
||||
|
@ -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
|
||||
///
|
||||
|
@ -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(_) => {
|
||||
|
@ -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"
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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(_) => {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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",
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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>>;
|
||||
}
|
||||
|
@ -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 }
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
use vek::Vec2;
|
||||
// TODO: Move this to common/src/, it's not a component
|
||||
|
||||
/// Cardinal directions
|
||||
pub enum Direction {
|
||||
|
@ -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)
|
||||
},
|
||||
}
|
||||
|
@ -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::{
|
||||
|
@ -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
128
common/src/comp/presence.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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),
|
||||
|
@ -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 {
|
||||
|
@ -10,6 +10,7 @@ bitflags::bitflags! {
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Flags: u8 {
|
||||
const SNOW_COVERED = 0b00000001;
|
||||
const IS_BUILDING = 0b00000010;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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,
|
||||
};
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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),
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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),
|
||||
|
@ -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()
|
||||
|
@ -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() {
|
||||
|
@ -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| {
|
||||
|
@ -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?))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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};
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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,
|
||||
);
|
||||
|
@ -1,4 +1,4 @@
|
||||
#![feature(btree_drain_filter)]
|
||||
#![feature(drain_filter)]
|
||||
#![allow(clippy::option_map_unit_fn)]
|
||||
|
||||
mod aura;
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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(())
|
||||
|
@ -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
23
rtsim/Cargo.toml
Normal 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
878
rtsim/src/ai/mod.rs
Normal 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
43
rtsim/src/data/faction.rs
Normal 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
152
rtsim/src/data/mod.rs
Normal 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
64
rtsim/src/data/nature.rs
Normal 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
396
rtsim/src/data/npc.rs
Normal 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
69
rtsim/src/data/report.rs
Normal 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
204
rtsim/src/data/sentiment.rs
Normal 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
86
rtsim/src/data/site.rs
Normal 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
38
rtsim/src/event.rs
Normal 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
14
rtsim/src/gen/faction.rs
Normal 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
222
rtsim/src/gen/mod.rs
Normal 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
22
rtsim/src/gen/name.rs
Normal 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
58
rtsim/src/gen/site.rs
Normal 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
189
rtsim/src/lib.rs
Normal 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
29
rtsim/src/rule.rs
Normal 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
70
rtsim/src/rule/cleanup.rs
Normal 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
87
rtsim/src/rule/migrate.rs
Normal 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
1034
rtsim/src/rule/npc_ai.rs
Normal file
File diff suppressed because it is too large
Load Diff
42
rtsim/src/rule/replenish_resources.rs
Normal file
42
rtsim/src/rule/replenish_resources.rs
Normal 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
46
rtsim/src/rule/report.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
275
rtsim/src/rule/simulate_npcs.rs
Normal file
275
rtsim/src/rule/simulate_npcs.rs
Normal 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
111
rtsim/src/rule/sync_npcs.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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"
|
||||
|
||||
|
@ -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"] }
|
||||
|
@ -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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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) {
|
||||
|
@ -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
Loading…
Reference in New Issue
Block a user