1use alloc::boxed::Box;
4use core::fmt;
5
6use bevy_ecs::prelude as ecs;
7use manyfmt::Fmt;
8use ordered_float::NotNan;
9
10#[cfg(not(feature = "std"))]
12#[allow(unused_imports)]
13use num_traits::float::Float as _;
14
15use crate::camera::ViewTransform;
16use crate::inv::{self, Inventory, InventoryComponent, InventoryTransaction, Slot, Tool};
17use crate::listen::{self, IntoListener};
18use crate::math::{Aab, Face6, FreePoint, FreeVector};
19use crate::physics::{self, Body, BodyTransaction, step::PhysicsOutputs};
20use crate::rerun_glue as rg;
21#[cfg(feature = "save")]
22use crate::save::schema;
23use crate::sound;
24use crate::space::Space;
25use crate::transaction::{self, Equal, Merge, Transaction, Transactional};
26use crate::universe::{
27 self, Handle, HandleError, HandleVisitor, ReadTicket, UniverseTransaction, VisitHandles,
28};
29use crate::util::{ConciseDebug, Refmt as _, StatusText};
30
31mod ambient_sound;
34
35mod cursor;
36pub use cursor::*;
37
38mod exposure;
39
40mod eye;
41pub(crate) use eye::add_eye_systems;
42
43mod spawn;
44pub use spawn::*;
45
46mod step;
47pub(crate) use step::add_main_systems;
48
49#[cfg(test)]
50mod tests;
51
52#[doc = include_str!("save/serde-warning.md")]
63pub struct Character {
65 core: CharacterCore,
66
67 pub body: Body,
69
70 pub space: Handle<Space>,
72
73 inventory: InventoryComponent,
74}
75
76#[derive(Debug, ecs::Component)]
79#[require(eye::CharacterEye, Input, rg::Destination)]
80pub(crate) struct CharacterCore {
81 selected_slots: [inv::Ix; inv::TOOL_SELECTIONS],
86
87 notifier: listen::Notifier<CharacterChange>,
91}
92
93#[derive(Clone, Debug, ecs::Component)]
97pub(crate) struct ParentSpace(pub Handle<Space>);
98
99#[derive(Clone, Debug, Default, ecs::Component)]
103#[non_exhaustive]
104pub struct Input {
105 pub velocity_input: FreeVector,
110
111 pub jump: bool,
113
114 pub set_selected_slots: [Option<inv::Ix>; inv::TOOL_SELECTIONS],
120}
121
122impl fmt::Debug for Character {
125 #[mutants::skip]
126 fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
127 let Self {
128 core:
129 CharacterCore {
130 selected_slots,
131 notifier: _,
132 },
133 body,
134 space: _,
135 inventory,
136 } = self;
137 fmt.debug_struct("Character")
138 .field("body", &body)
139 .field("inventory", &inventory)
140 .field("selected_slots", selected_slots)
141 .finish_non_exhaustive()
142 }
143}
144
145impl Character {
146 pub fn spawn(spawn: &Spawn, space: Handle<Space>) -> Self {
149 const SLOT_COUNT: inv::Ix = 11;
154 const INVISIBLE_SLOT: inv::Ix = SLOT_COUNT - 1;
155 let mut inventory = vec![Slot::Empty; usize::from(SLOT_COUNT)].into_boxed_slice();
156 inventory[usize::from(INVISIBLE_SLOT)] = Tool::CopyFromSpace.into();
157 let mut free: usize = 0;
158 let mut ordinary_tool_selection: inv::Ix = 0;
159 'fill: for item in spawn.inventory.iter() {
160 while inventory[free] != Slot::Empty {
161 free += 1;
162 if free >= inventory.len() {
163 break 'fill;
164 }
165 }
166 inventory[free] = item.clone();
167
168 if matches!(
170 item,
171 Slot::Stack(_, Tool::RemoveBlock { .. } | Tool::Jetpack { .. })
172 ) && usize::from(ordinary_tool_selection) == free
173 {
174 ordinary_tool_selection += 1;
175 }
176 }
177 let selected_slots = [
178 0,
179 ordinary_tool_selection.min(INVISIBLE_SLOT - 1),
180 INVISIBLE_SLOT,
181 ];
182
183 let look_direction = spawn.look_direction.map(|c| c.into_inner());
184 let yaw = f64::atan2(look_direction.x, -look_direction.z).to_degrees();
185 let pitch =
186 f64::atan2(-look_direction.y, look_direction.z.hypot(look_direction.x)).to_degrees();
187
188 let collision_box = Aab::new(-0.35, 0.35, -1.75, 0.15, -0.35, 0.35);
191
192 let position: FreePoint = match spawn.eye_position {
196 Some(pos) => pos.map(NotNan::into_inner),
197 None => {
198 let mut pos: FreePoint = spawn.bounds.center();
201 pos.y = collision_box.face_coordinate(Face6::NY)
202 - spawn.bounds.to_free().face_coordinate(Face6::NY);
203 pos
204 }
205 };
206
207 Self {
208 body: {
209 let mut body = Body::new_minimal(position, collision_box);
210 body.flying = false; body.yaw = yaw;
212 body.pitch = pitch;
213 body
214 },
215 core: CharacterCore {
216 selected_slots,
217 notifier: listen::Notifier::new(),
218 },
219 space,
220 inventory: InventoryComponent::new(Inventory::from_slots(inventory)),
221 }
222 }
223
224 pub fn spawn_default(
227 read_ticket: ReadTicket<'_>,
228 space: Handle<Space>,
229 ) -> Result<Self, HandleError> {
230 Ok(Self::spawn(space.read(read_ticket)?.spawn(), space))
231 }
232
233 pub fn view<'t>(
248 handle: &Handle<Self>,
249 read_ticket: ReadTicket<'t>,
250 ) -> Result<(&'t Handle<Space>, ViewTransform, f32), HandleError> {
251 let body = handle.query::<Body>(read_ticket)?;
252 let space = &handle.query::<ParentSpace>(read_ticket)?.0;
253 let eye = handle.query::<eye::CharacterEye>(read_ticket)?; let transform = eye.view_transform.unwrap_or_else(|| {
255 eye::compute_view_transform(body, FreeVector::zero())
257 });
258 Ok((
259 space,
260 transform,
261 handle.query::<exposure::State>(read_ticket)?.exposure(),
262 ))
263 }
264
265 pub fn inventory(&self) -> &Inventory {
267 self.inventory.inventory()
268 }
269
270 pub fn selected_slots(&self) -> [inv::Ix; inv::TOOL_SELECTIONS] {
275 self.core.selected_slots
276 }
277
278 pub fn set_selected_slot(&mut self, which_selection: usize, slot: inv::Ix) {
280 let s = &mut self.core.selected_slots;
281 if which_selection < s.len() && slot != s[which_selection] {
282 s[which_selection] = slot;
283 self.core.notifier.notify(&CharacterChange::Selections);
284 }
285 }
286
287 pub fn click(
293 read_ticket: ReadTicket<'_>,
294 this: Handle<Character>,
295 cursor: Option<&Cursor>,
296 button: usize,
297 ) -> Result<UniverseTransaction, inv::ToolError> {
298 let tb = this.read(read_ticket).unwrap();
299
300 if let Some(cursor_space) = cursor.map(Cursor::space) {
304 let our_space = tb.space();
305 if cursor_space != our_space {
306 return Err(inv::ToolError::Internal(format!(
307 "space mismatch: cursor {cursor_space:?} != character {our_space:?}"
308 )));
309 }
310 }
311
312 let slot_index = tb.selected_slots().get(button).copied().unwrap_or(tb.selected_slots()[0]);
313 tb.inventory().use_tool(read_ticket, cursor, this, slot_index)
314 }
315}
316
317impl universe::SealedMember for Character {
318 type Bundle = (CharacterCore, Body, ParentSpace, InventoryComponent);
319 type ReadQueryData = (
320 &'static CharacterCore,
321 &'static Body,
322 &'static ParentSpace,
323 &'static InventoryComponent,
324 &'static PhysicsOutputs,
325 &'static ambient_sound::State,
326 );
327
328 fn register_all_member_components(world: &mut ecs::World) {
329 universe::VisitableComponents::register::<CharacterCore>(world);
330 universe::VisitableComponents::register::<ParentSpace>(world);
331 universe::VisitableComponents::register::<InventoryComponent>(world);
332 }
334
335 fn read_from_standalone(value: &Self) -> <Self as universe::UniverseMember>::Read<'_> {
336 Read {
337 core: &value.core,
338 body: &value.body,
339 space: &value.space,
340 inventory: &value.inventory,
341 physics: None,
342 ambient_sound: &sound::SpatialAmbient::SILENT,
343 }
344 }
345 fn read_from_query(
346 data: <Self::ReadQueryData as ::bevy_ecs::query::QueryData>::Item<'_>,
347 ) -> <Self as universe::UniverseMember>::Read<'_> {
348 let (core, body, ParentSpace(space), inventory, physics, sound_state) = data;
349 Read {
350 core,
351 body,
352 space,
353 inventory,
354 physics: Some(physics),
355 ambient_sound: sound_state.sound_average(),
356 }
357 }
358 fn read_from_entity_ref(
359 entity: ::bevy_ecs::world::EntityRef<'_>,
360 ) -> Option<<Self as universe::UniverseMember>::Read<'_>> {
361 Some(Read {
362 core: entity.get()?,
363 body: entity.get()?,
364 space: &entity.get::<ParentSpace>()?.0,
365 inventory: entity.get::<InventoryComponent>()?,
366 physics: entity.get::<PhysicsOutputs>(),
367 ambient_sound: entity.get::<ambient_sound::State>()?.sound_average(),
368 })
369 }
370 fn into_bundle(value: Box<Self>) -> Self::Bundle {
371 let Self {
372 core,
373 body,
374 space,
375 inventory,
376 } = *value;
377 (core, body, ParentSpace(space), inventory)
378 }
379}
380impl universe::UniverseMember for Character {
381 type Read<'ticket> = Read<'ticket>;
382}
383
384impl universe::PubliclyMutableComponent<Character> for Body {}
386impl universe::PubliclyMutableComponent<Character> for Input {}
387
388impl VisitHandles for Character {
389 fn visit_handles(&self, visitor: &mut dyn HandleVisitor) {
390 let Self {
393 core,
394 body: _,
395 space,
396 inventory,
397 } = self;
398 core.visit_handles(visitor);
399 visitor.visit(space);
400 inventory.visit_handles(visitor);
401 }
402}
403impl VisitHandles for CharacterCore {
404 fn visit_handles(&self, _visitor: &mut dyn HandleVisitor) {
405 let Self {
406 selected_slots: _,
407 notifier: _,
408 } = self;
409 }
410}
411impl VisitHandles for ParentSpace {
412 fn visit_handles(&self, visitor: &mut dyn HandleVisitor) {
413 let Self(handle) = self;
414 visitor.visit(handle);
415 }
416}
417impl listen::Listen for Character {
420 type Msg = CharacterChange;
421 type Listener = <listen::Notifier<Self::Msg> as listen::Listen>::Listener;
422 fn listen_raw(&self, listener: Self::Listener) {
423 universe::SealedMember::read_from_standalone(self).listen_raw(listener);
424 }
425}
426
427impl Transactional for Character {
428 type Transaction = CharacterTransaction;
429}
430
431#[cfg(feature = "save")]
432impl serde::Serialize for Read<'_> {
433 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
434 where
435 S: serde::Serializer,
436 {
437 use alloc::borrow::Cow::Borrowed;
438
439 let &Read {
440 core:
441 &CharacterCore {
442 selected_slots,
443 notifier: _,
445 },
446 body,
447 space,
448 inventory,
449 physics: _,
450 ambient_sound: _,
451 } = self;
452 schema::CharacterSer::CharacterV1 {
453 space: space.clone(),
454 body: Borrowed(body),
455 inventory: Borrowed(inventory.inventory()),
456 selected_slots,
457 }
458 .serialize(serializer)
459 }
460}
461
462#[cfg(feature = "save")]
463impl serde::Serialize for Character {
464 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
465 where
466 S: serde::Serializer,
467 {
468 universe::SealedMember::read_from_standalone(self).serialize(serializer)
469 }
470}
471
472#[cfg(feature = "save")]
473impl<'de> serde::Deserialize<'de> for Character {
474 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
475 where
476 D: serde::Deserializer<'de>,
477 {
478 match schema::CharacterSer::deserialize(deserializer)? {
479 schema::CharacterSer::CharacterV1 {
480 space,
481 body,
482 inventory,
483 selected_slots,
484 } => Ok(Character {
485 core: CharacterCore {
486 selected_slots,
487 notifier: listen::Notifier::new(),
488 },
489 body: body.into_owned(),
490 space,
491 inventory: InventoryComponent::new(inventory.into_owned()),
492 }),
493 }
494 }
495}
496
497#[derive(Clone, Copy, Debug)]
501pub struct Read<'ticket> {
502 core: &'ticket CharacterCore,
503 body: &'ticket Body,
504 space: &'ticket Handle<Space>,
505 inventory: &'ticket InventoryComponent,
506 physics: Option<&'ticket PhysicsOutputs>,
507 ambient_sound: &'ticket sound::SpatialAmbient,
508}
509
510impl<'t> Read<'t> {
511 pub fn body(&self) -> &'t Body {
513 self.body
514 }
515
516 pub fn inventory(&self) -> &'t Inventory {
518 self.inventory.inventory()
519 }
520
521 pub fn space(&self) -> &'t Handle<Space> {
523 self.space
524 }
525
526 pub fn selected_slots(&self) -> [inv::Ix; inv::TOOL_SELECTIONS] {
529 self.core.selected_slots
530 }
531
532 #[doc(hidden)] pub fn physics(&self) -> Option<&PhysicsOutputs> {
534 self.physics
535 }
536
537 pub fn ambient_sound(&self) -> &sound::SpatialAmbient {
539 self.ambient_sound
540 }
541}
542
543impl listen::Listen for Read<'_> {
545 type Msg = CharacterChange;
546 type Listener = <listen::Notifier<Self::Msg> as listen::Listen>::Listener;
547 fn listen_raw(&self, listener: Self::Listener) {
548 use listen::Listener; self.core.notifier.listen_raw(listener.clone());
551 self.inventory.listen_raw(
552 listener
553 .filter(|change: &inv::InventoryChange| {
554 Some(CharacterChange::Inventory(change.clone()))
555 })
556 .into_listener(),
557 );
558 }
559}
560
561impl Fmt<StatusText> for Read<'_> {
562 #[mutants::skip] fn fmt(&self, fmt: &mut fmt::Formatter<'_>, fopt: &StatusText) -> fmt::Result {
564 write!(fmt, "{}", self.body().refmt(fopt))?;
565 if let Some(physics) = self.physics {
566 writeln!(fmt, "\n")?;
567 if let Some(info) = &physics.last_step_info {
568 writeln!(fmt, "Last step: {:#?}", info.refmt(&ConciseDebug))?;
569 }
570 write!(fmt, "Colliding: {:?}", physics.colliding_cubes.len())?;
571 }
572 Ok(())
573 }
574}
575
576#[derive(Clone, Debug, Default, PartialEq)]
580#[must_use]
581#[expect(clippy::module_name_repetitions)] pub struct CharacterTransaction {
583 set_space: Equal<Handle<Space>>,
584 body: BodyTransaction,
585 inventory: InventoryTransaction,
586}
587
588impl CharacterTransaction {
589 pub fn move_to_space(space: Handle<Space>) -> Self {
594 CharacterTransaction {
595 set_space: Equal(Some(space)),
596 ..Default::default()
597 }
598 }
599
600 pub fn body(t: BodyTransaction) -> Self {
602 CharacterTransaction {
603 body: t,
604 ..Default::default()
605 }
606 }
607
608 pub fn inventory(t: InventoryTransaction) -> Self {
610 CharacterTransaction {
611 inventory: t,
612 ..Default::default()
613 }
614 }
615}
616
617impl Transaction for CharacterTransaction {
618 type Target = Character;
619 type Context<'a> = ReadTicket<'a>;
621 type CommitCheck = (
622 <BodyTransaction as Transaction>::CommitCheck,
623 <InventoryTransaction as Transaction>::CommitCheck,
624 );
625 type Output = transaction::NoOutput;
626 type Mismatch = CharacterTransactionMismatch;
627
628 fn check(
629 &self,
630 target: &Character,
631 _: Self::Context<'_>,
632 ) -> Result<Self::CommitCheck, Self::Mismatch> {
633 let Self {
634 set_space: _, body,
636 inventory,
637 } = self;
638 Ok((
639 body.check(&target.body, ()).map_err(CharacterTransactionMismatch::Body)?,
640 inventory
641 .check(target.inventory.inventory(), ())
642 .map_err(CharacterTransactionMismatch::Inventory)?,
643 ))
644 }
645
646 fn commit(
647 self,
648 target: &mut Character,
649 (body_check, inventory_check): Self::CommitCheck,
650 outputs: &mut dyn FnMut(Self::Output),
651 ) -> Result<(), transaction::CommitError> {
652 self.set_space.commit(&mut target.space);
653
654 self.body
655 .commit(&mut target.body, body_check, outputs)
656 .map_err(|e| e.context("body".into()))?;
657
658 target
659 .inventory
660 .commit_inventory_transaction(self.inventory, inventory_check)
661 .map_err(|e| e.context("inventory".into()))?;
662
663 Ok(())
664 }
665}
666
667impl universe::TransactionOnEcs for CharacterTransaction {
668 type WriteQueryData = (
669 &'static mut Body,
670 &'static mut InventoryComponent,
671 &'static mut ParentSpace,
672 );
673
674 fn check(
675 &self,
676 target: Read<'_>,
677 _: ReadTicket<'_>,
678 ) -> Result<Self::CommitCheck, Self::Mismatch> {
679 let Self {
680 set_space: _, body,
682 inventory,
683 } = self;
684 Ok((
685 body.check(target.body, ()).map_err(CharacterTransactionMismatch::Body)?,
686 inventory
687 .check(target.inventory(), ())
688 .map_err(CharacterTransactionMismatch::Inventory)?,
689 ))
690 }
691
692 fn commit(
693 self,
694 (mut body, mut inventory, mut space): (
695 ecs::Mut<'_, Body>,
696 ecs::Mut<'_, InventoryComponent>,
697 ecs::Mut<'_, ParentSpace>,
698 ),
699 (body_check, inventory_check): Self::CommitCheck,
700 ) -> Result<(), transaction::CommitError> {
701 self.set_space.commit(&mut space.0);
702
703 self.body
704 .commit(&mut *body, body_check, &mut transaction::no_outputs)
705 .map_err(|e| e.context("body".into()))?;
706
707 inventory
708 .commit_inventory_transaction(self.inventory, inventory_check)
709 .map_err(|e| e.context("inventory".into()))?;
710
711 Ok(())
712 }
713}
714
715impl Merge for CharacterTransaction {
716 type MergeCheck = (
717 <BodyTransaction as Merge>::MergeCheck,
718 <InventoryTransaction as Merge>::MergeCheck,
719 );
720 type Conflict = CharacterTransactionConflict;
721
722 fn check_merge(&self, other: &Self) -> Result<Self::MergeCheck, Self::Conflict> {
723 use CharacterTransactionConflict as C;
724 if self.set_space.check_merge(&other.set_space).is_err() {
725 return Err(CharacterTransactionConflict::SetSpace);
726 }
727 Ok((
728 self.body.check_merge(&other.body).map_err(C::Body)?,
729 self.inventory.check_merge(&other.inventory).map_err(C::Inventory)?,
730 ))
731 }
732
733 fn commit_merge(&mut self, other: Self, (body_check, inventory_check): Self::MergeCheck) {
734 let Self {
735 set_space,
736 body,
737 inventory,
738 } = self;
739 set_space.commit_merge(other.set_space, ());
740 body.commit_merge(other.body, body_check);
741 inventory.commit_merge(other.inventory, inventory_check);
742 }
743}
744
745#[derive(Clone, Debug, Eq, PartialEq, displaydoc::Display)]
747#[non_exhaustive]
748#[expect(clippy::module_name_repetitions)]
749pub enum CharacterTransactionMismatch {
750 Body(<BodyTransaction as Transaction>::Mismatch),
752 Inventory(<InventoryTransaction as Transaction>::Mismatch),
754}
755
756#[derive(Clone, Debug, Eq, PartialEq, displaydoc::Display)]
758#[non_exhaustive]
759#[expect(clippy::module_name_repetitions)]
760pub enum CharacterTransactionConflict {
761 SetSpace,
763 Body(physics::BodyConflict),
765 Inventory(inv::InventoryConflict),
767}
768
769impl core::error::Error for CharacterTransactionMismatch {
770 fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
771 match self {
772 CharacterTransactionMismatch::Body(e) => Some(e),
773 CharacterTransactionMismatch::Inventory(e) => Some(e),
774 }
775 }
776}
777
778impl core::error::Error for CharacterTransactionConflict {
779 fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
780 match self {
781 CharacterTransactionConflict::SetSpace => None,
782 CharacterTransactionConflict::Body(_) => None,
783 CharacterTransactionConflict::Inventory(e) => Some(e),
784 }
785 }
786}
787
788#[derive(Clone, Debug, Eq, Hash, PartialEq)]
792#[expect(clippy::exhaustive_enums)] #[expect(clippy::module_name_repetitions)] pub enum CharacterChange {
795 Inventory(inv::InventoryChange),
797 Selections,
799}