//! [`BlockAttributes`] and closely related types.
use std::borrow::Cow;
use std::fmt;
use crate::drawing::VoxelBrush;
use crate::math::{Face6, Rgb};
#[cfg(doc)]
use crate::{
block::{Block, BlockDef},
space::Space,
};
/// Collection of miscellaneous attribute data for blocks that doesn't come in variants.
///
/// `BlockAttributes::default()` will produce a reasonable set of defaults for “ordinary”
/// blocks.
#[derive(Clone, Eq, Hash, PartialEq)]
#[allow(clippy::exhaustive_structs)] // TODO: Make this non_exhaustive but give users a way to construct it easily, possibly via BlockBuilder.
pub struct BlockAttributes {
/// The name that should be displayed to players.
///
/// The default value is the empty string. The empty string should be considered a
/// reasonable choice for solid-color blocks with no special features.
pub display_name: Cow<'static, str>,
/// Whether players' cursors target it or pass through it.
///
/// The default value is `true`.
pub selectable: bool,
/// The effect on a [`Body`](crate::physics::Body) of colliding with this block.
///
/// The default value is [`BlockCollision::Hard`].
pub collision: BlockCollision,
/// Rule about how this block should be rotated, or not, when placed in a [`Space`] by
/// some agent not otherwise specifying rotation.
///
/// The default value is [`RotationPlacementRule::Never`].
pub rotation_rule: RotationPlacementRule,
/// Light emitted by the block.
///
/// The default value is [`Rgb::ZERO`].
pub light_emission: Rgb,
/// Something this block does when time passes.
///
/// Currently the only possibility is “turn into another block”.
///
/// TODO: Very placeholder. This needs more possible effects and also time/probability options.
pub tick_action: Option<VoxelBrush<'static>>,
/// Advice to the renderer about how to expect this block to change, and hence
/// what rendering strategy to use.
pub animation_hint: AnimationHint,
//
// Reminder: When adding new fields, add them to the Debug implementation
// and BlockBuilder.
//
// TODO: add 'behavior' functionality, if we don't come up with something else
}
impl fmt::Debug for BlockAttributes {
/// Only attributes which differ from the default are shown.
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self == &Self::default() {
// Avoid the braceless formatting used for structs with literally no fields.
write!(f, "BlockAttributes {{}}")
} else {
let mut s = f.debug_struct("BlockAttributes");
if self.display_name != Self::default().display_name {
// Unwrap the `Cow` for tidier formatting.
s.field("display_name", &&*self.display_name);
}
if self.selectable != Self::default().selectable {
s.field("selectable", &self.selectable);
}
if self.collision != Self::default().collision {
s.field("collision", &self.collision);
}
if self.rotation_rule != Self::default().rotation_rule {
s.field("rotation_rule", &self.rotation_rule);
}
if self.light_emission != Self::default().light_emission {
s.field("light_emission", &self.light_emission);
}
if self.tick_action != Self::default().tick_action {
s.field("tick_action", &self.tick_action);
}
if self.animation_hint != Self::default().animation_hint {
s.field("animation_hint", &self.animation_hint);
}
s.finish()
}
}
}
impl BlockAttributes {
/// Block attributes suitable as default values for in-game use.
///
/// This function differs from the [`Default::default`] trait implementation only
/// in that it is a `const fn`.
pub const fn default() -> BlockAttributes {
BlockAttributes {
display_name: Cow::Borrowed(""),
selectable: true,
collision: BlockCollision::Hard,
rotation_rule: RotationPlacementRule::Never,
light_emission: Rgb::ZERO,
tick_action: None,
animation_hint: AnimationHint::UNCHANGING,
}
}
}
impl Default for BlockAttributes {
/// Block attributes suitable as default values for in-game use.
fn default() -> BlockAttributes {
// Delegate to the inherent impl `const fn`.
BlockAttributes::default()
}
}
#[cfg(feature = "arbitrary")]
impl<'a> arbitrary::Arbitrary<'a> for BlockAttributes {
fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
Ok(BlockAttributes {
display_name: Cow::Owned(u.arbitrary()?),
selectable: u.arbitrary()?,
collision: u.arbitrary()?,
rotation_rule: u.arbitrary()?,
light_emission: u.arbitrary()?,
tick_action: None, // TODO: need Arbitrary for Block
animation_hint: u.arbitrary()?,
})
}
fn size_hint(depth: usize) -> (usize, Option<usize>) {
arbitrary::size_hint::and_all(&[
String::size_hint(depth),
bool::size_hint(depth),
BlockCollision::size_hint(depth),
RotationPlacementRule::size_hint(depth),
Rgb::size_hint(depth),
AnimationHint::size_hint(depth),
])
}
}
/// Specifies the effect on a [`Body`](crate::physics::Body) of colliding with the
/// [`Block`] this applies to.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[non_exhaustive]
pub enum BlockCollision {
/// The block can be passed through; it is not an obstacle (though intersecting it
/// might cause other effects not directly part of collision response).
None,
/// The block is a perfectly solid obstacle occupying its entire bounding cube.
///
/// This is the default value used for most blocks. (Caveat: The default might be
/// changed to `Recur` if that proves more ergonomic.)
Hard,
/// Collide with the block's component voxels individually.
///
/// If the block does not have voxels then this is equivalent to [`Hard`](Self::Hard).
Recur,
// Future values might include bouncy solid, water-like resistance, force fields, etc.
}
/// Rule about how this block should be rotated, or not, when placed in a [`Space`] by
/// some agent not otherwise specifying rotation.
///
/// TODO: We may want to replace this with a struct that also carries declared symmetries
/// ("this is a vertical pillar so never make it upside down") and/or prohibited rotations
/// rather than requiring each individual rule variant to be sufficiently expressive.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[non_exhaustive]
pub enum RotationPlacementRule {
/// Never rotate the block.
Never,
/// Rotate the block so that the specified face meets the face it was placed against.
Attach {
/// This face of the placed block will meet the face it was placed against.
///
/// If the block was somehow placed without such an adjacent block, it will not be
/// rotated.
by: Face6,
// TODO: control rotation about additional axis
},
}
/// Specifies how a [`Block`] might change in the very near future, for the benefit
/// of rendering algorithms. Does not currently describe non-visual aspects of a block.
///
/// This should be configured for blocks which either are continuously animated in some
/// fashion, or for which it is especially important that the specified changes are handled
/// efficiently, at the possible cost of spending more resources on those blocks. Blocks
/// which merely might change in response to user action should not set this hint.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[non_exhaustive]
pub struct AnimationHint {
/// Ways in which the block's definition might change (via modification to a
/// [`BlockDef`] or recursive [`Space`]) such that many instances of this block
/// will become another simultaneously.
pub redefinition: AnimationChange,
/// If this block is likely to be replaced in a [`Space`] by another, this field
/// specifies the replacement's relation to this.
pub replacement: AnimationChange,
}
impl AnimationHint {
// TODO: get rid of these constants or replace them with a clearer new system
/// There are no expectations that the block is soon going to change.
///
/// This is the default value of this type and within [`BlockAttributes`].
pub const UNCHANGING: Self = Self {
redefinition: AnimationChange::None,
replacement: AnimationChange::None,
};
/// The block is not going to exist in its current form for long.
///
/// This suggests using a rendering technique which is comparatively expensive
/// per-block but allows it (and any successors that are also `TEMPORARY`) to be added
/// and removed cheaply.
pub const TEMPORARY: Self = Self {
redefinition: AnimationChange::None,
replacement: AnimationChange::Shape,
};
/// The block's appearance is expected to change very frequently, but not by replacing
/// the block in its [`Space`].
///
/// This suggests using a rendering technique which optimizes for not needing to e.g.
/// rebuild chunk meshes.
pub const CONTINUOUS: Self = Self {
redefinition: AnimationChange::Shape,
replacement: AnimationChange::None,
..Self::UNCHANGING
};
/// Returns whether this block's value for [`EvaluatedBlock::visible`] is likely to
/// change from `false` to `true` for animation reasons.
pub(crate) fn might_become_visible(&self) -> bool {
self.redefinition.might_become_visible() || self.replacement.might_become_visible()
}
}
impl Default for AnimationHint {
fn default() -> Self {
Self::UNCHANGING
}
}
/// Component of [`AnimationHint`], describing the type of change predicted.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[non_exhaustive]
pub enum AnimationChange {
/// Expect no changes.
None,
/// Expect that the block’s voxels’ colors will change, while remaining within the
/// same [`OpacityCategory`](crate::math::OpacityCategory); that is, the alpha will
/// remain 0, remain 1, or remain neither 0 nor 1.
///
/// Suggestion to renderers: prepare to update texturing without recomputing an
/// otherwise identical mesh.
ColorSameCategory,
/// Expect that the block’s colors and shape will change; that is, at least some
/// voxels’ alpha will move from one [`OpacityCategory`](crate::math::OpacityCategory)
/// to another.
///
/// Suggestion to renderers: use a rendering strategy which is shape-independent, or
/// prepare to efficiently recompute the mesh (don't merge with neighbors).
Shape,
}
impl AnimationChange {
/// Helper for [`AnimationHint::might_become_visible`].
fn might_become_visible(&self) -> bool {
match self {
AnimationChange::None => false,
// same category implies not becoming visible if invisible
AnimationChange::ColorSameCategory => false,
AnimationChange::Shape => true,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
/// [`BlockAttributes`] has an inherent `default()` function, which should be
/// equivalent to the [`Default`] trait function.
#[test]
fn default_equivalent() {
assert_eq!(
BlockAttributes::default(),
<BlockAttributes as Default>::default()
);
}
#[test]
fn debug() {
let default = BlockAttributes::default;
fn debug(a: BlockAttributes) -> String {
format!("{a:?}")
}
assert_eq!(&*debug(BlockAttributes::default()), "BlockAttributes {}",);
assert_eq!(
&*debug(BlockAttributes {
display_name: "x".into(),
..default()
}),
"BlockAttributes { display_name: \"x\" }",
);
assert_eq!(
&*debug(BlockAttributes {
selectable: false,
..default()
}),
"BlockAttributes { selectable: false }",
);
assert_eq!(
&*debug(BlockAttributes {
collision: BlockCollision::None,
..default()
}),
"BlockAttributes { collision: None }",
);
assert_eq!(
&*debug(BlockAttributes {
light_emission: Rgb::new(1.0, 2.0, 3.0),
..default()
}),
"BlockAttributes { light_emission: Rgb(1.0, 2.0, 3.0) }",
);
assert_eq!(
&*debug(BlockAttributes {
animation_hint: AnimationHint::TEMPORARY,
..default()
}),
"BlockAttributes { animation_hint: \
AnimationHint { redefinition: None, replacement: Shape } }",
);
// Test a case of multiple attributes
assert_eq!(
&*debug(BlockAttributes {
display_name: "y".into(),
selectable: false,
..default()
}),
"BlockAttributes { display_name: \"y\", selectable: false }",
);
}
}