diff --git a/assets/voxygen/shaders/particle-vert.glsl b/assets/voxygen/shaders/particle-vert.glsl index 5bb05aa7b3..370945eca2 100644 --- a/assets/voxygen/shaders/particle-vert.glsl +++ b/assets/voxygen/shaders/particle-vert.glsl @@ -81,6 +81,7 @@ const int BARRELORGAN = 40; const int POTION_SICKNESS = 41; const int GIGA_SNOW = 42; const int CYCLOPS_CHARGE = 43; +const int PORTAL_FIZZ = 45; // meters per second squared (acceleration) const float earth_gravity = 9.807; @@ -677,6 +678,19 @@ void main() { spin_in_axis(vec3(rand6, rand7, rand8), percent() * 10 + 3 * rand9) ); break; + case PORTAL_FIZZ: + attr = Attr( + inst_dir * (0.7 + pow(percent(), 5)) + vec3( + sin(lifetime * 1.25 + rand0 * 10) + sin(lifetime * 1.3 + rand3 * 10), + sin(lifetime * 1.2 + rand1 * 10) + sin(lifetime * 1.4 + rand4 * 10), + sin(lifetime * 5 + rand2) + ) * 0.03, + vec3(pow(1.0 - abs(percent() - 0.5) * 2.0, 0.2)), + vec4(mix(vec3(0.4, 0.2, 0.8), vec3(5, 2, 10), pow(percent(), 2)), 1), + /* vec4(vec3(1.8 - percent() * 2, 0.4 + percent() * 2, 5.0 + rand6), 1), */ + spin_in_axis(vec3(rand6, rand7, rand8), rand9 * 3 + lifetime * 5) + ); + break; default: attr = Attr( linear_motion( diff --git a/assets/voxygen/voxel/sprite_manifest.ron b/assets/voxygen/voxel/sprite_manifest.ron index 25cf9057d9..aa96094c80 100644 --- a/assets/voxygen/voxel/sprite_manifest.ron +++ b/assets/voxygen/voxel/sprite_manifest.ron @@ -4416,4 +4416,8 @@ GlowIceCrystal: Some(( ], wind_sway: 0.0, )), +OneWayWall: Some(( + variations: [], + wind_sway: 0.0, +)), } diff --git a/common/src/terrain/block.rs b/common/src/terrain/block.rs index 25970fa28c..c9c0d55b63 100644 --- a/common/src/terrain/block.rs +++ b/common/src/terrain/block.rs @@ -45,6 +45,8 @@ make_case_elim!( // 0x12 <= x < 0x20 is reserved for future rocks Grass = 0x20, // Note: *not* the same as grass sprites Snow = 0x21, + // Snow to use with sites, to not attract snowfall particles + ArtSnow = 0x22, // 0x21 <= x < 0x30 is reserved for future grasses Earth = 0x30, Sand = 0x31, @@ -58,8 +60,6 @@ make_case_elim!( // often want to experiment with new kinds of block without allocating them a // dedicated block kind. Misc = 0xFE, - // Snow to use with sites, to not attract snowfall particles - ArtSnow = 0xFF, } ); @@ -368,6 +368,17 @@ impl Block { .unwrap_or(!matches!(self.kind, BlockKind::Lava)) } + pub fn valid_collision_dir( + &self, + entity_aabb: Aabb, + block_aabb: Aabb, + move_dir: Vec3, + ) -> bool { + self.get_sprite().map_or(true, |sprite| { + sprite.valid_collision_dir(entity_aabb, block_aabb, move_dir, self) + }) + } + /// Can this block be exploded? If so, what 'power' is required to do so? /// Note that we don't really define what 'power' is. Consider the units /// arbitrary and only important when compared to one-another. @@ -387,7 +398,8 @@ impl Block { SpriteKind::Keyhole | SpriteKind::KeyDoor | SpriteKind::BoneKeyhole - | SpriteKind::BoneKeyDoor => None, + | SpriteKind::BoneKeyDoor + | SpriteKind::OneWayWall => None, SpriteKind::Anvil | SpriteKind::Cauldron | SpriteKind::CookingPot diff --git a/common/src/terrain/sprite.rs b/common/src/terrain/sprite.rs index c387eb0b79..de3d9d472f 100644 --- a/common/src/terrain/sprite.rs +++ b/common/src/terrain/sprite.rs @@ -5,6 +5,7 @@ use crate::{ }, lottery::LootSpec, make_case_elim, + terrain::Block, }; use hashbrown::HashMap; use lazy_static::lazy_static; @@ -12,7 +13,7 @@ use num_derive::FromPrimitive; use serde::{Deserialize, Serialize}; use std::{convert::TryFrom, fmt}; use strum::EnumIter; -use vek::Vec3; +use vek::*; make_case_elim!( sprite_kind, @@ -250,6 +251,7 @@ make_case_elim!( FireBlock = 0xDF, IceCrystal = 0xE0, GlowIceCrystal = 0xE1, + OneWayWall = 0xE2, } ); @@ -343,7 +345,8 @@ impl SpriteKind { | SpriteKind::KeyDoor | SpriteKind::BoneKeyhole | SpriteKind::BoneKeyDoor - | SpriteKind::Bomb => 1.0, + | SpriteKind::Bomb + | SpriteKind::OneWayWall => 1.0, // TODO: Figure out if this should be solid or not. SpriteKind::Shelf => 1.0, SpriteKind::Lantern => 0.9, @@ -390,6 +393,44 @@ impl SpriteKind { }) } + pub fn valid_collision_dir( + &self, + entity_aabb: Aabb, + block_aabb: Aabb, + move_dir: Vec3, + parent: &Block, + ) -> bool { + match self { + SpriteKind::OneWayWall => { + // Find the intrusion vector of the collision + let dir = entity_aabb.collision_vector_with_aabb(block_aabb); + + // Determine an appropriate resolution vector (i.e: the minimum distance + // needed to push out of the block) + let max_axis = dir.map(|e| e.abs()).reduce_partial_min(); + let resolve_dir = -dir.map(|e| { + if e.abs().to_bits() == max_axis.to_bits() { + e.signum() + } else { + 0.0 + } + }); + + let is_moving_into = move_dir.dot(resolve_dir) <= 0.0; + + is_moving_into + && parent.get_ori().map_or(false, |ori| { + Vec2::unit_y() + .rotated_z(std::f32::consts::PI * 0.25 * ori as f32) + .with_z(0.0) + .map2(resolve_dir, |e, r| (e - r).abs() < 0.1) + .reduce_and() + }) + }, + _ => true, + } + } + /// What loot table does collecting this sprite draw from? /// None = block cannot be collected /// Some(None) = block can be collected, but does not give back an item @@ -661,7 +702,8 @@ impl SpriteKind { | SpriteKind::BoneKeyhole | SpriteKind::BoneKeyDoor | SpriteKind::IceCrystal - | SpriteKind::GlowIceCrystal, + | SpriteKind::GlowIceCrystal + | SpriteKind::OneWayWall, ) } } diff --git a/common/systems/src/phys.rs b/common/systems/src/phys.rs index 4e13468bdf..7a1fc28b4c 100644 --- a/common/systems/src/phys.rs +++ b/common/systems/src/phys.rs @@ -1389,10 +1389,10 @@ fn box_voxel_collision + ReadVol>( fn collision_with + ReadVol>( pos: Vec3, terrain: &T, - hit: impl Fn(&Block) -> bool, near_aabb: Aabb, radius: f32, z_range: Range, + move_dir: Vec3, ) -> bool { let player_aabb = player_aabb(pos, radius, z_range); @@ -1402,12 +1402,14 @@ fn box_voxel_collision + ReadVol>( let mut collision = false; // TODO: could short-circuit here terrain.for_each_in(near_aabb, |block_pos, block| { - if block.is_solid() && hit(&block) { + if block.is_solid() { let block_aabb = Aabb { min: block_pos.map(|e| e as f32), max: block_pos.map(|e| e as f32) + Vec3::new(1.0, 1.0, block.solid_height()), }; - if player_aabb.collides_with_aabb(block_aabb) { + if player_aabb.collides_with_aabb(block_aabb) + && block.valid_collision_dir(player_aabb, block_aabb, move_dir) + { collision = true; } } @@ -1416,10 +1418,6 @@ fn box_voxel_collision + ReadVol>( collision } - // Should be easy to just make clippy happy if we want? - #[allow(clippy::trivially_copy_pass_by_ref)] - fn always_hits(_: &Block) -> bool { true } - let (radius, z_min, z_max) = (Vec3::from(cylinder) * scale).into_tuple(); // Probe distances @@ -1459,6 +1457,7 @@ fn box_voxel_collision + ReadVol>( const MAX_ATTEMPTS: usize = 16; pos.0 += pos_delta / increments as f32; + let vel2 = *vel; let try_colliding_block = |pos: &Pos| { //prof_span!("most colliding check"); // Calculate the player's AABB @@ -1487,16 +1486,14 @@ fn box_voxel_collision + ReadVol>( }; // Determine whether the block's AABB collides with the player's AABB - if block_aabb.collides_with_aabb(player_aabb) { + if player_aabb.collides_with_aabb(block_aabb) + && block.valid_collision_dir(player_aabb, block_aabb, vel2.0) + { match &most_colliding { // Select the minimum of the value from `player_overlap` Some((_, other_block_aabb, _)) - if { - // TODO: comment below is outdated (as of ~1 year ago) - // Find the maximum of the minimum collision axes (this bit - // is weird, trust me that it works) - player_overlap(block_aabb) >= player_overlap(*other_block_aabb) - } => {}, + if player_overlap(block_aabb) + >= player_overlap(*other_block_aabb) => {}, _ => most_colliding = Some((block_pos, block_aabb, block)), } } @@ -1554,10 +1551,10 @@ fn box_voxel_collision + ReadVol>( !collision_with( Vec3::new(pos.0.x, pos.0.y, (pos.0.z + 0.1).ceil()), &terrain, - always_hits, near_aabb, radius, z_range.clone(), + vel.0, ) } // ...and there is a collision with a block beneath our current hitbox... @@ -1566,10 +1563,10 @@ fn box_voxel_collision + ReadVol>( collision_with( pos.0 + resolve_dir - Vec3::unit_z() * 1.25, &terrain, - always_hits, near_aabb, radius, z_range.clone(), + vel.0, ) } { // ...block-hop! @@ -1620,10 +1617,10 @@ fn box_voxel_collision + ReadVol>( collision_with( pos.0 - Vec3::unit_z() * 1.1, &terrain, - always_hits, near_aabb, radius, z_range.clone(), + vel.0, ) } { //prof_span!("snap!!"); @@ -1694,7 +1691,9 @@ fn box_voxel_collision + ReadVol>( }; for dir in 0..4 { - if player_wall_aabbs[dir].collides_with_aabb(block_aabb) { + if player_wall_aabbs[dir].collides_with_aabb(block_aabb) + && block.valid_collision_dir(player_wall_aabbs[dir], block_aabb, vel.0) + { wall_dir_collisions[dir] = true; } } diff --git a/voxygen/src/render/pipelines/particle.rs b/voxygen/src/render/pipelines/particle.rs index cb0accb69d..6b14e0e9e0 100644 --- a/voxygen/src/render/pipelines/particle.rs +++ b/voxygen/src/render/pipelines/particle.rs @@ -96,6 +96,7 @@ pub enum ParticleMode { GigaSnow = 42, CyclopsCharge = 43, SnowStorm = 44, + PortalFizz = 45, } impl ParticleMode { diff --git a/voxygen/src/scene/particle.rs b/voxygen/src/scene/particle.rs index 8a28fa0c79..dad9b80c5b 100644 --- a/voxygen/src/scene/particle.rs +++ b/voxygen/src/scene/particle.rs @@ -1456,7 +1456,7 @@ impl ParticleMgr { struct BlockParticles<'a> { // The function to select the blocks of interest that we should emit from - blocks: fn(&'a BlocksOfInterest) -> &'a [Vec3], + blocks: fn(&'a BlocksOfInterest) -> BlockParticleSlice<'a>, // The range, in chunks, that the particles should be generated in from the player range: usize, // The emission rate, per block per second, of the generated particles @@ -1469,9 +1469,23 @@ impl ParticleMgr { cond: fn(&SceneData) -> bool, } + enum BlockParticleSlice<'a> { + Positions(&'a [Vec3]), + PositionsAndDirs(&'a [(Vec3, Vec3)]), + } + + impl<'a> BlockParticleSlice<'a> { + fn len(&self) -> usize { + match self { + Self::Positions(blocks) => blocks.len(), + Self::PositionsAndDirs(blocks) => blocks.len(), + } + } + } + let particles: &[BlockParticles] = &[ BlockParticles { - blocks: |boi| &boi.leaves, + blocks: |boi| BlockParticleSlice::Positions(&boi.leaves), range: 4, rate: 0.001, lifetime: 30.0, @@ -1479,7 +1493,7 @@ impl ParticleMgr { cond: |_| true, }, BlockParticles { - blocks: |boi| &boi.drip, + blocks: |boi| BlockParticleSlice::Positions(&boi.drip), range: 4, rate: 0.004, lifetime: 20.0, @@ -1487,7 +1501,7 @@ impl ParticleMgr { cond: |_| true, }, BlockParticles { - blocks: |boi| &boi.fires, + blocks: |boi| BlockParticleSlice::Positions(&boi.fires), range: 2, rate: 20.0, lifetime: 0.25, @@ -1495,7 +1509,7 @@ impl ParticleMgr { cond: |_| true, }, BlockParticles { - blocks: |boi| &boi.fire_bowls, + blocks: |boi| BlockParticleSlice::Positions(&boi.fire_bowls), range: 2, rate: 20.0, lifetime: 0.25, @@ -1503,7 +1517,7 @@ impl ParticleMgr { cond: |_| true, }, BlockParticles { - blocks: |boi| &boi.fireflies, + blocks: |boi| BlockParticleSlice::Positions(&boi.fireflies), range: 6, rate: 0.004, lifetime: 40.0, @@ -1511,7 +1525,7 @@ impl ParticleMgr { cond: |sd| sd.state.get_day_period().is_dark(), }, BlockParticles { - blocks: |boi| &boi.flowers, + blocks: |boi| BlockParticleSlice::Positions(&boi.flowers), range: 5, rate: 0.002, lifetime: 40.0, @@ -1519,7 +1533,7 @@ impl ParticleMgr { cond: |sd| sd.state.get_day_period().is_dark(), }, BlockParticles { - blocks: |boi| &boi.beehives, + blocks: |boi| BlockParticleSlice::Positions(&boi.beehives), range: 3, rate: 0.5, lifetime: 30.0, @@ -1527,13 +1541,21 @@ impl ParticleMgr { cond: |sd| sd.state.get_day_period().is_light(), }, BlockParticles { - blocks: |boi| &boi.snow, + blocks: |boi| BlockParticleSlice::Positions(&boi.snow), range: 4, rate: 0.025, lifetime: 15.0, mode: ParticleMode::Snow, cond: |_| true, }, + BlockParticles { + blocks: |boi| BlockParticleSlice::PositionsAndDirs(&boi.one_way_walls), + range: 2, + rate: 12.0, + lifetime: 1.5, + mode: ParticleMode::PortalFizz, + cond: |_| true, + }, ]; let ecs = scene_data.state.ecs(); @@ -1555,16 +1577,37 @@ impl ParticleMgr { self.particles .resize_with(self.particles.len() + particle_count, || { - let block_pos = - Vec3::from(chunk_pos * TerrainChunk::RECT_SIZE.map(|e| e as i32)) - + blocks.choose(&mut rng).copied().unwrap(); // Can't fail - - Particle::new( - Duration::from_secs_f32(particles.lifetime), - time, - particles.mode, - block_pos.map(|e: i32| e as f32 + rng.gen::()), - ) + match blocks { + BlockParticleSlice::Positions(blocks) => { + // Can't fail, resize only occurs if blocks > 0 + let block_pos = Vec3::from( + chunk_pos * TerrainChunk::RECT_SIZE.map(|e| e as i32), + ) + blocks.choose(&mut rng).copied().unwrap(); + Particle::new( + Duration::from_secs_f32(particles.lifetime), + time, + particles.mode, + block_pos.map(|e: i32| e as f32 + rng.gen::()), + ) + }, + BlockParticleSlice::PositionsAndDirs(blocks) => { + // Can't fail, resize only occurs if blocks > 0 + let (block_offset, particle_dir) = + blocks.choose(&mut rng).copied().unwrap(); + let block_pos = Vec3::from( + chunk_pos * TerrainChunk::RECT_SIZE.map(|e| e as i32), + ) + block_offset; + let particle_pos = + block_pos.map(|e: i32| e as f32 + rng.gen::()); + Particle::new_directed( + Duration::from_secs_f32(particles.lifetime), + time, + particles.mode, + particle_pos, + particle_pos + particle_dir, + ) + }, + } }) }); } @@ -1592,19 +1635,39 @@ impl ParticleMgr { 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::()); // Can't fail - let wpos = mat.mul_point(rel_pos); + match blocks { + BlockParticleSlice::Positions(blocks) => { + let rel_pos = blocks + .choose(&mut rng) + .copied() + // Can't fail, resize only occurs if blocks > 0 + .unwrap() + .map(|e: i32| e as f32 + rng.gen::()); + let wpos = mat.mul_point(rel_pos); - Particle::new( - Duration::from_secs_f32(particles.lifetime), - time, - particles.mode, - wpos, - ) + Particle::new( + Duration::from_secs_f32(particles.lifetime), + time, + particles.mode, + wpos, + ) + }, + BlockParticleSlice::PositionsAndDirs(blocks) => { + // Can't fail, resize only occurs if blocks > 0 + let (block_offset, particle_dir) = + blocks.choose(&mut rng).copied().unwrap(); + let particle_pos = + block_offset.map(|e: i32| e as f32 + rng.gen::()); + let wpos = mat.mul_point(particle_pos); + Particle::new_directed( + Duration::from_secs_f32(particles.lifetime), + time, + particles.mode, + wpos, + wpos + mat.mul_direction(particle_dir), + ) + }, + } }) } } diff --git a/voxygen/src/scene/terrain.rs b/voxygen/src/scene/terrain.rs index 65eba8986e..155d48738e 100644 --- a/voxygen/src/scene/terrain.rs +++ b/voxygen/src/scene/terrain.rs @@ -270,41 +270,43 @@ pub fn get_sprite_instances<'a, I: 'a>( .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); + if !cfg.variations.is_empty() { + 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); + // 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); + 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); + } } } } diff --git a/voxygen/src/scene/terrain/watcher.rs b/voxygen/src/scene/terrain/watcher.rs index 659ffade8b..15471d02de 100644 --- a/voxygen/src/scene/terrain/watcher.rs +++ b/voxygen/src/scene/terrain/watcher.rs @@ -48,6 +48,7 @@ pub struct BlocksOfInterest { pub cricket2: Vec>, pub cricket3: Vec>, pub frogs: Vec>, + pub one_way_walls: Vec<(Vec3, Vec3)>, // Note: these are only needed for chunks within the iteraction range so this is a potential // area for optimization pub interactables: Vec<(Vec3, Interaction)>, @@ -87,6 +88,7 @@ impl BlocksOfInterest { let mut cricket2 = Vec::new(); let mut cricket3 = Vec::new(); let mut frogs = Vec::new(); + let mut one_way_walls = Vec::new(); let mut rng = ChaCha8Rng::from_seed(thread_rng().gen()); @@ -172,6 +174,14 @@ impl BlocksOfInterest { Some(SpriteKind::RepairBench) => { interactables.push((pos, Interaction::Craft(CraftingTab::All))) }, + Some(SpriteKind::OneWayWall) => one_way_walls.push(( + pos, + Vec2::unit_y() + .rotated_z( + std::f32::consts::PI * 0.25 * block.get_ori().unwrap_or(0) as f32, + ) + .with_z(0.0), + )), _ if block.is_mountable() => interactables.push((pos, Interaction::Mount)), _ => {}, }, @@ -217,6 +227,7 @@ impl BlocksOfInterest { cricket2, cricket3, frogs, + one_way_walls, interactables, lights, temperature,