Merge remote-tracking branch 'origin/master' into sharp/zoomy-worldgen

This commit is contained in:
Joshua Yanovski 2023-04-12 19:39:23 -07:00
commit 233d2e2685
214 changed files with 9556 additions and 5713 deletions

View File

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

87
Cargo.lock generated
View File

@ -145,6 +145,12 @@ version = "1.0.65"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98161a4e3e2184da77bb14f02184cdd111e83bbbcc9979dfee3c44b9a85f5602"
[[package]]
name = "anymap2"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d301b3b94cb4b2f23d7917810addbbaff90738e0ca2be692bd027e70d7e0330c"
[[package]]
name = "app_dirs2"
version = "2.5.4"
@ -1862,6 +1868,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"
@ -4345,6 +4372,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"
@ -5139,6 +5172,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"
@ -6758,6 +6813,7 @@ dependencies = [
"serde",
"tracing",
"unic-langid",
"veloren-common",
"veloren-common-assets",
]
@ -6780,6 +6836,7 @@ dependencies = [
"crossbeam-utils 0.8.11",
"csv",
"dot_vox",
"enum-map",
"executors",
"futures",
"fxhash",
@ -6956,7 +7013,7 @@ dependencies = [
"crossbeam-channel",
"futures-core",
"futures-util",
"hashbrown 0.11.2",
"hashbrown 0.12.3",
"lazy_static",
"lz-fear",
"prometheus",
@ -7019,6 +7076,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"
@ -7031,6 +7111,7 @@ dependencies = [
"chrono-tz",
"crossbeam-channel",
"drop_guard",
"enum-map",
"enumset",
"futures-util",
"hashbrown 0.12.3",
@ -7068,6 +7149,7 @@ dependencies = [
"veloren-common-systems",
"veloren-network",
"veloren-plugin-api",
"veloren-rtsim",
"veloren-server-agent",
"veloren-world",
]
@ -7086,6 +7168,8 @@ dependencies = [
"veloren-common-base",
"veloren-common-dynlib",
"veloren-common-ecs",
"veloren-common-net",
"veloren-rtsim",
]
[[package]]
@ -7248,6 +7332,7 @@ dependencies = [
"csv",
"deflate",
"enum-iterator 1.1.3",
"enum-map",
"fallible-iterator",
"flate2",
"fxhash",

View File

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

View File

@ -476,6 +476,24 @@
Simple(None, "common.abilities.custom.minotaur.frenzy"),
],
),
Custom("Dullahan"): (
primary: Simple(None, "common.abilities.custom.dullahan.melee"),
secondary: Simple(None, "common.abilities.custom.dullahan.fierce_darts"),
abilities: [
Simple(None, "common.abilities.custom.dullahan.knife_rain"),
Simple(None, "common.abilities.custom.dullahan.dash"),
],
),
Custom("Cyclops"): (
primary: Simple(None, "common.abilities.custom.cyclops.doublestrike"),
secondary: Simple(None, "common.abilities.custom.cyclops.optic_blast"),
abilities: [
Simple(None, "common.abilities.custom.cyclops.hammer_shockwave"),
Simple(None, "common.abilities.custom.cyclops.dash"),
Simple(None, "common.abilities.custom.cyclops.reinforce"),
],
),
Custom("Clay Golem"): (
primary: Simple(None, "common.abilities.custom.claygolem.strike"),
secondary: Simple(None, "common.abilities.custom.claygolem.laser"),

View File

@ -0,0 +1,28 @@
DashMelee(
energy_cost: 0,
melee_constructor: (
kind: Bash(
damage: 28.0,
poise: 20.0,
knockback: 2.0,
energy_regen: 0.0,
),
scaled: Some(Bash(
damage: 36.0,
poise: 60.0,
knockback: 5.0,
energy_regen: 0.0,
)),
range: 6.0,
angle: 90.0,
multi_target: Some(Normal),
),
energy_drain: 0,
forward_speed: 9.0,
buildup_duration: 0.8,
charge_duration: 2.0,
swing_duration: 0.1,
recover_duration: 0.8,
ori_modifier: 0.1,
charge_through: false,
)

View File

@ -0,0 +1,49 @@
ComboMelee(
stage_data: [
(
stage: 1,
base_damage: 32.0,
damage_increase: 0.0,
base_poise_damage: 20,
poise_damage_increase: 0.0,
knockback: 5.0,
range: 6,
angle: 90.0,
base_buildup_duration: 0.5,
base_swing_duration: 0.4,
hit_timing: 0.4,
base_recover_duration: 0.4,
forward_movement: 0.3,
damage_kind: Crushing,
),
(
stage: 2,
base_damage: 36.0,
damage_increase: 0.0,
base_poise_damage: 40.0,
poise_damage_increase: 0.0,
knockback: 10.0,
range: 8,
angle: 45.0,
base_buildup_duration: 0.6,
base_swing_duration: 0.6,
hit_timing: 0.3,
base_recover_duration: 1.2,
forward_movement: 0.2,
damage_kind: Crushing,
damage_effect: Some(Buff((
kind: Crippled,
dur_secs: 3.0,
strength: DamageFraction(0.1),
chance: 1.0,
))),
),
],
initial_energy_gain: 0,
max_energy_gain: 0,
energy_increase: 0,
speed_increase: 0.0,
max_speed_increase: 0.0,
scales_from_combo: 0,
ori_modifier: 0.65,
)

View File

@ -0,0 +1,18 @@
Shockwave(
energy_cost: 0,
buildup_duration: 1.2,
swing_duration: 0.12,
recover_duration: 0.8,
damage: 45.0,
poise_damage: 60,
knockback: (strength: 10.0, direction: TowardsUp),
shockwave_angle: 360.0,
shockwave_vertical_angle: 360.0,
shockwave_speed: 40.0,
shockwave_duration: 0.4,
requires_ground: true,
move_efficiency: 0.0,
damage_kind: Piercing,
specifier: Ground,
ori_rate: 1.0,
)

View File

@ -0,0 +1,16 @@
BasicRanged(
energy_cost: 0.0,
buildup_duration: 1.6,
recover_duration: 1.2,
projectile: LaserBeam(
damage: 48.0,
radius: 8.0,
knockback: 5.0,
energy_regen: 20.0,
min_falloff: 0.0,
),
projectile_body: Object(LaserBeam),
projectile_speed: 100.0,
num_projectiles: 1,
projectile_spread: 0,
)

View File

@ -0,0 +1,9 @@
SelfBuff(
buildup_duration: 0.4,
cast_duration: 0.8,
recover_duration: 0.3,
buff_kind: ProtectingWard,
buff_strength: 2.0,
buff_duration: Some(300.0),
energy_cost: 0,
)

View File

@ -0,0 +1,28 @@
DashMelee(
energy_cost: 0,
melee_constructor: (
kind: Bash(
damage: 28.5,
poise: 30.0,
knockback: 2.0,
energy_regen: 0.0,
),
scaled: Some(Bash(
damage: 36.0,
poise: 38.6,
knockback: 3.0,
energy_regen: 0.0,
)),
range: 5.0,
angle: 90.0,
multi_target: Some(Normal),
),
energy_drain: 0,
forward_speed: 8.0,
buildup_duration: 0.6,
charge_duration: 2.0,
swing_duration: 0.1,
recover_duration: 1.2,
ori_modifier: 0.1,
charge_through: false,
)

View File

@ -0,0 +1,15 @@
BasicRanged(
energy_cost: 0.0,
buildup_duration: 0.55,
recover_duration: 0.45,
projectile: Knife(
damage: 31.0,
knockback: 5.0,
energy_regen: 20.0,
min_falloff: 0.0,
),
projectile_body: Object(SpectralSwordLarge),
projectile_speed: 120.0,
num_projectiles: 3,
projectile_spread: 0.075,
)

View File

@ -0,0 +1,16 @@
BasicRanged(
energy_cost: 0.0,
buildup_duration: 1.1,
recover_duration: 0.2,
projectile: Knife(
damage: 34.0,
radius: 3.8,
knockback: 5.0,
energy_regen: 20.0,
min_falloff: 0.3,
),
projectile_body: Object(SpectralSwordSmall),
projectile_speed: 20.0,
num_projectiles: 36,
projectile_spread: 0.4,
)

View File

@ -0,0 +1,55 @@
ComboMelee(
stage_data: [
(
stage: 1,
base_damage: 35.5,
damage_increase: 0.0,
base_poise_damage: 15.0,
poise_damage_increase: 0.0,
knockback: 2.0,
range: 6.0,
angle: 60.0,
base_buildup_duration: 0.8,
base_swing_duration: 0.1,
hit_timing: 0.4,
base_recover_duration: 0.3,
forward_movement: 0.8,
damage_kind: Slashing,
damage_effect: Some(Buff((
kind: Bleeding,
dur_secs: 3.0,
strength: DamageFraction(0.05),
chance: 0.3,
))),
),
(
stage: 2,
base_damage: 38.5,
damage_increase: 0.0,
base_poise_damage: 20.0,
poise_damage_increase: 0.0,
knockback: 8.0,
range: 6.0,
angle: 60.0,
base_buildup_duration: 0.7,
base_swing_duration: 0.1,
hit_timing: 0.4,
base_recover_duration: 1.3,
forward_movement: 0.2,
damage_kind: Slashing,
damage_effect: Some(Buff((
kind: Bleeding,
dur_secs: 3.0,
strength: DamageFraction(0.1),
chance: 0.15,
))),
),
],
initial_energy_gain: 0,
max_energy_gain: 0,
energy_increase: 0,
speed_increase: 0.0,
max_speed_increase: 0.0,
scales_from_combo: 0,
ori_modifier: 0.6,
)

View File

@ -1,7 +1,7 @@
#![enable(implicit_some)]
(
name: Name("Dullahan"),
body: RandomWith("dullahan"),
name: Automatic,
body: RandomWith("cyclops"),
alignment: Alignment(Enemy),
loot: LootTable("common.loot_tables.dungeon.tier-4.miniboss"),
inventory: (

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
#![enable(implicit_some)]
(
name: Automatic,
body: RandomWith("cyclops"),
name: Name("Dullahan"),
body: RandomWith("dullahan"),
alignment: Alignment(Enemy),
loot: LootTable("common.loot_tables.creature.biped_large.default"),
inventory: (

View File

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

View File

@ -2,7 +2,7 @@
(
name: Automatic,
body: RandomWith("mammoth"),
alignment: Alignment(Enemy),
alignment: Alignment(Wild),
loot: LootTable("common.loot_tables.creature.quad_medium.mammoth"),
inventory: (
loadout: FromBody,

View File

@ -0,0 +1,13 @@
ItemDef(
name: "Cyclops Armor",
description: "Made of mysteries.",
kind: Armor((
kind: Chest,
stats: Direct((
protection: Some(Normal(120.0)),
poise_resilience: Some(Normal(60.0)),
)),
)),
quality: Moderate,
tags: [],
)

View File

@ -0,0 +1,13 @@
ItemDef(
name: "Dullahan Itself Armor",
description: "Made of It ownself.",
kind: Armor((
kind: Chest,
stats: Direct((
protection: Some(Normal(200.0)),
poise_resilience: Some(Normal(10.0)),
)),
)),
quality: Moderate,
tags: [],
)

View File

@ -17,5 +17,5 @@ ItemDef(
)),
quality: Low,
tags: [],
ability_spec: Some(Custom("Hammer Simple")),
ability_spec: Some(Custom("Cyclops")),
)

View File

@ -5,11 +5,11 @@ ItemDef(
kind: Sword,
hands: Two,
stats: (
equip_time_secs: 0.5,
power: 1.5,
equip_time_secs: 0.01,
power: 1.0,
effect_power: 1.0,
speed: 0.75,
crit_chance: 0.0625,
speed: 1.0,
crit_chance: 0.0645,
range: 1.0,
energy_efficiency: 1.0,
buff_strength: 1.0,
@ -17,5 +17,5 @@ ItemDef(
)),
quality: Low,
tags: [],
ability_spec: Some(Custom("Sword Simple")),
ability_spec: Some(Custom("Dullahan")),
)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1244,6 +1244,18 @@
],
threshold: 0.2,
),
LaserBeam: (
files: [
"voxygen.audio.sfx.abilities.laser_beam",
],
threshold: 1.25,
),
CyclopsCharge: (
files: [
"voxygen.audio.sfx.abilities.cyclops_charge",
],
threshold: 0.3,
),
GigaRoar: (
files: [
"voxygen.audio.sfx.abilities.gigas_frost_roar",

BIN
assets/voxygen/audio/sfx/abilities/cyclops_charge.ogg (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/audio/sfx/abilities/laser_beam.ogg (Stored with Git LFS) Normal file

Binary file not shown.

View File

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

View File

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

View File

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

View File

@ -80,6 +80,7 @@ const int STEAM = 39;
const int BARRELORGAN = 40;
const int POTION_SICKNESS = 41;
const int GIGA_SNOW = 42;
const int CYCLOPS_CHARGE = 43;
// meters per second squared (acceleration)
const float earth_gravity = 9.807;
@ -666,6 +667,16 @@ void main() {
spin_in_axis(vec3(rand6, rand7, rand8), percent() * 10 + 3 * rand9)
);
break;
case CYCLOPS_CHARGE:
f_reflect = 0.0;
float burn_size = 8.0 * (1 - slow_start(0.1)) * slow_end(0.15);
attr = Attr(
(inst_dir * slow_end(1.5)) + vec3(rand0, rand1, rand2) * (percent() + 2) * 0.1,
vec3(burn_size),
vec4(vec3(6.9, 0.0, 0.0), 1),
spin_in_axis(vec3(rand6, rand7, rand8), percent() * 10 + 3 * rand9)
);
break;
default:
attr = Attr(
linear_motion(

View File

@ -889,4 +889,34 @@
central: ("armor.empty"),
)
),
SpectralSwordSmall: (
bone0: (
offset: (-0.5, -25.0, -8.5),
central: ("weapon.projectile.spectral_sword_small"),
),
bone1: (
offset: (0.0, 0.0, 0.0),
central: ("armor.empty"),
)
),
SpectralSwordLarge: (
bone0: (
offset: (-0.5, -30.0, -8.5),
central: ("weapon.projectile.spectral_sword_large"),
),
bone1: (
offset: (0.0, 0.0, 0.0),
central: ("armor.empty"),
)
),
LaserBeam: (
bone0: (
offset: (-6.0, -60.0, -17.0),
central: ("weapon.projectile.laser_beam"),
),
bone1: (
offset: (0.0, 0.0, 0.0),
central: ("armor.empty"),
)
),
})

View File

@ -177,7 +177,7 @@
central: ("npc.salamander.male.tail_rear"),
),
tail_front: (
offset: (-4.5, -9.0, -3.0),
offset: (-5.5, -9.0, -3.0),
central: ("npc.salamander.male.tail_front"),
),
),
@ -203,7 +203,7 @@
central: ("npc.salamander.male.tail_rear"),
),
tail_front: (
offset: (-4.5, -9.0, -3.0),
offset: (-5.5, -9.0, -3.0),
central: ("npc.salamander.male.tail_front"),
),
),

BIN
assets/voxygen/voxel/weapon/projectile/laser_beam.vox (Stored with Git LFS) Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -6,7 +6,7 @@ SpawnEntry (
groups: [
(1, (1, 1, "common.entity.wild.aggressive.ogre")),
(1, (1, 1, "common.entity.wild.aggressive.swamp_troll")),
(1, (1, 1, "common.entity.wild.aggressive.cyclops")),
(1, (1, 1, "common.entity.wild.aggressive.dullahan")),
],
spawn_mode: Land,
day_period: [Night, Morning, Noon, Evening],

View File

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

View File

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

View File

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

View File

@ -30,7 +30,7 @@ use common::{
slot::{EquipSlot, InvSlotId, Slot},
CharacterState, ChatMode, ControlAction, ControlEvent, Controller, ControllerInputs,
GroupManip, InputKind, InventoryAction, InventoryEvent, InventoryUpdateEvent,
MapMarkerChange, UtteranceKind,
MapMarkerChange, PresenceKind, UtteranceKind,
},
event::{EventBus, LocalEvent, UpdateCharacterMetadata},
grid::Grid,
@ -60,8 +60,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,
};
@ -1904,6 +1904,7 @@ impl Client {
true,
None,
&self.connected_server_constants,
|_, _| {},
);
// TODO: avoid emitting these in the first place
let _ = self
@ -2332,13 +2333,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())
)),
@ -2355,7 +2358,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())
)),
@ -2889,20 +2893,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
@ -3069,7 +3073,7 @@ mod tests {
&localisation.read(),
true,
)
.message;
.1;
},
Event::Disconnect => {},
Event::DisconnectionNotification(_) => {

View File

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

View File

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

View File

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

View File

@ -6,7 +6,7 @@ use crate::sync;
use common::{
calendar::Calendar,
character::{self, CharacterItem},
comp::{self, invite::InviteKind, item::MaterialStatManifest},
comp::{self, invite::InviteKind, item::MaterialStatManifest, Content},
event::UpdateCharacterMetadata,
lod,
outcome::Outcome,
@ -222,11 +222,10 @@ pub enum ServerGeneral<'a> {
}
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()))
}
}
@ -303,7 +302,7 @@ impl ServerMsg<'_> {
&self,
c_type: ClientType,
registered: bool,
presence: Option<super::PresenceKind>,
presence: Option<comp::PresenceKind>,
) -> bool {
match self {
ServerMsg::Info(_) | ServerMsg::Init(_) | ServerMsg::RegisterAnswer(_) => {

View File

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

View File

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

View File

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

View File

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

View File

@ -23,17 +23,16 @@ use crate::{
util::Dir,
};
#[cfg(not(target_arch = "wasm32"))]
use rand::{thread_rng, Rng};
use serde::{Deserialize, Serialize};
use crate::{comp::Group, resources::Time};
#[cfg(not(target_arch = "wasm32"))]
use specs::{saveload::MarkerAllocator, Entity as EcsEntity, ReadStorage};
#[cfg(not(target_arch = "wasm32"))]
use std::ops::{Mul, MulAssign};
#[cfg(not(target_arch = "wasm32"))] use vek::*;
use {
rand::Rng,
specs::{saveload::MarkerAllocator, Entity as EcsEntity, ReadStorage},
std::ops::{Mul, MulAssign},
vek::*,
};
#[cfg(not(target_arch = "wasm32"))]
#[derive(Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
@ -199,10 +198,10 @@ impl Attack {
time: Time,
mut emit: impl FnMut(ServerEvent),
mut emit_outcome: impl FnMut(Outcome),
rng: &mut rand::rngs::ThreadRng,
) -> bool {
// TODO: Maybe move this higher and pass it as argument into this function?
let msm = &MaterialStatManifest::load().read();
let mut rng = thread_rng();
let AttackOptions {
target_dodging,
@ -518,7 +517,7 @@ impl Attack {
.filter(|e| e.target.map_or(true, |t| t == target_group))
.filter(|e| !avoid_effect(e))
{
if effect.requirements.iter().all(|req| match req {
let requirements_met = effect.requirements.iter().all(|req| match req {
CombatRequirement::AnyDamage => accumulated_damage > 0.0 && target.health.is_some(),
CombatRequirement::Energy(r) => {
if let Some(AttackerInfo {
@ -560,7 +559,8 @@ impl Attack {
false
}
},
}) {
});
if requirements_met {
is_applied = true;
match effect.effect {
CombatEffect::Knockback(kb) => {

View File

@ -912,7 +912,11 @@ impl CharacterAbility {
..
} => {
// If either in the air or is on ground and able to be activated from
// ground
// ground.
//
// NOTE: there is a check in CharacterState::from below that must be kept in
// sync with the conditions here (it determines whether this starts in a
// movement or buildup stage).
(data.physics.on_ground.is_none() || buildup_duration.is_some())
&& update.energy.try_change_by(-*energy_cost).is_ok()
},
@ -2792,7 +2796,7 @@ impl From<(&CharacterAbility, AbilityInfo, &JoinData<'_>)> for CharacterState {
ability_info,
},
timer: Duration::default(),
stage_section: if data.vel.0.z < -*vertical_speed || buildup_duration.is_none() {
stage_section: if data.physics.on_ground.is_none() || buildup_duration.is_none() {
StageSection::Movement
} else {
StageSection::Buildup

View File

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

View File

@ -783,12 +783,12 @@ impl Body {
Body::FishSmall(_) => 3,
Body::BipedLarge(biped_large) => match biped_large.species {
biped_large::Species::Ogre => 320,
biped_large::Species::Cyclops => 320,
biped_large::Species::Cyclops => 1000,
biped_large::Species::Wendigo => 280,
biped_large::Species::Cavetroll => 240,
biped_large::Species::Mountaintroll => 240,
biped_large::Species::Swamptroll => 240,
biped_large::Species::Dullahan => 700,
biped_large::Species::Dullahan => 600,
biped_large::Species::Mindflayer => 1250,
biped_large::Species::Tidalwarrior => 1600,
biped_large::Species::Yeti => 1200,
@ -895,10 +895,17 @@ impl Body {
),
Body::BipedLarge(b) => matches!(
b.species,
biped_large::Species::Huskbrute | biped_large::Species::Gigasfrost
biped_large::Species::Huskbrute
| biped_large::Species::Gigasfrost
| biped_large::Species::Dullahan
),
_ => false,
},
BuffKind::Crippled => match self {
Body::Object(_) | Body::Golem(_) | Body::Ship(_) => true,
Body::BipedLarge(b) => matches!(b.species, biped_large::Species::Dullahan),
_ => false,
},
BuffKind::Burning => match self {
Body::Golem(g) => matches!(g.species, golem::Species::ClayGolem),
Body::BipedSmall(b) => matches!(b.species, biped_small::Species::Haniwa),
@ -915,6 +922,7 @@ impl Body {
| bird_large::Species::WealdWyvern
),
Body::Arthropod(b) => matches!(b.species, arthropod::Species::Moltencrawler),
Body::BipedLarge(b) => matches!(b.species, biped_large::Species::Cyclops),
_ => false,
},
BuffKind::Ensnared => match self {
@ -983,7 +991,7 @@ impl Body {
}
/// Returns the eye height for this creature.
pub fn eye_height(&self) -> f32 { self.height() * 0.9 }
pub fn eye_height(&self, scale: f32) -> f32 { self.height() * 0.9 * scale }
pub fn default_light_offset(&self) -> Vec3<f32> {
// TODO: Make this a manifest

View File

@ -101,6 +101,9 @@ make_case_elim!(
DagonBomb = 86,
BarrelOrgan = 87,
IceBomb = 88,
SpectralSwordSmall = 89,
SpectralSwordLarge = 90,
LaserBeam = 91,
}
);
@ -111,7 +114,7 @@ impl Body {
}
}
pub const ALL_OBJECTS: [Body; 89] = [
pub const ALL_OBJECTS: [Body; 92] = [
Body::Arrow,
Body::Bomb,
Body::Scarecrow,
@ -124,6 +127,8 @@ pub const ALL_OBJECTS: [Body; 89] = [
Body::ChestLight,
Body::ChestOpen,
Body::ChestSkull,
Body::SpectralSwordSmall,
Body::SpectralSwordLarge,
Body::Pumpkin,
Body::Pumpkin2,
Body::Pumpkin3,
@ -201,6 +206,7 @@ pub const ALL_OBJECTS: [Body; 89] = [
Body::DagonBomb,
Body::BarrelOrgan,
Body::IceBomb,
Body::LaserBeam,
];
impl From<Body> for super::Body {
@ -299,6 +305,9 @@ impl Body {
Body::DagonBomb => "dagon_bomb",
Body::BarrelOrgan => "barrel_organ",
Body::IceBomb => "ice_bomb",
Body::SpectralSwordSmall => "spectral_sword_small",
Body::SpectralSwordLarge => "spectral_sword_large",
Body::LaserBeam => "laser_beam",
}
}
@ -321,7 +330,9 @@ impl Body {
| Body::ArrowTurret
| Body::MultiArrow
| Body::Dart
| Body::DagonBomb => 500.0,
| Body::DagonBomb
| Body::SpectralSwordSmall
| Body::SpectralSwordLarge => 500.0,
Body::Bomb => 2000.0, // I have no idea what it's supposed to be
Body::Crate => 300.0, // let's say it's a lot of wood and maybe some contents
Body::Scarecrow => 900.0,
@ -341,6 +352,8 @@ impl Body {
Body::Arrow | Body::ArrowSnake | Body::ArrowTurret | Body::MultiArrow | Body::Dart => {
0.003
},
Body::SpectralSwordSmall => 0.5,
Body::SpectralSwordLarge => 50.0,
Body::BedBlue => 50.0,
Body::Bedroll => 3.0,
Body::Bench => 100.0,
@ -413,6 +426,7 @@ impl Body {
Body::Coconut => 2.0,
Body::GnarlingTotemRed | Body::GnarlingTotemGreen | Body::GnarlingTotemWhite => 100.0,
Body::IceBomb => 12298.0, // 2.5 m diamter but ice
Body::LaserBeam => 80000.0,
};
Mass(m)
@ -424,6 +438,8 @@ impl Body {
Vec3::new(0.01, 0.8, 0.01)
},
Body::BoltFire => Vec3::new(0.1, 0.1, 0.1),
Body::SpectralSwordSmall => Vec3::new(0.2, 0.9, 0.1),
Body::SpectralSwordLarge => Vec3::new(0.2, 1.5, 0.1),
Body::Crossbow => Vec3::new(3.0, 3.0, 1.5),
Body::HaniwaSentry => Vec3::new(0.8, 0.8, 1.4),
Body::SeaLantern => Vec3::new(0.8, 0.8, 1.4),
@ -435,6 +451,7 @@ impl Body {
},
Body::BarrelOrgan => Vec3::new(4.0, 2.0, 3.0),
Body::IceBomb => Vec3::broadcast(2.5),
Body::LaserBeam => Vec3::new(8.0, 8.0, 8.0),
// FIXME: this *must* be exhaustive match
_ => Vec3::broadcast(0.5),
}

View File

@ -22,67 +22,67 @@ use super::Body;
)]
pub enum BuffKind {
// Buffs
/// Restores health/time for some period
/// Strength should be the healing per second
/// Restores health/time for some period.
/// Strength should be the healing per second.
Regeneration,
/// Restores health/time for some period for consumables
/// Strength should be the healing per second
/// Restores health/time for some period for consumables.
/// Strength should be the healing per second.
Saturation,
/// Applied when drinking a potion
/// Strength should be the healing per second
/// Applied when drinking a potion.
/// Strength should be the healing per second.
Potion,
/// Applied when sitting at a campfire
/// Strength is fraction of health restored per second
/// Applied when sitting at a campfire.
/// Strength is fraction of health restored per second.
CampfireHeal,
/// Restores energy/time for some period
/// Strength should be the healing per second
/// Restores energy/time for some period.
/// Strength should be the healing per second.
EnergyRegen,
/// Raises maximum energy
/// Strength should be 10x the effect to max energy
/// Raises maximum energy.
/// Strength should be 10x the effect to max energy.
IncreaseMaxEnergy,
/// Raises maximum health
/// Strength should be the effect to max health
/// Raises maximum health.
/// Strength should be the effect to max health.
IncreaseMaxHealth,
/// Makes you immune to attacks
/// Strength does not affect this buff
/// Makes you immune to attacks.
/// Strength does not affect this buff.
Invulnerability,
/// Reduces incoming damage
/// Reduces incoming damage.
/// Strength scales the damage reduction non-linearly. 0.5 provides 50% DR,
/// 1.0 provides 67% DR
/// 1.0 provides 67% DR.
ProtectingWard,
/// Increases movement speed and gives health regeneration
/// Increases movement speed and gives health regeneration.
/// Strength scales the movement speed linearly. 0.5 is 150% speed, 1.0 is
/// 200% speed. Provides regeneration at 10x the value of the strength
/// 200% speed. Provides regeneration at 10x the value of the strength.
Frenzied,
/// Increases movement and attack speed, but removes chance to get critical
/// hits. Strength scales strength of both effects linearly. 0.5 is a
/// 50% increase, 1.0 is a 100% increase.
Hastened,
/// Increases resistance to incoming poise, and poise damage dealt as health
/// is lost from the time the buff activated
/// is lost from the time the buff activated.
/// Strength scales the resistance non-linearly. 0.5 provides 50%, 1.0
/// provides 67%
/// provides 67%.
/// Strength scales the poise damage increase linearly, a strength of 1.0
/// and n health less from activation will cause poise damage to increase by
/// n%
/// n%.
Fortitude,
/// Increases both attack damage and vulnerability to damage
/// Damage increases linearly with strength, 1.0 is a 100% increase
/// Increases both attack damage and vulnerability to damage.
/// Damage increases linearly with strength, 1.0 is a 100% increase.
/// Damage reduction decreases linearly with strength, 1.0 is a 100%
/// decrease
/// decrease.
Reckless,
// Debuffs
/// Does damage to a creature over time
/// Strength should be the DPS of the debuff
/// Does damage to a creature over time.
/// Strength should be the DPS of the debuff.
Burning,
/// Lowers health over time for some duration
/// Strength should be the DPS of the debuff
/// Lowers health over time for some duration.
/// Strength should be the DPS of the debuff.
Bleeding,
/// Lower a creature's max health over time
/// Lower a creature's max health over time.
/// Strength only affects the target max health, 0.5 targets 50% of base
/// max, 1.0 targets 100% of base max
/// max, 1.0 targets 100% of base max.
Cursed,
/// Reduces movement speed and causes bleeding damage
/// Reduces movement speed and causes bleeding damage.
/// Strength scales the movement speed debuff non-linearly. 0.5 is 50%
/// speed, 1.0 is 33% speed. Bleeding is at 4x the value of the strength.
Crippled,
@ -99,8 +99,8 @@ pub enum BuffKind {
/// Strength scales the movement speed debuff non-linearly. 0.5 is 50%
/// speed, 1.0 is 33% speed.
Ensnared,
/// Drain stamina to a creature over time
/// Strength should be the energy per second of the debuff
/// Drain stamina to a creature over time.
/// Strength should be the energy per second of the debuff.
Poisoned,
/// Results from having an attack parried.
/// Causes your attack speed to be slower to emulate the recover duration of
@ -115,7 +115,7 @@ pub enum BuffKind {
#[cfg(not(target_arch = "wasm32"))]
impl BuffKind {
/// Checks if buff is buff or debuff
/// Checks if buff is buff or debuff.
pub fn is_buff(self) -> bool {
match self {
BuffKind::Regeneration
@ -145,7 +145,7 @@ impl BuffKind {
}
}
/// Checks if buff should queue
/// Checks if buff should queue.
pub fn queues(self) -> bool { matches!(self, BuffKind::Saturation) }
/// Checks if the buff can affect other buff effects applied in the same
@ -417,7 +417,7 @@ pub enum BuffChange {
any_required: Vec<BuffCategory>,
none_required: Vec<BuffCategory>,
},
// Refreshes durations of all buffs with this kind
/// Refreshes durations of all buffs with this kind.
Refresh(BuffKind),
}

View File

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

View File

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

View File

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

View File

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

View File

@ -366,6 +366,10 @@ impl<T> AbilityKind<T> {
#[derive(Clone, Debug, Serialize, Deserialize, Copy, Eq, PartialEq, Hash)]
pub enum AbilityContext {
/// Note, in this context `Stance::None` isn't intended to be used. e.g.
/// `AbilityContext::None` should always be used instead of
/// `AbilityContext::Stance(Stance::None)` in the ability map config
/// files(s).
Stance(Stance),
None,
}

View File

@ -910,13 +910,16 @@ impl LoadoutBuilder {
Some("common.items.npc_armor.biped_large.harvester")
},
biped_large::Species::Ogre
| biped_large::Species::Cyclops
| biped_large::Species::Blueoni
| biped_large::Species::Redoni
| biped_large::Species::Cavetroll
| biped_large::Species::Wendigo => {
Some("common.items.npc_armor.biped_large.generic")
},
biped_large::Species::Cyclops => Some("common.items.npc_armor.biped_large.cyclops"),
biped_large::Species::Dullahan => {
Some("common.items.npc_armor.biped_large.dullahan")
},
biped_large::Species::Cultistwarlord => {
Some("common.items.npc_armor.biped_large.warlord")
},

View File

@ -53,7 +53,9 @@ impl Component for Melee {
#[serde(deny_unknown_fields)]
pub struct MeleeConstructor {
pub kind: MeleeConstructorKind,
// This multiplied by a fraction is added to what is specified in kind
/// This multiplied by a fraction is added to what is specified in `kind`.
///
/// Note, that this must be the same variant as what is specified in `kind`.
pub scaled: Option<MeleeConstructorKind>,
pub range: f32,
pub angle: f32,

View File

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

View File

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

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

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

View File

@ -53,6 +53,11 @@ pub enum ProjectileConstructor {
knockback: f32,
energy_regen: f32,
},
Knife {
damage: f32,
knockback: f32,
energy_regen: f32,
},
Fireball {
damage: f32,
radius: f32,
@ -114,6 +119,12 @@ pub enum ProjectileConstructor {
knockback: f32,
min_falloff: f32,
},
LaserBeam {
damage: f32,
radius: f32,
knockback: f32,
min_falloff: f32,
},
}
impl ProjectileConstructor {
@ -181,6 +192,59 @@ impl ProjectileConstructor {
is_point: true,
}
},
Knife {
damage,
knockback,
energy_regen,
} => {
let knockback = AttackEffect::new(
Some(GroupTarget::OutOfGroup),
CombatEffect::Knockback(Knockback {
strength: knockback,
direction: KnockbackDir::Away,
})
.adjusted_by_stats(tool_stats),
)
.with_requirement(CombatRequirement::AnyDamage);
let energy = AttackEffect::new(None, CombatEffect::EnergyReward(energy_regen))
.with_requirement(CombatRequirement::AnyDamage);
let buff = CombatEffect::Buff(CombatBuff {
kind: BuffKind::Bleeding,
dur_secs: 10.0,
strength: CombatBuffStrength::DamageFraction(0.1),
chance: 0.1,
})
.adjusted_by_stats(tool_stats);
let mut damage = AttackDamage::new(
Damage {
source: DamageSource::Projectile,
kind: DamageKind::Piercing,
value: damage,
},
Some(GroupTarget::OutOfGroup),
instance,
)
.with_effect(buff);
if let Some(damage_effect) = damage_effect {
damage = damage.with_effect(damage_effect);
}
let attack = Attack::default()
.with_damage(damage)
.with_crit(crit_chance, crit_mult)
.with_effect(energy)
.with_effect(knockback)
.with_combo_increment();
Projectile {
hit_solid: vec![Effect::Stick, Effect::Bonk],
hit_entity: vec![Effect::Attack(attack), Effect::Vanish],
time_left: Duration::from_secs(15),
owner,
ignore_group: true,
is_sticky: true,
is_point: true,
}
},
Fireball {
damage,
radius,
@ -679,6 +743,53 @@ impl ProjectileConstructor {
is_point: true,
}
},
LaserBeam {
damage,
radius,
knockback,
min_falloff,
} => {
let knockback = AttackEffect::new(
Some(GroupTarget::OutOfGroup),
CombatEffect::Knockback(Knockback {
strength: knockback,
direction: KnockbackDir::Away,
})
.adjusted_by_stats(tool_stats),
)
.with_requirement(CombatRequirement::AnyDamage);
let damage = AttackDamage::new(
Damage {
source: DamageSource::Explosion,
kind: DamageKind::Energy,
value: damage,
},
Some(GroupTarget::OutOfGroup),
instance,
);
let attack = Attack::default()
.with_damage(damage)
.with_crit(crit_chance, crit_mult)
.with_effect(knockback);
let explosion = Explosion {
effects: vec![
RadiusEffect::Attack(attack),
RadiusEffect::TerrainDestruction(10.0, Rgb::black()),
],
radius,
reagent: Some(Reagent::Yellow),
min_falloff,
};
Projectile {
hit_solid: vec![Effect::Explode(explosion.clone()), Effect::Vanish],
hit_entity: vec![Effect::Explode(explosion), Effect::Vanish],
time_left: Duration::from_secs(10),
owner,
ignore_group: true,
is_sticky: true,
is_point: true,
}
},
}
}
@ -695,6 +806,14 @@ impl ProjectileConstructor {
*damage *= power;
*energy_regen *= regen;
},
Knife {
ref mut damage,
ref mut energy_regen,
..
} => {
*damage *= power;
*energy_regen *= regen;
},
Fireball {
ref mut damage,
ref mut energy_regen,
@ -786,6 +905,14 @@ impl ProjectileConstructor {
*damage *= power;
*radius *= range;
},
LaserBeam {
ref mut damage,
ref mut radius,
..
} => {
*damage *= power;
*radius *= range;
},
}
self
}
@ -794,6 +921,7 @@ impl ProjectileConstructor {
use ProjectileConstructor::*;
match self {
Arrow { .. } => false,
Knife { .. } => false,
Fireball { .. } => true,
Frostball { .. } => true,
Poisonball { .. } => true,
@ -806,6 +934,7 @@ impl ProjectileConstructor {
SeaBomb { .. } => true,
WindBomb { .. } => true,
IceBomb { .. } => true,
LaserBeam { .. } => true,
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -97,6 +97,12 @@ pub enum Outcome {
FlashFreeze {
pos: Vec3<f32>,
},
LaserBeam {
pos: Vec3<f32>,
},
CyclopsCharge {
pos: Vec3<f32>,
},
Utterance {
pos: Vec3<f32>,
body: comp::Body,
@ -138,6 +144,8 @@ impl Outcome {
| Outcome::IceCrack { pos }
| Outcome::Utterance { pos, .. }
| Outcome::SpriteDelete { pos, .. }
| Outcome::CyclopsCharge { pos }
| Outcome::LaserBeam { pos }
| Outcome::Glider { pos, .. } => Some(*pos),
Outcome::BreakBlock { pos, .. }
| Outcome::SpriteUnlocked { pos }

View File

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

View File

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

View File

@ -1,10 +1,11 @@
use crate::{
combat::CombatEffect,
comp::{
character_state::OutputEvents, Body, CharacterState, LightEmitter, Pos,
ProjectileConstructor, StateUpdate,
character_state::OutputEvents, object::Body::LaserBeam, Body, CharacterState, LightEmitter,
Pos, ProjectileConstructor, StateUpdate,
},
event::ServerEvent,
event::{LocalEvent, ServerEvent},
outcome::Outcome,
states::{
behavior::{CharacterBehavior, JoinData},
utils::*,
@ -66,6 +67,14 @@ impl CharacterBehavior for Data {
timer: tick_attack_or_default(data, self.timer, None),
..*self
});
if self.static_data.projectile_body == Body::Object(LaserBeam) {
// Send local event used for frontend shenanigans
output_events.emit_local(LocalEvent::CreateOutcome(
Outcome::CyclopsCharge {
pos: data.pos.0 + *data.ori.look_dir() * (data.body.max_radius()),
},
));
}
} else {
// Transitions to recover section of stage
update.character = CharacterState::BasicRanged(Data {
@ -91,7 +100,10 @@ impl CharacterBehavior for Data {
// Shoots all projectiles simultaneously
for i in 0..self.static_data.num_projectiles {
// Gets offsets
let body_offsets = data.body.projectile_offsets(update.ori.look_vec());
let body_offsets = data.body.projectile_offsets(
update.ori.look_vec(),
data.scale.map_or(1.0, |s| s.0),
);
let pos = Pos(data.pos.0 + body_offsets);
// Adds a slight spread to the projectiles. First projectile has no spread,
// and spread increases linearly with number of projectiles created.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -143,10 +143,24 @@ impl CharacterBehavior for Data {
..*self
});
// Send local event used for frontend shenanigans
if self.static_data.specifier == shockwave::FrontendSpecifier::IceSpikes {
output_events.emit_local(LocalEvent::CreateOutcome(Outcome::FlashFreeze {
pos: data.pos.0 + *data.ori.look_dir() * (data.body.max_radius()),
}));
match self.static_data.specifier {
shockwave::FrontendSpecifier::IceSpikes => {
output_events.emit_local(LocalEvent::CreateOutcome(
Outcome::FlashFreeze {
pos: data.pos.0
+ *data.ori.look_dir() * (data.body.max_radius()),
},
));
},
shockwave::FrontendSpecifier::Ground => {
output_events.emit_local(LocalEvent::CreateOutcome(
Outcome::GroundSlam {
pos: data.pos.0
+ *data.ori.look_dir() * (data.body.max_radius()),
},
));
},
_ => {},
}
} else {
// Transitions to recover

View File

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

View File

@ -4,7 +4,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;
@ -254,6 +254,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(always)]
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(always)]
pub fn get_glow(&self) -> Option<u8> {
self.get_glow_raw().or_else(|| self.get_sprite().and_then(|sprite| sprite.get_glow()))

View File

@ -84,7 +84,9 @@ pub trait CoordinateConversions {
impl CoordinateConversions for Vec2<i32> {
#[inline]
fn wpos_to_cpos(&self) -> Self { self.map2(TerrainChunkSize::RECT_SIZE, |e, sz| e / sz as i32) }
fn wpos_to_cpos(&self) -> Self {
self.map2(TerrainChunkSize::RECT_SIZE, |e, sz| e.div_euclid(sz as i32))
}
#[inline]
fn cpos_to_wpos(&self) -> Self { self.map2(TerrainChunkSize::RECT_SIZE, |e, sz| e * sz as i32) }
@ -223,11 +225,13 @@ pub type TerrainChunk = chonk::Chonk<Block, BlockVec, TerrainChunkSize, TerrainC
pub type TerrainSubChunk = chonk::SubChunk<Block, BlockVec, 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 {
@ -238,8 +242,14 @@ impl TerrainGrid {
}
pub fn try_find_space(&self, pos: Vec3<i32>) -> Option<Vec3<i32>> {
const SEARCH_DIST: i32 = 63;
(0..SEARCH_DIST * 2 + 1)
(0..TERRAIN_GRID_SEARCH_DIST * 2 + 1)
.map(|i| if i % 2 == 0 { i } else { -i } / 2)
.map(|z_diff| pos + Vec3::unit_z() * z_diff)
.find(|pos| self.is_space(*pos))
}
pub fn try_find_ground(&self, pos: Vec3<i32>) -> Option<Vec3<i32>> {
(0..TERRAIN_GRID_SEARCH_DIST * 2 + 1)
.map(|i| if i % 2 == 0 { i } else { -i } / 2)
.map(|z_diff| pos + Vec3::unit_z() * z_diff)
.find(|pos| {

View File

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

View File

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

View File

@ -114,6 +114,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.
@ -429,6 +436,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>();
@ -763,7 +771,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].
@ -773,7 +783,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",
@ -814,17 +828,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;
}
@ -836,6 +863,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");
@ -882,7 +910,7 @@ impl State {
drop(guard);
if update_terrain_and_regions {
self.apply_terrain_changes_internal(true);
self.apply_terrain_changes_internal(true, block_update);
}
// Process local events

View File

@ -14,7 +14,7 @@ use common::{
GroupTarget,
};
use common_ecs::{Job, Origin, ParMode, Phase, System};
use rand::{thread_rng, Rng};
use rand::Rng;
use rayon::iter::ParallelIterator;
use specs::{
saveload::MarkerAllocator, shred::ResourceId, Entities, Join, ParJoin, Read, ReadExpect,
@ -99,7 +99,9 @@ impl<'a> System<'a> for Sys {
read_data.uid_allocator.retrieve_entity_internal(uid.into())
});
let mut rng = thread_rng();
// Note: rayon makes it difficult to hold onto a thread-local RNG, if grabbing
// this becomes a bottleneck we can look into alternatives.
let mut rng = rand::thread_rng();
if rng.gen_bool(0.005) {
server_events.push(ServerEvent::Sound {
sound: Sound::new(SoundKind::Beam, pos.0, 13.0, time),
@ -261,6 +263,7 @@ impl<'a> System<'a> for Sys {
*read_data.time,
|e| server_events.push(e),
|o| outcomes.push(o),
&mut rng,
);
add_hit_entities.push((beam_owner, *uid_b));

View File

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

View File

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

View File

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

View File

@ -66,15 +66,17 @@ impl<'a> System<'a> for Sys {
fn run(_job: &mut Job<Self>, (read_data, mut melee_attacks, outcomes): Self::SystemData) {
let mut server_emitter = read_data.server_bus.emitter();
let mut outcomes_emitter = outcomes.emitter();
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()
{
@ -87,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
@ -239,6 +241,7 @@ impl<'a> System<'a> for Sys {
*read_data.time,
|e| server_emitter.emit(e),
|o| outcomes_emitter.emit(o),
&mut rng,
);
if is_applied {

View File

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

View File

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

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