mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
Merge branch 'isse/sittable-sprites' into 'master'
Sprites on airships, and mountable sprites See merge request veloren/veloren!3886
This commit is contained in:
commit
2bc63d22ae
@ -46,6 +46,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Added accessibility settings tab.
|
||||
- Setting to enable subtitles describing sfx.
|
||||
- Item drops that are spatially close and compatible will now merge with one-another to reduce performance problems.
|
||||
- Airships can now have sprites, which can be interacted with.
|
||||
- Some sprites can be sat on.
|
||||
|
||||
### Changed
|
||||
|
||||
|
@ -1,94 +1,96 @@
|
||||
({
|
||||
DefaultAirship: (
|
||||
bone0: (
|
||||
//offset: (3.0, 7.0, 1.0),
|
||||
//offset: (-20.75, -34.75, 1.25),
|
||||
//offset: (0.0, 0.0, 0.0),
|
||||
offset: (-17.5, -39.0, 1.0),
|
||||
//phys_offset: (0.25, 0.25, 0.25),
|
||||
phys_offset: (0.0, 0.0, 0.0),
|
||||
central: ("airship_human.structure"),
|
||||
),
|
||||
bone1: (
|
||||
offset: (-8.5, -2.0, -8.5),
|
||||
phys_offset: (0.0, 0.0, 0.0),
|
||||
central: ("airship_human.propeller-l"),
|
||||
),
|
||||
bone2: (
|
||||
offset: (-8.5, -2.0, -8.5),
|
||||
phys_offset: (0.0, 0.0, 0.0),
|
||||
central: ("airship_human.propeller-r"),
|
||||
),
|
||||
bone3: (
|
||||
offset: (-1.5, -11.0, -5.5),
|
||||
phys_offset: (0.0, 0.0, 0.0),
|
||||
offset: (-1.5, -10.0, -4.5),
|
||||
central: ("airship_human.rudder"),
|
||||
),
|
||||
|
||||
custom_indices: {
|
||||
1: Air(ChairSingle, 4),
|
||||
2: Air(Helm, 0),
|
||||
3: Air(DoorWide, 4),
|
||||
8: Air(DoorWide, 0),
|
||||
9: Air(CraftingBench, 0),
|
||||
11: Air(RepairBench, 0),
|
||||
12: Air(DismantlingBench, 4),
|
||||
15: Air(Anvil, 2),
|
||||
17: Air(CookingPot, 0),
|
||||
18: Air(WallLamp, 4),
|
||||
23: Air(FireBowlGround, 4),
|
||||
},
|
||||
),
|
||||
AirBalloon: (
|
||||
bone0: (
|
||||
offset: (-14.5, -16.0, 0.0),
|
||||
phys_offset: (0.0, 0.0, 0.0),
|
||||
central: ("air_balloon.structure"),
|
||||
),
|
||||
bone1: (
|
||||
offset: (0.0, 0.0, 0.0),
|
||||
phys_offset: (0.0, 0.0, 0.0),
|
||||
central: ("empty"),
|
||||
),
|
||||
bone2: (
|
||||
offset: (0.0, 0.0, 0.0),
|
||||
phys_offset: (0.0, 0.0, 0.0),
|
||||
central: ("empty"),
|
||||
),
|
||||
bone3: (
|
||||
offset: (-1.5, -6.0, -5.0),
|
||||
phys_offset: (0.0, 0.0, 0.0),
|
||||
offset: (-1.5, -6.0, -6.0),
|
||||
central: ("air_balloon.rudder"),
|
||||
),
|
||||
|
||||
custom_indices: {
|
||||
1: Air(Helm),
|
||||
},
|
||||
),
|
||||
SailBoat: (
|
||||
bone0: (
|
||||
offset: (-6.5, -15.5, 0.0),
|
||||
phys_offset: (0.0, 0.0, 0.0),
|
||||
central: ("sail_boat.structure"),
|
||||
),
|
||||
bone1: (
|
||||
offset: (0.0, 0.0, 0.0),
|
||||
phys_offset: (0.0, 0.0, 0.0),
|
||||
central: ("empty"),
|
||||
),
|
||||
bone2: (
|
||||
offset: (0.0, 0.0, 0.0),
|
||||
phys_offset: (0.0, 0.0, 0.0),
|
||||
central: ("empty"),
|
||||
),
|
||||
bone3: (
|
||||
offset: (-1.5, -6.0, -5.0),
|
||||
phys_offset: (0.0, 0.0, 0.0),
|
||||
central: ("empty"),
|
||||
),
|
||||
),
|
||||
Galleon: (
|
||||
bone0: (
|
||||
offset: (-16, -16, -3.0),
|
||||
phys_offset: (0.0, 0.0, 0.0),
|
||||
central: ("galleon.structure"),
|
||||
),
|
||||
bone1: (
|
||||
offset: (0.0, 0.0, 0.0),
|
||||
phys_offset: (0.0, 0.0, 0.0),
|
||||
central: ("empty"),
|
||||
),
|
||||
bone2: (
|
||||
offset: (0.0, 0.0, 0.0),
|
||||
phys_offset: (0.0, 0.0, 0.0),
|
||||
central: ("empty"),
|
||||
),
|
||||
bone3: (
|
||||
offset: (0.0, 0.0, 0.0),
|
||||
phys_offset: (0.0, 0.0, 0.0),
|
||||
central: ("empty"),
|
||||
),
|
||||
|
||||
custom_indices: {
|
||||
1: Air(Helm, 0),
|
||||
},
|
||||
),
|
||||
})
|
||||
|
BIN
assets/common/voxel/air_balloon/structure.vox
(Stored with Git LFS)
BIN
assets/common/voxel/air_balloon/structure.vox
(Stored with Git LFS)
Binary file not shown.
BIN
assets/common/voxel/airship_human/structure.vox
(Stored with Git LFS)
BIN
assets/common/voxel/airship_human/structure.vox
(Stored with Git LFS)
Binary file not shown.
BIN
assets/common/voxel/galleon/structure.vox
(Stored with Git LFS)
BIN
assets/common/voxel/galleon/structure.vox
(Stored with Git LFS)
Binary file not shown.
@ -112,3 +112,4 @@ common-material-stone = Stone
|
||||
common-material-cloth = Cloth
|
||||
common-material-hide = Hide
|
||||
common-sprite-chest = Chest
|
||||
common-sprite-chair = Chair
|
||||
|
@ -52,3 +52,4 @@ hud-talk = Talk
|
||||
hud-trade = Trade
|
||||
hud-mount = Mount
|
||||
hud-sit = Sit
|
||||
hud-steer = Steer
|
||||
|
@ -167,7 +167,9 @@ void main() {
|
||||
// If the figure is large enough to be 'terrain-like', we apply a noise effect to it
|
||||
#ifndef EXPERIMENTAL_NONOISE
|
||||
if (scale >= 0.5) {
|
||||
float noise = hash(vec4(floor(m_pos * 3.0 - f_norm * 0.5), 0));
|
||||
// TODO: Fix this, it isn't cprrect to use `f_norm` here. Would need something like
|
||||
// `m_norm` which is a normal relative to the figure.
|
||||
float noise = hash(vec4(floor(m_pos * 3.0 - vec3(0.5, 0, 0) - f_norm * 0.1), 0));
|
||||
|
||||
const float A = 0.055;
|
||||
const float W_INV = 1 / (1 + A);
|
||||
|
@ -40,9 +40,9 @@ layout(location = 2) in vec2 f_vel;
|
||||
|
||||
layout(std140, set = 2, binding = 0)
|
||||
uniform u_locals {
|
||||
vec3 model_offs;
|
||||
float load_time;
|
||||
mat4 model_mat;
|
||||
ivec4 atlas_offs;
|
||||
float load_time;
|
||||
};
|
||||
|
||||
layout(location = 0) out vec4 tgt_color;
|
||||
|
@ -42,9 +42,9 @@ layout(location = 2) in vec2 f_vel;
|
||||
|
||||
layout(std140, set = 2, binding = 0)
|
||||
uniform u_locals {
|
||||
vec3 model_offs;
|
||||
float load_time;
|
||||
mat4 model_mat;
|
||||
ivec4 atlas_offs;
|
||||
float load_time;
|
||||
};
|
||||
|
||||
layout(location = 0) out vec4 tgt_color;
|
||||
|
@ -26,9 +26,9 @@ layout(location = 1) in uint v_vel;
|
||||
|
||||
layout(std140, set = 2, binding = 0)
|
||||
uniform u_locals {
|
||||
vec3 model_offs;
|
||||
float load_time;
|
||||
mat4 model_mat;
|
||||
ivec4 atlas_offs;
|
||||
float load_time;
|
||||
};
|
||||
|
||||
// struct ShadowLocals {
|
||||
@ -51,7 +51,9 @@ layout(location = 2) out vec2 f_vel;
|
||||
const float EXTRA_NEG_Z = 65536.0/*65536.1*/;
|
||||
|
||||
void main() {
|
||||
f_pos = vec3(v_pos_norm & 0x3Fu, (v_pos_norm >> 6) & 0x3Fu, float((v_pos_norm >> 12) & 0x1FFFFu) - EXTRA_NEG_Z) + model_offs - focus_off.xyz;
|
||||
vec3 rel_pos = vec3(v_pos_norm & 0x3Fu, (v_pos_norm >> 6) & 0x3Fu, float((v_pos_norm >> 12) & 0x1FFFFu) - EXTRA_NEG_Z);
|
||||
f_pos = (model_mat * vec4(rel_pos, 1.0)).xyz - focus_off.xyz;
|
||||
|
||||
f_vel = vec2(
|
||||
(float(v_vel & 0xFFFFu) - 32768.0) / 1000.0,
|
||||
(float((v_vel >> 16u) & 0xFFFFu) - 32768.0) / 1000.0
|
||||
@ -68,7 +70,7 @@ void main() {
|
||||
#endif
|
||||
#endif
|
||||
|
||||
float pull_down = pow(distance(focus_pos.xy, f_pos.xy) / (view_distance.x * 0.95), 20.0) * 0.7;
|
||||
// float pull_down = pow(distance(focus_pos.xy, f_pos.xy) / (view_distance.x * 0.95), 20.0) * 0.7;
|
||||
//f_pos.z -= pull_down;
|
||||
|
||||
#ifdef EXPERIMENTAL_CURVEDWORLD
|
||||
|
@ -44,9 +44,9 @@ layout(location = 0) in uint v_pos_norm;
|
||||
// Light projection matrices.
|
||||
layout (std140, set = 1, binding = 0)
|
||||
uniform u_locals {
|
||||
vec3 model_offs;
|
||||
float load_time;
|
||||
mat4 model_mat;
|
||||
ivec4 atlas_offs;
|
||||
float load_time;
|
||||
};
|
||||
|
||||
// out vec4 shadowMapCoord;
|
||||
@ -55,11 +55,9 @@ const float EXTRA_NEG_Z = 32768.0;
|
||||
|
||||
void main() {
|
||||
vec3 f_chunk_pos = vec3(v_pos_norm & 0x3Fu, (v_pos_norm >> 6) & 0x3Fu, float((v_pos_norm >> 12) & 0xFFFFu) - EXTRA_NEG_Z);
|
||||
vec3 f_pos = f_chunk_pos + (model_offs - focus_off.xyz);
|
||||
vec3 f_pos = (model_mat * vec4(f_chunk_pos, 1.0)).xyz - focus_off.xyz;
|
||||
// f_pos = v_pos;
|
||||
// vec3 f_pos = f_chunk_pos + model_offs;
|
||||
|
||||
// gl_Position = v_pos + vec4(model_offs, 0.0);
|
||||
gl_Position = /*all_mat * */shadowMatrices * vec4(f_pos/*, 1.0*/, /*float(((f_pos_norm >> 29) & 0x7u) ^ 0x1)*//*uintBitsToFloat(v_pos_norm)*/1.0);
|
||||
// gl_Position.z = -gl_Position.z;
|
||||
// gl_Position.z = clamp(gl_Position.z, -abs(gl_Position.w), abs(gl_Position.w));
|
||||
|
@ -33,9 +33,9 @@ layout(location = 1) in uint v_pos_norm;
|
||||
// Light projection matrices.
|
||||
layout (std140, set = 1, binding = 0)
|
||||
uniform u_locals {
|
||||
vec3 model_offs;
|
||||
float load_time;
|
||||
mat4 model_mat;
|
||||
ivec4 atlas_offs;
|
||||
float load_time;
|
||||
};
|
||||
|
||||
// out vec4 shadowMapCoord;
|
||||
@ -44,11 +44,9 @@ const int EXTRA_NEG_Z = 32768;
|
||||
|
||||
void main() {
|
||||
vec3 f_chunk_pos = vec3(ivec3((uvec3(v_pos_norm) >> uvec3(0, 6, 12)) & uvec3(0x3Fu, 0x3Fu, 0xFFFFu)) - ivec3(0, 0, EXTRA_NEG_Z));
|
||||
vec3 f_pos = f_chunk_pos + model_offs - focus_off.xyz;
|
||||
vec3 f_pos = (model_mat * vec4(f_chunk_pos, 1.0)).xyz - focus_off.xyz;
|
||||
// f_pos = v_pos;
|
||||
// vec3 f_pos = f_chunk_pos + model_offs;
|
||||
|
||||
// gl_Position = v_pos + vec4(model_offs, 0.0);
|
||||
gl_Position = /*all_mat * */vec4(f_pos/*, 1.0*/, /*float(((f_pos_norm >> 29) & 0x7u) ^ 0x1)*//*uintBitsToFloat(v_pos_norm)*/1.0);
|
||||
// shadowMapCoord = lights[gl_InstanceID].light_pos * gl_Vertex;
|
||||
// vec4(v_pos, 0.0, 1.0);
|
||||
|
@ -34,9 +34,9 @@ layout(location = 0) in uint v_pos_norm;
|
||||
// Light projection matrices.
|
||||
layout (std140, set = 1, binding = 0)
|
||||
uniform u_locals {
|
||||
vec3 model_offs;
|
||||
float load_time;
|
||||
mat4 model_mat;
|
||||
ivec4 atlas_offs;
|
||||
float load_time;
|
||||
};
|
||||
|
||||
// out vec4 shadowMapCoord;
|
||||
@ -49,11 +49,9 @@ layout( push_constant ) uniform PointLightMatrix {
|
||||
|
||||
void main() {
|
||||
vec3 f_chunk_pos = vec3(v_pos_norm & 0x3Fu, (v_pos_norm >> 6) & 0x3Fu, float((v_pos_norm >> 12) & 0xFFFFu) - EXTRA_NEG_Z);
|
||||
vec3 f_pos = f_chunk_pos + model_offs - focus_off.xyz;
|
||||
vec3 f_pos = (model_mat * vec4(f_chunk_pos, 1.0)).xyz - focus_off.xyz;
|
||||
// f_pos = v_pos;
|
||||
// vec3 f_pos = f_chunk_pos + model_offs;
|
||||
|
||||
// gl_Position = v_pos + vec4(model_offs, 0.0);
|
||||
// gl_Position = /*all_mat * */vec4(f_pos/*, 1.0*/, /*float(((f_pos_norm >> 29) & 0x7u) ^ 0x1)*//*uintBitsToFloat(v_pos_norm)*/1.0);
|
||||
// shadowMapCoord = lights[gl_InstanceID].light_pos * gl_Vertex;
|
||||
// vec4(v_pos, 0.0, 1.0);
|
||||
|
@ -48,9 +48,9 @@ layout(location = 0) in uint v_pos_norm;
|
||||
// Light projection matrices.
|
||||
layout (std140, set = 1, binding = 0)
|
||||
uniform u_locals {
|
||||
vec3 model_offs;
|
||||
float load_time;
|
||||
mat4 model_mat;
|
||||
ivec4 atlas_offs;
|
||||
float load_time;
|
||||
};
|
||||
|
||||
// out vec4 shadowMapCoord;
|
||||
@ -59,7 +59,7 @@ const float EXTRA_NEG_Z = 32768.0;
|
||||
|
||||
void main() {
|
||||
vec3 f_chunk_pos = vec3(v_pos_norm & 0x3Fu, (v_pos_norm >> 6) & 0x3Fu, float((v_pos_norm >> 12) & 0xFFFFu) - EXTRA_NEG_Z);
|
||||
vec3 f_pos = f_chunk_pos + (model_offs - focus_off.xyz);
|
||||
vec3 f_pos = (model_mat * vec4(f_chunk_pos, 1.0)).xyz - focus_off.xyz;
|
||||
|
||||
gl_Position = rain_occlusion_matrices * vec4(f_pos, 1.0);
|
||||
}
|
||||
|
@ -37,9 +37,9 @@ layout(set = 0, binding = 15) restrict readonly buffer sprite_verts {
|
||||
|
||||
layout (std140, set = 3, binding = 0)
|
||||
uniform u_terrain_locals {
|
||||
vec3 model_offs;
|
||||
float load_time;
|
||||
mat4 model_mat;
|
||||
ivec4 atlas_offs;
|
||||
float load_time;
|
||||
};
|
||||
|
||||
// TODO: consider grouping into vec4's
|
||||
@ -94,10 +94,12 @@ void main() {
|
||||
inst_mat[0] = inst_mat0;
|
||||
inst_mat[1] = inst_mat1;
|
||||
inst_mat[2] = inst_mat2;
|
||||
inst_mat[3] = inst_mat3;
|
||||
inst_mat[3] = inst_mat3;// + vec4(-14.5, -16.5, 0.0, 0.0);
|
||||
|
||||
inst_mat = model_mat * inst_mat;
|
||||
|
||||
// Worldpos of the chunk that this sprite is in
|
||||
vec3 chunk_offs = model_offs - focus_off.xyz;
|
||||
vec3 chunk_offs = -focus_off.xyz;
|
||||
|
||||
f_inst_light = vec2(inst_light, inst_glow);
|
||||
|
||||
@ -116,15 +118,14 @@ void main() {
|
||||
|
||||
// Position of the sprite block in the chunk
|
||||
// Used for highlighting the selected sprite, and for opening doors
|
||||
vec3 inst_chunk_pos = vec3(inst_pos_ori_door & 0x3Fu, (inst_pos_ori_door >> 6) & 0x3Fu, float((inst_pos_ori_door >> 12) & 0xFFFFu) - EXTRA_NEG_Z);
|
||||
vec3 sprite_pos = inst_chunk_pos + chunk_offs;
|
||||
vec3 sprite_pos = inst_mat[3].xyz + chunk_offs;
|
||||
float sprite_ori = (inst_pos_ori_door >> 29) & 0x7u;
|
||||
|
||||
#ifndef EXPERIMENTAL_BAREMINIMUM
|
||||
if((inst_pos_ori_door & (1 << 28)) != 0) {
|
||||
const float MIN_OPEN_DIST = 0.2;
|
||||
const float MAX_OPEN_DIST = 1.5;
|
||||
float min_entity_dist = nearest_entity(sprite_pos + 0.5, 1.0).w;
|
||||
float min_entity_dist = nearest_entity(sprite_pos, 1.0).w;
|
||||
|
||||
if (min_entity_dist < MAX_OPEN_DIST) {
|
||||
float flip = sprite_ori <= 3 ? 1.0 : -1.0;
|
||||
|
@ -52,9 +52,9 @@ uniform sampler s_col_light;
|
||||
|
||||
layout (std140, set = 3, binding = 0)
|
||||
uniform u_locals {
|
||||
vec3 model_offs;
|
||||
float load_time;
|
||||
mat4 model_mat;
|
||||
ivec4 atlas_offs;
|
||||
float load_time;
|
||||
};
|
||||
|
||||
layout(location = 0) out vec4 tgt_color;
|
||||
@ -452,7 +452,8 @@ void main() {
|
||||
// light_reflection_factorplight_reflection_factor
|
||||
|
||||
// vec3 surf_color = illuminate(srgb_to_linear(f_col), light, diffuse_light, ambient_light);
|
||||
vec3 f_chunk_pos = f_pos - (model_offs - focus_off.xyz);
|
||||
|
||||
vec3 f_chunk_pos = f_pos - (model_mat[3].xyz - focus_off.xyz);
|
||||
#ifdef EXPERIMENTAL_NONOISE
|
||||
float noise = 0.0;
|
||||
#else
|
||||
|
@ -30,10 +30,10 @@ layout(location = 1) in uint v_atlas_pos;
|
||||
|
||||
layout (std140, set = 3, binding = 0)
|
||||
uniform u_locals {
|
||||
vec3 model_offs;
|
||||
float load_time;
|
||||
mat4 model_mat;
|
||||
// TODO: consider whether these need to be signed
|
||||
ivec4 atlas_offs;
|
||||
float load_time;
|
||||
};
|
||||
|
||||
//struct ShadowLocals {
|
||||
@ -73,7 +73,7 @@ void main() {
|
||||
// over it (if this vertex to see if it intersects.
|
||||
// f_chunk_pos = vec3(ivec3((uvec3(v_pos_norm) >> uvec3(0, 6, 12)) & uvec3(0x3Fu, 0x3Fu, 0xFFFFu)) - ivec3(0, 0, EXTRA_NEG_Z));
|
||||
vec3 f_chunk_pos = vec3(v_pos_norm & 0x3Fu, (v_pos_norm >> 6) & 0x3Fu, float((v_pos_norm >> 12) & 0xFFFFu) - EXTRA_NEG_Z);
|
||||
f_pos = f_chunk_pos + model_offs - focus_off.xyz;
|
||||
f_pos = (model_mat * vec4(f_chunk_pos, 1.0)).xyz - focus_off.xyz;
|
||||
|
||||
f_load_time = load_time;
|
||||
|
||||
|
BIN
assets/voxygen/voxel/sprite/door/door-wide.vox
(Stored with Git LFS)
Normal file
BIN
assets/voxygen/voxel/sprite/door/door-wide.vox
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
assets/voxygen/voxel/sprite/furniture/helm.vox
(Stored with Git LFS)
Normal file
BIN
assets/voxygen/voxel/sprite/furniture/helm.vox
(Stored with Git LFS)
Normal file
Binary file not shown.
@ -1871,6 +1871,16 @@ DoorDark: Some((
|
||||
],
|
||||
wind_sway: 0.0,
|
||||
)),
|
||||
DoorWide: Some((
|
||||
variations: [
|
||||
(
|
||||
model: "voxygen.voxel.sprite.door.door-wide",
|
||||
offset: (-5.5, -5.5, 0.0),
|
||||
lod_axes: (1.0, 1.0, 1.0),
|
||||
),
|
||||
],
|
||||
wind_sway: 0.0,
|
||||
)),
|
||||
// Bed
|
||||
Bed: Some((
|
||||
variations: [
|
||||
@ -1924,6 +1934,17 @@ ChairDouble: Some((
|
||||
],
|
||||
wind_sway: 0.0,
|
||||
)),
|
||||
// Helm
|
||||
Helm: Some((
|
||||
variations: [
|
||||
(
|
||||
model: "voxygen.voxel.sprite.furniture.helm",
|
||||
offset: (-5.5, -5.5, 0.0),
|
||||
lod_axes: (1.0, 1.0, 1.0),
|
||||
),
|
||||
],
|
||||
wind_sway: 0.0,
|
||||
)),
|
||||
// CoatRack
|
||||
CoatRack: Some((
|
||||
variations: [
|
||||
|
@ -37,7 +37,7 @@ use common::{
|
||||
grid::Grid,
|
||||
link::Is,
|
||||
lod,
|
||||
mounting::Rider,
|
||||
mounting::{Rider, VolumePos, VolumeRider},
|
||||
outcome::Outcome,
|
||||
recipe::{ComponentRecipeBook, RecipeBook, RepairRecipeBook},
|
||||
resources::{GameMode, PlayerEntity, Time, TimeOfDay},
|
||||
@ -1185,7 +1185,7 @@ impl Client {
|
||||
&mut self,
|
||||
recipe: &str,
|
||||
slots: Vec<(u32, InvSlotId)>,
|
||||
craft_sprite: Option<(Vec3<i32>, SpriteKind)>,
|
||||
craft_sprite: Option<(VolumePos, SpriteKind)>,
|
||||
amount: u32,
|
||||
) -> bool {
|
||||
let (can_craft, required_sprite) = self.can_craft_recipe(recipe, amount);
|
||||
@ -1217,7 +1217,7 @@ impl Client {
|
||||
|
||||
/// Salvage the item in the given inventory slot. `salvage_pos` should be
|
||||
/// the location of a relevant crafting station within range of the player.
|
||||
pub fn salvage_item(&mut self, slot: InvSlotId, salvage_pos: Vec3<i32>) -> bool {
|
||||
pub fn salvage_item(&mut self, slot: InvSlotId, salvage_pos: VolumePos) -> bool {
|
||||
let is_salvageable = self.can_salvage_item(slot);
|
||||
if is_salvageable {
|
||||
self.send_msg(ClientGeneral::ControlEvent(ControlEvent::InventoryEvent(
|
||||
@ -1239,7 +1239,7 @@ impl Client {
|
||||
&mut self,
|
||||
primary_component: InvSlotId,
|
||||
secondary_component: InvSlotId,
|
||||
sprite_pos: Option<Vec3<i32>>,
|
||||
sprite_pos: Option<VolumePos>,
|
||||
) -> bool {
|
||||
let inventories = self.inventories();
|
||||
let inventory = inventories.get(self.entity());
|
||||
@ -1288,7 +1288,7 @@ impl Client {
|
||||
material: InvSlotId,
|
||||
modifier: Option<InvSlotId>,
|
||||
slots: Vec<(u32, InvSlotId)>,
|
||||
sprite_pos: Option<Vec3<i32>>,
|
||||
sprite_pos: Option<VolumePos>,
|
||||
) {
|
||||
self.send_msg(ClientGeneral::ControlEvent(ControlEvent::InventoryEvent(
|
||||
InventoryEvent::CraftRecipe {
|
||||
@ -1309,7 +1309,7 @@ impl Client {
|
||||
&mut self,
|
||||
item: Slot,
|
||||
slots: Vec<(u32, InvSlotId)>,
|
||||
sprite_pos: Vec3<i32>,
|
||||
sprite_pos: VolumePos,
|
||||
) -> bool {
|
||||
let is_repairable = {
|
||||
let inventories = self.inventories();
|
||||
@ -1448,6 +1448,12 @@ impl Client {
|
||||
.read_storage::<Is<Rider>>()
|
||||
.get(self.entity())
|
||||
.is_some()
|
||||
|| self
|
||||
.state
|
||||
.ecs()
|
||||
.read_storage::<Is<VolumeRider>>()
|
||||
.get(self.entity())
|
||||
.is_some()
|
||||
}
|
||||
|
||||
pub fn is_lantern_enabled(&self) -> bool {
|
||||
@ -1464,6 +1470,13 @@ impl Client {
|
||||
}
|
||||
}
|
||||
|
||||
/// Mount a block at a `VolumePos`.
|
||||
pub fn mount_volume(&mut self, volume_pos: VolumePos) {
|
||||
self.send_msg(ClientGeneral::ControlEvent(ControlEvent::MountVolume(
|
||||
volume_pos,
|
||||
)));
|
||||
}
|
||||
|
||||
pub fn unmount(&mut self) { self.send_msg(ClientGeneral::ControlEvent(ControlEvent::Unmount)); }
|
||||
|
||||
pub fn respawn(&mut self) {
|
||||
|
@ -34,6 +34,7 @@ macro_rules! synced_components {
|
||||
group: Group,
|
||||
is_mount: IsMount,
|
||||
is_rider: IsRider,
|
||||
is_volume_rider: IsVolumeRider,
|
||||
mass: Mass,
|
||||
density: Density,
|
||||
collider: Collider,
|
||||
@ -72,7 +73,7 @@ macro_rules! reexport_comps {
|
||||
mod inner {
|
||||
pub use common::comp::*;
|
||||
use common::link::Is;
|
||||
use common::mounting::{Mount, Rider};
|
||||
use common::mounting::{Mount, Rider, VolumeRider};
|
||||
|
||||
// We alias these because the identifier used for the
|
||||
// component's type is reused as an enum variant name
|
||||
@ -82,6 +83,7 @@ macro_rules! reexport_comps {
|
||||
// we can't just re-export all the types directly from `common::comp`.
|
||||
pub type IsMount = Is<Mount>;
|
||||
pub type IsRider = Is<Rider>;
|
||||
pub type IsVolumeRider = Is<VolumeRider>;
|
||||
}
|
||||
|
||||
// Re-export all the component types. So that uses of `synced_components!` outside this
|
||||
@ -178,6 +180,10 @@ impl NetSync for IsRider {
|
||||
const SYNC_FROM: SyncFrom = SyncFrom::AnyEntity;
|
||||
}
|
||||
|
||||
impl NetSync for IsVolumeRider {
|
||||
const SYNC_FROM: SyncFrom = SyncFrom::AnyEntity;
|
||||
}
|
||||
|
||||
impl NetSync for Mass {
|
||||
const SYNC_FROM: SyncFrom = SyncFrom::AnyEntity;
|
||||
}
|
||||
|
@ -461,6 +461,7 @@ impl ServerChatCommand {
|
||||
Float("x", 0.0, Required),
|
||||
Float("y", 0.0, Required),
|
||||
Float("z", 0.0, Required),
|
||||
Boolean("Dismount from ship", "true".to_string(), Optional),
|
||||
],
|
||||
"Teleport to a position",
|
||||
Some(Admin),
|
||||
@ -505,6 +506,7 @@ impl ServerChatCommand {
|
||||
Float("x", 0.0, Required),
|
||||
Float("y", 0.0, Required),
|
||||
Float("z", 0.0, Required),
|
||||
Boolean("Dismount from ship", "true".to_string(), Optional),
|
||||
],
|
||||
"Offset your current position",
|
||||
Some(Admin),
|
||||
@ -637,7 +639,10 @@ impl ServerChatCommand {
|
||||
// Uses Message because site names can contain spaces,
|
||||
// which would be assumed to be separators otherwise
|
||||
ServerChatCommand::Site => cmd(
|
||||
vec![SiteName(Required)],
|
||||
vec![
|
||||
SiteName(Required),
|
||||
Boolean("Dismount from ship", "true".to_string(), Optional),
|
||||
],
|
||||
"Teleport to a site",
|
||||
Some(Moderator),
|
||||
),
|
||||
@ -681,12 +686,18 @@ impl ServerChatCommand {
|
||||
Some(Admin),
|
||||
),
|
||||
ServerChatCommand::Tp => cmd(
|
||||
vec![PlayerName(Optional)],
|
||||
vec![
|
||||
PlayerName(Optional),
|
||||
Boolean("Dismount from ship", "true".to_string(), Optional),
|
||||
],
|
||||
"Teleport to another player",
|
||||
Some(Moderator),
|
||||
),
|
||||
ServerChatCommand::RtsimTp => cmd(
|
||||
vec![Integer("npc index", 0, Required)],
|
||||
vec![
|
||||
Integer("npc index", 0, Required),
|
||||
Boolean("Dismount from ship", "true".to_string(), Optional),
|
||||
],
|
||||
"Teleport to an rtsim npc",
|
||||
Some(Moderator),
|
||||
),
|
||||
@ -737,9 +748,11 @@ impl ServerChatCommand {
|
||||
"Send messages to everyone on the server",
|
||||
None,
|
||||
),
|
||||
ServerChatCommand::MakeVolume => {
|
||||
cmd(vec![], "Create a volume (experimental)", Some(Admin))
|
||||
},
|
||||
ServerChatCommand::MakeVolume => cmd(
|
||||
vec![Integer("size", 15, Optional)],
|
||||
"Create a volume (experimental)",
|
||||
Some(Admin),
|
||||
),
|
||||
ServerChatCommand::Location => {
|
||||
cmd(vec![Any("name", Required)], "Teleport to a location", None)
|
||||
},
|
||||
|
@ -100,7 +100,7 @@ impl Body {
|
||||
pub fn density(&self) -> Density {
|
||||
match self {
|
||||
Body::DefaultAirship | Body::AirBalloon | Body::Volume => Density(AIR_DENSITY),
|
||||
_ => Density(AIR_DENSITY * 0.8 + WATER_DENSITY * 0.2), // Most boats should be buoyant
|
||||
_ => Density(AIR_DENSITY * 0.2 + WATER_DENSITY * 0.8), // Most boats should be buoyant
|
||||
}
|
||||
}
|
||||
|
||||
@ -146,17 +146,17 @@ pub const AIRSHIP_SCALE: f32 = 11.0;
|
||||
pub mod figuredata {
|
||||
use crate::{
|
||||
assets::{self, AssetExt, AssetHandle, DotVoxAsset, Ron},
|
||||
figure::cell::Cell,
|
||||
figure::TerrainSegment,
|
||||
terrain::{
|
||||
block::{Block, BlockKind},
|
||||
sprite::SpriteKind,
|
||||
structure::load_base_structure,
|
||||
},
|
||||
volumes::dyna::{ColumnAccess, Dyna},
|
||||
};
|
||||
use hashbrown::HashMap;
|
||||
use lazy_static::lazy_static;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use vek::Vec3;
|
||||
use vek::{Rgb, Vec3};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct VoxSimple(pub String);
|
||||
@ -164,18 +164,46 @@ pub mod figuredata {
|
||||
#[derive(Deserialize)]
|
||||
pub struct ShipCentralSpec(pub HashMap<super::Body, SidedShipCentralVoxSpec>);
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub enum DeBlock {
|
||||
Block(BlockKind),
|
||||
Air(SpriteKind, #[serde(default)] u8),
|
||||
Water(SpriteKind, #[serde(default)] u8),
|
||||
}
|
||||
|
||||
impl DeBlock {
|
||||
fn to_block(&self, color: Rgb<u8>) -> Block {
|
||||
match *self {
|
||||
DeBlock::Block(block) => Block::new(block, color),
|
||||
DeBlock::Air(sprite, ori) => {
|
||||
let block = Block::new(BlockKind::Air, color).with_sprite(sprite);
|
||||
block.with_ori(ori).unwrap_or(block)
|
||||
},
|
||||
DeBlock::Water(sprite, ori) => {
|
||||
let block = Block::new(BlockKind::Water, color).with_sprite(sprite);
|
||||
block.with_ori(ori).unwrap_or(block)
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct SidedShipCentralVoxSpec {
|
||||
pub bone0: ShipCentralSubSpec,
|
||||
pub bone1: ShipCentralSubSpec,
|
||||
pub bone2: ShipCentralSubSpec,
|
||||
pub bone3: ShipCentralSubSpec,
|
||||
|
||||
// TODO: Use StructureBlock here instead. Which would require passing `IndexRef` and
|
||||
// `Calendar` when loading the voxel colliders, which wouldn't work while it's stored in a
|
||||
// static.
|
||||
#[serde(default)]
|
||||
pub custom_indices: HashMap<u8, DeBlock>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct ShipCentralSubSpec {
|
||||
pub offset: [f32; 3],
|
||||
pub phys_offset: [f32; 3],
|
||||
pub central: VoxSimple,
|
||||
#[serde(default)]
|
||||
pub model_index: u32,
|
||||
@ -190,7 +218,7 @@ pub mod figuredata {
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct VoxelCollider {
|
||||
pub(super) dyna: Dyna<Block, (), ColumnAccess>,
|
||||
pub(super) dyna: TerrainSegment,
|
||||
pub translation: Vec3<f32>,
|
||||
/// This value should be incremented every time the volume is mutated
|
||||
/// and can be used to keep track of volume changes.
|
||||
@ -200,13 +228,13 @@ pub mod figuredata {
|
||||
impl VoxelCollider {
|
||||
pub fn from_fn<F: FnMut(Vec3<i32>) -> Block>(sz: Vec3<u32>, f: F) -> Self {
|
||||
Self {
|
||||
dyna: Dyna::from_fn(sz, (), f),
|
||||
dyna: TerrainSegment::from_fn(sz, (), f),
|
||||
translation: -sz.map(|e| e as f32) / 2.0,
|
||||
mut_count: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn volume(&self) -> &Dyna<Block, (), ColumnAccess> { &self.dyna }
|
||||
pub fn volume(&self) -> &TerrainSegment { &self.dyna }
|
||||
}
|
||||
|
||||
impl assets::Compound for ShipSpec {
|
||||
@ -218,27 +246,32 @@ pub mod figuredata {
|
||||
AssetExt::load("common.manifests.ship_manifest")?;
|
||||
let mut colliders = HashMap::new();
|
||||
for (_, spec) in (manifest.read().0).0.iter() {
|
||||
for bone in [&spec.bone0, &spec.bone1, &spec.bone2].iter() {
|
||||
for (index, bone) in [&spec.bone0, &spec.bone1, &spec.bone2, &spec.bone3]
|
||||
.iter()
|
||||
.enumerate()
|
||||
{
|
||||
// TODO: Currently both client and server load models and manifests from
|
||||
// "common.voxel.". In order to support CSG procedural airships, we probably
|
||||
// need to load them in the server and sync them as an ECS resource.
|
||||
let vox =
|
||||
cache.load::<DotVoxAsset>(&["common.voxel.", &bone.central.0].concat())?;
|
||||
let dyna = Dyna::<Cell, (), ColumnAccess>::from_vox(
|
||||
&vox.read().0,
|
||||
false,
|
||||
bone.model_index as usize,
|
||||
);
|
||||
let dyna = dyna.map_into(|cell| {
|
||||
if let Some(rgb) = cell.get_color() {
|
||||
Block::new(BlockKind::Misc, rgb)
|
||||
|
||||
let base_structure = load_base_structure(&vox.read().0, |col| col);
|
||||
let dyna = base_structure.vol.map_into(|cell| {
|
||||
if let Some(i) = cell {
|
||||
let color = base_structure.palette[u8::from(i) as usize];
|
||||
if let Some(block) = spec.custom_indices.get(&i.get()) && index == 0 {
|
||||
block.to_block(color)
|
||||
} else {
|
||||
Block::new(BlockKind::Misc, color)
|
||||
}
|
||||
} else {
|
||||
Block::air(SpriteKind::Empty)
|
||||
Block::empty()
|
||||
}
|
||||
});
|
||||
let collider = VoxelCollider {
|
||||
dyna,
|
||||
translation: Vec3::from(bone.offset) + Vec3::from(bone.phys_offset),
|
||||
translation: Vec3::from(bone.offset),
|
||||
mut_count: 0,
|
||||
};
|
||||
colliders.insert(bone.central.0.clone(), collider);
|
||||
|
@ -9,6 +9,7 @@ use crate::{
|
||||
invite::{InviteKind, InviteResponse},
|
||||
BuffKind,
|
||||
},
|
||||
mounting::VolumePos,
|
||||
trade::{TradeAction, TradeId},
|
||||
uid::Uid,
|
||||
util::Dir,
|
||||
@ -28,7 +29,7 @@ pub enum InventoryEvent {
|
||||
Sort,
|
||||
CraftRecipe {
|
||||
craft_event: CraftEvent,
|
||||
craft_sprite: Option<Vec3<i32>>,
|
||||
craft_sprite: Option<VolumePos>,
|
||||
},
|
||||
}
|
||||
|
||||
@ -57,7 +58,7 @@ pub enum InventoryManip {
|
||||
Sort,
|
||||
CraftRecipe {
|
||||
craft_event: CraftEvent,
|
||||
craft_sprite: Option<Vec3<i32>>,
|
||||
craft_sprite: Option<VolumePos>,
|
||||
},
|
||||
SwapEquippedWeapons,
|
||||
}
|
||||
@ -143,6 +144,7 @@ pub enum ControlEvent {
|
||||
InviteResponse(InviteResponse),
|
||||
PerformTradeAction(TradeId, TradeAction),
|
||||
Mount(Uid),
|
||||
MountVolume(VolumePos),
|
||||
Unmount,
|
||||
InventoryEvent(InventoryEvent),
|
||||
GroupManip(GroupManip),
|
||||
|
@ -1,4 +1,4 @@
|
||||
use super::{Fluid, Ori};
|
||||
use super::{ship::figuredata::ShipSpec, Fluid, Ori};
|
||||
use crate::{
|
||||
comp::{body::ship::figuredata::VoxelCollider, inventory::item::armor::Friction},
|
||||
consts::WATER_DENSITY,
|
||||
@ -127,6 +127,14 @@ pub enum Collider {
|
||||
impl Collider {
|
||||
pub fn is_voxel(&self) -> bool { matches!(self, Collider::Voxel { .. } | Collider::Volume(_)) }
|
||||
|
||||
pub fn get_vol<'a>(&'a self, ship_spec: &'a ShipSpec) -> Option<&'a VoxelCollider> {
|
||||
match self {
|
||||
Collider::Voxel { id } => ship_spec.colliders.get(id),
|
||||
Collider::Volume(vol) => Some(&**vol),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn bounding_radius(&self) -> f32 {
|
||||
match self {
|
||||
Collider::Voxel { .. } | Collider::Volume(_) => 1.0,
|
||||
|
@ -1,6 +1,7 @@
|
||||
// The limit on distance between the entity and a collectible (squared)
|
||||
pub const MAX_PICKUP_RANGE: f32 = 5.0;
|
||||
pub const MAX_MOUNT_RANGE: f32 = 5.0;
|
||||
pub const MAX_SPRITE_MOUNT_RANGE: f32 = 2.0;
|
||||
pub const MAX_TRADE_RANGE: f32 = 20.0;
|
||||
|
||||
pub const GRAVITY: f32 = 25.0;
|
||||
|
@ -8,6 +8,7 @@ use crate::{
|
||||
DisconnectReason, Ori, Pos,
|
||||
},
|
||||
lottery::LootSpec,
|
||||
mounting::VolumePos,
|
||||
outcome::Outcome,
|
||||
rtsim::{RtSimEntity, RtSimVehicle},
|
||||
terrain::SpriteKind,
|
||||
@ -195,6 +196,7 @@ pub enum ServerEvent {
|
||||
InitiateInvite(EcsEntity, Uid, InviteKind),
|
||||
ProcessTradeAction(EcsEntity, TradeId, TradeAction),
|
||||
Mount(EcsEntity, EcsEntity),
|
||||
MountVolume(EcsEntity, VolumePos),
|
||||
Unmount(EcsEntity),
|
||||
Possess(Uid, Uid),
|
||||
/// Inserts default components for a character when loading into the game
|
||||
|
@ -1,6 +1,6 @@
|
||||
use std::num::NonZeroU8;
|
||||
|
||||
use crate::vol::Vox;
|
||||
use crate::vol::FilledVox;
|
||||
use vek::*;
|
||||
|
||||
const GLOWY: u8 = 1 << 1;
|
||||
@ -72,15 +72,10 @@ impl Cell {
|
||||
}
|
||||
}
|
||||
|
||||
impl Vox for Cell {
|
||||
fn empty() -> Self { Cell::Empty }
|
||||
impl FilledVox for Cell {
|
||||
fn default_non_filled() -> Self { Cell::Empty }
|
||||
|
||||
fn is_empty(&self) -> bool {
|
||||
match self {
|
||||
Cell::Filled(_) => false,
|
||||
Cell::Empty => true,
|
||||
}
|
||||
}
|
||||
fn is_filled(&self) -> bool { matches!(self, Cell::Filled(_)) }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -1,5 +1,5 @@
|
||||
use super::cell::CellData;
|
||||
use crate::vol::Vox;
|
||||
use crate::vol::FilledVox;
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
pub enum Material {
|
||||
@ -22,10 +22,10 @@ pub enum MatCell {
|
||||
Normal(CellData),
|
||||
}
|
||||
|
||||
impl Vox for MatCell {
|
||||
fn empty() -> Self { MatCell::None }
|
||||
impl FilledVox for MatCell {
|
||||
fn default_non_filled() -> Self { MatCell::None }
|
||||
|
||||
fn is_empty(&self) -> bool { matches!(self, MatCell::None) }
|
||||
fn is_filled(&self) -> bool { !matches!(self, MatCell::None) }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -9,12 +9,32 @@ pub use self::{
|
||||
};
|
||||
|
||||
use crate::{
|
||||
vol::{IntoFullPosIterator, IntoFullVolIterator, ReadVol, SizedVol, Vox, WriteVol},
|
||||
terrain::{Block, BlockKind, SpriteKind},
|
||||
vol::{FilledVox, IntoFullPosIterator, IntoFullVolIterator, ReadVol, SizedVol, WriteVol},
|
||||
volumes::dyna::Dyna,
|
||||
};
|
||||
use dot_vox::DotVoxData;
|
||||
use vek::*;
|
||||
|
||||
pub type TerrainSegment = Dyna<Block, ()>;
|
||||
|
||||
impl From<Segment> for TerrainSegment {
|
||||
fn from(value: Segment) -> Self {
|
||||
TerrainSegment::from_fn(value.sz, (), |pos| match value.get(pos) {
|
||||
Err(_) | Ok(Cell::Empty) => Block::air(SpriteKind::Empty),
|
||||
Ok(cell) => {
|
||||
if cell.is_hollow() {
|
||||
Block::air(SpriteKind::Empty)
|
||||
} else if cell.is_glowy() {
|
||||
Block::new(BlockKind::GlowingRock, cell.get_color().unwrap())
|
||||
} else {
|
||||
Block::new(BlockKind::Misc, cell.get_color().unwrap())
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// A type representing a volume that may be part of an animated figure.
|
||||
///
|
||||
/// Figures are used to represent things like characters, NPCs, mobs, etc.
|
||||
@ -45,7 +65,7 @@ impl Segment {
|
||||
|
||||
let mut segment = Segment::filled(
|
||||
Vec3::new(model.size.x, model.size.y, model.size.z),
|
||||
Cell::empty(),
|
||||
Cell::Empty,
|
||||
(),
|
||||
);
|
||||
|
||||
@ -76,7 +96,7 @@ impl Segment {
|
||||
|
||||
segment
|
||||
} else {
|
||||
Segment::filled(Vec3::zero(), Cell::empty(), ())
|
||||
Segment::filled(Vec3::zero(), Cell::Empty, ())
|
||||
}
|
||||
}
|
||||
|
||||
@ -110,9 +130,9 @@ impl Segment {
|
||||
|
||||
// TODO: move
|
||||
/// A `Dyna` builder that combines Dynas
|
||||
pub struct DynaUnionizer<V: Vox>(Vec<(Dyna<V, ()>, Vec3<i32>)>);
|
||||
pub struct DynaUnionizer<V: FilledVox>(Vec<(Dyna<V, ()>, Vec3<i32>)>);
|
||||
|
||||
impl<V: Vox + Copy> DynaUnionizer<V> {
|
||||
impl<V: FilledVox + Copy> DynaUnionizer<V> {
|
||||
#[allow(clippy::new_without_default)]
|
||||
pub fn new() -> Self { DynaUnionizer(Vec::new()) }
|
||||
|
||||
@ -134,7 +154,10 @@ impl<V: Vox + Copy> DynaUnionizer<V> {
|
||||
|
||||
pub fn unify_with(self, mut f: impl FnMut(V) -> V) -> (Dyna<V, ()>, Vec3<i32>) {
|
||||
if self.0.is_empty() {
|
||||
return (Dyna::filled(Vec3::zero(), V::empty(), ()), Vec3::zero());
|
||||
return (
|
||||
Dyna::filled(Vec3::zero(), V::default_non_filled(), ()),
|
||||
Vec3::zero(),
|
||||
);
|
||||
}
|
||||
|
||||
// Determine size of the new Dyna
|
||||
@ -147,12 +170,12 @@ impl<V: Vox + Copy> DynaUnionizer<V> {
|
||||
}
|
||||
let new_size = (max_point - min_point).map(|e| e as u32);
|
||||
// Allocate new segment
|
||||
let mut combined = Dyna::filled(new_size, V::empty(), ());
|
||||
let mut combined = Dyna::filled(new_size, V::default_non_filled(), ());
|
||||
// Copy segments into combined
|
||||
let origin = min_point.map(|e| -e);
|
||||
for (dyna, offset) in self.0 {
|
||||
for (pos, vox) in dyna.full_vol_iter() {
|
||||
if !vox.is_empty() {
|
||||
if vox.is_filled() {
|
||||
combined.set(origin + offset + pos, f(*vox)).unwrap();
|
||||
}
|
||||
}
|
||||
@ -166,7 +189,7 @@ pub type MatSegment = Dyna<MatCell, ()>;
|
||||
|
||||
impl MatSegment {
|
||||
pub fn to_segment(&self, map: impl Fn(Material) -> Rgb<u8>) -> Segment {
|
||||
let mut vol = Dyna::filled(self.size(), Cell::empty(), ());
|
||||
let mut vol = Dyna::filled(self.size(), Cell::Empty, ());
|
||||
for (pos, vox) in self.full_vol_iter() {
|
||||
let data = match vox {
|
||||
MatCell::None => continue,
|
||||
@ -216,7 +239,7 @@ impl MatSegment {
|
||||
|
||||
let mut vol = Dyna::filled(
|
||||
Vec3::new(model.size.x, model.size.y, model.size.z),
|
||||
MatCell::empty(),
|
||||
MatCell::None,
|
||||
(),
|
||||
);
|
||||
|
||||
@ -262,7 +285,7 @@ impl MatSegment {
|
||||
|
||||
vol
|
||||
} else {
|
||||
Dyna::filled(Vec3::zero(), MatCell::empty(), ())
|
||||
Dyna::filled(Vec3::zero(), MatCell::None, ())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,16 @@
|
||||
use crate::{
|
||||
comp,
|
||||
comp::{self, ship::figuredata::VOXEL_COLLIDER_MANIFEST},
|
||||
link::{Is, Link, LinkHandle, Role},
|
||||
terrain::TerrainGrid,
|
||||
terrain::{Block, TerrainGrid},
|
||||
uid::{Uid, UidAllocator},
|
||||
vol::ReadVol,
|
||||
};
|
||||
use hashbrown::HashSet;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specs::{saveload::MarkerAllocator, Entities, Read, ReadExpect, ReadStorage, WriteStorage};
|
||||
use specs::{
|
||||
saveload::MarkerAllocator, storage::GenericWriteStorage, Component, DenseVecStorage, Entities,
|
||||
Entity, Read, ReadExpect, ReadStorage, Write, WriteStorage,
|
||||
};
|
||||
use vek::*;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
@ -39,6 +44,7 @@ impl Link for Mounting {
|
||||
Read<'a, UidAllocator>,
|
||||
WriteStorage<'a, Is<Mount>>,
|
||||
WriteStorage<'a, Is<Rider>>,
|
||||
ReadStorage<'a, Is<VolumeRider>>,
|
||||
);
|
||||
type DeleteData<'a> = (
|
||||
Read<'a, UidAllocator>,
|
||||
@ -59,7 +65,7 @@ impl Link for Mounting {
|
||||
|
||||
fn create(
|
||||
this: &LinkHandle<Self>,
|
||||
(uid_allocator, mut is_mounts, mut is_riders): Self::CreateData<'_>,
|
||||
(uid_allocator, mut is_mounts, mut is_riders, is_volume_rider): Self::CreateData<'_>,
|
||||
) -> Result<(), Self::Error> {
|
||||
let entity = |uid: Uid| uid_allocator.retrieve_entity_internal(uid.into());
|
||||
|
||||
@ -67,8 +73,11 @@ impl Link for Mounting {
|
||||
// Forbid self-mounting
|
||||
Err(MountingError::NotMountable)
|
||||
} else if let Some((mount, rider)) = entity(this.mount).zip(entity(this.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.contains(entity)
|
||||
&& !is_riders.contains(entity)
|
||||
&& !is_volume_rider.contains(entity)
|
||||
};
|
||||
|
||||
// Ensure that neither mount or rider are already part of a mounting
|
||||
// relationship
|
||||
@ -145,3 +154,260 @@ impl Link for Mounting {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct VolumeRider;
|
||||
|
||||
impl Role for VolumeRider {
|
||||
type Link = VolumeMounting;
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Debug, Hash)]
|
||||
pub enum Volume {
|
||||
Terrain,
|
||||
Entity(Uid),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Debug, Hash)]
|
||||
pub struct VolumePos {
|
||||
pub kind: Volume,
|
||||
pub pos: Vec3<i32>,
|
||||
}
|
||||
|
||||
impl VolumePos {
|
||||
pub fn terrain(block_pos: Vec3<i32>) -> Self {
|
||||
Self {
|
||||
kind: Volume::Terrain,
|
||||
pos: block_pos,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn entity(block_pos: Vec3<i32>, uid: Uid) -> Self {
|
||||
Self {
|
||||
kind: Volume::Entity(uid),
|
||||
pos: block_pos,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl VolumePos {
|
||||
/// Retrieves the block and matrix transformation for this `VolumeBlock`
|
||||
///
|
||||
/// The transform is located in the blocks minimum position relative to the
|
||||
/// volume.
|
||||
pub fn get_block_and_transform(
|
||||
&self,
|
||||
terrain: &TerrainGrid,
|
||||
uid_allocator: &UidAllocator,
|
||||
mut read_pos_and_ori: impl FnMut(Entity) -> Option<(comp::Pos, comp::Ori)>,
|
||||
colliders: &ReadStorage<comp::Collider>,
|
||||
) -> Option<(Mat4<f32>, Block)> {
|
||||
match self.kind {
|
||||
Volume::Terrain => Some((
|
||||
Mat4::translation_3d(self.pos.as_()),
|
||||
*terrain.get(self.pos).ok()?,
|
||||
)),
|
||||
Volume::Entity(uid) => {
|
||||
uid_allocator
|
||||
.retrieve_entity_internal(uid.0)
|
||||
.and_then(|entity| {
|
||||
let collider = colliders.get(entity)?;
|
||||
let (pos, ori) = read_pos_and_ori(entity)?;
|
||||
|
||||
let voxel_colliders_manifest = VOXEL_COLLIDER_MANIFEST.read();
|
||||
let voxel_collider = collider.get_vol(&voxel_colliders_manifest)?;
|
||||
|
||||
let block = *voxel_collider.volume().get(self.pos).ok()?;
|
||||
|
||||
let local_translation = voxel_collider.translation + self.pos.as_();
|
||||
|
||||
let trans = Mat4::from(ori.to_quat()).translated_3d(pos.0)
|
||||
* Mat4::<f32>::translation_3d(local_translation);
|
||||
|
||||
Some((trans, block))
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the block at this `VolumePos`.
|
||||
pub fn get_block(
|
||||
&self,
|
||||
terrain: &TerrainGrid,
|
||||
uid_allocator: &UidAllocator,
|
||||
colliders: &ReadStorage<comp::Collider>,
|
||||
) -> Option<Block> {
|
||||
match self.kind {
|
||||
Volume::Terrain => Some(*terrain.get(self.pos).ok()?),
|
||||
Volume::Entity(uid) => {
|
||||
uid_allocator
|
||||
.retrieve_entity_internal(uid.0)
|
||||
.and_then(|entity| {
|
||||
let collider = colliders.get(entity)?;
|
||||
|
||||
let voxel_colliders_manifest = VOXEL_COLLIDER_MANIFEST.read();
|
||||
let voxel_collider = collider.get_vol(&voxel_colliders_manifest)?;
|
||||
|
||||
let block = *voxel_collider.volume().get(self.pos).ok()?;
|
||||
|
||||
Some(block)
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct VolumeRiders {
|
||||
riders: HashSet<Vec3<i32>>,
|
||||
}
|
||||
|
||||
impl Component for VolumeRiders {
|
||||
type Storage = DenseVecStorage<Self>;
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct VolumeMounting {
|
||||
pub pos: VolumePos,
|
||||
pub block: Block,
|
||||
pub rider: Uid,
|
||||
}
|
||||
|
||||
impl Link for VolumeMounting {
|
||||
type CreateData<'a> = (
|
||||
Write<'a, VolumeRiders>,
|
||||
WriteStorage<'a, VolumeRiders>,
|
||||
WriteStorage<'a, Is<VolumeRider>>,
|
||||
ReadStorage<'a, Is<Rider>>,
|
||||
ReadStorage<'a, Is<Mount>>,
|
||||
ReadExpect<'a, TerrainGrid>,
|
||||
Read<'a, UidAllocator>,
|
||||
ReadStorage<'a, comp::Collider>,
|
||||
);
|
||||
type DeleteData<'a> = (
|
||||
Write<'a, VolumeRiders>,
|
||||
WriteStorage<'a, VolumeRiders>,
|
||||
WriteStorage<'a, Is<VolumeRider>>,
|
||||
Read<'a, UidAllocator>,
|
||||
);
|
||||
type Error = MountingError;
|
||||
type PersistData<'a> = (
|
||||
Entities<'a>,
|
||||
ReadStorage<'a, comp::Health>,
|
||||
Read<'a, VolumeRiders>,
|
||||
ReadStorage<'a, VolumeRiders>,
|
||||
ReadStorage<'a, Is<VolumeRider>>,
|
||||
ReadExpect<'a, TerrainGrid>,
|
||||
Read<'a, UidAllocator>,
|
||||
ReadStorage<'a, comp::Collider>,
|
||||
);
|
||||
|
||||
fn create(
|
||||
this: &LinkHandle<Self>,
|
||||
(
|
||||
mut terrain_riders,
|
||||
mut volume_riders,
|
||||
mut is_volume_riders,
|
||||
is_riders,
|
||||
is_mounts,
|
||||
terrain_grid,
|
||||
uid_allocator,
|
||||
colliders,
|
||||
): Self::CreateData<'_>,
|
||||
) -> Result<(), Self::Error> {
|
||||
let entity = |uid: Uid| uid_allocator.retrieve_entity_internal(uid.into());
|
||||
|
||||
let riders = match this.pos.kind {
|
||||
Volume::Terrain => &mut *terrain_riders,
|
||||
Volume::Entity(uid) => entity(uid)
|
||||
.and_then(|entity| volume_riders.get_mut_or_default(entity))
|
||||
.ok_or(MountingError::NoSuchEntity)?,
|
||||
};
|
||||
let rider = entity(this.rider).ok_or(MountingError::NoSuchEntity)?;
|
||||
|
||||
if !riders.riders.contains(&this.pos.pos)
|
||||
&& !is_volume_riders.contains(rider)
|
||||
&& !is_volume_riders.contains(rider)
|
||||
&& !is_riders.contains(rider)
|
||||
&& !is_mounts.contains(rider)
|
||||
{
|
||||
let block = this
|
||||
.pos
|
||||
.get_block(&terrain_grid, &uid_allocator, &colliders)
|
||||
.ok_or(MountingError::NoSuchEntity)?;
|
||||
|
||||
if block == this.block {
|
||||
let _ = is_volume_riders.insert(rider, this.make_role());
|
||||
riders.riders.insert(this.pos.pos);
|
||||
Ok(())
|
||||
} else {
|
||||
Err(MountingError::NotMountable)
|
||||
}
|
||||
} else {
|
||||
Err(MountingError::NotMountable)
|
||||
}
|
||||
}
|
||||
|
||||
fn persist(
|
||||
this: &LinkHandle<Self>,
|
||||
(
|
||||
entities,
|
||||
healths,
|
||||
terrain_riders,
|
||||
volume_riders,
|
||||
is_volume_riders,
|
||||
terrain_grid,
|
||||
uid_allocator,
|
||||
colliders,
|
||||
): Self::PersistData<'_>,
|
||||
) -> bool {
|
||||
let entity = |uid: Uid| uid_allocator.retrieve_entity_internal(uid.into());
|
||||
let is_alive =
|
||||
|entity| entities.is_alive(entity) && healths.get(entity).map_or(true, |h| !h.is_dead);
|
||||
let riders = match this.pos.kind {
|
||||
Volume::Terrain => &*terrain_riders,
|
||||
Volume::Entity(uid) => {
|
||||
let Some(riders) = entity(uid)
|
||||
.filter(|entity| is_alive(*entity))
|
||||
.and_then(|entity| volume_riders.get(entity)) else {
|
||||
return false;
|
||||
};
|
||||
riders
|
||||
},
|
||||
};
|
||||
|
||||
let rider_exists = entity(this.rider).map_or(false, |rider| {
|
||||
is_volume_riders.contains(rider) && is_alive(rider)
|
||||
});
|
||||
let mount_spot_exists = riders.riders.contains(&this.pos.pos);
|
||||
|
||||
let block_exists = this
|
||||
.pos
|
||||
.get_block(&terrain_grid, &uid_allocator, &colliders)
|
||||
.map_or(false, |block| block == this.block);
|
||||
|
||||
rider_exists && mount_spot_exists && block_exists
|
||||
}
|
||||
|
||||
fn delete(
|
||||
this: &LinkHandle<Self>,
|
||||
(mut terrain_riders, mut volume_riders, mut is_rider, uid_allocator): Self::DeleteData<'_>,
|
||||
) {
|
||||
let entity = |uid: Uid| uid_allocator.retrieve_entity_internal(uid.into());
|
||||
|
||||
let riders = match this.pos.kind {
|
||||
Volume::Terrain => Some(&mut *terrain_riders),
|
||||
Volume::Entity(uid) => {
|
||||
entity(uid).and_then(|entity| volume_riders.get_mut_or_default(entity))
|
||||
},
|
||||
};
|
||||
|
||||
if let Some(riders) = riders {
|
||||
riders.riders.remove(&this.pos.pos);
|
||||
}
|
||||
|
||||
if let Some(entity) = entity(this.rider) {
|
||||
is_rider.remove(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ use crate::{
|
||||
Stats, Vel,
|
||||
},
|
||||
link::Is,
|
||||
mounting::Rider,
|
||||
mounting::{Rider, VolumeRider},
|
||||
resources::{DeltaTime, Time},
|
||||
terrain::TerrainGrid,
|
||||
uid::Uid,
|
||||
@ -97,7 +97,7 @@ pub trait CharacterBehavior {
|
||||
ControlAction::Sit => self.sit(data, output_events),
|
||||
ControlAction::Dance => self.dance(data, output_events),
|
||||
ControlAction::Sneak => {
|
||||
if data.mount_data.is_none() {
|
||||
if data.mount_data.is_none() && data.volume_mount_data.is_none() {
|
||||
self.sneak(data, output_events)
|
||||
} else {
|
||||
self.stand(data, output_events)
|
||||
@ -147,6 +147,7 @@ pub struct JoinData<'a> {
|
||||
pub alignment: Option<&'a comp::Alignment>,
|
||||
pub terrain: &'a TerrainGrid,
|
||||
pub mount_data: Option<&'a Is<Rider>>,
|
||||
pub volume_mount_data: Option<&'a Is<VolumeRider>>,
|
||||
pub stance: Option<&'a Stance>,
|
||||
}
|
||||
|
||||
@ -176,6 +177,7 @@ pub struct JoinStruct<'a> {
|
||||
pub alignment: Option<&'a comp::Alignment>,
|
||||
pub terrain: &'a TerrainGrid,
|
||||
pub mount_data: Option<&'a Is<Rider>>,
|
||||
pub volume_mount_data: Option<&'a Is<VolumeRider>>,
|
||||
pub stance: Option<&'a Stance>,
|
||||
}
|
||||
|
||||
@ -219,6 +221,7 @@ impl<'a> JoinData<'a> {
|
||||
terrain: j.terrain,
|
||||
active_abilities: j.active_abilities,
|
||||
mount_data: j.mount_data,
|
||||
volume_mount_data: j.volume_mount_data,
|
||||
stance: j.stance,
|
||||
}
|
||||
}
|
||||
|
@ -357,6 +357,9 @@ pub fn handle_skating(data: &JoinData, update: &mut StateUpdate) {
|
||||
|
||||
/// Handles updating `Components` to move player based on state of `JoinData`
|
||||
pub fn handle_move(data: &JoinData<'_>, update: &mut StateUpdate, efficiency: f32) {
|
||||
if data.volume_mount_data.is_some() {
|
||||
return;
|
||||
}
|
||||
let submersion = data
|
||||
.physics
|
||||
.in_liquid()
|
||||
@ -854,6 +857,80 @@ pub fn attempt_swap_equipped_weapons(data: &JoinData<'_>, update: &mut StateUpda
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if a block can be reached from a position.
|
||||
fn can_reach_block(
|
||||
player_pos: Vec3<f32>,
|
||||
block_pos: Vec3<i32>,
|
||||
range: f32,
|
||||
body: &Body,
|
||||
terrain: &TerrainGrid,
|
||||
) -> bool {
|
||||
let block_pos_f32 = block_pos.map(|x| x as f32 + 0.5);
|
||||
// Closure to check if distance between a point and the block is less than
|
||||
// MAX_PICKUP_RANGE and the radius of the body
|
||||
let block_range_check = |pos: Vec3<f32>| {
|
||||
(block_pos_f32 - pos).magnitude_squared() < (range + body.max_radius()).powi(2)
|
||||
};
|
||||
|
||||
// Checks if player's feet or head is near to block
|
||||
let close_to_block = block_range_check(player_pos)
|
||||
|| block_range_check(player_pos + Vec3::new(0.0, 0.0, body.height()));
|
||||
if close_to_block {
|
||||
// Do a check that a path can be found between sprite and entity
|
||||
// interacting with sprite Use manhattan distance * 1.5 for number
|
||||
// of iterations
|
||||
let iters = (3.0 * (block_pos_f32 - player_pos).map(|x| x.abs()).sum()) as usize;
|
||||
// Heuristic compares manhattan distance of start and end pos
|
||||
let heuristic =
|
||||
move |pos: &Vec3<i32>, _: &Vec3<i32>| (block_pos - pos).map(|x| x.abs()).sum() as f32;
|
||||
|
||||
let mut astar = Astar::new(
|
||||
iters,
|
||||
player_pos.map(|x| x.floor() as i32),
|
||||
BuildHasherDefault::<FxHasher64>::default(),
|
||||
);
|
||||
|
||||
// Transition uses manhattan distance as the cost, with a slightly lower cost
|
||||
// for z transitions
|
||||
let transition = |a: Vec3<i32>, b: Vec3<i32>| {
|
||||
let (a, b) = (a.map(|x| x as f32), b.map(|x| x as f32));
|
||||
((a - b) * Vec3::new(1.0, 1.0, 0.9)).map(|e| e.abs()).sum()
|
||||
};
|
||||
// Neighbors are all neighboring blocks that are air
|
||||
let neighbors = |pos: &Vec3<i32>| {
|
||||
const DIRS: [Vec3<i32>; 6] = [
|
||||
Vec3::new(1, 0, 0),
|
||||
Vec3::new(-1, 0, 0),
|
||||
Vec3::new(0, 1, 0),
|
||||
Vec3::new(0, -1, 0),
|
||||
Vec3::new(0, 0, 1),
|
||||
Vec3::new(0, 0, -1),
|
||||
];
|
||||
let pos = *pos;
|
||||
DIRS.iter()
|
||||
.map(move |dir| {
|
||||
let dest = dir + pos;
|
||||
(dest, transition(pos, dest))
|
||||
})
|
||||
.filter(|(pos, _)| {
|
||||
terrain
|
||||
.get(*pos)
|
||||
.ok()
|
||||
.map_or(false, |block| !block.is_filled())
|
||||
})
|
||||
};
|
||||
// Pathing satisfied when it reaches the sprite position
|
||||
let satisfied = |pos: &Vec3<i32>| *pos == block_pos;
|
||||
|
||||
astar
|
||||
.poll(iters, heuristic, neighbors, satisfied)
|
||||
.into_path()
|
||||
.is_some()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles inventory manipulations that affect the loadout
|
||||
pub fn handle_manipulate_loadout(
|
||||
data: &JoinData<'_>,
|
||||
@ -895,90 +972,30 @@ pub fn handle_manipulate_loadout(
|
||||
}
|
||||
},
|
||||
InventoryAction::Collect(sprite_pos) => {
|
||||
let sprite_pos_f32 = sprite_pos.map(|x| x as f32 + 0.5);
|
||||
// Closure to check if distance between a point and the sprite is less than
|
||||
// MAX_PICKUP_RANGE and the radius of the body
|
||||
let sprite_range_check = |pos: Vec3<f32>| {
|
||||
(sprite_pos_f32 - pos).magnitude_squared()
|
||||
< (MAX_PICKUP_RANGE + data.body.max_radius()).powi(2)
|
||||
};
|
||||
|
||||
// Checks if player's feet or head is near to sprite
|
||||
let close_to_sprite = sprite_range_check(data.pos.0)
|
||||
|| sprite_range_check(data.pos.0 + Vec3::new(0.0, 0.0, data.body.height()));
|
||||
if close_to_sprite {
|
||||
// First, get sprite data for position, if there is a sprite
|
||||
use sprite_interact::SpriteInteractKind;
|
||||
let sprite_chunk_pos = TerrainGrid::chunk_offs(sprite_pos);
|
||||
let sprite_cfg = data
|
||||
.terrain
|
||||
.pos_chunk(sprite_pos)
|
||||
.and_then(|chunk| chunk.meta().sprite_cfg_at(sprite_chunk_pos));
|
||||
let sprite_at_pos = data
|
||||
.terrain
|
||||
.get(sprite_pos)
|
||||
.ok()
|
||||
.copied()
|
||||
.and_then(|b| b.get_sprite());
|
||||
|
||||
// Checks if position has a collectible sprite as well as what sprite is at the
|
||||
// position
|
||||
let sprite_interact = sprite_at_pos.and_then(Option::<SpriteInteractKind>::from);
|
||||
|
||||
if let Some(sprite_interact) = sprite_interact {
|
||||
// Do a check that a path can be found between sprite and entity
|
||||
// interacting with sprite Use manhattan distance * 1.5 for number
|
||||
// of iterations
|
||||
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>, _: &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),
|
||||
BuildHasherDefault::<FxHasher64>::default(),
|
||||
);
|
||||
|
||||
// Transition uses manhattan distance as the cost, with a slightly lower cost
|
||||
// for z transitions
|
||||
let transition = |a: Vec3<i32>, b: Vec3<i32>| {
|
||||
let (a, b) = (a.map(|x| x as f32), b.map(|x| x as f32));
|
||||
((a - b) * Vec3::new(1.0, 1.0, 0.9)).map(|e| e.abs()).sum()
|
||||
};
|
||||
// Neighbors are all neighboring blocks that are air
|
||||
let neighbors = |pos: &Vec3<i32>| {
|
||||
const DIRS: [Vec3<i32>; 6] = [
|
||||
Vec3::new(1, 0, 0),
|
||||
Vec3::new(-1, 0, 0),
|
||||
Vec3::new(0, 1, 0),
|
||||
Vec3::new(0, -1, 0),
|
||||
Vec3::new(0, 0, 1),
|
||||
Vec3::new(0, 0, -1),
|
||||
];
|
||||
let pos = *pos;
|
||||
DIRS.iter()
|
||||
.map(move |dir| {
|
||||
let dest = dir + pos;
|
||||
(dest, transition(pos, dest))
|
||||
})
|
||||
.filter(|(pos, _)| {
|
||||
data.terrain
|
||||
.get(*pos)
|
||||
.ok()
|
||||
.map_or(false, |block| !block.is_filled())
|
||||
})
|
||||
};
|
||||
// Pathing satisfied when it reaches the sprite position
|
||||
let satisfied = |pos: &Vec3<i32>| *pos == sprite_pos;
|
||||
|
||||
let not_blocked_by_terrain = astar
|
||||
.poll(iters, heuristic, neighbors, satisfied)
|
||||
.into_path()
|
||||
.is_some();
|
||||
|
||||
// First, get sprite data for position, if there is a sprite
|
||||
let sprite_at_pos = data
|
||||
.terrain
|
||||
.get(sprite_pos)
|
||||
.ok()
|
||||
.copied()
|
||||
.and_then(|b| b.get_sprite());
|
||||
// Checks if position has a collectible sprite as well as what sprite is at the
|
||||
// position
|
||||
let sprite_interact =
|
||||
sprite_at_pos.and_then(Option::<sprite_interact::SpriteInteractKind>::from);
|
||||
if let Some(sprite_interact) = sprite_interact {
|
||||
if can_reach_block(
|
||||
data.pos.0,
|
||||
sprite_pos,
|
||||
MAX_PICKUP_RANGE,
|
||||
data.body,
|
||||
data.terrain,
|
||||
) {
|
||||
let sprite_chunk_pos = TerrainGrid::chunk_offs(sprite_pos);
|
||||
let sprite_cfg = data
|
||||
.terrain
|
||||
.pos_chunk(sprite_pos)
|
||||
.and_then(|chunk| chunk.meta().sprite_cfg_at(sprite_chunk_pos));
|
||||
let required_item =
|
||||
sprite_at_pos.and_then(|s| match s.unlock_condition(sprite_cfg.cloned()) {
|
||||
UnlockKind::Free => None,
|
||||
@ -997,38 +1014,32 @@ pub fn handle_manipulate_loadout(
|
||||
.map(|slot| Some((item_id, slot, consume))),
|
||||
None => Some(None),
|
||||
};
|
||||
if let Some(required_item) = has_required_items {
|
||||
// If the sprite is collectible, enter the sprite interaction character
|
||||
// state TODO: Handle cases for sprite being
|
||||
// interactible, but not collectible (none currently
|
||||
// exist)
|
||||
let (buildup_duration, use_duration, recover_duration) =
|
||||
sprite_interact.durations();
|
||||
|
||||
// If path can be found between entity interacting with sprite and entity, start
|
||||
// interaction with sprite
|
||||
if not_blocked_by_terrain {
|
||||
if let Some(required_item) = has_required_items {
|
||||
// If the sprite is collectible, enter the sprite interaction character
|
||||
// state TODO: Handle cases for sprite being
|
||||
// interactible, but not collectible (none currently
|
||||
// exist)
|
||||
let (buildup_duration, use_duration, recover_duration) =
|
||||
sprite_interact.durations();
|
||||
|
||||
update.character =
|
||||
CharacterState::SpriteInteract(sprite_interact::Data {
|
||||
static_data: sprite_interact::StaticData {
|
||||
buildup_duration,
|
||||
use_duration,
|
||||
recover_duration,
|
||||
sprite_pos,
|
||||
sprite_kind: sprite_interact,
|
||||
was_wielded: data.character.is_wield(),
|
||||
was_sneak: data.character.is_stealthy(),
|
||||
required_item,
|
||||
},
|
||||
timer: Duration::default(),
|
||||
stage_section: StageSection::Buildup,
|
||||
})
|
||||
} else {
|
||||
output_events.emit_local(LocalEvent::CreateOutcome(
|
||||
Outcome::FailedSpriteUnlock { pos: sprite_pos },
|
||||
));
|
||||
}
|
||||
update.character = CharacterState::SpriteInteract(sprite_interact::Data {
|
||||
static_data: sprite_interact::StaticData {
|
||||
buildup_duration,
|
||||
use_duration,
|
||||
recover_duration,
|
||||
sprite_pos,
|
||||
sprite_kind: sprite_interact,
|
||||
was_wielded: data.character.is_wield(),
|
||||
was_sneak: data.character.is_stealthy(),
|
||||
required_item,
|
||||
},
|
||||
timer: Duration::default(),
|
||||
stage_section: StageSection::Buildup,
|
||||
})
|
||||
} else {
|
||||
output_events.emit_local(LocalEvent::CreateOutcome(
|
||||
Outcome::FailedSpriteUnlock { pos: sprite_pos },
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ use crate::{
|
||||
consts::FRIC_GROUND,
|
||||
lottery::LootSpec,
|
||||
make_case_elim, rtsim,
|
||||
vol::FilledVox,
|
||||
};
|
||||
use num_derive::FromPrimitive;
|
||||
use num_traits::FromPrimitive;
|
||||
@ -117,6 +118,12 @@ pub struct Block {
|
||||
attr: [u8; 3],
|
||||
}
|
||||
|
||||
impl FilledVox for Block {
|
||||
fn default_non_filled() -> Self { Block::air(SpriteKind::Empty) }
|
||||
|
||||
fn is_filled(&self) -> bool { self.kind.is_filled() }
|
||||
}
|
||||
|
||||
impl Deref for Block {
|
||||
type Target = BlockKind;
|
||||
|
||||
@ -420,6 +427,19 @@ impl Block {
|
||||
self.collectible_id().is_some() && self.mine_tool().is_none()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_mountable(&self) -> bool { self.mount_offset().is_some() }
|
||||
|
||||
/// Get the position and direction to mount this block if any.
|
||||
pub fn mount_offset(&self) -> Option<(Vec3<f32>, Vec3<f32>)> {
|
||||
self.get_sprite().and_then(|sprite| sprite.mount_offset())
|
||||
}
|
||||
|
||||
pub fn is_controller(&self) -> bool {
|
||||
self.get_sprite()
|
||||
.map_or(false, |sprite| sprite.is_controller())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_bonkable(&self) -> bool {
|
||||
match self.get_sprite() {
|
||||
|
@ -12,6 +12,7 @@ use num_derive::FromPrimitive;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{convert::TryFrom, fmt};
|
||||
use strum::EnumIter;
|
||||
use vek::Vec3;
|
||||
|
||||
make_case_elim!(
|
||||
sprite_kind,
|
||||
@ -241,6 +242,8 @@ make_case_elim!(
|
||||
KeyDoor = 0xD8,
|
||||
CommonLockedChest = 0xD9,
|
||||
RepairBench = 0xDA,
|
||||
Helm = 0xDB,
|
||||
DoorWide = 0xDC,
|
||||
}
|
||||
);
|
||||
|
||||
@ -374,6 +377,7 @@ impl SpriteKind {
|
||||
SpriteKind::Bamboo => 9.0 / 11.0,
|
||||
SpriteKind::MagicalBarrier => 3.0,
|
||||
SpriteKind::MagicalSeal => 1.0,
|
||||
SpriteKind::Helm => 1.7,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
@ -474,6 +478,44 @@ impl SpriteKind {
|
||||
matches!(self.collectible_id(), Some(Some(LootSpec::LootTable(_))))
|
||||
}
|
||||
|
||||
/// Get the position and direction to mount this sprite if any.
|
||||
#[inline]
|
||||
pub fn mount_offset(&self) -> Option<(Vec3<f32>, Vec3<f32>)> {
|
||||
match self {
|
||||
SpriteKind::ChairSingle | SpriteKind::ChairDouble | SpriteKind::Bench => Some((
|
||||
Vec3 {
|
||||
x: 0.0,
|
||||
y: 0.0,
|
||||
z: 0.5,
|
||||
},
|
||||
-Vec3::unit_y(),
|
||||
)),
|
||||
SpriteKind::Helm => Some((
|
||||
Vec3 {
|
||||
x: 0.0,
|
||||
y: -0.6,
|
||||
z: 0.2,
|
||||
},
|
||||
Vec3::unit_y(),
|
||||
)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_mountable(&self) -> bool { self.mount_offset().is_some() }
|
||||
|
||||
#[inline]
|
||||
pub fn is_controller(&self) -> bool { matches!(self, SpriteKind::Helm) }
|
||||
|
||||
#[inline]
|
||||
pub fn is_door(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
SpriteKind::Door | SpriteKind::DoorWide | SpriteKind::DoorDark
|
||||
)
|
||||
}
|
||||
|
||||
/// Which tool (if any) is needed to collect this sprite?
|
||||
#[inline]
|
||||
pub fn mine_tool(&self) -> Option<ToolKind> {
|
||||
@ -603,6 +645,8 @@ impl SpriteKind {
|
||||
| SpriteKind::Grave
|
||||
| SpriteKind::Gravestone
|
||||
| SpriteKind::MagicalBarrier
|
||||
| SpriteKind::Helm
|
||||
| SpriteKind::DoorWide,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ use crate::{
|
||||
vol::{BaseVol, ReadVol, SizedVol, WriteVol},
|
||||
volumes::dyna::{Dyna, DynaError},
|
||||
};
|
||||
use dot_vox::DotVoxData;
|
||||
use hashbrown::HashMap;
|
||||
use serde::Deserialize;
|
||||
use std::{num::NonZeroU8, sync::Arc};
|
||||
@ -44,6 +45,12 @@ make_case_elim!(
|
||||
}
|
||||
);
|
||||
|
||||
// We can't derive this because of the `make_case_elim` macro.
|
||||
#[allow(clippy::derivable_impls)]
|
||||
impl Default for StructureBlock {
|
||||
fn default() -> Self { StructureBlock::None }
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum StructureError {
|
||||
OutOfBounds,
|
||||
@ -52,14 +59,14 @@ pub enum StructureError {
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Structure {
|
||||
center: Vec3<i32>,
|
||||
base: Arc<BaseStructure>,
|
||||
base: Arc<BaseStructure<StructureBlock>>,
|
||||
custom_indices: [Option<StructureBlock>; 256],
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct BaseStructure {
|
||||
vol: Dyna<Option<NonZeroU8>, ()>,
|
||||
palette: [StructureBlock; 256],
|
||||
pub(crate) struct BaseStructure<B> {
|
||||
pub(crate) vol: Dyna<Option<NonZeroU8>, ()>,
|
||||
pub(crate) palette: [B; 256],
|
||||
}
|
||||
|
||||
pub struct StructuresGroup(Vec<Structure>);
|
||||
@ -79,7 +86,9 @@ impl assets::Compound for StructuresGroup {
|
||||
.0
|
||||
.iter()
|
||||
.map(|sp| {
|
||||
let base = cache.load::<Arc<BaseStructure>>(&sp.specifier)?.cloned();
|
||||
let base = cache
|
||||
.load::<Arc<BaseStructure<StructureBlock>>>(&sp.specifier)?
|
||||
.cloned();
|
||||
Ok(Structure {
|
||||
center: Vec3::from(sp.center),
|
||||
base,
|
||||
@ -138,43 +147,51 @@ impl ReadVol for Structure {
|
||||
}
|
||||
}
|
||||
|
||||
impl assets::Compound for BaseStructure {
|
||||
pub(crate) fn load_base_structure<B: Default>(
|
||||
dot_vox_data: &DotVoxData,
|
||||
mut to_block: impl FnMut(Rgb<u8>) -> B,
|
||||
) -> BaseStructure<B> {
|
||||
let mut palette = std::array::from_fn(|_| B::default());
|
||||
if let Some(model) = dot_vox_data.models.get(0) {
|
||||
for (i, col) in dot_vox_data
|
||||
.palette
|
||||
.iter()
|
||||
.map(|col| Rgb::new(col.r, col.g, col.b))
|
||||
.enumerate()
|
||||
{
|
||||
palette[(i + 1).min(255)] = to_block(col);
|
||||
}
|
||||
|
||||
let mut vol = Dyna::filled(
|
||||
Vec3::new(model.size.x, model.size.y, model.size.z),
|
||||
None,
|
||||
(),
|
||||
);
|
||||
|
||||
for voxel in &model.voxels {
|
||||
let _ = vol.set(
|
||||
Vec3::new(voxel.x, voxel.y, voxel.z).map(i32::from),
|
||||
Some(NonZeroU8::new(voxel.i + 1).unwrap()),
|
||||
);
|
||||
}
|
||||
|
||||
BaseStructure { vol, palette }
|
||||
} else {
|
||||
BaseStructure {
|
||||
vol: Dyna::filled(Vec3::zero(), None, ()),
|
||||
palette,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl assets::Compound for BaseStructure<StructureBlock> {
|
||||
fn load(cache: assets::AnyCache, specifier: &assets::SharedString) -> Result<Self, BoxedError> {
|
||||
let dot_vox_data = cache.load::<DotVoxAsset>(specifier)?.read();
|
||||
let dot_vox_data = &dot_vox_data.0;
|
||||
|
||||
if let Some(model) = dot_vox_data.models.get(0) {
|
||||
let mut palette = std::array::from_fn(|_| StructureBlock::None);
|
||||
|
||||
for (i, col) in dot_vox_data
|
||||
.palette
|
||||
.iter()
|
||||
.map(|col| Rgb::new(col.r, col.g, col.b))
|
||||
.enumerate()
|
||||
{
|
||||
palette[(i + 1).min(255)] = StructureBlock::Filled(BlockKind::Misc, col);
|
||||
}
|
||||
|
||||
let mut vol = Dyna::filled(
|
||||
Vec3::new(model.size.x, model.size.y, model.size.z),
|
||||
None,
|
||||
(),
|
||||
);
|
||||
|
||||
for voxel in &model.voxels {
|
||||
let _ = vol.set(
|
||||
Vec3::new(voxel.x, voxel.y, voxel.z).map(i32::from),
|
||||
Some(NonZeroU8::new(voxel.i + 1).unwrap()),
|
||||
);
|
||||
}
|
||||
|
||||
Ok(BaseStructure { vol, palette })
|
||||
} else {
|
||||
Ok(BaseStructure {
|
||||
vol: Dyna::filled(Vec3::zero(), None, ()),
|
||||
palette: std::array::from_fn(|_| StructureBlock::None),
|
||||
})
|
||||
}
|
||||
Ok(load_base_structure(dot_vox_data, |col| {
|
||||
StructureBlock::Filled(BlockKind::Misc, col)
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -149,6 +149,16 @@ impl FindDist<Vec3<f32>> for Cylinder {
|
||||
}
|
||||
}
|
||||
|
||||
impl FindDist<Cylinder> for Vec3<f32> {
|
||||
#[inline]
|
||||
fn approx_in_range(self, other: Cylinder, range: f32) -> bool {
|
||||
other.approx_in_range(self, range)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn min_distance(self, other: Cylinder) -> f32 { other.min_distance(self) }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
@ -13,12 +13,12 @@ pub trait RectVolSize: Clone {
|
||||
}
|
||||
|
||||
/// A voxel.
|
||||
pub trait Vox: Sized + Clone + PartialEq {
|
||||
fn empty() -> Self;
|
||||
fn is_empty(&self) -> bool;
|
||||
pub trait FilledVox: Sized + Clone + PartialEq {
|
||||
fn default_non_filled() -> Self;
|
||||
fn is_filled(&self) -> bool;
|
||||
|
||||
#[must_use]
|
||||
fn or(self, other: Self) -> Self { if self.is_empty() { other } else { self } }
|
||||
fn or(self, other: Self) -> Self { if self.is_filled() { self } else { other } }
|
||||
}
|
||||
|
||||
/// A volume that contains voxel data.
|
||||
@ -241,6 +241,7 @@ where
|
||||
|
||||
/// Convenience iterator type that can be used to quickly implement
|
||||
/// `IntoPosIterator`.
|
||||
#[derive(Clone)]
|
||||
pub struct DefaultPosIterator {
|
||||
current: Vec3<i32>,
|
||||
begin: Vec2<i32>,
|
||||
@ -288,6 +289,7 @@ impl Iterator for DefaultPosIterator {
|
||||
|
||||
/// Convenience iterator type that can be used to quickly implement
|
||||
/// `IntoVolIterator`.
|
||||
#[derive(Clone)]
|
||||
pub struct DefaultVolIterator<'a, T: ReadVol> {
|
||||
vol: &'a T,
|
||||
pos_iter: DefaultPosIterator,
|
||||
|
@ -1,4 +1,4 @@
|
||||
use crate::vol::{BaseVol, ReadVol, SizedVol, Vox};
|
||||
use crate::vol::{BaseVol, FilledVox, ReadVol, SizedVol};
|
||||
use vek::*;
|
||||
|
||||
pub struct Scaled<V> {
|
||||
@ -13,7 +13,7 @@ impl<V: BaseVol> BaseVol for Scaled<V> {
|
||||
|
||||
impl<V: ReadVol> ReadVol for Scaled<V>
|
||||
where
|
||||
V::Vox: Vox,
|
||||
V::Vox: FilledVox,
|
||||
{
|
||||
#[inline(always)]
|
||||
fn get(&self, pos: Vec3<i32>) -> Result<&Self::Vox, Self::Error> {
|
||||
@ -43,7 +43,7 @@ where
|
||||
})
|
||||
.flatten()
|
||||
.map(|offs| self.inner.get(pos + offs))
|
||||
.find(|vox| vox.as_ref().map(|v| !v.is_empty()).unwrap_or(false))
|
||||
.find(|vox| vox.as_ref().map_or(false, |v| v.is_filled()))
|
||||
.unwrap_or_else(|| self.inner.get(pos))
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ use common::{
|
||||
comp,
|
||||
event::{EventBus, LocalEvent, ServerEvent},
|
||||
link::Is,
|
||||
mounting::{Mount, Rider},
|
||||
mounting::{Mount, Rider, VolumeRider, VolumeRiders},
|
||||
outcome::Outcome,
|
||||
region::RegionMap,
|
||||
resources::{
|
||||
@ -201,6 +201,7 @@ impl State {
|
||||
ecs.register::<comp::Scale>();
|
||||
ecs.register::<Is<Mount>>();
|
||||
ecs.register::<Is<Rider>>();
|
||||
ecs.register::<Is<VolumeRider>>();
|
||||
ecs.register::<comp::Mass>();
|
||||
ecs.register::<comp::Density>();
|
||||
ecs.register::<comp::Collider>();
|
||||
@ -260,6 +261,7 @@ impl State {
|
||||
ecs.register::<comp::invite::Invite>();
|
||||
ecs.register::<comp::invite::PendingInvites>();
|
||||
ecs.register::<comp::Beam>();
|
||||
ecs.register::<VolumeRiders>();
|
||||
|
||||
// Register synced resources used by the ECS.
|
||||
ecs.insert(TimeOfDay(0.0));
|
||||
@ -294,6 +296,7 @@ impl State {
|
||||
ecs.insert(PhysicsMetrics::default());
|
||||
ecs.insert(Trades::default());
|
||||
ecs.insert(PlayerPhysicsSettings::default());
|
||||
ecs.insert(VolumeRiders::default());
|
||||
|
||||
// Load plugins from asset directory
|
||||
#[cfg(feature = "plugins")]
|
||||
|
@ -14,7 +14,7 @@ use common::{
|
||||
},
|
||||
event::{EventBus, LocalEvent, ServerEvent},
|
||||
link::Is,
|
||||
mounting::Rider,
|
||||
mounting::{Rider, VolumeRider},
|
||||
outcome::Outcome,
|
||||
resources::{DeltaTime, Time},
|
||||
states::{
|
||||
@ -43,6 +43,7 @@ pub struct ReadData<'a> {
|
||||
beams: ReadStorage<'a, Beam>,
|
||||
uids: ReadStorage<'a, Uid>,
|
||||
is_riders: ReadStorage<'a, Is<Rider>>,
|
||||
is_volume_riders: ReadStorage<'a, Is<VolumeRider>>,
|
||||
stats: ReadStorage<'a, Stats>,
|
||||
skill_sets: ReadStorage<'a, SkillSet>,
|
||||
active_abilities: ReadStorage<'a, ActiveAbilities>,
|
||||
@ -207,6 +208,7 @@ impl<'a> System<'a> for Sys {
|
||||
alignment: read_data.alignments.get(entity),
|
||||
terrain: &read_data.terrain,
|
||||
mount_data: read_data.is_riders.get(entity),
|
||||
volume_mount_data: read_data.is_volume_riders.get(entity),
|
||||
stance: read_data.stances.get(entity),
|
||||
};
|
||||
|
||||
|
@ -2,16 +2,17 @@ use common::{
|
||||
comp::{
|
||||
ability::Stance,
|
||||
agent::{Sound, SoundKind},
|
||||
Body, BuffChange, ControlEvent, Controller, Pos, Scale,
|
||||
Body, BuffChange, Collider, ControlEvent, Controller, Pos, Scale,
|
||||
},
|
||||
event::{EventBus, ServerEvent},
|
||||
terrain::TerrainGrid,
|
||||
uid::UidAllocator,
|
||||
};
|
||||
use common_ecs::{Job, Origin, Phase, System};
|
||||
use specs::{
|
||||
saveload::{Marker, MarkerAllocator},
|
||||
shred::ResourceId,
|
||||
Entities, Join, Read, ReadStorage, SystemData, World, WriteStorage,
|
||||
Entities, Join, Read, ReadExpect, ReadStorage, SystemData, World, WriteStorage,
|
||||
};
|
||||
use vek::*;
|
||||
|
||||
@ -20,9 +21,11 @@ pub struct ReadData<'a> {
|
||||
entities: Entities<'a>,
|
||||
uid_allocator: Read<'a, UidAllocator>,
|
||||
server_bus: Read<'a, EventBus<ServerEvent>>,
|
||||
terrain_grid: ReadExpect<'a, TerrainGrid>,
|
||||
positions: ReadStorage<'a, Pos>,
|
||||
bodies: ReadStorage<'a, Body>,
|
||||
scales: ReadStorage<'a, Scale>,
|
||||
colliders: ReadStorage<'a, Collider>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
@ -53,6 +56,17 @@ impl<'a> System<'a> for Sys {
|
||||
server_emitter.emit(ServerEvent::Mount(entity, mountee_entity));
|
||||
}
|
||||
},
|
||||
ControlEvent::MountVolume(volume) => {
|
||||
if let Some(block) = volume.get_block(
|
||||
&read_data.terrain_grid,
|
||||
&read_data.uid_allocator,
|
||||
&read_data.colliders,
|
||||
) {
|
||||
if block.is_mountable() {
|
||||
server_emitter.emit(ServerEvent::MountVolume(entity, volume));
|
||||
}
|
||||
}
|
||||
},
|
||||
ControlEvent::RemoveBuff(buff_id) => {
|
||||
server_emitter.emit(ServerEvent::Buff {
|
||||
entity,
|
||||
|
@ -1,14 +1,16 @@
|
||||
use common::{
|
||||
comp::{Body, ControlAction, Controller, InputKind, Ori, Pos, Scale, Vel},
|
||||
comp::{Body, Collider, ControlAction, Controller, InputKind, Ori, Pos, Scale, Vel},
|
||||
link::Is,
|
||||
mounting::Mount,
|
||||
mounting::{Mount, VolumeRider},
|
||||
terrain::TerrainGrid,
|
||||
uid::UidAllocator,
|
||||
};
|
||||
use common_ecs::{Job, Origin, Phase, System};
|
||||
use specs::{
|
||||
saveload::{Marker, MarkerAllocator},
|
||||
Entities, Join, Read, ReadStorage, WriteStorage,
|
||||
Entities, Join, Read, ReadExpect, ReadStorage, WriteStorage,
|
||||
};
|
||||
use tracing::error;
|
||||
use vek::*;
|
||||
|
||||
/// This system is responsible for controlling mounts
|
||||
@ -17,14 +19,17 @@ pub struct Sys;
|
||||
impl<'a> System<'a> for Sys {
|
||||
type SystemData = (
|
||||
Read<'a, UidAllocator>,
|
||||
ReadExpect<'a, TerrainGrid>,
|
||||
Entities<'a>,
|
||||
WriteStorage<'a, Controller>,
|
||||
ReadStorage<'a, Is<Mount>>,
|
||||
ReadStorage<'a, Is<VolumeRider>>,
|
||||
WriteStorage<'a, Pos>,
|
||||
WriteStorage<'a, Vel>,
|
||||
WriteStorage<'a, Ori>,
|
||||
ReadStorage<'a, Body>,
|
||||
ReadStorage<'a, Scale>,
|
||||
ReadStorage<'a, Collider>,
|
||||
);
|
||||
|
||||
const NAME: &'static str = "mount";
|
||||
@ -35,14 +40,17 @@ impl<'a> System<'a> for Sys {
|
||||
_job: &mut Job<Self>,
|
||||
(
|
||||
uid_allocator,
|
||||
terrain,
|
||||
entities,
|
||||
mut controllers,
|
||||
is_mounts,
|
||||
is_volume_riders,
|
||||
mut positions,
|
||||
mut velocities,
|
||||
mut orientations,
|
||||
bodies,
|
||||
scales,
|
||||
colliders,
|
||||
): Self::SystemData,
|
||||
) {
|
||||
// For each mount...
|
||||
@ -84,5 +92,86 @@ impl<'a> System<'a> for Sys {
|
||||
controller.actions = actions;
|
||||
}
|
||||
}
|
||||
|
||||
// For each volume rider.
|
||||
for (entity, is_volume_rider) in (&entities, &is_volume_riders).join() {
|
||||
if let Some((mut mat, _)) = is_volume_rider.pos.get_block_and_transform(
|
||||
&terrain,
|
||||
&uid_allocator,
|
||||
|e| positions.get(e).copied().zip(orientations.get(e).copied()),
|
||||
&colliders,
|
||||
) {
|
||||
let Some((mount_offset, mount_dir)) = is_volume_rider.block.mount_offset() else {
|
||||
error!("Mounted on unmountable block");
|
||||
continue;
|
||||
};
|
||||
|
||||
if let Some(ori) = is_volume_rider.block.get_ori() {
|
||||
mat *= Mat4::identity()
|
||||
.translated_3d(mount_offset)
|
||||
.rotated_z(std::f32::consts::PI * 0.25 * ori as f32)
|
||||
.translated_3d(Vec3::new(0.5, 0.5, 0.0));
|
||||
} else {
|
||||
mat *= Mat4::identity().translated_3d(mount_offset + Vec3::new(0.5, 0.5, 0.0));
|
||||
}
|
||||
|
||||
if let Some(pos) = positions.get_mut(entity) {
|
||||
pos.0 = mat.mul_point(Vec3::zero());
|
||||
}
|
||||
if let Some(ori) = orientations.get_mut(entity) {
|
||||
*ori = Ori::from_unnormalized_vec(mat.mul_direction(mount_dir))
|
||||
.unwrap_or_default();
|
||||
}
|
||||
}
|
||||
let v = match is_volume_rider.pos.kind {
|
||||
common::mounting::Volume::Terrain => Vec3::zero(),
|
||||
common::mounting::Volume::Entity(uid) => {
|
||||
if let Some(v) = uid_allocator
|
||||
.retrieve_entity_internal(uid.into())
|
||||
.and_then(|e| velocities.get(e))
|
||||
{
|
||||
v.0
|
||||
} else {
|
||||
Vec3::zero()
|
||||
}
|
||||
},
|
||||
};
|
||||
if let Some(vel) = velocities.get_mut(entity) {
|
||||
vel.0 = v;
|
||||
}
|
||||
|
||||
let inputs = controllers.get_mut(entity).map(|c| {
|
||||
let actions: Vec<_> = c
|
||||
.actions
|
||||
.drain_filter(|action| match action {
|
||||
ControlAction::StartInput { input: i, .. }
|
||||
| ControlAction::CancelInput(i) => {
|
||||
matches!(i, InputKind::Jump | InputKind::Fly | InputKind::Roll)
|
||||
},
|
||||
_ => false,
|
||||
})
|
||||
.collect();
|
||||
let inputs = c.inputs.clone();
|
||||
|
||||
(actions, inputs)
|
||||
});
|
||||
|
||||
if is_volume_rider.block.is_controller() {
|
||||
if let Some((actions, inputs)) = inputs {
|
||||
match is_volume_rider.pos.kind {
|
||||
common::mounting::Volume::Entity(uid) => {
|
||||
if let Some(controller) = uid_allocator
|
||||
.retrieve_entity_internal(uid.into())
|
||||
.and_then(|e| controllers.get_mut(e))
|
||||
{
|
||||
controller.inputs = inputs;
|
||||
controller.actions = actions;
|
||||
}
|
||||
},
|
||||
common::mounting::Volume::Terrain => {},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ use common::{
|
||||
consts::{AIR_DENSITY, FRIC_GROUND, GRAVITY},
|
||||
event::{EventBus, ServerEvent},
|
||||
link::Is,
|
||||
mounting::Rider,
|
||||
mounting::{Rider, VolumeRider},
|
||||
outcome::Outcome,
|
||||
resources::DeltaTime,
|
||||
states,
|
||||
@ -122,6 +122,7 @@ pub struct PhysicsRead<'a> {
|
||||
masses: ReadStorage<'a, Mass>,
|
||||
colliders: ReadStorage<'a, Collider>,
|
||||
is_ridings: ReadStorage<'a, Is<Rider>>,
|
||||
is_volume_ridings: ReadStorage<'a, Is<VolumeRider>>,
|
||||
projectiles: ReadStorage<'a, Projectile>,
|
||||
char_states: ReadStorage<'a, CharacterState>,
|
||||
bodies: ReadStorage<'a, Body>,
|
||||
@ -335,6 +336,7 @@ impl<'a> PhysicsData<'a> {
|
||||
&read.masses,
|
||||
&read.colliders,
|
||||
read.is_ridings.maybe(),
|
||||
read.is_volume_ridings.maybe(),
|
||||
read.stickies.maybe(),
|
||||
read.immovables.maybe(),
|
||||
&mut write.physics_states,
|
||||
@ -359,6 +361,7 @@ impl<'a> PhysicsData<'a> {
|
||||
mass,
|
||||
collider,
|
||||
is_riding,
|
||||
is_volume_riding,
|
||||
sticky,
|
||||
immovable,
|
||||
physics,
|
||||
@ -491,7 +494,9 @@ impl<'a> PhysicsData<'a> {
|
||||
mass: *mass_other,
|
||||
},
|
||||
vel,
|
||||
is_riding.is_some() || other_is_riding_maybe.is_some(),
|
||||
is_riding.is_some()
|
||||
|| is_volume_riding.is_some()
|
||||
|| other_is_riding_maybe.is_some(),
|
||||
);
|
||||
}
|
||||
},
|
||||
@ -546,11 +551,7 @@ impl<'a> PhysicsData<'a> {
|
||||
)
|
||||
.join()
|
||||
{
|
||||
let vol = match collider {
|
||||
Collider::Voxel { id } => voxel_colliders_manifest.colliders.get(id),
|
||||
Collider::Volume(vol) => Some(&**vol),
|
||||
_ => None,
|
||||
};
|
||||
let vol = collider.get_vol(&voxel_colliders_manifest);
|
||||
|
||||
if let Some(vol) = vol {
|
||||
let sphere = voxel_collider_bounding_sphere(vol, pos, ori);
|
||||
@ -587,6 +588,7 @@ impl<'a> PhysicsData<'a> {
|
||||
!&write.pos_vel_ori_defers, // This is the one we are adding
|
||||
write.previous_phys_cache.mask(),
|
||||
!&read.is_ridings,
|
||||
!&read.is_volume_ridings,
|
||||
)
|
||||
.join()
|
||||
.map(|t| (t.0, *t.2, *t.3, *t.4))
|
||||
@ -619,6 +621,7 @@ impl<'a> PhysicsData<'a> {
|
||||
&read.densities,
|
||||
read.scales.maybe(),
|
||||
!&read.is_ridings,
|
||||
!&read.is_volume_ridings,
|
||||
)
|
||||
.par_join()
|
||||
.for_each_init(
|
||||
@ -638,6 +641,7 @@ impl<'a> PhysicsData<'a> {
|
||||
density,
|
||||
scale,
|
||||
_,
|
||||
_,
|
||||
)| {
|
||||
let in_loaded_chunk = read
|
||||
.terrain
|
||||
@ -749,6 +753,7 @@ impl<'a> PhysicsData<'a> {
|
||||
&mut write.pos_vel_ori_defers,
|
||||
previous_phys_cache,
|
||||
!&read.is_ridings,
|
||||
!&read.is_volume_ridings,
|
||||
)
|
||||
.par_join()
|
||||
.filter(|tuple| tuple.3.is_voxel() == terrain_like_entities)
|
||||
@ -772,6 +777,7 @@ impl<'a> PhysicsData<'a> {
|
||||
pos_vel_ori_defer,
|
||||
previous_cache,
|
||||
_,
|
||||
_,
|
||||
)| {
|
||||
let mut land_on_ground = None;
|
||||
let mut outcomes = Vec::new();
|
||||
@ -1061,13 +1067,8 @@ impl<'a> PhysicsData<'a> {
|
||||
return;
|
||||
}
|
||||
|
||||
let voxel_collider = match collider_other {
|
||||
Collider::Voxel { id } => {
|
||||
voxel_colliders_manifest.colliders.get(id)
|
||||
},
|
||||
Collider::Volume(vol) => Some(&**vol),
|
||||
_ => None,
|
||||
};
|
||||
let voxel_collider =
|
||||
collider_other.get_vol(&voxel_colliders_manifest);
|
||||
|
||||
// use bounding cylinder regardless of our collider
|
||||
// TODO: extract point-terrain collision above to its own
|
||||
|
@ -10,7 +10,7 @@ use common::{
|
||||
SkillSet, Stance, Stats, Vel,
|
||||
},
|
||||
link::Is,
|
||||
mounting::{Mount, Rider},
|
||||
mounting::{Mount, Rider, VolumeRider},
|
||||
path::TraversalConfig,
|
||||
resources::{DeltaTime, Time, TimeOfDay},
|
||||
rtsim::{Actor, RtSimEntity},
|
||||
@ -236,6 +236,7 @@ pub struct ReadData<'a> {
|
||||
pub bodies: ReadStorage<'a, Body>,
|
||||
pub is_mounts: ReadStorage<'a, Is<Mount>>,
|
||||
pub is_riders: ReadStorage<'a, Is<Rider>>,
|
||||
pub is_volume_riders: ReadStorage<'a, Is<VolumeRider>>,
|
||||
pub time_of_day: Read<'a, TimeOfDay>,
|
||||
pub light_emitter: ReadStorage<'a, LightEmitter>,
|
||||
#[cfg(feature = "worldgen")]
|
||||
|
@ -40,7 +40,7 @@ use common::{
|
||||
event::{EventBus, ServerEvent},
|
||||
generation::{EntityConfig, EntityInfo},
|
||||
link::Is,
|
||||
mounting::Rider,
|
||||
mounting::{Rider, VolumeRider},
|
||||
npc::{self, get_npc_name},
|
||||
outcome::Outcome,
|
||||
parse_cmd_args,
|
||||
@ -227,21 +227,47 @@ fn position_mut<T>(
|
||||
server: &mut Server,
|
||||
entity: EcsEntity,
|
||||
descriptor: &str,
|
||||
dismount_volume: Option<bool>,
|
||||
f: impl for<'a> FnOnce(&'a mut comp::Pos) -> T,
|
||||
) -> CmdResult<T> {
|
||||
let entity = server
|
||||
.state
|
||||
.ecs()
|
||||
.read_storage::<Is<Rider>>()
|
||||
.get(entity)
|
||||
.and_then(|is_rider| {
|
||||
server
|
||||
let entity = if dismount_volume.unwrap_or(true) {
|
||||
server
|
||||
.state
|
||||
.ecs()
|
||||
.write_storage::<Is<VolumeRider>>()
|
||||
.remove(entity);
|
||||
entity
|
||||
} else {
|
||||
server
|
||||
.state
|
||||
.read_storage::<Is<Rider>>()
|
||||
.get(entity)
|
||||
.and_then(|is_rider| {
|
||||
server
|
||||
.state
|
||||
.ecs()
|
||||
.read_resource::<UidAllocator>()
|
||||
.retrieve_entity_internal(is_rider.mount.into())
|
||||
})
|
||||
.or(server
|
||||
.state
|
||||
.ecs()
|
||||
.read_resource::<UidAllocator>()
|
||||
.retrieve_entity_internal(is_rider.mount.into())
|
||||
})
|
||||
.unwrap_or(entity);
|
||||
.read_storage::<Is<VolumeRider>>()
|
||||
.get(entity)
|
||||
.and_then(|volume_rider| {
|
||||
Some(match volume_rider.pos.kind {
|
||||
common::mounting::Volume::Terrain => {
|
||||
Err("Tried to move the world.".to_string())
|
||||
},
|
||||
common::mounting::Volume::Entity(uid) => Ok(server
|
||||
.state
|
||||
.ecs()
|
||||
.read_resource::<UidAllocator>()
|
||||
.retrieve_entity_internal(uid.into())?),
|
||||
})
|
||||
})
|
||||
.transpose()?)
|
||||
.unwrap_or(entity)
|
||||
};
|
||||
|
||||
let mut maybe_pos = None;
|
||||
|
||||
@ -829,8 +855,9 @@ fn handle_jump(
|
||||
args: Vec<String>,
|
||||
action: &ServerChatCommand,
|
||||
) -> CmdResult<()> {
|
||||
if let (Some(x), Some(y), Some(z)) = parse_cmd_args!(args, f32, f32, f32) {
|
||||
position_mut(server, target, "target", |current_pos| {
|
||||
if let (Some(x), Some(y), Some(z), dismount_volume) = parse_cmd_args!(args, f32, f32, f32, bool)
|
||||
{
|
||||
position_mut(server, target, "target", dismount_volume, |current_pos| {
|
||||
current_pos.0 += Vec3::new(x, y, z)
|
||||
})
|
||||
} else {
|
||||
@ -845,8 +872,9 @@ fn handle_goto(
|
||||
args: Vec<String>,
|
||||
action: &ServerChatCommand,
|
||||
) -> CmdResult<()> {
|
||||
if let (Some(x), Some(y), Some(z)) = parse_cmd_args!(args, f32, f32, f32) {
|
||||
position_mut(server, target, "target", |current_pos| {
|
||||
if let (Some(x), Some(y), Some(z), dismount_volume) = parse_cmd_args!(args, f32, f32, f32, bool)
|
||||
{
|
||||
position_mut(server, target, "target", dismount_volume, |current_pos| {
|
||||
current_pos.0 = Vec3::new(x, y, z)
|
||||
})
|
||||
} else {
|
||||
@ -864,7 +892,7 @@ fn handle_site(
|
||||
action: &ServerChatCommand,
|
||||
) -> CmdResult<()> {
|
||||
#[cfg(feature = "worldgen")]
|
||||
if let Some(dest_name) = parse_cmd_args!(args, String) {
|
||||
if let (Some(dest_name), dismount_volume) = parse_cmd_args!(args, String, bool) {
|
||||
let site = server
|
||||
.world
|
||||
.civs()
|
||||
@ -881,7 +909,7 @@ fn handle_site(
|
||||
false,
|
||||
);
|
||||
|
||||
position_mut(server, target, "target", |current_pos| {
|
||||
position_mut(server, target, "target", dismount_volume, |current_pos| {
|
||||
current_pos.0 = site_pos
|
||||
})
|
||||
} else {
|
||||
@ -906,7 +934,7 @@ fn handle_respawn(
|
||||
.ok_or("No waypoint set")?
|
||||
.get_pos();
|
||||
|
||||
position_mut(server, target, "target", |current_pos| {
|
||||
position_mut(server, target, "target", Some(true), |current_pos| {
|
||||
current_pos.0 = waypoint;
|
||||
})
|
||||
}
|
||||
@ -1205,7 +1233,8 @@ fn handle_tp(
|
||||
args: Vec<String>,
|
||||
action: &ServerChatCommand,
|
||||
) -> CmdResult<()> {
|
||||
let player = if let Some(alias) = parse_cmd_args!(args, String) {
|
||||
let (player, dismount_volume) = parse_cmd_args!(args, String, bool);
|
||||
let player = if let Some(alias) = player {
|
||||
find_alias(server.state.ecs(), &alias)?.0
|
||||
} else if client != target {
|
||||
client
|
||||
@ -1213,7 +1242,7 @@ fn handle_tp(
|
||||
return Err(action.help_string());
|
||||
};
|
||||
let player_pos = position(server, player, "player")?;
|
||||
position_mut(server, target, "target", |target_pos| {
|
||||
position_mut(server, target, "target", dismount_volume, |target_pos| {
|
||||
*target_pos = player_pos
|
||||
})
|
||||
}
|
||||
@ -1226,7 +1255,8 @@ fn handle_rtsim_tp(
|
||||
action: &ServerChatCommand,
|
||||
) -> CmdResult<()> {
|
||||
use crate::rtsim::RtSim;
|
||||
let pos = if let Some(id) = parse_cmd_args!(args, u32) {
|
||||
let (npc_index, dismount_volume) = parse_cmd_args!(args, u32, bool);
|
||||
let pos = if let Some(id) = npc_index {
|
||||
// TODO: Take some other identifier than an integer to this command.
|
||||
server
|
||||
.state
|
||||
@ -1242,7 +1272,7 @@ fn handle_rtsim_tp(
|
||||
} else {
|
||||
return Err(action.help_string());
|
||||
};
|
||||
position_mut(server, target, "target", |target_pos| {
|
||||
position_mut(server, target, "target", dismount_volume, |target_pos| {
|
||||
target_pos.0 = pos;
|
||||
})
|
||||
}
|
||||
@ -1689,7 +1719,7 @@ fn handle_make_volume(
|
||||
server: &mut Server,
|
||||
client: EcsEntity,
|
||||
target: EcsEntity,
|
||||
_args: Vec<String>,
|
||||
args: Vec<String>,
|
||||
_action: &ServerChatCommand,
|
||||
) -> CmdResult<()> {
|
||||
use comp::body::ship::figuredata::VoxelCollider;
|
||||
@ -1697,7 +1727,11 @@ fn handle_make_volume(
|
||||
//let () = parse_args!(args);
|
||||
let pos = position(server, target, "target")?;
|
||||
let ship = comp::ship::Body::Volume;
|
||||
let sz = Vec3::new(15, 15, 15);
|
||||
let sz = parse_cmd_args!(args, u32).unwrap_or(15);
|
||||
if !(1..=127).contains(&sz) {
|
||||
return Err("Size has to be between 1 and 127.".to_string());
|
||||
};
|
||||
let sz = Vec3::broadcast(sz);
|
||||
let collider = {
|
||||
let terrain = server.state().terrain();
|
||||
comp::Collider::Volume(Arc::new(VoxelCollider::from_fn(sz, |rpos| {
|
||||
@ -1711,7 +1745,7 @@ fn handle_make_volume(
|
||||
server
|
||||
.state
|
||||
.create_ship(
|
||||
comp::Pos(pos.0 + Vec3::unit_z() * 50.0),
|
||||
comp::Pos(pos.0 + Vec3::unit_z() * (50.0 + sz.z as f32 / 2.0)),
|
||||
comp::Ori::default(),
|
||||
ship,
|
||||
move |_| collider,
|
||||
@ -3930,7 +3964,7 @@ fn handle_location(
|
||||
if let Some(name) = parse_cmd_args!(args, String) {
|
||||
let loc = server.state.ecs().read_resource::<Locations>().get(&name);
|
||||
match loc {
|
||||
Ok(loc) => position_mut(server, target, "target", |target_pos| {
|
||||
Ok(loc) => position_mut(server, target, "target", Some(true), |target_pos| {
|
||||
target_pos.0 = loc;
|
||||
}),
|
||||
Err(e) => Err(e.to_string()),
|
||||
|
@ -9,16 +9,18 @@ use common::{
|
||||
aura::{Aura, AuraKind, AuraTarget},
|
||||
beam,
|
||||
buff::{BuffCategory, BuffData, BuffKind, BuffSource},
|
||||
ship::figuredata::VOXEL_COLLIDER_MANIFEST,
|
||||
shockwave, Alignment, BehaviorCapability, Body, ItemDrops, LightEmitter, Object, Ori, Pos,
|
||||
Projectile, TradingBehavior, Vel, WaypointArea,
|
||||
},
|
||||
event::{EventBus, NpcBuilder, UpdateCharacterMetadata},
|
||||
mounting::Mounting,
|
||||
mounting::{Mounting, Volume, VolumeMounting, VolumePos},
|
||||
outcome::Outcome,
|
||||
resources::{Secs, Time},
|
||||
rtsim::RtSimVehicle,
|
||||
uid::Uid,
|
||||
util::Dir,
|
||||
vol::IntoFullVolIterator,
|
||||
ViewDistances,
|
||||
};
|
||||
use common_net::{msg::ServerGeneral, sync::WorldSyncExt};
|
||||
@ -200,9 +202,30 @@ pub fn handle_create_ship(
|
||||
driver: Option<NpcBuilder>,
|
||||
passengers: Vec<NpcBuilder>,
|
||||
) {
|
||||
let mut entity = server
|
||||
.state
|
||||
.create_ship(pos, ori, ship, |ship| ship.make_collider());
|
||||
let collider = ship.make_collider();
|
||||
let voxel_colliders_manifest = VOXEL_COLLIDER_MANIFEST.read();
|
||||
|
||||
// TODO: Find better solution for this, maybe something like a serverside block
|
||||
// of interests.
|
||||
let (mut steering, mut seats) = {
|
||||
let mut steering = Vec::new();
|
||||
let mut seats = Vec::new();
|
||||
|
||||
for (pos, block) in collider
|
||||
.get_vol(&voxel_colliders_manifest)
|
||||
.iter()
|
||||
.flat_map(|voxel_collider| voxel_collider.volume().full_vol_iter())
|
||||
{
|
||||
match (block.is_controller(), block.is_mountable()) {
|
||||
(true, true) => steering.push((pos, *block)),
|
||||
(false, true) => seats.push((pos, *block)),
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
(steering.into_iter(), seats.into_iter())
|
||||
};
|
||||
|
||||
let mut entity = server.state.create_ship(pos, ori, ship, |_| collider);
|
||||
/*
|
||||
if let Some(mut agent) = agent {
|
||||
let (kp, ki, kd) = pid_coefficients(&Body::Ship(ship));
|
||||
@ -221,10 +244,26 @@ pub fn handle_create_ship(
|
||||
let npc_entity = handle_create_npc(server, pos, driver);
|
||||
|
||||
let uids = server.state.ecs().read_storage::<Uid>();
|
||||
if let (Some(rider_uid), Some(mount_uid)) =
|
||||
(uids.get(npc_entity).copied(), uids.get(entity).copied())
|
||||
{
|
||||
drop(uids);
|
||||
let (rider_uid, mount_uid) = uids
|
||||
.get(npc_entity)
|
||||
.copied()
|
||||
.zip(uids.get(entity).copied())
|
||||
.expect("Couldn't get Uid from newly created ship and npc");
|
||||
drop(uids);
|
||||
|
||||
if let Some((steering_pos, steering_block)) = steering.next() {
|
||||
server
|
||||
.state
|
||||
.link(VolumeMounting {
|
||||
pos: VolumePos {
|
||||
kind: Volume::Entity(mount_uid),
|
||||
pos: steering_pos,
|
||||
},
|
||||
block: steering_block,
|
||||
rider: rider_uid,
|
||||
})
|
||||
.expect("Failed to link driver to ship");
|
||||
} else {
|
||||
server
|
||||
.state
|
||||
.link(Mounting {
|
||||
@ -232,13 +271,32 @@ pub fn handle_create_ship(
|
||||
rider: rider_uid,
|
||||
})
|
||||
.expect("Failed to link driver to ship");
|
||||
} else {
|
||||
panic!("Couldn't get Uid from newly created ship and npc");
|
||||
}
|
||||
}
|
||||
|
||||
for passenger in passengers {
|
||||
handle_create_npc(server, Pos(pos.0 + Vec3::unit_z() * 5.0), passenger);
|
||||
let npc_entity = handle_create_npc(server, Pos(pos.0 + Vec3::unit_z() * 5.0), passenger);
|
||||
if let Some((rider_pos, rider_block)) = seats.next() {
|
||||
let uids = server.state.ecs().read_storage::<Uid>();
|
||||
let (rider_uid, mount_uid) = uids
|
||||
.get(npc_entity)
|
||||
.copied()
|
||||
.zip(uids.get(entity).copied())
|
||||
.expect("Couldn't get Uid from newly created ship and npc");
|
||||
drop(uids);
|
||||
|
||||
server
|
||||
.state
|
||||
.link(VolumeMounting {
|
||||
pos: VolumePos {
|
||||
kind: Volume::Entity(mount_uid),
|
||||
pos: rider_pos,
|
||||
},
|
||||
block: rider_block,
|
||||
rider: rider_uid,
|
||||
})
|
||||
.expect("Failed to link passanger to ship");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -14,10 +14,10 @@ use common::{
|
||||
tool::{AbilityMap, ToolKind},
|
||||
Inventory, LootOwner, Pos, SkillGroupKind,
|
||||
},
|
||||
consts::{MAX_MOUNT_RANGE, SOUND_TRAVEL_DIST_PER_VOLUME},
|
||||
consts::{MAX_MOUNT_RANGE, MAX_SPRITE_MOUNT_RANGE, SOUND_TRAVEL_DIST_PER_VOLUME},
|
||||
event::EventBus,
|
||||
link::Is,
|
||||
mounting::{Mount, Mounting, Rider},
|
||||
mounting::{Mounting, Rider, VolumeMounting, VolumePos, VolumeRider},
|
||||
outcome::Outcome,
|
||||
terrain::{Block, SpriteKind},
|
||||
uid::Uid,
|
||||
@ -103,53 +103,83 @@ pub fn handle_npc_interaction(
|
||||
pub fn handle_mount(server: &mut Server, rider: EcsEntity, mount: EcsEntity) {
|
||||
let state = server.state_mut();
|
||||
|
||||
if state.ecs().read_storage::<Is<Rider>>().get(rider).is_none() {
|
||||
let not_mounting_yet = state.ecs().read_storage::<Is<Mount>>().get(mount).is_none();
|
||||
let within_range = {
|
||||
let positions = state.ecs().read_storage::<Pos>();
|
||||
within_mounting_range(positions.get(rider), positions.get(mount))
|
||||
};
|
||||
|
||||
let within_range = || {
|
||||
let positions = state.ecs().read_storage::<Pos>();
|
||||
within_mounting_range(positions.get(rider), positions.get(mount))
|
||||
};
|
||||
let healths = state.ecs().read_storage::<comp::Health>();
|
||||
let alive = |e| healths.get(e).map_or(true, |h| !h.is_dead);
|
||||
|
||||
if not_mounting_yet && within_range() && alive(rider) && alive(mount) {
|
||||
let uids = state.ecs().read_storage::<Uid>();
|
||||
if let (Some(rider_uid), Some(mount_uid)) =
|
||||
(uids.get(rider).copied(), uids.get(mount).copied())
|
||||
{
|
||||
let is_pet = matches!(
|
||||
state
|
||||
.ecs()
|
||||
.read_storage::<comp::Alignment>()
|
||||
.get(mount),
|
||||
Some(comp::Alignment::Owned(owner)) if *owner == rider_uid,
|
||||
);
|
||||
|
||||
let can_ride = state
|
||||
if within_range {
|
||||
let uids = state.ecs().read_storage::<Uid>();
|
||||
if let (Some(rider_uid), Some(mount_uid)) =
|
||||
(uids.get(rider).copied(), uids.get(mount).copied())
|
||||
{
|
||||
let is_pet = matches!(
|
||||
state
|
||||
.ecs()
|
||||
.read_storage()
|
||||
.get(mount)
|
||||
.map_or(false, |mount_body| {
|
||||
is_mountable(mount_body, state.ecs().read_storage().get(rider))
|
||||
});
|
||||
.read_storage::<comp::Alignment>()
|
||||
.get(mount),
|
||||
Some(comp::Alignment::Owned(owner)) if *owner == rider_uid,
|
||||
);
|
||||
|
||||
if is_pet && can_ride {
|
||||
drop(uids);
|
||||
drop(healths);
|
||||
let _ = state.link(Mounting {
|
||||
mount: mount_uid,
|
||||
rider: rider_uid,
|
||||
});
|
||||
}
|
||||
let can_ride = state
|
||||
.ecs()
|
||||
.read_storage()
|
||||
.get(mount)
|
||||
.map_or(false, |mount_body| {
|
||||
is_mountable(mount_body, state.ecs().read_storage().get(rider))
|
||||
});
|
||||
|
||||
if is_pet && can_ride {
|
||||
drop(uids);
|
||||
let _ = state.link(Mounting {
|
||||
mount: mount_uid,
|
||||
rider: rider_uid,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_mount_volume(server: &mut Server, rider: EcsEntity, volume_pos: VolumePos) {
|
||||
let state = server.state_mut();
|
||||
|
||||
let block_transform = volume_pos.get_block_and_transform(
|
||||
&state.terrain(),
|
||||
&state.ecs().read_resource(),
|
||||
|e| {
|
||||
state
|
||||
.read_storage()
|
||||
.get(e)
|
||||
.copied()
|
||||
.zip(state.read_storage().get(e).copied())
|
||||
},
|
||||
&state.read_storage(),
|
||||
);
|
||||
|
||||
if let Some((mat, block)) = block_transform
|
||||
&& let Some(mount_offset) = block.mount_offset() {
|
||||
let mount_pos = (mat * mount_offset.0.with_w(1.0)).xyz();
|
||||
let within_range = {
|
||||
let positions = state.ecs().read_storage::<Pos>();
|
||||
positions.get(rider).map_or(false, |pos| pos.0.distance_squared(mount_pos) < MAX_SPRITE_MOUNT_RANGE.powi(2))
|
||||
};
|
||||
|
||||
let maybe_uid = state.ecs().read_storage::<Uid>().get(rider).copied();
|
||||
|
||||
if let Some(rider) = maybe_uid && within_range {
|
||||
let _ = state.link(VolumeMounting {
|
||||
pos: volume_pos,
|
||||
block,
|
||||
rider,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_unmount(server: &mut Server, rider: EcsEntity) {
|
||||
let state = server.state_mut();
|
||||
state.ecs().write_storage::<Is<Rider>>().remove(rider);
|
||||
state.ecs().write_storage::<Is<VolumeRider>>().remove(rider);
|
||||
}
|
||||
|
||||
fn within_mounting_range(player_position: Option<&Pos>, mount_position: Option<&Pos>) -> bool {
|
||||
|
@ -13,6 +13,7 @@ use common::{
|
||||
InventoryUpdate,
|
||||
},
|
||||
consts::MAX_PICKUP_RANGE,
|
||||
mounting::VolumePos,
|
||||
recipe::{
|
||||
self, default_component_recipe_book, default_recipe_book, default_repair_recipe_book,
|
||||
},
|
||||
@ -745,27 +746,42 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv
|
||||
let ability_map = &state.ecs().read_resource::<AbilityMap>();
|
||||
let msm = state.ecs().read_resource::<MaterialStatManifest>();
|
||||
|
||||
let get_craft_sprite = |state, sprite_pos: Option<Vec3<i32>>| {
|
||||
let get_craft_sprite = |state, sprite_pos: Option<VolumePos>| {
|
||||
sprite_pos
|
||||
.filter(|pos| {
|
||||
let entity_cylinder = get_cylinder(state, entity);
|
||||
let in_range = within_pickup_range(entity_cylinder, || {
|
||||
Some(find_dist::Cube {
|
||||
min: pos.as_(),
|
||||
side_length: 1.0,
|
||||
})
|
||||
pos.get_block_and_transform(
|
||||
&state.terrain(),
|
||||
&state.ecs().read_resource(),
|
||||
|e| {
|
||||
state
|
||||
.read_storage()
|
||||
.get(e)
|
||||
.copied()
|
||||
.zip(state.read_storage().get(e).copied())
|
||||
},
|
||||
&state.read_storage(),
|
||||
)
|
||||
.map(|(mat, _)| mat.mul_point(Vec3::broadcast(0.5)))
|
||||
});
|
||||
if !in_range {
|
||||
debug!(
|
||||
?entity_cylinder,
|
||||
"Failed to craft recipe as not within range of required sprite, \
|
||||
sprite pos: {}",
|
||||
sprite pos: {:?}",
|
||||
pos
|
||||
);
|
||||
}
|
||||
in_range
|
||||
})
|
||||
.and_then(|pos| state.terrain().get(pos).ok().copied())
|
||||
.and_then(|pos| {
|
||||
pos.get_block(
|
||||
&state.terrain(),
|
||||
&state.ecs().read_resource(),
|
||||
&state.read_storage(),
|
||||
)
|
||||
})
|
||||
.and_then(|block| block.get_sprite())
|
||||
};
|
||||
|
||||
|
@ -1,5 +1,7 @@
|
||||
use crate::{
|
||||
events::interaction::handle_tame_pet, persistence::PersistedComponents, state_ext::StateExt,
|
||||
events::interaction::{handle_mount_volume, handle_tame_pet},
|
||||
persistence::PersistedComponents,
|
||||
state_ext::StateExt,
|
||||
Server,
|
||||
};
|
||||
use common::event::{EventBus, ServerEvent, ServerEventDiscriminants};
|
||||
@ -136,6 +138,9 @@ impl Server {
|
||||
handle_process_trade_action(self, entity, trade_id, action);
|
||||
},
|
||||
ServerEvent::Mount(mounter, mountee) => handle_mount(self, mounter, mountee),
|
||||
ServerEvent::MountVolume(mounter, volume) => {
|
||||
handle_mount_volume(self, mounter, volume)
|
||||
},
|
||||
ServerEvent::Unmount(mounter) => handle_unmount(self, mounter),
|
||||
ServerEvent::Possess(possessor_uid, possesse_uid) => {
|
||||
handle_possess(self, possessor_uid, possesse_uid)
|
||||
|
@ -23,7 +23,7 @@ use common::{
|
||||
},
|
||||
effect::Effect,
|
||||
link::{Link, LinkHandle},
|
||||
mounting::Mounting,
|
||||
mounting::{Mounting, VolumeMounting},
|
||||
resources::{Secs, Time, TimeOfDay},
|
||||
rtsim::{Actor, RtSimEntity},
|
||||
slowjob::SlowJobPool,
|
||||
@ -1099,6 +1099,7 @@ impl StateExt for State {
|
||||
}
|
||||
|
||||
maintain_link::<Mounting>(self);
|
||||
maintain_link::<VolumeMounting>(self);
|
||||
}
|
||||
|
||||
fn delete_entity_recorded(
|
||||
|
@ -12,6 +12,7 @@ use common::{
|
||||
Controller, Health, InputKind, Scale,
|
||||
},
|
||||
event::{EventBus, ServerEvent},
|
||||
mounting::Volume,
|
||||
path::TraversalConfig,
|
||||
};
|
||||
use common_base::prof_span;
|
||||
@ -69,6 +70,7 @@ impl<'a> System<'a> for Sys {
|
||||
read_data.rtsim_entities.maybe(),
|
||||
!&read_data.is_mounts,
|
||||
read_data.is_riders.maybe(),
|
||||
read_data.is_volume_riders.maybe(),
|
||||
)
|
||||
.par_join()
|
||||
.for_each_init(
|
||||
@ -93,6 +95,7 @@ impl<'a> System<'a> for Sys {
|
||||
rtsim_entity,
|
||||
_,
|
||||
is_rider,
|
||||
is_volume_rider,
|
||||
)| {
|
||||
let mut event_emitter = event_bus.emitter();
|
||||
let mut rng = thread_rng();
|
||||
@ -104,6 +107,16 @@ impl<'a> System<'a> for Sys {
|
||||
.uid_allocator
|
||||
.retrieve_entity_internal(is_rider.mount.into())
|
||||
})
|
||||
.or_else(|| {
|
||||
is_volume_rider.and_then(|is_volume_rider| {
|
||||
match is_volume_rider.pos.kind {
|
||||
Volume::Terrain => None,
|
||||
Volume::Entity(uid) => {
|
||||
read_data.uid_allocator.retrieve_entity_internal(uid.into())
|
||||
},
|
||||
}
|
||||
})
|
||||
})
|
||||
.unwrap_or(entity);
|
||||
|
||||
let moving_body = read_data.bodies.get(moving_entity);
|
||||
|
@ -8,7 +8,7 @@ use common::{
|
||||
},
|
||||
event::{EventBus, ServerEvent},
|
||||
link::Is,
|
||||
mounting::Rider,
|
||||
mounting::{Rider, VolumeRider},
|
||||
resources::{PlayerPhysicsSetting, PlayerPhysicsSettings},
|
||||
slowjob::SlowJobPool,
|
||||
terrain::TerrainGrid,
|
||||
@ -52,6 +52,7 @@ impl Sys {
|
||||
terrain: &ReadExpect<'_, TerrainGrid>,
|
||||
can_build: &ReadStorage<'_, CanBuild>,
|
||||
is_rider: &ReadStorage<'_, Is<Rider>>,
|
||||
is_volume_rider: &ReadStorage<'_, Is<VolumeRider>>,
|
||||
force_updates: &ReadStorage<'_, ForceUpdate>,
|
||||
skill_set: &mut Option<Cow<'_, SkillSet>>,
|
||||
healths: &ReadStorage<'_, Health>,
|
||||
@ -126,6 +127,7 @@ impl Sys {
|
||||
&& force_updates.get(entity).map_or(true, |force_update| force_update.counter() == force_counter)
|
||||
&& healths.get(entity).map_or(true, |h| !h.is_dead)
|
||||
&& is_rider.get(entity).is_none()
|
||||
&& is_volume_rider.get(entity).is_none()
|
||||
&& player_physics_setting
|
||||
.as_ref()
|
||||
.map_or(true, |s| s.client_authoritative())
|
||||
@ -322,6 +324,7 @@ impl<'a> System<'a> for Sys {
|
||||
ReadStorage<'a, CanBuild>,
|
||||
ReadStorage<'a, ForceUpdate>,
|
||||
ReadStorage<'a, Is<Rider>>,
|
||||
ReadStorage<'a, Is<VolumeRider>>,
|
||||
WriteStorage<'a, SkillSet>,
|
||||
ReadStorage<'a, Health>,
|
||||
Write<'a, BlockChange>,
|
||||
@ -353,6 +356,7 @@ impl<'a> System<'a> for Sys {
|
||||
can_build,
|
||||
force_updates,
|
||||
is_rider,
|
||||
is_volume_rider,
|
||||
mut skill_sets,
|
||||
healths,
|
||||
mut block_changes,
|
||||
@ -430,6 +434,7 @@ impl<'a> System<'a> for Sys {
|
||||
&terrain,
|
||||
&can_build,
|
||||
&is_rider,
|
||||
&is_volume_rider,
|
||||
&force_updates,
|
||||
&mut skill_set,
|
||||
&healths,
|
||||
|
@ -25,6 +25,7 @@ use common::{
|
||||
slot::{InvSlotId, Slot},
|
||||
Inventory,
|
||||
},
|
||||
mounting::VolumePos,
|
||||
recipe::{ComponentKey, Recipe, RecipeInput},
|
||||
terrain::SpriteKind,
|
||||
};
|
||||
@ -123,7 +124,7 @@ pub enum Event {
|
||||
pub struct CraftingShow {
|
||||
pub crafting_tab: CraftingTab,
|
||||
pub crafting_search_key: Option<String>,
|
||||
pub craft_sprite: Option<(Vec3<i32>, SpriteKind)>,
|
||||
pub craft_sprite: Option<(VolumePos, SpriteKind)>,
|
||||
pub salvage: bool,
|
||||
pub initialize_repair: bool,
|
||||
// TODO: Maybe try to do something that doesn't need to allocate?
|
||||
|
@ -107,7 +107,7 @@ use common::{
|
||||
},
|
||||
consts::MAX_PICKUP_RANGE,
|
||||
link::Is,
|
||||
mounting::Mount,
|
||||
mounting::{Mount, VolumePos},
|
||||
outcome::Outcome,
|
||||
resources::{Secs, Time},
|
||||
slowjob::SlowJobPool,
|
||||
@ -710,27 +710,27 @@ pub enum Event {
|
||||
|
||||
CraftRecipe {
|
||||
recipe_name: String,
|
||||
craft_sprite: Option<(Vec3<i32>, SpriteKind)>,
|
||||
craft_sprite: Option<(VolumePos, SpriteKind)>,
|
||||
amount: u32,
|
||||
},
|
||||
SalvageItem {
|
||||
slot: InvSlotId,
|
||||
salvage_pos: Vec3<i32>,
|
||||
salvage_pos: VolumePos,
|
||||
},
|
||||
CraftModularWeapon {
|
||||
primary_slot: InvSlotId,
|
||||
secondary_slot: InvSlotId,
|
||||
craft_sprite: Option<Vec3<i32>>,
|
||||
craft_sprite: Option<VolumePos>,
|
||||
},
|
||||
CraftModularWeaponComponent {
|
||||
toolkind: ToolKind,
|
||||
material: InvSlotId,
|
||||
modifier: Option<InvSlotId>,
|
||||
craft_sprite: Option<Vec3<i32>>,
|
||||
craft_sprite: Option<VolumePos>,
|
||||
},
|
||||
RepairItem {
|
||||
item: Slot,
|
||||
sprite_pos: Vec3<i32>,
|
||||
sprite_pos: VolumePos,
|
||||
},
|
||||
InviteMember(Uid),
|
||||
AcceptInvite,
|
||||
@ -995,7 +995,7 @@ impl Show {
|
||||
pub fn open_crafting_tab(
|
||||
&mut self,
|
||||
tab: CraftingTab,
|
||||
craft_sprite: Option<(Vec3<i32>, SpriteKind)>,
|
||||
craft_sprite: Option<(VolumePos, SpriteKind)>,
|
||||
) {
|
||||
self.selected_crafting_tab(tab);
|
||||
self.crafting(true);
|
||||
@ -1289,7 +1289,7 @@ pub struct Hud {
|
||||
item_imgs: ItemImgs,
|
||||
fonts: Fonts,
|
||||
rot_imgs: ImgsRot,
|
||||
failed_block_pickups: HashMap<Vec3<i32>, CollectFailedData>,
|
||||
failed_block_pickups: HashMap<VolumePos, CollectFailedData>,
|
||||
failed_entity_pickups: HashMap<EcsEntity, CollectFailedData>,
|
||||
new_loot_messages: VecDeque<LootMessage>,
|
||||
new_messages: VecDeque<comp::ChatMsg>,
|
||||
@ -2040,7 +2040,13 @@ impl Hud {
|
||||
}
|
||||
|
||||
// Render overtime for an interactable block
|
||||
if let Some(Interactable::Block(block, pos, interaction)) = interactable {
|
||||
if let Some(Interactable::Block(block, pos, interaction)) = interactable
|
||||
&& let Some((mat, _)) = pos.get_block_and_transform(
|
||||
&ecs.read_resource(),
|
||||
&ecs.read_resource(),
|
||||
|e| ecs.read_storage::<vcomp::Interpolated>().get(e).map(|interpolated| (comp::Pos(interpolated.pos), interpolated.ori)),
|
||||
&ecs.read_storage(),
|
||||
) {
|
||||
let overitem_id = overitem_walker.next(
|
||||
&mut self.ids.overitems,
|
||||
&mut ui_widgets.widget_id_generator(),
|
||||
@ -2050,7 +2056,8 @@ impl Hud {
|
||||
active: true,
|
||||
pickup_failed_pulse: self.failed_block_pickups.get(pos).cloned(),
|
||||
};
|
||||
let pos = pos.map(|e| e as f32 + 0.5);
|
||||
|
||||
let pos = mat.mul_point(Vec3::broadcast(0.5));
|
||||
let over_pos = pos + Vec3::unit_z() * 0.7;
|
||||
|
||||
let interaction_text = || match interaction {
|
||||
@ -2107,6 +2114,13 @@ impl Hud {
|
||||
}
|
||||
}
|
||||
},
|
||||
BlockInteraction::Mount => {
|
||||
let key = match block.get_sprite() {
|
||||
Some(SpriteKind::Helm) => "hud-steer",
|
||||
_ => "hud-sit",
|
||||
};
|
||||
vec![(Some(GameInput::Mount), i18n.get_msg(key).to_string())]
|
||||
},
|
||||
};
|
||||
|
||||
// This is only done once per frame, so it's not a performance issue
|
||||
@ -4325,7 +4339,7 @@ impl Hud {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_failed_block_pickup(&mut self, pos: Vec3<i32>, reason: HudCollectFailedReason) {
|
||||
pub fn add_failed_block_pickup(&mut self, pos: VolumePos, reason: HudCollectFailedReason) {
|
||||
self.failed_block_pickups
|
||||
.insert(pos, CollectFailedData::new(self.pulse, reason));
|
||||
}
|
||||
@ -4706,16 +4720,38 @@ impl Hud {
|
||||
.handle_event(conrod_core::event::Input::Text("\t".to_string()));
|
||||
}
|
||||
|
||||
// Stop selecting a sprite to perform crafting with when out of range
|
||||
// Stop selecting a sprite to perform crafting with when out of range or sprite
|
||||
// has been removed
|
||||
self.show.crafting_fields.craft_sprite =
|
||||
self.show.crafting_fields.craft_sprite.filter(|(pos, _)| {
|
||||
self.show.crafting
|
||||
&& if let Some(player_pos) = client.position() {
|
||||
pos.map(|e| e as f32 + 0.5).distance(player_pos) < MAX_PICKUP_RANGE
|
||||
} else {
|
||||
false
|
||||
}
|
||||
});
|
||||
self.show
|
||||
.crafting_fields
|
||||
.craft_sprite
|
||||
.filter(|(pos, sprite)| {
|
||||
self.show.crafting
|
||||
&& if let Some(player_pos) = client.position() {
|
||||
pos.get_block_and_transform(
|
||||
&client.state().terrain(),
|
||||
&client.state().ecs().read_resource(),
|
||||
|e| {
|
||||
client
|
||||
.state()
|
||||
.read_storage::<vcomp::Interpolated>()
|
||||
.get(e)
|
||||
.map(|interpolated| {
|
||||
(comp::Pos(interpolated.pos), interpolated.ori)
|
||||
})
|
||||
},
|
||||
&client.state().read_storage(),
|
||||
)
|
||||
.map_or(false, |(mat, block)| {
|
||||
block.get_sprite() == Some(*sprite)
|
||||
&& mat.mul_point(Vec3::broadcast(0.5)).distance(player_pos)
|
||||
< MAX_PICKUP_RANGE
|
||||
})
|
||||
} else {
|
||||
false
|
||||
}
|
||||
});
|
||||
|
||||
// Optimization: skip maintaining UI when it's off.
|
||||
if !self.show.ui {
|
||||
@ -5130,6 +5166,7 @@ pub fn get_sprite_desc(sprite: SpriteKind, localized_strings: &Localization) ->
|
||||
| SpriteKind::DungeonChest3
|
||||
| SpriteKind::DungeonChest4
|
||||
| SpriteKind::DungeonChest5 => "common-sprite-chest",
|
||||
SpriteKind::ChairSingle | SpriteKind::ChairDouble => "common-sprite-chair",
|
||||
sprite => return Some(Cow::Owned(format!("{:?}", sprite))),
|
||||
};
|
||||
Some(localized_strings.get_msg(i18n_key))
|
||||
|
@ -1,6 +1,7 @@
|
||||
use crate::{
|
||||
mesh::{
|
||||
greedy::{self, GreedyConfig, GreedyMesh},
|
||||
terrain::FaceKind,
|
||||
MeshGen,
|
||||
},
|
||||
render::{Mesh, ParticleVertex, SpriteVertex, TerrainVertex},
|
||||
@ -8,7 +9,8 @@ use crate::{
|
||||
};
|
||||
use common::{
|
||||
figure::Cell,
|
||||
vol::{BaseVol, ReadVol, SizedVol, Vox},
|
||||
terrain::Block,
|
||||
vol::{BaseVol, FilledVox, ReadVol, SizedVol},
|
||||
};
|
||||
use core::convert::TryFrom;
|
||||
use vek::*;
|
||||
@ -16,7 +18,7 @@ use vek::*;
|
||||
// /// NOTE: bone_idx must be in [0, 15] (may be bumped to [0, 31] at some
|
||||
// /// point).
|
||||
// TODO: this function name...
|
||||
pub fn generate_mesh_base_vol_terrain<'a: 'b, 'b, V: 'a>(
|
||||
pub fn generate_mesh_base_vol_figure<'a: 'b, 'b, V: 'a>(
|
||||
vol: V,
|
||||
(greedy, opaque_mesh, offs, scale, bone_idx): (
|
||||
&'b mut GreedyMesh<'a>,
|
||||
@ -54,19 +56,14 @@ where
|
||||
let draw_delta = lower_bound;
|
||||
|
||||
let get_light = |vol: &mut V, pos: Vec3<i32>| {
|
||||
if vol.get(pos).map(|vox| vox.is_empty()).unwrap_or(true) {
|
||||
1.0
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
vol.get(pos).map_or(true, |vox| !vox.is_filled()) as i32 as f32
|
||||
};
|
||||
let get_glow = |_vol: &mut V, _pos: Vec3<i32>| 0.0;
|
||||
let get_opacity = |vol: &mut V, pos: Vec3<i32>| vol.get(pos).map_or(true, |vox| vox.is_empty());
|
||||
let get_opacity =
|
||||
|vol: &mut V, pos: Vec3<i32>| vol.get(pos).map_or(true, |vox| !vox.is_filled());
|
||||
let should_draw = |vol: &mut V, pos: Vec3<i32>, delta: Vec3<i32>, uv| {
|
||||
should_draw_greedy(pos, delta, uv, |vox| {
|
||||
vol.get(vox)
|
||||
.map(|vox| *vox)
|
||||
.unwrap_or_else(|_| Cell::empty())
|
||||
vol.get(vox).map(|vox| *vox).unwrap_or_else(|_| Cell::Empty)
|
||||
})
|
||||
};
|
||||
let create_opaque = |atlas_pos, pos, norm| {
|
||||
@ -115,6 +112,114 @@ where
|
||||
(Mesh::new(), Mesh::new(), Mesh::new(), bounds)
|
||||
}
|
||||
|
||||
// /// NOTE: bone_idx must be in [0, 15] (may be bumped to [0, 31] at some
|
||||
// /// point).
|
||||
// TODO: this function name...
|
||||
pub fn generate_mesh_base_vol_terrain<'a: 'b, 'b, V: 'a>(
|
||||
vol: V,
|
||||
(greedy, opaque_mesh, offs, scale, bone_idx): (
|
||||
&'b mut GreedyMesh<'a>,
|
||||
&'b mut Mesh<TerrainVertex>,
|
||||
Vec3<f32>,
|
||||
Vec3<f32>,
|
||||
u8,
|
||||
),
|
||||
) -> MeshGen<TerrainVertex, TerrainVertex, TerrainVertex, math::Aabb<f32>>
|
||||
where
|
||||
V: BaseVol<Vox = Block> + ReadVol + SizedVol,
|
||||
{
|
||||
assert!(bone_idx <= 15, "Bone index for figures must be in [0, 15]");
|
||||
let max_size = greedy.max_size();
|
||||
// NOTE: Required because we steal two bits from the normal in the shadow uint
|
||||
// in order to store the bone index. The two bits are instead taken out
|
||||
// of the atlas coordinates, which is why we "only" allow 1 << 15 per
|
||||
// coordinate instead of 1 << 16.
|
||||
assert!(max_size.reduce_max() < 1 << 15);
|
||||
|
||||
let lower_bound = vol.lower_bound();
|
||||
let upper_bound = vol.upper_bound();
|
||||
assert!(
|
||||
lower_bound.x <= upper_bound.x
|
||||
&& lower_bound.y <= upper_bound.y
|
||||
&& lower_bound.z <= upper_bound.z
|
||||
);
|
||||
// NOTE: Figure sizes should be no more than 512 along each axis.
|
||||
let greedy_size = upper_bound - lower_bound + 1;
|
||||
assert!(greedy_size.x <= 512 && greedy_size.y <= 512 && greedy_size.z <= 512);
|
||||
// NOTE: Cast to usize is safe because of previous check, since all values fit
|
||||
// into u16 which is safe to cast to usize.
|
||||
let greedy_size = greedy_size.as_::<usize>();
|
||||
let greedy_size_cross = greedy_size;
|
||||
let draw_delta = lower_bound;
|
||||
|
||||
let get_light =
|
||||
|vol: &mut V, pos: Vec3<i32>| vol.get(pos).map_or(true, |vox| vox.is_fluid()) as i32 as f32;
|
||||
let get_ao = |vol: &mut V, pos: Vec3<i32>| {
|
||||
vol.get(pos).map_or(false, |vox| vox.is_opaque()) as i32 as f32
|
||||
};
|
||||
let get_glow = |vol: &mut V, pos: Vec3<i32>| {
|
||||
vol.get(pos)
|
||||
.ok()
|
||||
.and_then(|vox| vox.get_glow())
|
||||
.unwrap_or(0) as f32
|
||||
/ 255.0
|
||||
};
|
||||
let get_opacity = |vol: &mut V, pos: Vec3<i32>| vol.get(pos).map_or(true, |vox| vox.is_fluid());
|
||||
let should_draw = |vol: &mut V, pos: Vec3<i32>, delta: Vec3<i32>, _uv| {
|
||||
super::terrain::should_draw_greedy(pos, delta, |vox| {
|
||||
vol.get(vox)
|
||||
.map(|vox| *vox)
|
||||
.unwrap_or_else(|_| Block::empty())
|
||||
})
|
||||
};
|
||||
|
||||
let create_opaque = |atlas_pos, pos, norm| {
|
||||
TerrainVertex::new_figure(atlas_pos, (pos + offs) * scale, norm, bone_idx)
|
||||
};
|
||||
|
||||
greedy.push(GreedyConfig {
|
||||
data: vol,
|
||||
draw_delta,
|
||||
greedy_size,
|
||||
greedy_size_cross,
|
||||
get_ao,
|
||||
get_light,
|
||||
get_glow,
|
||||
get_opacity,
|
||||
should_draw,
|
||||
push_quad: |atlas_origin, dim, origin, draw_dim, norm, meta: &FaceKind| match meta {
|
||||
FaceKind::Opaque(meta) => {
|
||||
opaque_mesh.push_quad(greedy::create_quad(
|
||||
atlas_origin,
|
||||
dim,
|
||||
origin,
|
||||
draw_dim,
|
||||
norm,
|
||||
meta,
|
||||
|atlas_pos, pos, norm, &_meta| create_opaque(atlas_pos, pos, norm),
|
||||
));
|
||||
},
|
||||
FaceKind::Fluid => {},
|
||||
},
|
||||
make_face_texel: |vol: &mut V, pos, light, _, _| {
|
||||
let block = vol.get(pos).ok();
|
||||
let glowy = block.map(|c| c.get_glow().is_some()).unwrap_or_default();
|
||||
let col = block
|
||||
.and_then(|vox| vox.get_color())
|
||||
.unwrap_or_else(Rgb::zero);
|
||||
TerrainVertex::make_col_light_figure(light, glowy, false, col)
|
||||
},
|
||||
});
|
||||
let bounds = math::Aabb {
|
||||
// NOTE: Casts are safe since lower_bound and upper_bound both fit in a i16.
|
||||
min: math::Vec3::from((lower_bound.as_::<f32>() + offs) * scale),
|
||||
max: math::Vec3::from((upper_bound.as_::<f32>() + offs) * scale),
|
||||
}
|
||||
.made_valid();
|
||||
|
||||
(Mesh::new(), Mesh::new(), Mesh::new(), bounds)
|
||||
}
|
||||
|
||||
pub fn generate_mesh_base_vol_sprite<'a: 'b, 'b, V: 'a>(
|
||||
vol: V,
|
||||
(greedy, opaque_mesh, vertical_stripes): (
|
||||
@ -160,13 +265,13 @@ where
|
||||
let (flat, flat_get) = {
|
||||
let (w, h, d) = (greedy_size + 2).into_tuple();
|
||||
let flat = {
|
||||
let mut flat = vec![Cell::empty(); (w * h * d) as usize];
|
||||
let mut flat = vec![Cell::Empty; (w * h * d) as usize];
|
||||
let mut i = 0;
|
||||
for x in -1..greedy_size.x + 1 {
|
||||
for y in -1..greedy_size.y + 1 {
|
||||
for z in -1..greedy_size.z + 1 {
|
||||
let wpos = lower_bound + Vec3::new(x, y, z);
|
||||
let block = vol.get(wpos).map(|b| *b).unwrap_or_else(|_| Cell::empty());
|
||||
let block = vol.get(wpos).map(|b| *b).unwrap_or_else(|_| Cell::Empty);
|
||||
flat[i] = block;
|
||||
i += 1;
|
||||
}
|
||||
@ -193,18 +298,13 @@ where
|
||||
let greedy_size_cross = greedy_size;
|
||||
let draw_delta = Vec3::new(1, 1, 1);
|
||||
|
||||
let get_light = move |flat: &mut _, pos: Vec3<i32>| {
|
||||
if flat_get(flat, pos).is_empty() {
|
||||
1.0
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
};
|
||||
let get_light =
|
||||
move |flat: &mut _, pos: Vec3<i32>| !flat_get(flat, pos).is_filled() as i32 as f32;
|
||||
let get_glow = |_flat: &mut _, _pos: Vec3<i32>| 0.0;
|
||||
let get_color = move |flat: &mut _, pos: Vec3<i32>| {
|
||||
flat_get(flat, pos).get_color().unwrap_or_else(Rgb::zero)
|
||||
};
|
||||
let get_opacity = move |flat: &mut _, pos: Vec3<i32>| flat_get(flat, pos).is_empty();
|
||||
let get_opacity = move |flat: &mut _, pos: Vec3<i32>| !flat_get(flat, pos).is_filled();
|
||||
let should_draw = move |flat: &mut _, pos: Vec3<i32>, delta: Vec3<i32>, uv| {
|
||||
should_draw_greedy_ao(vertical_stripes, pos, delta, uv, |vox| flat_get(flat, vox))
|
||||
};
|
||||
@ -281,11 +381,7 @@ where
|
||||
let draw_delta = lower_bound;
|
||||
|
||||
let get_light = |vol: &mut V, pos: Vec3<i32>| {
|
||||
if vol.get(pos).map(|vox| vox.is_empty()).unwrap_or(true) {
|
||||
1.0
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
vol.get(pos).map_or(true, |vox| !vox.is_filled()) as i32 as f32
|
||||
};
|
||||
let get_glow = |_vol: &mut V, _pos: Vec3<i32>| 0.0;
|
||||
let get_color = |vol: &mut V, pos: Vec3<i32>| {
|
||||
@ -294,12 +390,11 @@ where
|
||||
.and_then(|vox| vox.get_color())
|
||||
.unwrap_or_else(Rgb::zero)
|
||||
};
|
||||
let get_opacity = |vol: &mut V, pos: Vec3<i32>| vol.get(pos).map_or(true, |vox| vox.is_empty());
|
||||
let get_opacity =
|
||||
|vol: &mut V, pos: Vec3<i32>| vol.get(pos).map_or(true, |vox| !vox.is_filled());
|
||||
let should_draw = |vol: &mut V, pos: Vec3<i32>, delta: Vec3<i32>, uv| {
|
||||
should_draw_greedy(pos, delta, uv, |vox| {
|
||||
vol.get(vox)
|
||||
.map(|vox| *vox)
|
||||
.unwrap_or_else(|_| Cell::empty())
|
||||
vol.get(vox).map(|vox| *vox).unwrap_or_else(|_| Cell::Empty)
|
||||
})
|
||||
};
|
||||
let create_opaque = |_atlas_pos, pos: Vec3<f32>, norm| ParticleVertex::new(pos, norm);
|
||||
@ -342,8 +437,8 @@ fn should_draw_greedy(
|
||||
) -> Option<(bool, /* u8 */ ())> {
|
||||
let from = flat_get(pos - delta);
|
||||
let to = flat_get(pos);
|
||||
let from_opaque = !from.is_empty();
|
||||
if from_opaque != to.is_empty() {
|
||||
let from_opaque = from.is_filled();
|
||||
if from_opaque != !to.is_filled() {
|
||||
None
|
||||
} else {
|
||||
// If going from transparent to opaque, backward facing; otherwise, forward
|
||||
@ -361,8 +456,8 @@ fn should_draw_greedy_ao(
|
||||
) -> Option<(bool, bool)> {
|
||||
let from = flat_get(pos - delta);
|
||||
let to = flat_get(pos);
|
||||
let from_opaque = !from.is_empty();
|
||||
if from_opaque != to.is_empty() {
|
||||
let from_opaque = from.is_filled();
|
||||
if from_opaque != !to.is_filled() {
|
||||
None
|
||||
} else {
|
||||
let faces_forward = from_opaque;
|
||||
|
@ -20,7 +20,7 @@ use tracing::error;
|
||||
use vek::*;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
enum FaceKind {
|
||||
pub enum FaceKind {
|
||||
/// Opaque face that is facing something non-opaque; either
|
||||
/// water (Opaque(true)) or something else (Opaque(false)).
|
||||
Opaque(bool),
|
||||
@ -537,7 +537,7 @@ pub fn generate_mesh<'a>(
|
||||
|
||||
/// NOTE: Make sure to reflect any changes to how meshing is performanced in
|
||||
/// [scene::terrain::Terrain::skip_remesh].
|
||||
fn should_draw_greedy(
|
||||
pub fn should_draw_greedy(
|
||||
pos: Vec3<i32>,
|
||||
delta: Vec3<i32>,
|
||||
flat_get: impl Fn(Vec3<i32>) -> Block,
|
||||
|
@ -137,17 +137,25 @@ impl VertexTrait for Vertex {
|
||||
#[derive(Copy, Clone, Debug, Zeroable, Pod)]
|
||||
// TODO: new function and private fields??
|
||||
pub struct Locals {
|
||||
model_offs: [f32; 3],
|
||||
load_time: f32,
|
||||
model_mat: [f32; 16],
|
||||
atlas_offs: [i32; 4],
|
||||
load_time: f32,
|
||||
_dummy: [f32; 3],
|
||||
}
|
||||
|
||||
impl Locals {
|
||||
pub fn new(model_offs: Vec3<f32>, atlas_offs: Vec2<u32>, load_time: f32) -> Self {
|
||||
pub fn new(
|
||||
model_offs: Vec3<f32>,
|
||||
ori: Quaternion<f32>,
|
||||
atlas_offs: Vec2<u32>,
|
||||
load_time: f32,
|
||||
) -> Self {
|
||||
let mat = Mat4::from(ori).translated_3d(model_offs);
|
||||
Self {
|
||||
model_offs: model_offs.into_array(),
|
||||
model_mat: mat.into_col_array(),
|
||||
load_time,
|
||||
atlas_offs: Vec4::new(atlas_offs.x as i32, atlas_offs.y as i32, 0, 0).into_array(),
|
||||
_dummy: [0.0; 3],
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -155,9 +163,10 @@ impl Locals {
|
||||
impl Default for Locals {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
model_offs: [0.0; 3],
|
||||
model_mat: Mat4::identity().into_col_array(),
|
||||
load_time: 0.0,
|
||||
atlas_offs: [0; 4],
|
||||
_dummy: [0.0; 3],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1266,13 +1266,10 @@ impl Renderer {
|
||||
}
|
||||
|
||||
/// Create a new set of instances with the provided values.
|
||||
pub fn create_instances<T: Copy + bytemuck::Pod>(
|
||||
&mut self,
|
||||
vals: &[T],
|
||||
) -> Result<Instances<T>, RenderError> {
|
||||
pub fn create_instances<T: Copy + bytemuck::Pod>(&mut self, vals: &[T]) -> Instances<T> {
|
||||
let mut instances = Instances::new(&self.device, vals.len());
|
||||
instances.update(&self.queue, vals, 0);
|
||||
Ok(instances)
|
||||
instances
|
||||
}
|
||||
|
||||
/// Ensure that the quad index buffer is large enough for a quad vertex
|
||||
|
@ -1,3 +1,5 @@
|
||||
use crate::render::Bound;
|
||||
|
||||
use super::{
|
||||
super::{
|
||||
buffer::Buffer,
|
||||
@ -1113,9 +1115,9 @@ pub struct SpriteDrawer<'pass_ref, 'pass: 'pass_ref> {
|
||||
}
|
||||
|
||||
impl<'pass_ref, 'pass: 'pass_ref> SpriteDrawer<'pass_ref, 'pass> {
|
||||
pub fn draw<'data: 'pass>(
|
||||
pub fn draw<'data: 'pass, T>(
|
||||
&mut self,
|
||||
terrain_locals: &'data terrain::BoundLocals,
|
||||
terrain_locals: &'data Bound<T>,
|
||||
instances: &'data Instances<sprite::Instance>,
|
||||
alt_indices: &'data AltIndices,
|
||||
culling_mode: CullingMode,
|
||||
|
@ -1,8 +1,21 @@
|
||||
use super::{load::BodySpec, FigureModelEntry};
|
||||
use super::{
|
||||
load::{BodySpec, ShipBoneMeshes},
|
||||
FigureModelEntry, ModelEntry, TerrainModelEntry,
|
||||
};
|
||||
use crate::{
|
||||
mesh::{greedy::GreedyMesh, segment::generate_mesh_base_vol_terrain},
|
||||
render::{BoneMeshes, ColLightInfo, FigureModel, Mesh, Renderer, TerrainVertex},
|
||||
scene::camera::CameraMode,
|
||||
mesh::{
|
||||
greedy::GreedyMesh,
|
||||
segment::{generate_mesh_base_vol_figure, generate_mesh_base_vol_terrain},
|
||||
},
|
||||
render::{
|
||||
BoneMeshes, ColLightInfo, FigureModel, Instances, Mesh, Renderer, SpriteInstance,
|
||||
TerrainVertex,
|
||||
},
|
||||
scene::{
|
||||
camera::CameraMode,
|
||||
terrain::{get_sprite_instances, BlocksOfInterest, SPRITE_LOD_LEVELS},
|
||||
Terrain,
|
||||
},
|
||||
};
|
||||
use anim::Skeleton;
|
||||
use common::{
|
||||
@ -15,36 +28,58 @@ use common::{
|
||||
item::{item_key::ItemKey, modular, Item, ItemDefinitionId},
|
||||
CharacterState,
|
||||
},
|
||||
figure::Segment,
|
||||
figure::{Segment, TerrainSegment},
|
||||
slowjob::SlowJobPool,
|
||||
vol::BaseVol,
|
||||
vol::{BaseVol, IntoVolIterator, ReadVol},
|
||||
};
|
||||
use core::{hash::Hash, ops::Range};
|
||||
use crossbeam_utils::atomic;
|
||||
use hashbrown::{hash_map::Entry, HashMap};
|
||||
use serde::Deserialize;
|
||||
use std::sync::Arc;
|
||||
use std::{array::from_fn, sync::Arc};
|
||||
use vek::*;
|
||||
|
||||
/// A type produced by mesh worker threads corresponding to the information
|
||||
/// needed to mesh figures.
|
||||
struct MeshWorkerResponse<const N: usize> {
|
||||
pub struct MeshWorkerResponse<const N: usize> {
|
||||
col_light: ColLightInfo,
|
||||
opaque: Mesh<TerrainVertex>,
|
||||
bounds: anim::vek::Aabb<f32>,
|
||||
vertex_range: [Range<u32>; N],
|
||||
}
|
||||
|
||||
/// A type produced by mesh worker threads corresponding to the information
|
||||
/// needed to mesh figures.
|
||||
pub struct TerrainMeshWorkerResponse<const N: usize> {
|
||||
col_light: ColLightInfo,
|
||||
opaque: Mesh<TerrainVertex>,
|
||||
bounds: anim::vek::Aabb<f32>,
|
||||
vertex_range: [Range<u32>; N],
|
||||
sprite_instances: [Vec<SpriteInstance>; SPRITE_LOD_LEVELS],
|
||||
blocks_of_interest: BlocksOfInterest,
|
||||
blocks_offset: Vec3<f32>,
|
||||
}
|
||||
|
||||
/// NOTE: To test this cell for validity, we currently first use
|
||||
/// Arc::get_mut(), and then only if that succeeds do we call AtomicCell::take.
|
||||
/// This way, we avoid all atomic updates for the fast path read in the "not yet
|
||||
/// updated" case (though it would be faster without weak pointers); since once
|
||||
/// it's updated, we switch from `Pending` to `Done`, this is only suboptimal
|
||||
/// for one frame.
|
||||
type MeshWorkerCell<const N: usize> = atomic::AtomicCell<Option<MeshWorkerResponse<N>>>;
|
||||
pub type MeshWorkerCell<const N: usize> = atomic::AtomicCell<Option<MeshWorkerResponse<N>>>;
|
||||
pub type TerrainMeshWorkerCell<const N: usize> =
|
||||
atomic::AtomicCell<Option<TerrainMeshWorkerResponse<N>>>;
|
||||
|
||||
pub trait ModelEntryFuture<const N: usize> {
|
||||
type ModelEntry: ModelEntry;
|
||||
|
||||
fn into_done(self) -> Option<Self::ModelEntry>;
|
||||
|
||||
fn get_done(&self) -> Option<&Self::ModelEntry>;
|
||||
}
|
||||
|
||||
/// A future FigureModelEntryLod.
|
||||
enum FigureModelEntryFuture<const N: usize> {
|
||||
pub enum FigureModelEntryFuture<const N: usize> {
|
||||
/// We can poll the future to see whether the figure model is ready.
|
||||
// TODO: See if we can find away to either get rid of this Arc, or reuse Arcs across different
|
||||
// figures. Updates to uvth for thread pool shared storage might obviate this requirement.
|
||||
@ -53,9 +88,56 @@ enum FigureModelEntryFuture<const N: usize> {
|
||||
Done(FigureModelEntry<N>),
|
||||
}
|
||||
|
||||
impl<const N: usize> ModelEntryFuture<N> for FigureModelEntryFuture<N> {
|
||||
type ModelEntry = FigureModelEntry<N>;
|
||||
|
||||
fn into_done(self) -> Option<Self::ModelEntry> {
|
||||
match self {
|
||||
Self::Pending(_) => None,
|
||||
Self::Done(d) => Some(d),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_done(&self) -> Option<&Self::ModelEntry> {
|
||||
match self {
|
||||
Self::Pending(_) => None,
|
||||
Self::Done(d) => Some(d),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A future TerrainModelEntryLod.
|
||||
pub enum TerrainModelEntryFuture<const N: usize> {
|
||||
/// We can poll the future to see whether the figure model is ready.
|
||||
// TODO: See if we can find away to either get rid of this Arc, or reuse Arcs across different
|
||||
// figures. Updates to uvth for thread pool shared storage might obviate this requirement.
|
||||
Pending(Arc<TerrainMeshWorkerCell<N>>),
|
||||
/// Stores the already-meshed model.
|
||||
Done(TerrainModelEntry<N>),
|
||||
}
|
||||
|
||||
impl<const N: usize> ModelEntryFuture<N> for TerrainModelEntryFuture<N> {
|
||||
type ModelEntry = TerrainModelEntry<N>;
|
||||
|
||||
fn into_done(self) -> Option<Self::ModelEntry> {
|
||||
match self {
|
||||
Self::Pending(_) => None,
|
||||
Self::Done(d) => Some(d),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_done(&self) -> Option<&Self::ModelEntry> {
|
||||
match self {
|
||||
Self::Pending(_) => None,
|
||||
Self::Done(d) => Some(d),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const LOD_COUNT: usize = 3;
|
||||
|
||||
type FigureModelEntryLod<'b> = Option<&'b FigureModelEntry<LOD_COUNT>>;
|
||||
type TerrainModelEntryLod<'b> = Option<&'b TerrainModelEntry<LOD_COUNT>>;
|
||||
|
||||
#[derive(Clone, Eq, Hash, PartialEq)]
|
||||
/// TODO: merge item_key and extra field into an enum
|
||||
@ -199,7 +281,16 @@ where
|
||||
Skel: Skeleton,
|
||||
Skel::Body: BodySpec,
|
||||
{
|
||||
models: HashMap<FigureKey<Skel::Body>, ((FigureModelEntryFuture<LOD_COUNT>, Skel::Attr), u64)>,
|
||||
models: HashMap<
|
||||
FigureKey<Skel::Body>,
|
||||
(
|
||||
(
|
||||
<Skel::Body as BodySpec>::ModelEntryFuture<LOD_COUNT>,
|
||||
Skel::Attr,
|
||||
),
|
||||
u64,
|
||||
),
|
||||
>,
|
||||
manifests: <Skel::Body as BodySpec>::Manifests,
|
||||
watcher: ReloadWatcher,
|
||||
}
|
||||
@ -240,7 +331,11 @@ where
|
||||
camera_mode: CameraMode,
|
||||
character_state: Option<&CharacterState>,
|
||||
item_key: Option<ItemKey>,
|
||||
) -> FigureModelEntryLod<'b> {
|
||||
) -> Option<
|
||||
&'b <<Skel::Body as BodySpec>::ModelEntryFuture<LOD_COUNT> as ModelEntryFuture<
|
||||
LOD_COUNT,
|
||||
>>::ModelEntry,
|
||||
> {
|
||||
// TODO: Use raw entries to avoid lots of allocation (among other things).
|
||||
let key = FigureKey {
|
||||
body,
|
||||
@ -254,13 +349,44 @@ where
|
||||
}),
|
||||
};
|
||||
|
||||
if let Some(((FigureModelEntryFuture::Done(model), _), _)) = self.models.get(&key) {
|
||||
if let Some(model) = self.models.get(&key).and_then(|d| d.0.0.get_done()) {
|
||||
Some(model)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear_models(&mut self) { self.models.clear(); }
|
||||
|
||||
pub fn clean(&mut self, col_lights: &mut super::FigureColLights, tick: u64)
|
||||
where
|
||||
<Skel::Body as BodySpec>::Spec: Clone,
|
||||
{
|
||||
// TODO: Don't hard-code this.
|
||||
if tick % 60 == 0 {
|
||||
self.models.retain(|_, ((model_entry, _), last_used)| {
|
||||
// Wait about a minute at 60 fps before invalidating old models.
|
||||
let delta = 60 * 60;
|
||||
let alive = *last_used + delta > tick;
|
||||
if !alive {
|
||||
if let Some(model_entry) = model_entry.get_done() {
|
||||
col_lights.atlas.deallocate(model_entry.allocation().id);
|
||||
}
|
||||
}
|
||||
alive
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Skel: Skeleton> FigureModelCache<Skel>
|
||||
where
|
||||
Skel::Body: BodySpec<
|
||||
BoneMesh = super::load::BoneMeshes,
|
||||
ModelEntryFuture<LOD_COUNT> = FigureModelEntryFuture<LOD_COUNT>,
|
||||
> + Eq
|
||||
+ Hash,
|
||||
{
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn get_or_create_model<'c>(
|
||||
&'c mut self,
|
||||
@ -408,7 +534,7 @@ where
|
||||
offset: Vec3<f32>,
|
||||
bone_idx: u8,
|
||||
) -> BoneMeshes {
|
||||
let (opaque, _, _, bounds) = generate_mesh_base_vol_terrain(
|
||||
let (opaque, _, _, bounds) = generate_mesh_base_vol_figure(
|
||||
segment,
|
||||
(greedy, opaque_mesh, offset, Vec3::one(), bone_idx),
|
||||
);
|
||||
@ -423,7 +549,7 @@ where
|
||||
bone_idx: u8,
|
||||
) -> BoneMeshes {
|
||||
let lod_scale = 0.6;
|
||||
let (opaque, _, _, bounds) = generate_mesh_base_vol_terrain(
|
||||
let (opaque, _, _, bounds) = generate_mesh_base_vol_figure(
|
||||
segment.scaled_by(Vec3::broadcast(lod_scale)),
|
||||
(
|
||||
greedy,
|
||||
@ -444,7 +570,7 @@ where
|
||||
bone_idx: u8,
|
||||
) -> BoneMeshes {
|
||||
let lod_scale = 0.3;
|
||||
let (opaque, _, _, bounds) = generate_mesh_base_vol_terrain(
|
||||
let (opaque, _, _, bounds) = generate_mesh_base_vol_figure(
|
||||
segment.scaled_by(Vec3::broadcast(lod_scale)),
|
||||
(
|
||||
greedy,
|
||||
@ -479,26 +605,322 @@ where
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear_models(&mut self) { self.models.clear(); }
|
||||
|
||||
pub fn clean(&mut self, col_lights: &mut super::FigureColLights, tick: u64)
|
||||
impl<Skel: Skeleton> FigureModelCache<Skel>
|
||||
where
|
||||
Skel::Body: BodySpec<
|
||||
BoneMesh = ShipBoneMeshes,
|
||||
ModelEntryFuture<LOD_COUNT> = TerrainModelEntryFuture<LOD_COUNT>,
|
||||
> + Eq
|
||||
+ Hash,
|
||||
{
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn get_or_create_terrain_model<'c>(
|
||||
&'c mut self,
|
||||
renderer: &mut Renderer,
|
||||
col_lights: &mut super::FigureColLights,
|
||||
body: Skel::Body,
|
||||
extra: <Skel::Body as BodySpec>::Extra,
|
||||
tick: u64,
|
||||
slow_jobs: &SlowJobPool,
|
||||
terrain: &Terrain,
|
||||
) -> (TerrainModelEntryLod<'c>, &'c Skel::Attr)
|
||||
where
|
||||
<Skel::Body as BodySpec>::Spec: Clone,
|
||||
for<'a> &'a Skel::Body: Into<Skel::Attr>,
|
||||
Skel::Body: Clone + Send + Sync + 'static,
|
||||
<Skel::Body as BodySpec>::Spec: Send + Sync + 'static,
|
||||
{
|
||||
// TODO: Don't hard-code this.
|
||||
if tick % 60 == 0 {
|
||||
self.models.retain(|_, ((model_entry, _), last_used)| {
|
||||
// Wait about a minute at 60 fps before invalidating old models.
|
||||
let delta = 60 * 60;
|
||||
let alive = *last_used + delta > tick;
|
||||
if !alive {
|
||||
if let FigureModelEntryFuture::Done(model_entry) = model_entry {
|
||||
col_lights.atlas.deallocate(model_entry.allocation.id);
|
||||
let skeleton_attr = (&body).into();
|
||||
let key = FigureKey {
|
||||
body,
|
||||
item_key: None,
|
||||
extra: None,
|
||||
};
|
||||
|
||||
// TODO: Use raw entries to avoid significant performance overhead.
|
||||
match self.models.entry(key) {
|
||||
Entry::Occupied(o) => {
|
||||
let ((model, skel), last_used) = o.into_mut();
|
||||
*last_used = tick;
|
||||
(
|
||||
match model {
|
||||
TerrainModelEntryFuture::Pending(recv) => {
|
||||
if let Some(TerrainMeshWorkerResponse {
|
||||
col_light,
|
||||
opaque,
|
||||
bounds,
|
||||
vertex_range,
|
||||
sprite_instances,
|
||||
blocks_of_interest,
|
||||
blocks_offset,
|
||||
}) = Arc::get_mut(recv).take().and_then(|cell| cell.take())
|
||||
{
|
||||
let model_entry = col_lights.create_terrain(
|
||||
renderer,
|
||||
col_light,
|
||||
(opaque, bounds),
|
||||
vertex_range,
|
||||
sprite_instances,
|
||||
blocks_of_interest,
|
||||
blocks_offset,
|
||||
);
|
||||
*model = TerrainModelEntryFuture::Done(model_entry);
|
||||
// NOTE: Borrow checker isn't smart enough to figure this out.
|
||||
if let TerrainModelEntryFuture::Done(model) = model {
|
||||
Some(model)
|
||||
} else {
|
||||
unreachable!();
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
},
|
||||
TerrainModelEntryFuture::Done(model) => Some(model),
|
||||
},
|
||||
skel,
|
||||
)
|
||||
},
|
||||
Entry::Vacant(v) => {
|
||||
let key = v.key().clone();
|
||||
let slot = Arc::new(atomic::AtomicCell::new(None));
|
||||
let manifests = self.manifests.clone();
|
||||
let sprite_data = Arc::clone(&terrain.sprite_data);
|
||||
let sprite_config = Arc::clone(&terrain.sprite_config);
|
||||
let slot_ = Arc::clone(&slot);
|
||||
|
||||
slow_jobs.spawn("FIGURE_MESHING", move || {
|
||||
// First, load all the base vertex data.
|
||||
let meshes =
|
||||
<Skel::Body as BodySpec>::bone_meshes(&key, &manifests, extra);
|
||||
|
||||
// Then, set up meshing context.
|
||||
let mut greedy = FigureModel::make_greedy();
|
||||
let mut opaque = Mesh::<TerrainVertex>::new();
|
||||
// Choose the most conservative bounds for any LOD model.
|
||||
let mut figure_bounds = anim::vek::Aabb {
|
||||
min: anim::vek::Vec3::zero(),
|
||||
max: anim::vek::Vec3::zero(),
|
||||
};
|
||||
// Meshes all bone models for this figure using the given mesh generation
|
||||
// function, attaching it to the current greedy mesher and opaque vertex
|
||||
// list. Returns the vertex bounds of the meshed model within the opaque
|
||||
// mesh.
|
||||
let mut make_model = |generate_mesh: for<'a, 'b> fn(
|
||||
&mut GreedyMesh<'a>,
|
||||
&'b mut _,
|
||||
&'a _,
|
||||
_,
|
||||
_,
|
||||
)
|
||||
-> _| {
|
||||
let vertex_start = opaque.vertices().len();
|
||||
meshes
|
||||
.iter()
|
||||
.enumerate()
|
||||
// NOTE: Cast to u8 is safe because i < 16.
|
||||
.filter_map(|(i, bm)| bm.as_ref().map(|bm| (i as u8, bm)))
|
||||
.for_each(|(i, (segment, offset))| {
|
||||
// Generate this mesh.
|
||||
let (_opaque_mesh, bounds) = generate_mesh(&mut greedy, &mut opaque, segment, *offset, i);
|
||||
// Update the figure bounds to the largest granularity seen so far
|
||||
// (NOTE: this is more than a little imperfect).
|
||||
//
|
||||
// FIXME: Maybe use the default bone position in the idle animation
|
||||
// to figure this out instead?
|
||||
figure_bounds.expand_to_contain(bounds);
|
||||
});
|
||||
// NOTE: vertex_start and vertex_end *should* fit in a u32, by the
|
||||
// following logic:
|
||||
//
|
||||
// Our new figure maximum is constrained to at most 2^8 × 2^8 × 2^8.
|
||||
// This uses at most 24 bits to store every vertex exactly once.
|
||||
// Greedy meshing can store each vertex in up to 3 quads, we have 3
|
||||
// greedy models, and we store 1.5x the vertex count, so the maximum
|
||||
// total space a model can take up is 3 * 3 * 1.5 * 2^24; rounding
|
||||
// up to 4 * 4 * 2^24 gets us to 2^28, which clearly still fits in a
|
||||
// u32.
|
||||
//
|
||||
// (We could also, though we prefer not to, reason backwards from the
|
||||
// maximum figure texture size of 2^15 × 2^15, also fits in a u32; we
|
||||
// can also see that, since we can have at most one texture entry per
|
||||
// vertex, any texture atlas of size 2^14 × 2^14 or higher should be
|
||||
// able to store data for any figure. So the only reason we would fail
|
||||
// here would be if the user's computer could not store a texture large
|
||||
// enough to fit all the LOD models for the figure, not for fundamental
|
||||
// reasons related to fitting in a u32).
|
||||
//
|
||||
// Therefore, these casts are safe.
|
||||
vertex_start as u32..opaque.vertices().len() as u32
|
||||
};
|
||||
|
||||
fn generate_mesh<'a>(
|
||||
greedy: &mut GreedyMesh<'a>,
|
||||
opaque_mesh: &mut Mesh<TerrainVertex>,
|
||||
segment: &'a TerrainSegment,
|
||||
offset: Vec3<f32>,
|
||||
bone_idx: u8,
|
||||
) -> BoneMeshes {
|
||||
let (opaque, _, _, bounds) = generate_mesh_base_vol_terrain(
|
||||
segment,
|
||||
(greedy, opaque_mesh, offset, Vec3::one(), bone_idx),
|
||||
);
|
||||
(opaque, bounds)
|
||||
}
|
||||
}
|
||||
alive
|
||||
});
|
||||
|
||||
fn generate_mesh_lod_mid<'a>(
|
||||
greedy: &mut GreedyMesh<'a>,
|
||||
opaque_mesh: &mut Mesh<TerrainVertex>,
|
||||
segment: &'a TerrainSegment,
|
||||
offset: Vec3<f32>,
|
||||
bone_idx: u8,
|
||||
) -> BoneMeshes {
|
||||
let lod_scale = 0.6;
|
||||
let (opaque, _, _, bounds) = generate_mesh_base_vol_terrain(
|
||||
segment.scaled_by(Vec3::broadcast(lod_scale)),
|
||||
(
|
||||
greedy,
|
||||
opaque_mesh,
|
||||
offset * lod_scale,
|
||||
Vec3::one() / lod_scale,
|
||||
bone_idx,
|
||||
),
|
||||
);
|
||||
(opaque, bounds)
|
||||
}
|
||||
|
||||
fn generate_mesh_lod_low<'a>(
|
||||
greedy: &mut GreedyMesh<'a>,
|
||||
opaque_mesh: &mut Mesh<TerrainVertex>,
|
||||
segment: &'a TerrainSegment,
|
||||
offset: Vec3<f32>,
|
||||
bone_idx: u8,
|
||||
) -> BoneMeshes {
|
||||
let lod_scale = 0.3;
|
||||
let (opaque, _, _, bounds) = generate_mesh_base_vol_terrain(
|
||||
segment.scaled_by(Vec3::broadcast(lod_scale)),
|
||||
(
|
||||
greedy,
|
||||
opaque_mesh,
|
||||
offset * lod_scale,
|
||||
Vec3::one() / lod_scale,
|
||||
bone_idx,
|
||||
),
|
||||
);
|
||||
(opaque, bounds)
|
||||
}
|
||||
|
||||
let models = [
|
||||
make_model(generate_mesh),
|
||||
make_model(generate_mesh_lod_mid),
|
||||
make_model(generate_mesh_lod_low),
|
||||
];
|
||||
|
||||
let (dyna, offset) = &meshes[0].as_ref().unwrap();
|
||||
let block_iter = dyna.vol_iter(Vec3::zero(), dyna.sz.as_()).map(|(pos, block)| (pos, *block));
|
||||
|
||||
slot_.store(Some(TerrainMeshWorkerResponse {
|
||||
col_light: greedy.finalize(),
|
||||
opaque,
|
||||
bounds: figure_bounds,
|
||||
vertex_range: models,
|
||||
sprite_instances: {
|
||||
let mut instances = from_fn(|_| Vec::new());
|
||||
get_sprite_instances(
|
||||
&mut instances,
|
||||
|lod, instance, _| {
|
||||
lod.push(instance);
|
||||
},
|
||||
block_iter.clone().map(|(pos, block)| (pos.as_() + *offset, block)),
|
||||
|p| p.as_(),
|
||||
|_| 1.0,
|
||||
|pos| dyna.get(pos).ok().and_then(|block| block.get_glow()).map(|glow| glow as f32 / 255.0).unwrap_or(0.0),
|
||||
&sprite_data,
|
||||
&sprite_config,
|
||||
);
|
||||
instances
|
||||
},
|
||||
blocks_of_interest: BlocksOfInterest::from_blocks(block_iter, 0.0, 10.0, 0.0),
|
||||
blocks_offset: *offset,
|
||||
}));
|
||||
});
|
||||
|
||||
let skel = &(v
|
||||
.insert((
|
||||
(TerrainModelEntryFuture::Pending(slot), skeleton_attr),
|
||||
tick,
|
||||
))
|
||||
.0)
|
||||
.1;
|
||||
(None, skel)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_blocks_of_interest(
|
||||
&self,
|
||||
body: Skel::Body,
|
||||
) -> Option<(&BlocksOfInterest, Vec3<f32>)> {
|
||||
let key = FigureKey {
|
||||
body,
|
||||
item_key: None,
|
||||
extra: None,
|
||||
};
|
||||
self.models.get(&key).and_then(|((model, _), _)| {
|
||||
let TerrainModelEntryFuture::Done(model) = model else {
|
||||
return None;
|
||||
};
|
||||
|
||||
Some((&model.blocks_of_interest, model.blocks_offset))
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_sprites(
|
||||
&self,
|
||||
body: Skel::Body,
|
||||
) -> Option<&[Instances<SpriteInstance>; SPRITE_LOD_LEVELS]> {
|
||||
let key = FigureKey {
|
||||
body,
|
||||
item_key: None,
|
||||
extra: None,
|
||||
};
|
||||
self.models.get(&key).and_then(|((model, _), _)| {
|
||||
let TerrainModelEntryFuture::Done(model) = model else {
|
||||
return None;
|
||||
};
|
||||
|
||||
Some(&model.sprite_instances)
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
pub fn update_terrain_locals(
|
||||
&mut self,
|
||||
renderer: &mut Renderer,
|
||||
entity: Entity,
|
||||
body: Skel::Body,
|
||||
pos: Vec3<f32>,
|
||||
ori: Quaternion<f32>,
|
||||
) {
|
||||
let key = FigureKey {
|
||||
body,
|
||||
item_key: None,
|
||||
extra: None,
|
||||
};
|
||||
if let Some(model) = self.models.get_mut(&key).and_then(|((model, _), _)| {
|
||||
if let TerrainModelEntryFuture::Done(model) = model {
|
||||
Some(model)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}) {
|
||||
renderer.update_consts(&mut *model.terrain_locals, &[TerrainLocals::new(
|
||||
pos,
|
||||
ori,
|
||||
Vec2::zero(),
|
||||
0.0,
|
||||
)])
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
@ -1,4 +1,6 @@
|
||||
use super::cache::{FigureKey, ToolKey};
|
||||
use super::cache::{
|
||||
FigureKey, FigureModelEntryFuture, ModelEntryFuture, TerrainModelEntryFuture, ToolKey,
|
||||
};
|
||||
use common::{
|
||||
assets::{self, AssetExt, AssetHandle, DotVoxAsset, ReloadWatcher, Ron},
|
||||
comp::{
|
||||
@ -19,12 +21,14 @@ use common::{
|
||||
quadruped_small::{self, BodyType as QSBodyType, Species as QSSpecies},
|
||||
ship::{
|
||||
self,
|
||||
figuredata::{ShipCentralSubSpec, ShipSpec},
|
||||
figuredata::{ShipSpec, VoxelCollider},
|
||||
},
|
||||
theropod::{self, BodyType as TBodyType, Species as TSpecies},
|
||||
},
|
||||
figure::{Cell, DynaUnionizer, MatCell, MatSegment, Material, Segment},
|
||||
vol::{IntoFullPosIterator, ReadVol, Vox},
|
||||
terrain::Block,
|
||||
vol::{IntoFullPosIterator, ReadVol},
|
||||
volumes::dyna::Dyna,
|
||||
};
|
||||
use hashbrown::HashMap;
|
||||
use serde::{Deserialize, Deserializer};
|
||||
@ -108,6 +112,8 @@ pub trait BodySpec: Sized {
|
||||
/// place it behind an [`Arc`].
|
||||
type Manifests: Send + Sync + Clone;
|
||||
type Extra: Send + Sync;
|
||||
type BoneMesh;
|
||||
type ModelEntryFuture<const N: usize>: ModelEntryFuture<N>;
|
||||
|
||||
/// Initialize all the specifications for this Body.
|
||||
fn load_spec() -> Result<Self::Manifests, assets::Error>;
|
||||
@ -126,7 +132,7 @@ pub trait BodySpec: Sized {
|
||||
key: &FigureKey<Self>,
|
||||
manifests: &Self::Manifests,
|
||||
extra: Self::Extra,
|
||||
) -> [Option<BoneMeshes>; anim::MAX_BONE_COUNT];
|
||||
) -> [Option<Self::BoneMesh>; anim::MAX_BONE_COUNT];
|
||||
}
|
||||
|
||||
macro_rules! make_vox_spec {
|
||||
@ -152,6 +158,8 @@ macro_rules! make_vox_spec {
|
||||
type Spec = $Spec;
|
||||
type Manifests = AssetHandle<Self::Spec>;
|
||||
type Extra = ();
|
||||
type BoneMesh = BoneMeshes;
|
||||
type ModelEntryFuture<const N: usize> = FigureModelEntryFuture<N>;
|
||||
|
||||
fn load_spec() -> Result<Self::Manifests, assets::Error> {
|
||||
Self::Spec::load("")
|
||||
@ -349,7 +357,7 @@ impl HumHeadSpec {
|
||||
.maybe_add(beard)
|
||||
.maybe_add(accessory)
|
||||
.maybe_add(helmet)
|
||||
.unify_with(|v| if v.is_hollow() { Cell::empty() } else { v });
|
||||
.unify_with(|v| if v.is_hollow() { Cell::Empty } else { v });
|
||||
(
|
||||
head,
|
||||
Vec3::from(spec.offset) + origin_offset.map(|e| e as f32 * -1.0),
|
||||
@ -5258,30 +5266,31 @@ fn segment_center(segment: &Segment) -> Option<Vec3<f32>> {
|
||||
}
|
||||
}
|
||||
|
||||
fn mesh_ship_bone<K: fmt::Debug + Eq + Hash, V, F: Fn(&V) -> &ShipCentralSubSpec>(
|
||||
pub type ShipBoneMeshes = (Dyna<Block, ()>, Vec3<f32>);
|
||||
|
||||
fn mesh_ship_bone<'a, K: fmt::Debug + Eq + Hash, V, F: Fn(&V) -> Option<&'a VoxelCollider>>(
|
||||
map: &HashMap<K, V>,
|
||||
obj: &K,
|
||||
f: F,
|
||||
) -> BoneMeshes {
|
||||
) -> Option<ShipBoneMeshes> {
|
||||
let spec = match map.get(obj) {
|
||||
Some(spec) => spec,
|
||||
None => {
|
||||
error!("No specification exists for {:?}", obj);
|
||||
return load_mesh("not_found", Vec3::new(-5.0, -5.0, -2.5));
|
||||
|
||||
return None;
|
||||
},
|
||||
};
|
||||
let bone = f(spec);
|
||||
let central = graceful_load_segment_fullspec(
|
||||
&format!("common.voxel.{}", &bone.central.0),
|
||||
bone.model_index,
|
||||
);
|
||||
|
||||
(central, Vec3::from(bone.offset))
|
||||
bone.map(|bone| (bone.volume().clone(), bone.translation))
|
||||
}
|
||||
|
||||
impl BodySpec for ship::Body {
|
||||
type BoneMesh = ShipBoneMeshes;
|
||||
type Extra = ();
|
||||
type Manifests = AssetHandle<Self::Spec>;
|
||||
type ModelEntryFuture<const N: usize> = TerrainModelEntryFuture<N>;
|
||||
type Spec = ShipSpec;
|
||||
|
||||
fn load_spec() -> Result<Self::Manifests, assets::Error> { Self::Spec::load("") }
|
||||
@ -5292,14 +5301,15 @@ impl BodySpec for ship::Body {
|
||||
FigureKey { body, .. }: &FigureKey<Self>,
|
||||
manifests: &Self::Manifests,
|
||||
_: Self::Extra,
|
||||
) -> [Option<BoneMeshes>; anim::MAX_BONE_COUNT] {
|
||||
let spec = &*manifests.read();
|
||||
let map = &(spec.central.read().0).0;
|
||||
) -> [Option<Self::BoneMesh>; anim::MAX_BONE_COUNT] {
|
||||
let spec = manifests.read();
|
||||
let spec = &*spec;
|
||||
let map = &spec.central.read().0.0;
|
||||
[
|
||||
Some(mesh_ship_bone(map, body, |spec| &spec.bone0)),
|
||||
Some(mesh_ship_bone(map, body, |spec| &spec.bone1)),
|
||||
Some(mesh_ship_bone(map, body, |spec| &spec.bone2)),
|
||||
Some(mesh_ship_bone(map, body, |spec| &spec.bone3)),
|
||||
mesh_ship_bone(map, body, |ship| spec.colliders.get(&ship.bone0.central.0)),
|
||||
mesh_ship_bone(map, body, |ship| spec.colliders.get(&ship.bone1.central.0)),
|
||||
mesh_ship_bone(map, body, |ship| spec.colliders.get(&ship.bone2.central.0)),
|
||||
mesh_ship_bone(map, body, |ship| spec.colliders.get(&ship.bone3.central.0)),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,14 +1,9 @@
|
||||
use super::{
|
||||
cache::FigureKey,
|
||||
load::{BodySpec, BoneMeshes},
|
||||
cache::{FigureKey, TerrainModelEntryFuture},
|
||||
load::{BodySpec, ShipBoneMeshes},
|
||||
EcsEntity,
|
||||
};
|
||||
use common::{
|
||||
assets,
|
||||
comp::ship::figuredata::VoxelCollider,
|
||||
figure::{Cell, Segment},
|
||||
vol::ReadVol,
|
||||
};
|
||||
use common::{assets, comp::ship::figuredata::VoxelCollider};
|
||||
use std::{convert::TryFrom, sync::Arc};
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Hash)]
|
||||
@ -46,9 +41,7 @@ impl anim::Skeleton for VolumeKey {
|
||||
buf: &mut [anim::FigureBoneData; anim::MAX_BONE_COUNT],
|
||||
_: Self::Body,
|
||||
) -> anim::Offsets {
|
||||
let scale_mat = anim::vek::Mat4::scaling_3d(1.0 / 11.0);
|
||||
|
||||
let bone = base_mat * scale_mat;
|
||||
let bone = base_mat;
|
||||
|
||||
*(<&mut [_; Self::BONE_COUNT]>::try_from(&mut buf[0..Self::BONE_COUNT]).unwrap()) = [
|
||||
anim::make_bone(bone),
|
||||
@ -68,8 +61,10 @@ impl anim::Skeleton for VolumeKey {
|
||||
}
|
||||
|
||||
impl BodySpec for VolumeKey {
|
||||
type BoneMesh = ShipBoneMeshes;
|
||||
type Extra = Arc<VoxelCollider>;
|
||||
type Manifests = ();
|
||||
type ModelEntryFuture<const N: usize> = TerrainModelEntryFuture<N>;
|
||||
type Spec = ();
|
||||
|
||||
fn load_spec() -> Result<Self::Manifests, assets::Error> { Ok(()) }
|
||||
@ -82,16 +77,11 @@ impl BodySpec for VolumeKey {
|
||||
_: &FigureKey<Self>,
|
||||
_: &Self::Manifests,
|
||||
collider: Self::Extra,
|
||||
) -> [Option<BoneMeshes>; anim::MAX_BONE_COUNT] {
|
||||
) -> [Option<Self::BoneMesh>; anim::MAX_BONE_COUNT] {
|
||||
println!("Generating segment...");
|
||||
[
|
||||
Some((
|
||||
Segment::from_fn(collider.volume().sz, (), |pos| {
|
||||
match collider.volume().get(pos).unwrap().get_color() {
|
||||
Some(col) => Cell::new(col, false, false, false),
|
||||
None => Cell::Empty,
|
||||
}
|
||||
}),
|
||||
collider.volume().clone(),
|
||||
-collider.volume().sz.map(|e| e as f32) / 2.0,
|
||||
)),
|
||||
None,
|
||||
|
@ -138,9 +138,7 @@ impl Lod {
|
||||
.into_iter()
|
||||
.map(|(kind, instances)| {
|
||||
(kind, ObjectGroup {
|
||||
instances: renderer
|
||||
.create_instances(&instances)
|
||||
.expect("Renderer error?!"),
|
||||
instances: renderer.create_instances(&instances),
|
||||
z_range: z_range.clone(),
|
||||
frustum_last_plane_index: 0,
|
||||
visible: false,
|
||||
|
@ -31,7 +31,7 @@ use crate::{
|
||||
use client::Client;
|
||||
use common::{
|
||||
calendar::Calendar,
|
||||
comp,
|
||||
comp::{self, ship::figuredata::VOXEL_COLLIDER_MANIFEST},
|
||||
outcome::Outcome,
|
||||
resources::DeltaTime,
|
||||
terrain::{BlockKind, TerrainChunk, TerrainGrid},
|
||||
@ -655,13 +655,19 @@ impl Scene {
|
||||
lights.clear();
|
||||
|
||||
// Maintain the particles.
|
||||
self.particle_mgr
|
||||
.maintain(renderer, scene_data, &self.terrain, lights);
|
||||
self.particle_mgr.maintain(
|
||||
renderer,
|
||||
scene_data,
|
||||
&self.terrain,
|
||||
&self.figure_mgr,
|
||||
lights,
|
||||
);
|
||||
|
||||
// Maintain the trails.
|
||||
self.trail_mgr.maintain(renderer, scene_data);
|
||||
|
||||
// Update light constants
|
||||
let max_light_dist = loaded_distance.powi(2) + LIGHT_DIST_RADIUS;
|
||||
lights.extend(
|
||||
(
|
||||
&scene_data.state.ecs().read_storage::<comp::Pos>(),
|
||||
@ -684,8 +690,7 @@ impl Scene {
|
||||
.filter(|(pos, _, light_anim, h)| {
|
||||
light_anim.col != Rgb::zero()
|
||||
&& light_anim.strength > 0.0
|
||||
&& pos.0.distance_squared(viewpoint_pos)
|
||||
< loaded_distance.powi(2) + LIGHT_DIST_RADIUS
|
||||
&& pos.0.distance_squared(viewpoint_pos) < max_light_dist
|
||||
&& h.map_or(true, |h| !h.is_dead)
|
||||
})
|
||||
.map(|(pos, interpolated, light_anim, _)| {
|
||||
@ -699,6 +704,54 @@ impl Scene {
|
||||
.map(|el| el.light.with_strength((el.fadeout)(el.timeout))),
|
||||
),
|
||||
);
|
||||
let voxel_colliders_manifest = VOXEL_COLLIDER_MANIFEST.read();
|
||||
let figure_mgr = &self.figure_mgr;
|
||||
lights.extend(
|
||||
(
|
||||
&scene_data.state.ecs().entities(),
|
||||
&scene_data
|
||||
.state
|
||||
.read_storage::<crate::ecs::comp::Interpolated>(),
|
||||
&scene_data.state.read_storage::<comp::Body>(),
|
||||
&scene_data.state.read_storage::<comp::Collider>(),
|
||||
)
|
||||
.join()
|
||||
.filter_map(|(entity, interpolated, body, collider)| {
|
||||
let vol = collider.get_vol(&voxel_colliders_manifest)?;
|
||||
let (blocks_of_interest, offset) =
|
||||
figure_mgr.get_blocks_of_interest(entity, body, Some(collider))?;
|
||||
|
||||
let mat = Mat4::from(interpolated.ori.to_quat())
|
||||
.translated_3d(interpolated.pos)
|
||||
* Mat4::translation_3d(offset);
|
||||
|
||||
let p = mat.inverted().mul_point(viewpoint_pos);
|
||||
let aabb = Aabb {
|
||||
min: Vec3::zero(),
|
||||
max: vol.volume().sz.as_(),
|
||||
};
|
||||
if aabb.contains_point(p) || aabb.distance_to_point(p) < max_light_dist {
|
||||
Some(
|
||||
blocks_of_interest
|
||||
.lights
|
||||
.iter()
|
||||
.map(move |(block_offset, level)| {
|
||||
let wpos = mat.mul_point(block_offset.as_() + 0.5);
|
||||
(wpos, level)
|
||||
})
|
||||
.filter(move |(wpos, _)| {
|
||||
wpos.distance_squared(viewpoint_pos) < max_light_dist
|
||||
})
|
||||
.map(|(wpos, level)| {
|
||||
Light::new(wpos, Rgb::white(), *level as f32 / 7.0)
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.flatten(),
|
||||
);
|
||||
lights.sort_by_key(|light| light.get_pos().distance_squared(viewpoint_pos) as i32);
|
||||
lights.truncate(MAX_LIGHT_COUNT);
|
||||
renderer.update_consts(&mut self.data.lights, lights);
|
||||
@ -1347,14 +1400,28 @@ impl Scene {
|
||||
// Render the skybox.
|
||||
first_pass.draw_skybox(&self.skybox.model);
|
||||
|
||||
// Draws translucent terrain and sprites
|
||||
self.terrain.render_translucent(
|
||||
&mut first_pass,
|
||||
// Draws sprites
|
||||
let mut sprite_drawer = first_pass.draw_sprites(
|
||||
&self.terrain.sprite_globals,
|
||||
&self.terrain.sprite_col_lights,
|
||||
);
|
||||
self.figure_mgr.render_sprites(
|
||||
&mut sprite_drawer,
|
||||
state,
|
||||
cam_pos,
|
||||
scene_data.sprite_render_distance,
|
||||
);
|
||||
self.terrain.render_sprites(
|
||||
&mut sprite_drawer,
|
||||
focus_pos,
|
||||
cam_pos,
|
||||
scene_data.sprite_render_distance,
|
||||
culling_mode,
|
||||
);
|
||||
drop(sprite_drawer);
|
||||
|
||||
// Draws translucent
|
||||
self.terrain.render_translucent(&mut first_pass, focus_pos);
|
||||
|
||||
// Render particle effects.
|
||||
self.particle_mgr
|
||||
|
@ -1,4 +1,4 @@
|
||||
use super::{terrain::BlocksOfInterest, SceneData, Terrain};
|
||||
use super::{terrain::BlocksOfInterest, FigureMgr, SceneData, Terrain};
|
||||
use crate::{
|
||||
ecs::comp::Interpolated,
|
||||
mesh::{greedy::GreedyMesh, segment::generate_mesh_base_vol_particle},
|
||||
@ -400,6 +400,7 @@ impl ParticleMgr {
|
||||
renderer: &mut Renderer,
|
||||
scene_data: &SceneData,
|
||||
terrain: &Terrain<TerrainChunk>,
|
||||
figure_mgr: &FigureMgr,
|
||||
lights: &mut Vec<Light>,
|
||||
) {
|
||||
span!(_guard, "maintain", "ParticleMgr::maintain");
|
||||
@ -415,7 +416,7 @@ impl ParticleMgr {
|
||||
self.maintain_body_particles(scene_data);
|
||||
self.maintain_char_state_particles(scene_data);
|
||||
self.maintain_beam_particles(scene_data, lights);
|
||||
self.maintain_block_particles(scene_data, terrain);
|
||||
self.maintain_block_particles(scene_data, terrain, figure_mgr);
|
||||
self.maintain_shockwave_particles(scene_data);
|
||||
self.maintain_aura_particles(scene_data);
|
||||
self.maintain_buff_particles(scene_data);
|
||||
@ -1434,6 +1435,7 @@ impl ParticleMgr {
|
||||
&mut self,
|
||||
scene_data: &SceneData,
|
||||
terrain: &Terrain<TerrainChunk>,
|
||||
figure_mgr: &FigureMgr,
|
||||
) {
|
||||
span!(
|
||||
_guard,
|
||||
@ -1533,6 +1535,7 @@ impl ParticleMgr {
|
||||
},
|
||||
];
|
||||
|
||||
let ecs = scene_data.state.ecs();
|
||||
let mut rng = thread_rng();
|
||||
for particles in particles.iter() {
|
||||
if !(particles.cond)(scene_data) {
|
||||
@ -1564,6 +1567,46 @@ impl ParticleMgr {
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
for (entity, body, interpolated, collider) in (
|
||||
&ecs.entities(),
|
||||
&ecs.read_storage::<comp::Body>(),
|
||||
&ecs.read_storage::<crate::ecs::comp::Interpolated>(),
|
||||
ecs.read_storage::<comp::Collider>().maybe(),
|
||||
)
|
||||
.join()
|
||||
{
|
||||
if let Some((blocks_of_interest, offset)) =
|
||||
figure_mgr.get_blocks_of_interest(entity, body, collider)
|
||||
{
|
||||
let mat = Mat4::from(interpolated.ori.to_quat())
|
||||
.translated_3d(interpolated.pos)
|
||||
* Mat4::translation_3d(offset);
|
||||
|
||||
let blocks = (particles.blocks)(blocks_of_interest);
|
||||
|
||||
let avg_particles = dt * blocks.len() as f32 * particles.rate;
|
||||
let particle_count = avg_particles.trunc() as usize
|
||||
+ (rng.gen::<f32>() < avg_particles.fract()) as usize;
|
||||
|
||||
self.particles
|
||||
.resize_with(self.particles.len() + particle_count, || {
|
||||
let rel_pos = blocks
|
||||
.choose(&mut rng)
|
||||
.copied()
|
||||
.unwrap()
|
||||
.map(|e: i32| e as f32 + rng.gen::<f32>()); // Can't fail
|
||||
let wpos = mat.mul_point(rel_pos);
|
||||
|
||||
Particle::new(
|
||||
Duration::from_secs_f32(particles.lifetime),
|
||||
time,
|
||||
particles.mode,
|
||||
wpos,
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
// smoke is more complex as it comes with varying rate and color
|
||||
{
|
||||
@ -1945,9 +1988,7 @@ impl ParticleMgr {
|
||||
.collect::<Vec<ParticleInstance>>();
|
||||
|
||||
// TODO: optimise buffer writes
|
||||
let gpu_instances = renderer
|
||||
.create_instances(&all_cpu_instances)
|
||||
.expect("Failed to upload particle instances to the GPU!");
|
||||
let gpu_instances = renderer.create_instances(&all_cpu_instances);
|
||||
|
||||
self.instances = gpu_instances;
|
||||
}
|
||||
@ -1972,9 +2013,7 @@ impl ParticleMgr {
|
||||
fn default_instances(renderer: &mut Renderer) -> Instances<ParticleInstance> {
|
||||
let empty_vec = Vec::new();
|
||||
|
||||
renderer
|
||||
.create_instances(&empty_vec)
|
||||
.expect("Failed to upload particle instances to the GPU!")
|
||||
renderer.create_instances(&empty_vec)
|
||||
}
|
||||
|
||||
const DEFAULT_MODEL_KEY: &str = "voxygen.voxel.particle";
|
||||
|
@ -1,5 +1,5 @@
|
||||
use crate::{
|
||||
mesh::{greedy::GreedyMesh, segment::generate_mesh_base_vol_terrain},
|
||||
mesh::{greedy::GreedyMesh, segment::generate_mesh_base_vol_figure},
|
||||
render::{
|
||||
create_skybox_mesh, BoneMeshes, Consts, FigureModel, FirstPassDrawer, GlobalModel, Globals,
|
||||
GlobalsBindGroup, Light, LodData, Mesh, Model, PointLightMatrix, RainOcclusionLocals,
|
||||
@ -34,6 +34,8 @@ use common::{
|
||||
use vek::*;
|
||||
use winit::event::MouseButton;
|
||||
|
||||
use super::figure::{ModelEntry, ModelEntryRef};
|
||||
|
||||
struct VoidVol;
|
||||
impl BaseVol for VoidVol {
|
||||
type Error = ();
|
||||
@ -51,7 +53,7 @@ fn generate_mesh(
|
||||
bone_idx: u8,
|
||||
) -> BoneMeshes {
|
||||
let (opaque, _, /* shadow */ _, bounds) =
|
||||
generate_mesh_base_vol_terrain(segment, (greedy, mesh, offset, Vec3::one(), bone_idx));
|
||||
generate_mesh_base_vol_figure(segment, (greedy, mesh, offset, Vec3::one(), bone_idx));
|
||||
(opaque /* , shadow */, bounds)
|
||||
}
|
||||
|
||||
@ -385,14 +387,22 @@ impl Scene {
|
||||
|
||||
if let Some((model, figure_state)) = model.zip(self.figure_state.as_ref()) {
|
||||
if let Some(lod) = model.lod_model(0) {
|
||||
figure_drawer.draw(lod, figure_state.bound(), self.col_lights.texture(model));
|
||||
figure_drawer.draw(
|
||||
lod,
|
||||
figure_state.bound(),
|
||||
self.col_lights.texture(ModelEntryRef::Figure(model)),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((model, state)) = &self.backdrop {
|
||||
if let Some(lod) = model.lod_model(0) {
|
||||
figure_drawer.draw(lod, state.bound(), self.col_lights.texture(model));
|
||||
figure_drawer.draw(
|
||||
lod,
|
||||
state.bound(),
|
||||
self.col_lights.texture(ModelEntryRef::Figure(model)),
|
||||
);
|
||||
}
|
||||
}
|
||||
drop(figure_drawer);
|
||||
|
@ -11,9 +11,9 @@ use crate::{
|
||||
render::{
|
||||
pipelines::{self, ColLights},
|
||||
AltIndices, ColLightInfo, CullingMode, FirstPassDrawer, FluidVertex, GlobalModel,
|
||||
Instances, LodData, Mesh, Model, RenderError, Renderer, SpriteGlobalsBindGroup,
|
||||
SpriteInstance, SpriteVertex, SpriteVerts, TerrainLocals, TerrainShadowDrawer,
|
||||
TerrainVertex, SPRITE_VERT_PAGE_SIZE,
|
||||
Instances, LodData, Mesh, Model, RenderError, Renderer, SpriteDrawer,
|
||||
SpriteGlobalsBindGroup, SpriteInstance, SpriteVertex, SpriteVerts, TerrainLocals,
|
||||
TerrainShadowDrawer, TerrainVertex, SPRITE_VERT_PAGE_SIZE,
|
||||
},
|
||||
};
|
||||
|
||||
@ -45,7 +45,7 @@ use treeculler::{BVol, Frustum, AABB};
|
||||
use vek::*;
|
||||
|
||||
const SPRITE_SCALE: Vec3<f32> = Vec3::new(1.0 / 11.0, 1.0 / 11.0, 1.0 / 11.0);
|
||||
const SPRITE_LOD_LEVELS: usize = 5;
|
||||
pub const SPRITE_LOD_LEVELS: usize = 5;
|
||||
|
||||
// For rain occlusion we only need to render the closest chunks.
|
||||
/// How many chunks are maximally rendered for rain occlusion.
|
||||
@ -182,7 +182,7 @@ struct SpriteConfig<Model> {
|
||||
/// NOTE: Model is an asset path to the appropriate sprite .vox model.
|
||||
#[derive(Deserialize)]
|
||||
#[serde(try_from = "HashMap<SpriteKind, Option<SpriteConfig<String>>>")]
|
||||
struct SpriteSpec([Option<SpriteConfig<String>>; 256]);
|
||||
pub struct SpriteSpec([Option<SpriteConfig<String>>; 256]);
|
||||
|
||||
impl SpriteSpec {
|
||||
fn get(&self, kind: SpriteKind) -> Option<&SpriteConfig<String>> {
|
||||
@ -240,6 +240,76 @@ impl assets::Asset for SpriteSpec {
|
||||
const EXTENSION: &'static str = "ron";
|
||||
}
|
||||
|
||||
pub fn get_sprite_instances<'a, I: 'a>(
|
||||
lod_levels: &'a mut [I; SPRITE_LOD_LEVELS],
|
||||
set_instance: impl Fn(&mut I, SpriteInstance, Vec3<i32>),
|
||||
blocks: impl Iterator<Item = (Vec3<f32>, Block)>,
|
||||
mut to_wpos: impl FnMut(Vec3<f32>) -> Vec3<i32>,
|
||||
mut light_map: impl FnMut(Vec3<i32>) -> f32,
|
||||
mut glow_map: impl FnMut(Vec3<i32>) -> f32,
|
||||
sprite_data: &HashMap<(SpriteKind, usize), [SpriteData; SPRITE_LOD_LEVELS]>,
|
||||
sprite_config: &SpriteSpec,
|
||||
) {
|
||||
prof_span!("extract sprite_instances");
|
||||
for (rel_pos, block) in blocks {
|
||||
let Some(sprite) = block.get_sprite() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let Some(cfg) = sprite_config.get(sprite) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let wpos = to_wpos(rel_pos);
|
||||
let seed = (wpos.x as u64)
|
||||
.overflowing_mul(3)
|
||||
.0
|
||||
.overflowing_add((wpos.y as u64).overflowing_mul(7).0)
|
||||
.0
|
||||
.overflowing_add((wpos.x as u64).overflowing_mul(wpos.y as u64).0)
|
||||
.0; // Awful PRNG
|
||||
|
||||
let ori = (block.get_ori().unwrap_or((seed % 4) as u8 * 2)) & 0b111;
|
||||
let variation = seed as usize % cfg.variations.len();
|
||||
let key = (sprite, variation);
|
||||
|
||||
// NOTE: Safe because we called sprite_config_for already.
|
||||
// NOTE: Safe because 0 ≤ ori < 8
|
||||
let light = light_map(wpos);
|
||||
let glow = glow_map(wpos);
|
||||
|
||||
for (lod_level, sprite_data) in lod_levels.iter_mut().zip(&sprite_data[&key]) {
|
||||
let mat = Mat4::identity()
|
||||
// Scaling for different LOD resolutions
|
||||
.scaled_3d(sprite_data.scale)
|
||||
// Offset
|
||||
.translated_3d(sprite_data.offset)
|
||||
.scaled_3d(SPRITE_SCALE)
|
||||
.rotated_z(f32::consts::PI * 0.25 * ori as f32)
|
||||
.translated_3d(
|
||||
rel_pos + Vec3::new(0.5, 0.5, 0.0)
|
||||
);
|
||||
// Add an instance for each page in the sprite model
|
||||
for page in sprite_data.vert_pages.clone() {
|
||||
// TODO: could be more efficient to create once and clone while
|
||||
// modifying vert_page
|
||||
let instance = SpriteInstance::new(
|
||||
mat,
|
||||
cfg.wind_sway,
|
||||
sprite_data.scale.z,
|
||||
rel_pos.as_(),
|
||||
ori,
|
||||
light,
|
||||
glow,
|
||||
page,
|
||||
sprite.is_door(),
|
||||
);
|
||||
set_instance(lod_level, instance, wpos);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Function executed by worker threads dedicated to chunk meshing.
|
||||
|
||||
/// skip_remesh is either None (do the full remesh, including recomputing the
|
||||
@ -257,7 +327,12 @@ fn mesh_worker(
|
||||
sprite_config: &SpriteSpec,
|
||||
) -> MeshWorkerResponse {
|
||||
span!(_guard, "mesh_worker");
|
||||
let blocks_of_interest = BlocksOfInterest::from_chunk(&chunk);
|
||||
let blocks_of_interest = BlocksOfInterest::from_blocks(
|
||||
chunk.iter_changed().map(|(pos, block)| (pos, *block)),
|
||||
chunk.meta().river_velocity().magnitude_squared(),
|
||||
chunk.meta().temp(),
|
||||
chunk.meta().humidity(),
|
||||
);
|
||||
|
||||
let mesh;
|
||||
let (light_map, glow_map) = if let Some((light_map, glow_map)) = &skip_remesh {
|
||||
@ -292,7 +367,9 @@ fn mesh_worker(
|
||||
let mesh = mesh.as_ref().unwrap();
|
||||
(&*mesh.light_map, &*mesh.glow_map)
|
||||
};
|
||||
|
||||
let to_wpos = |rel_pos: Vec3<f32>| {
|
||||
Vec3::from(pos * TerrainChunk::RECT_SIZE.map(|e: u32| e as i32)) + rel_pos.as_()
|
||||
};
|
||||
MeshWorkerResponse {
|
||||
pos,
|
||||
// Extract sprite locations from volume
|
||||
@ -312,77 +389,31 @@ fn mesh_worker(
|
||||
(c.meta().alt() - SHALLOW_ALT, c.meta().alt() - DEEP_ALT)
|
||||
});
|
||||
|
||||
for x in 0..TerrainChunk::RECT_SIZE.x as i32 {
|
||||
for y in 0..TerrainChunk::RECT_SIZE.y as i32 {
|
||||
for z in z_bounds.0 as i32..z_bounds.1 as i32 + 1 {
|
||||
let rel_pos = Vec3::new(x, y, z);
|
||||
let wpos = Vec3::from(pos * TerrainChunk::RECT_SIZE.map(|e: u32| e as i32))
|
||||
+ rel_pos;
|
||||
|
||||
let block = if let Ok(block) = volume.get(wpos) {
|
||||
block
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
let sprite = if let Some(sprite) = block.get_sprite() {
|
||||
sprite
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if let Some(cfg) = sprite_config.get(sprite) {
|
||||
let seed = wpos.x as u64 * 3
|
||||
+ wpos.y as u64 * 7
|
||||
+ wpos.x as u64 * wpos.y as u64; // Awful PRNG
|
||||
let ori = (block.get_ori().unwrap_or((seed % 4) as u8 * 2)) & 0b111;
|
||||
let variation = seed as usize % cfg.variations.len();
|
||||
let key = (sprite, variation);
|
||||
// NOTE: Safe because we called sprite_config_for already.
|
||||
// NOTE: Safe because 0 ≤ ori < 8
|
||||
let light = light_map(wpos);
|
||||
let glow = glow_map(wpos);
|
||||
|
||||
for ((deep_level, shallow_level, surface_level), sprite_data) in
|
||||
instances.iter_mut().zip(&sprite_data[&key])
|
||||
{
|
||||
let mat = Mat4::identity()
|
||||
// Scaling for different LOD resolutions
|
||||
.scaled_3d(sprite_data.scale)
|
||||
// Offset
|
||||
.translated_3d(sprite_data.offset)
|
||||
.scaled_3d(SPRITE_SCALE)
|
||||
.rotated_z(f32::consts::PI * 0.25 * ori as f32)
|
||||
.translated_3d(
|
||||
rel_pos.map(|e| e as f32) + Vec3::new(0.5, 0.5, 0.0)
|
||||
);
|
||||
// Add an instance for each page in the sprite model
|
||||
for page in sprite_data.vert_pages.clone() {
|
||||
// TODO: could be more efficient to create once and clone while
|
||||
// modifying vert_page
|
||||
let instance = SpriteInstance::new(
|
||||
mat,
|
||||
cfg.wind_sway,
|
||||
sprite_data.scale.z,
|
||||
rel_pos,
|
||||
ori,
|
||||
light,
|
||||
glow,
|
||||
page,
|
||||
matches!(sprite, SpriteKind::Door | SpriteKind::DoorDark),
|
||||
);
|
||||
if (wpos.z as f32) < deep_alt {
|
||||
deep_level.push(instance);
|
||||
} else if wpos.z as f32 > underground_alt {
|
||||
surface_level.push(instance);
|
||||
} else {
|
||||
shallow_level.push(instance);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
get_sprite_instances(
|
||||
&mut instances,
|
||||
|(deep_level, shallow_level, surface_level), instance, wpos| {
|
||||
if (wpos.z as f32) < deep_alt {
|
||||
deep_level.push(instance);
|
||||
} else if wpos.z as f32 > underground_alt {
|
||||
surface_level.push(instance);
|
||||
} else {
|
||||
shallow_level.push(instance);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
(0..TerrainChunk::RECT_SIZE.x as i32)
|
||||
.flat_map(|x| {
|
||||
(0..TerrainChunk::RECT_SIZE.y as i32).flat_map(move |y| {
|
||||
(z_bounds.0 as i32..z_bounds.1 as i32)
|
||||
.map(move |z| Vec3::new(x, y, z).as_())
|
||||
})
|
||||
})
|
||||
.filter_map(|rel_pos| Some((rel_pos, *volume.get(to_wpos(rel_pos)).ok()?))),
|
||||
to_wpos,
|
||||
light_map,
|
||||
glow_map,
|
||||
sprite_data,
|
||||
sprite_config,
|
||||
);
|
||||
|
||||
instances.map(|(deep_level, shallow_level, surface_level)| {
|
||||
let deep_end = deep_level.len();
|
||||
@ -406,7 +437,7 @@ fn mesh_worker(
|
||||
}
|
||||
}
|
||||
|
||||
struct SpriteData {
|
||||
pub struct SpriteData {
|
||||
// Sprite vert page ranges that need to be drawn
|
||||
vert_pages: core::ops::Range<u32>,
|
||||
// Scale
|
||||
@ -433,7 +464,7 @@ pub struct Terrain<V: RectRasterableVol = TerrainChunk> {
|
||||
/// FIXME: This could possibly become an `AssetHandle<SpriteSpec>`, to get
|
||||
/// hot-reloading for free, but I am not sure if sudden changes of this
|
||||
/// value would break something
|
||||
sprite_config: Arc<SpriteSpec>,
|
||||
pub sprite_config: Arc<SpriteSpec>,
|
||||
chunks: HashMap<Vec2<i32>, TerrainChunkData>,
|
||||
/// Temporary storage for dead chunks that might still be shadowing chunks
|
||||
/// in view. We wait until either the chunk definitely cannot be
|
||||
@ -463,9 +494,9 @@ pub struct Terrain<V: RectRasterableVol = TerrainChunk> {
|
||||
|
||||
// GPU data
|
||||
// Maps sprite kind + variant to data detailing how to render it
|
||||
sprite_data: Arc<HashMap<(SpriteKind, usize), [SpriteData; SPRITE_LOD_LEVELS]>>,
|
||||
sprite_globals: SpriteGlobalsBindGroup,
|
||||
sprite_col_lights: Arc<ColLights<pipelines::sprite::Locals>>,
|
||||
pub sprite_data: Arc<HashMap<(SpriteKind, usize), [SpriteData; SPRITE_LOD_LEVELS]>>,
|
||||
pub sprite_globals: SpriteGlobalsBindGroup,
|
||||
pub sprite_col_lights: Arc<ColLights<pipelines::sprite::Locals>>,
|
||||
/// As stated previously, this is always the very latest texture into which
|
||||
/// we allocate. Code cannot assume that this is the assigned texture
|
||||
/// for any particular chunk; look at the `texture` field in
|
||||
@ -1207,12 +1238,7 @@ impl<V: RectRasterableVol> Terrain<V> {
|
||||
|
||||
let sprite_instances =
|
||||
response.sprite_instances.map(|(instances, alt_indices)| {
|
||||
(
|
||||
renderer
|
||||
.create_instances(&instances)
|
||||
.expect("Failed to upload chunk sprite instances to the GPU!"),
|
||||
alt_indices,
|
||||
)
|
||||
(renderer.create_instances(&instances), alt_indices)
|
||||
});
|
||||
|
||||
if let Some(mesh) = response.mesh {
|
||||
@ -1282,6 +1308,7 @@ impl<V: RectRasterableVol> Terrain<V> {
|
||||
e as f32 * sz as f32
|
||||
}),
|
||||
),
|
||||
Quaternion::identity(),
|
||||
atlas_offs,
|
||||
load_time,
|
||||
)]),
|
||||
@ -1694,15 +1721,16 @@ impl<V: RectRasterableVol> Terrain<V> {
|
||||
});
|
||||
}
|
||||
|
||||
pub fn render_translucent<'a>(
|
||||
pub fn render_sprites<'a>(
|
||||
&'a self,
|
||||
drawer: &mut FirstPassDrawer<'a>,
|
||||
sprite_drawer: &mut SpriteDrawer<'_, 'a>,
|
||||
focus_pos: Vec3<f32>,
|
||||
cam_pos: Vec3<f32>,
|
||||
sprite_render_distance: f32,
|
||||
culling_mode: CullingMode,
|
||||
) {
|
||||
span!(_guard, "render_translucent", "Terrain::render_translucent");
|
||||
span!(_guard, "render_sprites", "Terrain::render_sprites");
|
||||
|
||||
let focus_chunk = Vec2::from(focus_pos).map2(TerrainChunk::RECT_SIZE, |e: f32, sz| {
|
||||
(e as i32).div_euclid(sz as i32)
|
||||
});
|
||||
@ -1715,9 +1743,6 @@ impl<V: RectRasterableVol> Terrain<V> {
|
||||
})
|
||||
.take(self.chunks.len());
|
||||
|
||||
// Terrain sprites
|
||||
// TODO: move to separate functions
|
||||
span!(guard, "Terrain sprites");
|
||||
let chunk_size = V::RECT_SIZE.map(|e| e as f32);
|
||||
|
||||
let sprite_low_detail_distance = sprite_render_distance * 0.75;
|
||||
@ -1725,7 +1750,6 @@ impl<V: RectRasterableVol> Terrain<V> {
|
||||
let sprite_hid_detail_distance = sprite_render_distance * 0.35;
|
||||
let sprite_high_detail_distance = sprite_render_distance * 0.15;
|
||||
|
||||
let mut sprite_drawer = drawer.draw_sprites(&self.sprite_globals, &self.sprite_col_lights);
|
||||
chunk_iter
|
||||
.clone()
|
||||
.filter(|(_, _, c)| c.visible.is_visible())
|
||||
@ -1772,15 +1796,32 @@ impl<V: RectRasterableVol> Terrain<V> {
|
||||
);
|
||||
}
|
||||
});
|
||||
drop(sprite_drawer);
|
||||
drop(guard);
|
||||
}
|
||||
|
||||
pub fn render_translucent<'a>(
|
||||
&'a self,
|
||||
drawer: &mut FirstPassDrawer<'a>,
|
||||
focus_pos: Vec3<f32>,
|
||||
) {
|
||||
span!(_guard, "render_translucent", "Terrain::render_translucent");
|
||||
let focus_chunk = Vec2::from(focus_pos).map2(TerrainChunk::RECT_SIZE, |e: f32, sz| {
|
||||
(e as i32).div_euclid(sz as i32)
|
||||
});
|
||||
|
||||
// Avoid switching textures
|
||||
let chunk_iter = Spiral2d::new()
|
||||
.filter_map(|rpos| {
|
||||
let pos = focus_chunk + rpos;
|
||||
self.chunks.get(&pos).map(|c| (pos, c))
|
||||
})
|
||||
.take(self.chunks.len());
|
||||
|
||||
// Translucent
|
||||
span!(guard, "Fluid chunks");
|
||||
let mut fluid_drawer = drawer.draw_fluid();
|
||||
chunk_iter
|
||||
.filter(|(_, _, chunk)| chunk.visible.is_visible())
|
||||
.filter_map(|(_, _, chunk)| {
|
||||
.filter(|(_, chunk)| chunk.visible.is_visible())
|
||||
.filter_map(|(_, chunk)| {
|
||||
chunk
|
||||
.fluid_model
|
||||
.as_ref()
|
||||
|
@ -1,5 +1,5 @@
|
||||
use crate::hud::CraftingTab;
|
||||
use common::terrain::{BlockKind, SpriteKind, TerrainChunk};
|
||||
use common::terrain::{Block, BlockKind, SpriteKind};
|
||||
use common_base::span;
|
||||
use rand::prelude::*;
|
||||
use rand_chacha::ChaCha8Rng;
|
||||
@ -11,6 +11,7 @@ pub enum Interaction {
|
||||
/// twigs).
|
||||
Collect,
|
||||
Craft(CraftingTab),
|
||||
Mount,
|
||||
}
|
||||
|
||||
pub enum FireplaceType {
|
||||
@ -57,7 +58,12 @@ pub struct BlocksOfInterest {
|
||||
}
|
||||
|
||||
impl BlocksOfInterest {
|
||||
pub fn from_chunk(chunk: &TerrainChunk) -> Self {
|
||||
pub fn from_blocks(
|
||||
blocks: impl Iterator<Item = (Vec3<i32>, Block)>,
|
||||
river_speed_sq: f32,
|
||||
temperature: f32,
|
||||
humidity: f32,
|
||||
) -> Self {
|
||||
span!(_guard, "from_chunk", "BlocksOfInterest::from_chunk");
|
||||
let mut leaves = Vec::new();
|
||||
let mut drip = Vec::new();
|
||||
@ -84,9 +90,7 @@ impl BlocksOfInterest {
|
||||
|
||||
let mut rng = ChaCha8Rng::from_seed(thread_rng().gen());
|
||||
|
||||
let river_speed_sq = chunk.meta().river_velocity().magnitude_squared();
|
||||
|
||||
chunk.iter_changed().for_each(|(pos, block)| {
|
||||
blocks.for_each(|(pos, block)| {
|
||||
match block.kind() {
|
||||
BlockKind::Leaves if rng.gen_range(0..16) == 0 => leaves.push(pos),
|
||||
BlockKind::WeakRock if rng.gen_range(0..6) == 0 => drip.push(pos),
|
||||
@ -168,6 +172,7 @@ impl BlocksOfInterest {
|
||||
Some(SpriteKind::RepairBench) => {
|
||||
interactables.push((pos, Interaction::Craft(CraftingTab::All)))
|
||||
},
|
||||
_ if block.is_mountable() => interactables.push((pos, Interaction::Mount)),
|
||||
_ => {},
|
||||
},
|
||||
}
|
||||
@ -214,8 +219,8 @@ impl BlocksOfInterest {
|
||||
frogs,
|
||||
interactables,
|
||||
lights,
|
||||
temperature: chunk.meta().temp(),
|
||||
humidity: chunk.meta().humidity(),
|
||||
temperature,
|
||||
humidity,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
use ordered_float::OrderedFloat;
|
||||
use specs::{Join, WorldExt};
|
||||
use specs::{Join, ReadStorage, WorldExt};
|
||||
use vek::*;
|
||||
|
||||
use super::{
|
||||
@ -9,13 +9,15 @@ use super::{
|
||||
use client::Client;
|
||||
use common::{
|
||||
comp,
|
||||
comp::tool::ToolKind,
|
||||
consts::MAX_PICKUP_RANGE,
|
||||
comp::{ship::figuredata::VOXEL_COLLIDER_MANIFEST, tool::ToolKind, Collider},
|
||||
consts::{MAX_PICKUP_RANGE, MAX_SPRITE_MOUNT_RANGE},
|
||||
link::Is,
|
||||
mounting::Mount,
|
||||
mounting::{Mount, VolumePos, VolumeRider},
|
||||
terrain::{Block, TerrainGrid, UnlockKind},
|
||||
uid::{Uid, UidAllocator},
|
||||
util::find_dist::{Cube, Cylinder, FindDist},
|
||||
vol::ReadVol,
|
||||
CachedSpatialGrid,
|
||||
};
|
||||
use common_base::span;
|
||||
|
||||
@ -32,11 +34,12 @@ pub enum BlockInteraction {
|
||||
// TODO: mining blocks don't use the interaction key, so it might not be the best abstraction
|
||||
// to have them here, will see how things turn out
|
||||
Mine(ToolKind),
|
||||
Mount,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Interactable {
|
||||
Block(Block, Vec3<i32>, BlockInteraction),
|
||||
Block(Block, VolumePos, BlockInteraction),
|
||||
Entity(specs::Entity),
|
||||
}
|
||||
|
||||
@ -50,28 +53,34 @@ impl Interactable {
|
||||
|
||||
fn from_block_pos(
|
||||
terrain: &TerrainGrid,
|
||||
pos: Vec3<i32>,
|
||||
uid_allocator: &UidAllocator,
|
||||
colliders: &ReadStorage<Collider>,
|
||||
volume_pos: VolumePos,
|
||||
interaction: Interaction,
|
||||
) -> Option<Self> {
|
||||
let Ok(&block) = terrain.get(pos) else { return None };
|
||||
let Some(block) = volume_pos.get_block(terrain, uid_allocator, colliders) else { return None };
|
||||
let block_interaction = match interaction {
|
||||
Interaction::Collect => {
|
||||
// Check if this is an unlockable sprite
|
||||
let unlock = block.get_sprite().and_then(|sprite| {
|
||||
let Some(chunk) = terrain.pos_chunk(pos) else { return None };
|
||||
let sprite_chunk_pos = TerrainGrid::chunk_offs(pos);
|
||||
let sprite_cfg = chunk.meta().sprite_cfg_at(sprite_chunk_pos);
|
||||
let unlock_condition = sprite.unlock_condition(sprite_cfg.cloned());
|
||||
// HACK: No other way to distinguish between things that should be unlockable
|
||||
// and regular sprites with the current unlock_condition method so we hack
|
||||
// around that by saying that it is a regular collectible sprite if
|
||||
// `unlock_condition` returns UnlockKind::Free and the cfg was `None`.
|
||||
if sprite_cfg.is_some() || !matches!(&unlock_condition, UnlockKind::Free) {
|
||||
Some(unlock_condition)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
let unlock = match volume_pos.kind {
|
||||
common::mounting::Volume::Terrain => block.get_sprite().and_then(|sprite| {
|
||||
let Some(chunk) = terrain.pos_chunk(volume_pos.pos) else { return None };
|
||||
let sprite_chunk_pos = TerrainGrid::chunk_offs(volume_pos.pos);
|
||||
let sprite_cfg = chunk.meta().sprite_cfg_at(sprite_chunk_pos);
|
||||
let unlock_condition = sprite.unlock_condition(sprite_cfg.cloned());
|
||||
// HACK: No other way to distinguish between things that should be
|
||||
// unlockable and regular sprites with the current
|
||||
// unlock_condition method so we hack around that by
|
||||
// saying that it is a regular collectible sprite if
|
||||
// `unlock_condition` returns UnlockKind::Free and the cfg was `None`.
|
||||
if sprite_cfg.is_some() || !matches!(&unlock_condition, UnlockKind::Free) {
|
||||
Some(unlock_condition)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}),
|
||||
common::mounting::Volume::Entity(_) => None,
|
||||
};
|
||||
|
||||
if let Some(unlock) = unlock {
|
||||
BlockInteraction::Unlock(unlock)
|
||||
@ -82,8 +91,9 @@ impl Interactable {
|
||||
}
|
||||
},
|
||||
Interaction::Craft(tab) => BlockInteraction::Craft(tab),
|
||||
Interaction::Mount => BlockInteraction::Mount,
|
||||
};
|
||||
Some(Self::Block(block, pos, block_interaction))
|
||||
Some(Self::Block(block, volume_pos, block_interaction))
|
||||
}
|
||||
}
|
||||
|
||||
@ -127,7 +137,11 @@ pub(super) fn select_interactable(
|
||||
collect_target.and_then(|t| {
|
||||
if Some(t.distance) == nearest_dist {
|
||||
terrain.get(t.position_int()).ok().map(|&b| {
|
||||
Interactable::Block(b, t.position_int(), BlockInteraction::Collect)
|
||||
Interactable::Block(
|
||||
b,
|
||||
VolumePos::terrain(t.position_int()),
|
||||
BlockInteraction::Collect,
|
||||
)
|
||||
})
|
||||
} else {
|
||||
None
|
||||
@ -146,7 +160,7 @@ pub(super) fn select_interactable(
|
||||
if let Some(mine_tool) = b.mine_tool() && b.is_air() {
|
||||
Some(Interactable::Block(
|
||||
b,
|
||||
t.position_int(),
|
||||
VolumePos::terrain(t.position_int()),
|
||||
BlockInteraction::Mine(mine_tool),
|
||||
))
|
||||
} else {
|
||||
@ -176,15 +190,19 @@ pub(super) fn select_interactable(
|
||||
let items = ecs.read_storage::<comp::Item>();
|
||||
let stats = ecs.read_storage::<comp::Stats>();
|
||||
|
||||
let player_char_state = char_states.get(player_entity);
|
||||
let player_cylinder = Cylinder::from_components(
|
||||
player_pos,
|
||||
scales.get(player_entity).copied(),
|
||||
colliders.get(player_entity),
|
||||
char_states.get(player_entity),
|
||||
player_char_state,
|
||||
);
|
||||
|
||||
let closest_interactable_entity = (
|
||||
&ecs.entities(),
|
||||
let spacial_grid = ecs.read_resource::<CachedSpatialGrid>();
|
||||
|
||||
let entities = ecs.entities();
|
||||
let mut entity_data = (
|
||||
&entities,
|
||||
&positions,
|
||||
&bodies,
|
||||
scales.maybe(),
|
||||
@ -193,8 +211,11 @@ pub(super) fn select_interactable(
|
||||
!&is_mount,
|
||||
(stats.mask() | items.mask()).maybe(),
|
||||
)
|
||||
.join()
|
||||
.filter(|&(e, _, _, _, _, _, _, _)| e != player_entity) // skip the player's entity
|
||||
.join();
|
||||
|
||||
let closest_interactable_entity = spacial_grid.0.in_circle_aabr(player_pos.xy(), MAX_PICKUP_RANGE)
|
||||
.filter(|&e| e != player_entity) // skip the player's entity
|
||||
.filter_map(|e| entity_data.get(e, &entities))
|
||||
.filter_map(|(e, p, b, s, c, cs, _, has_stats_or_item)| {
|
||||
// Note, if this becomes expensive to compute do it after the distance check!
|
||||
//
|
||||
@ -228,6 +249,50 @@ pub(super) fn select_interactable(
|
||||
});
|
||||
let scene_terrain = scene.terrain();
|
||||
|
||||
let voxel_colliders_manifest = VOXEL_COLLIDER_MANIFEST.read();
|
||||
|
||||
let volumes_data = (
|
||||
&entities,
|
||||
&ecs.read_storage::<Uid>(),
|
||||
&ecs.read_storage::<comp::Body>(),
|
||||
&ecs.read_storage::<crate::ecs::comp::Interpolated>(),
|
||||
&ecs.read_storage::<comp::Collider>(),
|
||||
);
|
||||
|
||||
let mut volumes_data = volumes_data.join();
|
||||
|
||||
let volumes = spacial_grid.0.in_circle_aabr(player_pos.xy(), search_dist)
|
||||
.filter(|&e| e != player_entity) // skip the player's entity
|
||||
.filter_map(|e| volumes_data.get(e, &entities))
|
||||
.filter_map(|(entity, uid, body, interpolated, collider)| {
|
||||
let vol = collider.get_vol(&voxel_colliders_manifest)?;
|
||||
let (blocks_of_interest, offset) =
|
||||
scene
|
||||
.figure_mgr()
|
||||
.get_blocks_of_interest(entity, body, Some(collider))?;
|
||||
|
||||
let mat = Mat4::from(interpolated.ori.to_quat()).translated_3d(interpolated.pos)
|
||||
* Mat4::translation_3d(offset);
|
||||
|
||||
let p = mat.inverted().mul_point(player_pos);
|
||||
let aabb = Aabb {
|
||||
min: Vec3::zero(),
|
||||
max: vol.volume().sz.as_(),
|
||||
};
|
||||
if aabb.contains_point(p) || aabb.distance_to_point(p) < search_dist {
|
||||
Some(blocks_of_interest.interactables.iter().map(
|
||||
move |(block_offset, interaction)| {
|
||||
let wpos = mat.mul_point(block_offset.as_() + 0.5);
|
||||
(wpos, VolumePos::entity(*block_offset, *uid), interaction)
|
||||
},
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.flatten();
|
||||
|
||||
let is_volume_rider = ecs.read_storage::<Is<VolumeRider>>();
|
||||
// Find closest interactable block
|
||||
// TODO: consider doing this one first?
|
||||
let closest_interactable_block_pos = Spiral2d::new()
|
||||
@ -248,26 +313,37 @@ pub(super) fn select_interactable(
|
||||
.interactables
|
||||
.iter()
|
||||
.map(move |(block_offset, interaction)| (chunk_pos + block_offset, interaction))
|
||||
.map(|(pos, interaction)| {
|
||||
(pos.as_::<f32>() + 0.5, VolumePos::terrain(pos), interaction)
|
||||
})
|
||||
})
|
||||
.map(|(block_pos, interaction)| (
|
||||
block_pos,
|
||||
block_pos.map(|e| e as f32 + 0.5)
|
||||
.distance_squared(player_pos),
|
||||
interaction,
|
||||
))
|
||||
.min_by_key(|(_, dist_sqr, _)| OrderedFloat(*dist_sqr))
|
||||
.map(|(block_pos, _, interaction)| (block_pos, interaction));
|
||||
.chain(volumes)
|
||||
.filter(|(wpos, volume_pos, interaction)| {
|
||||
match interaction {
|
||||
Interaction::Mount => !is_volume_rider.contains(player_entity)
|
||||
&& wpos.distance_squared(player_pos) < MAX_SPRITE_MOUNT_RANGE * MAX_SPRITE_MOUNT_RANGE
|
||||
&& !is_volume_rider.join().any(|is_volume_rider| is_volume_rider.pos == *volume_pos),
|
||||
_ => true,
|
||||
}
|
||||
})
|
||||
.min_by_key(|(wpos, _, _)| OrderedFloat(wpos.distance_squared(player_pos)));
|
||||
|
||||
// Return the closest of the 2 closest
|
||||
closest_interactable_block_pos
|
||||
.filter(|(block_pos, _)| {
|
||||
.filter(|(wpos, _, _)| {
|
||||
player_cylinder.min_distance(Cube {
|
||||
min: block_pos.as_(),
|
||||
min: *wpos,
|
||||
side_length: 1.0,
|
||||
}) < search_dist
|
||||
})
|
||||
.and_then(|(block_pos, interaction)| {
|
||||
Interactable::from_block_pos(&terrain, block_pos, *interaction)
|
||||
.and_then(|(_, block_pos, interaction)| {
|
||||
Interactable::from_block_pos(
|
||||
&terrain,
|
||||
&ecs.read_resource::<UidAllocator>(),
|
||||
&ecs.read_storage(),
|
||||
block_pos,
|
||||
*interaction,
|
||||
)
|
||||
})
|
||||
.or_else(|| closest_interactable_entity.map(|(e, _)| Interactable::Entity(e)))
|
||||
}
|
||||
|
@ -25,7 +25,7 @@ use common::{
|
||||
consts::MAX_MOUNT_RANGE,
|
||||
event::UpdateCharacterMetadata,
|
||||
link::Is,
|
||||
mounting::Mount,
|
||||
mounting::{Mount, VolumePos},
|
||||
outcome::Outcome,
|
||||
recipe,
|
||||
terrain::{Block, BlockKind},
|
||||
@ -350,7 +350,8 @@ impl SessionState {
|
||||
match inv_event {
|
||||
InventoryUpdateEvent::BlockCollectFailed { pos, reason } => {
|
||||
self.hud.add_failed_block_pickup(
|
||||
pos,
|
||||
// TODO: Possibly support volumes.
|
||||
VolumePos::terrain(pos),
|
||||
HudCollectFailedReason::from_server_reason(
|
||||
&reason,
|
||||
client.state().ecs(),
|
||||
@ -888,6 +889,16 @@ impl PlayState for SessionState {
|
||||
if client.is_riding() {
|
||||
client.unmount();
|
||||
} else {
|
||||
if let Some(interactable) = &self.interactable {
|
||||
match interactable {
|
||||
Interactable::Block(_, pos, interaction) => {
|
||||
if matches!(interaction, BlockInteraction::Mount) {
|
||||
client.mount_volume(*pos)
|
||||
}
|
||||
},
|
||||
Interactable::Entity(entity) => client.mount(*entity),
|
||||
}
|
||||
}
|
||||
let player_pos = client
|
||||
.state()
|
||||
.read_storage::<Pos>()
|
||||
@ -921,15 +932,22 @@ impl PlayState for SessionState {
|
||||
},
|
||||
GameInput::Interact => {
|
||||
if state {
|
||||
let mut client = self.client.borrow_mut();
|
||||
if let Some(interactable) = &self.interactable {
|
||||
let mut client = self.client.borrow_mut();
|
||||
match interactable {
|
||||
Interactable::Block(block, pos, interaction) => {
|
||||
match interaction {
|
||||
BlockInteraction::Collect
|
||||
| BlockInteraction::Unlock(_) => {
|
||||
if block.is_collectible() {
|
||||
client.collect_block(*pos);
|
||||
match pos.kind {
|
||||
common::mounting::Volume::Terrain => {
|
||||
client.collect_block(pos.pos);
|
||||
}
|
||||
common::mounting::Volume::Entity(_) => {
|
||||
// TODO: Do we want to implement this?
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
BlockInteraction::Craft(tab) => {
|
||||
@ -938,7 +956,8 @@ impl PlayState for SessionState {
|
||||
block.get_sprite().map(|s| (*pos, s)),
|
||||
)
|
||||
},
|
||||
BlockInteraction::Mine(_) => {},
|
||||
BlockInteraction::Mine(_)
|
||||
| BlockInteraction::Mount => {},
|
||||
}
|
||||
},
|
||||
Interactable::Entity(entity) => {
|
||||
|
@ -1,7 +1,7 @@
|
||||
use common::{
|
||||
figure::Segment,
|
||||
util::{linear_to_srgba, srgb_to_linear},
|
||||
vol::{IntoFullVolIterator, ReadVol, SizedVol, Vox},
|
||||
vol::{FilledVox, IntoFullVolIterator, ReadVol, SizedVol},
|
||||
};
|
||||
use euc::{buffer::Buffer2d, rasterizer, Pipeline};
|
||||
use image::{DynamicImage, RgbaImage};
|
||||
@ -263,24 +263,24 @@ fn generate_mesh(segment: &Segment, offs: Vec3<f32>) -> Vec<Vert> {
|
||||
if let Some(col) = vox.get_color() {
|
||||
let col = col.map(|e| e as f32 / 255.0);
|
||||
|
||||
let is_empty = |pos| segment.get(pos).map(|v| v.is_empty()).unwrap_or(true);
|
||||
let is_filled = |pos| segment.get(pos).map(|v| v.is_filled()).unwrap_or(false);
|
||||
|
||||
let occluders = |unit_x, unit_y, dir| {
|
||||
// Would be nice to generate unit_x and unit_y from a given direction.
|
||||
[
|
||||
!is_empty(pos + dir - unit_x),
|
||||
!is_empty(pos + dir - unit_x - unit_y),
|
||||
!is_empty(pos + dir - unit_y),
|
||||
!is_empty(pos + dir + unit_x - unit_y),
|
||||
!is_empty(pos + dir + unit_x),
|
||||
!is_empty(pos + dir + unit_x + unit_y),
|
||||
!is_empty(pos + dir + unit_y),
|
||||
!is_empty(pos + dir - unit_x + unit_y),
|
||||
is_filled(pos + dir - unit_x),
|
||||
is_filled(pos + dir - unit_x - unit_y),
|
||||
is_filled(pos + dir - unit_y),
|
||||
is_filled(pos + dir + unit_x - unit_y),
|
||||
is_filled(pos + dir + unit_x),
|
||||
is_filled(pos + dir + unit_x + unit_y),
|
||||
is_filled(pos + dir + unit_y),
|
||||
is_filled(pos + dir - unit_x + unit_y),
|
||||
]
|
||||
};
|
||||
|
||||
// -x
|
||||
if is_empty(pos - Vec3::unit_x()) {
|
||||
if !is_filled(pos - Vec3::unit_x()) {
|
||||
vertices.extend_from_slice(&create_quad(
|
||||
offs + pos.map(|e| e as f32) + Vec3::unit_y(),
|
||||
-Vec3::unit_y(),
|
||||
@ -291,7 +291,7 @@ fn generate_mesh(segment: &Segment, offs: Vec3<f32>) -> Vec<Vert> {
|
||||
));
|
||||
}
|
||||
// +x
|
||||
if is_empty(pos + Vec3::unit_x()) {
|
||||
if !is_filled(pos + Vec3::unit_x()) {
|
||||
vertices.extend_from_slice(&create_quad(
|
||||
offs + pos.map(|e| e as f32) + Vec3::unit_x(),
|
||||
Vec3::unit_y(),
|
||||
@ -302,7 +302,7 @@ fn generate_mesh(segment: &Segment, offs: Vec3<f32>) -> Vec<Vert> {
|
||||
));
|
||||
}
|
||||
// -y
|
||||
if is_empty(pos - Vec3::unit_y()) {
|
||||
if !is_filled(pos - Vec3::unit_y()) {
|
||||
vertices.extend_from_slice(&create_quad(
|
||||
offs + pos.map(|e| e as f32),
|
||||
Vec3::unit_x(),
|
||||
@ -313,7 +313,7 @@ fn generate_mesh(segment: &Segment, offs: Vec3<f32>) -> Vec<Vert> {
|
||||
));
|
||||
}
|
||||
// +y
|
||||
if is_empty(pos + Vec3::unit_y()) {
|
||||
if !is_filled(pos + Vec3::unit_y()) {
|
||||
vertices.extend_from_slice(&create_quad(
|
||||
offs + pos.map(|e| e as f32) + Vec3::unit_y(),
|
||||
Vec3::unit_z(),
|
||||
@ -324,7 +324,7 @@ fn generate_mesh(segment: &Segment, offs: Vec3<f32>) -> Vec<Vert> {
|
||||
));
|
||||
}
|
||||
// -z
|
||||
if is_empty(pos - Vec3::unit_z()) {
|
||||
if !is_filled(pos - Vec3::unit_z()) {
|
||||
vertices.extend_from_slice(&create_quad(
|
||||
offs + pos.map(|e| e as f32),
|
||||
Vec3::unit_y(),
|
||||
@ -335,7 +335,7 @@ fn generate_mesh(segment: &Segment, offs: Vec3<f32>) -> Vec<Vert> {
|
||||
));
|
||||
}
|
||||
// +z
|
||||
if is_empty(pos + Vec3::unit_z()) {
|
||||
if !is_filled(pos + Vec3::unit_z()) {
|
||||
vertices.extend_from_slice(&create_quad(
|
||||
offs + pos.map(|e| e as f32) + Vec3::unit_z(),
|
||||
Vec3::unit_x(),
|
||||
|
Loading…
Reference in New Issue
Block a user