use std::collections::HashSet;
use std::fmt;
use std::sync::Arc;
use cgmath::{
Angle as _, Basis3, Decomposed, Deg, ElementWise as _, EuclideanSpace as _, Matrix3, Point3,
Rotation3, Transform, Vector3,
};
use num_traits::identities::Zero;
use ordered_float::NotNan;
use crate::behavior::{Behavior, BehaviorSet, BehaviorSetTransaction};
use crate::camera::ViewTransform;
use crate::inv::{
Inventory, InventoryChange, InventoryTransaction, Slot, Tool, ToolError, TOOL_SELECTIONS,
};
use crate::listen::{Listener, Notifier};
use crate::math::{Aab, Face6, Face7, FreeCoordinate, Rgb};
use crate::physics::{Body, BodyStepInfo, BodyTransaction, Contact};
use crate::raycast::Ray;
use crate::space::Space;
use crate::time::Tick;
use crate::transaction::{
CommitError, Merge, PreconditionFailed, Transaction, TransactionConflict, Transactional,
};
use crate::universe::{RefVisitor, URef, UniverseTransaction, VisitRefs};
use crate::util::{ConciseDebug, CustomFormat, StatusText};
mod cursor;
pub use cursor::*;
mod spawn;
pub use spawn::*;
#[cfg(test)]
mod tests;
const WALKING_SPEED: FreeCoordinate = 4.0;
const FLYING_SPEED: FreeCoordinate = 10.0;
const JUMP_SPEED: FreeCoordinate = 8.0;
pub struct Character {
pub body: Body,
pub space: URef<Space>,
velocity_input: Vector3<FreeCoordinate>,
eye_displacement_pos: Vector3<FreeCoordinate>,
eye_displacement_vel: Vector3<FreeCoordinate>,
#[doc(hidden)] pub colliding_cubes: HashSet<Contact>,
pub(crate) last_step_info: Option<BodyStepInfo>,
light_samples: [Rgb; 100],
light_sample_index: usize,
exposure_log: f32,
inventory: Inventory,
selected_slots: [usize; TOOL_SELECTIONS],
notifier: Notifier<CharacterChange>,
pub(crate) behaviors: BehaviorSet<Character>,
}
impl fmt::Debug for Character {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt.debug_struct("Character")
.field("body", &self.body)
.field(
"velocity_input",
&self.velocity_input.custom_format(ConciseDebug),
)
.field("colliding_cubes", &self.colliding_cubes)
.field("exposure", &self.exposure_log.exp())
.field("inventory", &self.inventory)
.field("behaviors", &self.behaviors)
.finish()
}
}
impl CustomFormat<StatusText> for Character {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>, _: StatusText) -> fmt::Result {
writeln!(fmt, "{}", self.body.custom_format(StatusText))?;
if let Some(info) = &self.last_step_info {
writeln!(fmt, "Last step: {:#?}", info.custom_format(ConciseDebug))?;
}
write!(fmt, "Colliding: {:?}", self.colliding_cubes.len())
}
}
impl Character {
pub fn spawn(spawn: &Spawn, space: URef<Space>) -> Self {
const SLOT_COUNT: usize = 11;
const INVISIBLE_SLOT: usize = SLOT_COUNT - 1;
let mut inventory = vec![Slot::Empty; SLOT_COUNT];
inventory[INVISIBLE_SLOT] = Tool::CopyFromSpace.into();
let mut free = 0;
let mut ordinary_tool_selection = 0;
'fill: for item in spawn.inventory.iter() {
while inventory[free] != Slot::Empty {
free += 1;
if free >= inventory.len() {
break 'fill;
}
}
inventory[free] = item.clone();
if matches!(
item,
Slot::Stack(_, Tool::RemoveBlock { .. } | Tool::Jetpack { .. })
) && ordinary_tool_selection == free
{
ordinary_tool_selection += 1;
}
}
let selected_slots = [
0,
ordinary_tool_selection.min(INVISIBLE_SLOT - 1),
INVISIBLE_SLOT,
];
let look_direction = spawn.look_direction.map(|c| c.into_inner());
let yaw = Deg::atan2(look_direction.x, -look_direction.z);
let pitch = Deg::atan2(-look_direction.y, look_direction.z.hypot(look_direction.x));
let collision_box = Aab::new(-0.35, 0.35, -1.75, 0.15, -0.35, 0.35);
let position = match spawn.eye_position {
Some(pos) => pos.map(NotNan::into_inner),
None => {
let mut pos = spawn.bounds.center();
pos.y = collision_box.face_coordinate(Face6::NY)
- Aab::from(spawn.bounds).face_coordinate(Face6::NY);
pos
}
};
Self {
body: Body {
flying: false, yaw: yaw.0,
pitch: pitch.0,
..Body::new_minimal(position, collision_box)
},
space,
velocity_input: Vector3::zero(),
eye_displacement_pos: Vector3::zero(),
eye_displacement_vel: Vector3::zero(),
colliding_cubes: HashSet::new(),
last_step_info: None,
light_samples: [Rgb::ONE; 100],
light_sample_index: 0,
exposure_log: 0.0,
inventory: Inventory::from_slots(inventory),
selected_slots,
notifier: Notifier::new(),
behaviors: BehaviorSet::new(),
}
}
pub fn spawn_default(space: URef<Space>) -> Self {
Self::spawn(space.read().unwrap().spawn(), space)
}
pub fn listen(&self, listener: impl Listener<CharacterChange> + Send + Sync + 'static) {
self.notifier.listen(listener)
}
pub fn view(&self) -> ViewTransform {
Decomposed {
scale: 1.0,
rot: Basis3::from_angle_y(Deg(-self.body.yaw))
* Basis3::from_angle_x(Deg(-self.body.pitch)),
disp: self.body.position.to_vec() + self.eye_displacement_pos,
}
}
pub fn inventory(&self) -> &Inventory {
&self.inventory
}
pub fn add_behavior<B>(&mut self, behavior: B)
where
B: Behavior<Character> + 'static,
{
BehaviorSetTransaction::insert((), Arc::new(behavior))
.execute(&mut self.behaviors)
.unwrap();
}
pub fn selected_slots(&self) -> [usize; TOOL_SELECTIONS] {
self.selected_slots
}
pub fn set_selected_slot(&mut self, which_selection: usize, slot: usize) {
if which_selection < self.selected_slots.len()
&& slot != self.selected_slots[which_selection]
{
self.selected_slots[which_selection] = slot;
self.notifier.notify(CharacterChange::Selections);
}
}
pub fn step(
&mut self,
self_ref: Option<&URef<Character>>,
tick: Tick,
) -> (Option<BodyStepInfo>, UniverseTransaction) {
let mut result_transaction = UniverseTransaction::default();
if tick.paused() {
return (None, result_transaction);
}
let flying = find_jetpacks(&self.inventory).any(|(_slot_index, active)| active);
self.body.flying = flying;
let dt = tick.delta_t.as_secs_f64();
let control_orientation: Matrix3<FreeCoordinate> =
Matrix3::from_angle_y(-Deg(self.body.yaw));
let initial_body_velocity = self.body.velocity;
let speed = if flying { FLYING_SPEED } else { WALKING_SPEED };
let mut velocity_target = control_orientation * self.velocity_input * speed;
if !flying {
velocity_target.y = 0.0;
}
let stiffness = if flying {
Vector3::new(10.8, 10.8, 10.8)
} else {
Vector3::new(10.8, 0., 10.8)
};
self.body.velocity +=
(velocity_target - self.body.velocity).mul_element_wise(stiffness) * dt;
let body_step_info = if let Ok(space) = self.space.read() {
self.update_exposure(&space, dt);
let colliding_cubes = &mut self.colliding_cubes;
colliding_cubes.clear();
Some(self.body.step(tick, Some(&*space), |cube| {
colliding_cubes.insert(cube);
}))
} else {
None
};
if let Some(self_ref) = self_ref.cloned() {
if self.velocity_input.y > 0. {
if let Some((slot_index, false)) = find_jetpacks(&self.inventory).next() {
if let Ok(t) = self.inventory.use_tool(None, self_ref, slot_index) {
result_transaction = result_transaction.merge(t).unwrap();
}
}
} else if self.is_on_ground() {
for (slot_index, active) in find_jetpacks(&self.inventory) {
if active {
if let Ok(t) = self.inventory.use_tool(None, self_ref.clone(), slot_index) {
result_transaction = result_transaction.merge(t).unwrap();
}
}
}
}
}
if let Some(self_ref) = self_ref {
let t = self.behaviors.step(
self,
&(|t: CharacterTransaction| t.bind(self_ref.clone())),
CharacterTransaction::behaviors,
tick,
);
result_transaction = result_transaction
.merge(t)
.expect("TODO: we should be applying these transactions separately");
};
let body_delta_v_this_frame = self.body.velocity - initial_body_velocity;
self.eye_displacement_vel -= body_delta_v_this_frame * 0.04;
self.eye_displacement_vel += self.eye_displacement_pos * -(0.005f64.powf(dt));
self.eye_displacement_vel *= 0.005f64.powf(dt);
self.eye_displacement_pos += self.eye_displacement_vel * dt;
self.last_step_info = body_step_info;
(body_step_info, result_transaction)
}
pub fn exposure(&self) -> f32 {
self.exposure_log.exp()
}
fn update_exposure(&mut self, space: &Space, dt: f64) {
#![allow(clippy::cast_lossless)]
if dt == 0. {
return;
}
{
let vt = self.view();
let sqrtedge = (self.light_samples.len() as FreeCoordinate).sqrt();
let ray_origin = vt.transform_point(Point3::origin());
'rays: for _ray in 0..10 {
let index = (self.light_sample_index + 1).rem_euclid(self.light_samples.len());
self.light_sample_index = index;
let indexf = index as FreeCoordinate;
let ray = Ray::new(
ray_origin,
vt.transform_vector(Vector3::new(
(indexf).rem_euclid(sqrtedge) / sqrtedge * 2. - 1.,
(indexf).div_euclid(sqrtedge) / sqrtedge * 2. - 1.,
-1.0,
)),
);
let bounds = space.bounds();
for step in ray.cast().take(20) {
if !bounds.contains_cube(step.cube_ahead()) {
self.light_samples[self.light_sample_index] = space.physics().sky_color;
continue 'rays;
} else if space.get_evaluated(step.cube_ahead()).visible {
let l = space.get_lighting(step.cube_behind());
if l.valid() {
self.light_samples[self.light_sample_index] = l.value();
continue 'rays;
}
}
}
self.light_samples[self.light_sample_index] = space.physics().sky_color;
}
}
const TARGET_LUMINANCE: f32 = 0.9;
const ADJUSTMENT_STRENGTH: f32 = 0.5;
const EXPOSURE_CHANGE_RATE: f32 = 2.0;
let light_average: Rgb = self.light_samples.iter().copied().sum::<Rgb>()
* (self.light_samples.len() as f32).recip();
let derived_exposure = (TARGET_LUMINANCE / light_average.luminance()).clamp(0.1, 10.);
let derived_exposure =
derived_exposure * ADJUSTMENT_STRENGTH + 1. * (1. - ADJUSTMENT_STRENGTH);
if derived_exposure.is_finite() {
let delta_log = derived_exposure.ln() - self.exposure_log;
self.exposure_log += delta_log * dt as f32 * EXPOSURE_CHANGE_RATE;
}
}
pub fn set_velocity_input(&mut self, velocity: Vector3<FreeCoordinate>) {
self.velocity_input = velocity;
}
pub fn click(
this: URef<Character>,
cursor: Option<&Cursor>,
button: usize,
) -> Result<UniverseTransaction, ToolError> {
let tb = this.read().unwrap();
if let Some(cursor_space) = cursor.map(Cursor::space) {
let our_space = &tb.space;
if cursor_space != our_space {
return Err(ToolError::Internal(format!(
"space mismatch: cursor {cursor_space:?} != character {our_space:?}"
)));
}
}
let slot_index = tb
.selected_slots
.get(button)
.copied()
.unwrap_or(tb.selected_slots[0]);
tb.inventory.use_tool(cursor, this, slot_index)
}
pub fn jump_if_able(&mut self) {
if self.is_on_ground() {
self.body.velocity += Vector3 {
x: 0.,
y: JUMP_SPEED,
z: 0.,
};
}
}
fn is_on_ground(&self) -> bool {
self.body.velocity.y <= 0.0
&& self
.colliding_cubes
.iter()
.any(|contact| contact.normal() == Face7::PY)
}
}
impl VisitRefs for Character {
fn visit_refs(&self, visitor: &mut dyn RefVisitor) {
let Self {
body: _,
space,
velocity_input: _,
eye_displacement_pos: _,
eye_displacement_vel: _,
colliding_cubes: _,
last_step_info: _,
light_samples: _,
light_sample_index: _,
exposure_log: _,
inventory,
selected_slots: _,
notifier: _,
behaviors,
} = self;
visitor.visit(space);
inventory.visit_refs(visitor);
behaviors.visit_refs(visitor);
}
}
impl Transactional for Character {
type Transaction = CharacterTransaction;
}
impl crate::behavior::BehaviorHost for Character {
type Attachment = ();
}
#[derive(Clone, Debug, Default, PartialEq)]
#[must_use]
pub struct CharacterTransaction {
body: BodyTransaction,
inventory: InventoryTransaction,
behaviors: BehaviorSetTransaction<Character>,
}
impl CharacterTransaction {
pub fn body(t: BodyTransaction) -> Self {
CharacterTransaction {
body: t,
..Default::default()
}
}
pub fn inventory(t: InventoryTransaction) -> Self {
CharacterTransaction {
inventory: t,
..Default::default()
}
}
fn behaviors(t: BehaviorSetTransaction<Character>) -> Self {
Self {
behaviors: t,
..Default::default()
}
}
}
#[allow(clippy::type_complexity)]
impl Transaction<Character> for CharacterTransaction {
type CommitCheck = (
<BodyTransaction as Transaction<Body>>::CommitCheck,
<InventoryTransaction as Transaction<Inventory>>::CommitCheck,
<BehaviorSetTransaction<Character> as Transaction<BehaviorSet<Character>>>::CommitCheck,
);
type Output = ();
fn check(&self, target: &Character) -> Result<Self::CommitCheck, PreconditionFailed> {
Ok((
self.body.check(&target.body)?,
self.inventory.check(&target.inventory)?,
self.behaviors.check(&target.behaviors)?,
))
}
fn commit(
&self,
target: &mut Character,
(body_check, inventory_check, behaviors_check): Self::CommitCheck,
) -> Result<(), CommitError> {
self.body
.commit(&mut target.body, body_check)
.map_err(|e| e.context("body".into()))?;
if self.inventory != Default::default() {
let change = self
.inventory
.commit(&mut target.inventory, inventory_check)
.map_err(|e| e.context("inventory".into()))?;
if let Some(change) = change {
target.notifier.notify(CharacterChange::Inventory(change));
}
}
self.behaviors
.commit(&mut target.behaviors, behaviors_check)
.map_err(|e| e.context("behaviors".into()))?;
Ok(())
}
}
impl Merge for CharacterTransaction {
type MergeCheck = (
<BodyTransaction as Merge>::MergeCheck,
<InventoryTransaction as Merge>::MergeCheck,
<BehaviorSetTransaction<Character> as Merge>::MergeCheck,
);
fn check_merge(&self, other: &Self) -> Result<Self::MergeCheck, TransactionConflict> {
Ok((
self.body.check_merge(&other.body)?,
self.inventory.check_merge(&other.inventory)?,
self.behaviors.check_merge(&other.behaviors)?,
))
}
fn commit_merge(
self,
other: Self,
(body_check, inventory_check, behaviors_check): Self::MergeCheck,
) -> Self {
Self {
body: self.body.commit_merge(other.body, body_check),
inventory: self
.inventory
.commit_merge(other.inventory, inventory_check),
behaviors: self
.behaviors
.commit_merge(other.behaviors, behaviors_check),
}
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
#[allow(clippy::exhaustive_enums)] pub enum CharacterChange {
Inventory(InventoryChange),
Selections,
}
fn find_jetpacks(inventory: &Inventory) -> impl Iterator<Item = (usize, bool)> + '_ {
inventory
.slots
.iter()
.enumerate()
.filter_map(|(index, slot)| {
if let Slot::Stack(_, Tool::Jetpack { active }) = *slot {
Some((index, active))
} else {
None
}
})
}