refactor(server): apply rustfmt and clippy suggestions

Formatting pass across simulation, perception, knowledge, NPC, and
test modules. Includes two clippy fixes in monologue.rs (.values()
instead of for (_, v) pattern).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-16 00:40:43 +01:00
co-authored by Claude Opus 4.6
parent 8ec875385a
commit d0663c4193
21 changed files with 600 additions and 274 deletions
+162 -76
View File
@@ -68,7 +68,12 @@ pub fn process_player_input(
mut commands: Commands,
registry: Res<EntityRegistry>,
mut player_query: Query<
(Entity, &TilePosition, Option<&mut Stance>, Option<&mut PlayerMoveCooldown>),
(
Entity,
&TilePosition,
Option<&mut Stance>,
Option<&mut PlayerMoveCooldown>,
),
With<PlayerCharacter>,
>,
inventory_items: Query<(Entity, &CarriedBy, &ItemName, &InventorySlot)>,
@@ -143,34 +148,30 @@ pub fn process_player_input(
time.tick_rate = rate;
tracing::debug!("Tick rate set to {:?} by player input", rate);
}
PlayerAction::Interact { target_entity_id, ref verb } => {
match verb.as_deref() {
Some("Take") => {
handle_take(
&mut commands,
&registry,
&player_query,
&inventory_items,
target_entity_id,
);
}
Some("Place") => {
handle_place(
&mut commands,
&registry,
&player_query,
target_entity_id,
);
}
_ => {
tracing::info!(
PlayerAction::Interact {
target_entity_id,
ref verb,
} => match verb.as_deref() {
Some("Take") => {
handle_take(
&mut commands,
&registry,
&player_query,
&inventory_items,
target_entity_id,
);
}
Some("Place") => {
handle_place(&mut commands, &registry, &player_query, target_entity_id);
}
_ => {
tracing::info!(
"Interact: target={:?}, verb={:?} — logged only, dialogue dispatch future scope (#415)",
target_entity_id,
verb,
);
}
}
}
},
PlayerAction::UsePerceptionMode(ref mode) => {
tracing::trace!("UsePerceptionMode({}) — no-op for Sprint 1", mode);
}
@@ -192,7 +193,12 @@ pub fn process_player_input(
#[allow(clippy::type_complexity)]
fn apply_move(
player_query: &mut Query<
(Entity, &TilePosition, Option<&mut Stance>, Option<&mut PlayerMoveCooldown>),
(
Entity,
&TilePosition,
Option<&mut Stance>,
Option<&mut PlayerMoveCooldown>,
),
With<PlayerCharacter>,
>,
commands: &mut Commands,
@@ -230,7 +236,12 @@ fn handle_take(
commands: &mut Commands,
registry: &EntityRegistry,
player_query: &Query<
(Entity, &TilePosition, Option<&mut Stance>, Option<&mut PlayerMoveCooldown>),
(
Entity,
&TilePosition,
Option<&mut Stance>,
Option<&mut PlayerMoveCooldown>,
),
With<PlayerCharacter>,
>,
inventory_items: &Query<(Entity, &CarriedBy, &ItemName, &InventorySlot)>,
@@ -260,12 +271,16 @@ fn handle_take(
// Check inventory capacity
let occupied = occupied_slots_for(player_sid, inventory_items);
let Some(slot) = find_next_slot(&occupied) else {
tracing::info!("Inventory full ({} slots), cannot take item", MAX_INVENTORY_SLOTS);
tracing::info!(
"Inventory full ({} slots), cannot take item",
MAX_INVENTORY_SLOTS
);
return;
};
// Remove TilePosition (item leaves the ground), add CarriedBy + InventorySlot
commands.entity(target_entity)
commands
.entity(target_entity)
.remove::<TilePosition>()
.insert((CarriedBy(player_sid), InventorySlot(slot)));
@@ -284,7 +299,12 @@ fn handle_place(
commands: &mut Commands,
registry: &EntityRegistry,
player_query: &Query<
(Entity, &TilePosition, Option<&mut Stance>, Option<&mut PlayerMoveCooldown>),
(
Entity,
&TilePosition,
Option<&mut Stance>,
Option<&mut PlayerMoveCooldown>,
),
With<PlayerCharacter>,
>,
target_entity_id: Option<u64>,
@@ -307,7 +327,8 @@ fn handle_place(
let place_pos = *player_pos;
// Remove inventory components, place item at player's tile
commands.entity(target_entity)
commands
.entity(target_entity)
.remove::<CarriedBy>()
.remove::<InventorySlot>()
.insert(place_pos);
@@ -338,7 +359,10 @@ mod tests {
});
queue.push(PlayerInput {
tick: 5,
action: PlayerAction::Interact { target_entity_id: None, verb: None },
action: PlayerAction::Interact {
target_entity_id: None,
verb: None,
},
});
let inputs = queue.drain_for_tick(3);
assert_eq!(inputs.len(), 2);
@@ -406,7 +430,10 @@ mod tests {
schedule.add_systems(process_player_input);
schedule.run(&mut world);
assert_eq!(world.resource::<SimulationTime>().tick_rate, TickRate::Paused);
assert_eq!(
world.resource::<SimulationTime>().tick_rate,
TickRate::Paused
);
}
#[test]
@@ -553,7 +580,10 @@ mod tests {
action: PlayerAction::MoveNorth,
});
schedule.run(&mut world);
assert!(world.get::<MoveIntent>(player).is_some(), "first move should succeed");
assert!(
world.get::<MoveIntent>(player).is_some(),
"first move should succeed"
);
// Remove MoveIntent (simulating validate_movement consuming it)
world.entity_mut(player).remove::<MoveIntent>();
@@ -564,7 +594,10 @@ mod tests {
action: PlayerAction::MoveNorth,
});
schedule.run(&mut world);
assert!(world.get::<MoveIntent>(player).is_none(), "second move should be throttled");
assert!(
world.get::<MoveIntent>(player).is_none(),
"second move should be throttled"
);
// Tick 0 again: move north — should succeed (cooldown elapsed)
world.resource_mut::<InputQueue>().push(PlayerInput {
@@ -572,7 +605,10 @@ mod tests {
action: PlayerAction::MoveNorth,
});
schedule.run(&mut world);
assert!(world.get::<MoveIntent>(player).is_some(), "third move should succeed after cooldown");
assert!(
world.get::<MoveIntent>(player).is_some(),
"third move should succeed after cooldown"
);
}
#[test]
@@ -609,7 +645,10 @@ mod tests {
action: PlayerAction::MoveNorth,
});
schedule.run(&mut world);
assert!(world.get::<MoveIntent>(player).is_some(), "sprint should allow every tick");
assert!(
world.get::<MoveIntent>(player).is_some(),
"sprint should allow every tick"
);
}
#[test]
@@ -655,16 +694,17 @@ mod tests {
let player = world
.spawn((PlayerCharacter, TilePosition::new(5, 5, 0)))
.id();
let player_sid = world.resource_mut::<crate::knowledge::EntityRegistry>().register(player);
let player_sid = world
.resource_mut::<crate::knowledge::EntityRegistry>()
.register(player);
// Spawn item near player
let item = world
.spawn((
TilePosition::new(5, 4, 0),
ItemName("Manifest Copy".into()),
))
.spawn((TilePosition::new(5, 4, 0), ItemName("Manifest Copy".into())))
.id();
let item_sid = world.resource_mut::<crate::knowledge::EntityRegistry>().register(item);
let item_sid = world
.resource_mut::<crate::knowledge::EntityRegistry>()
.register(item);
// Issue Take verb
world.resource_mut::<InputQueue>().push(PlayerInput {
@@ -680,10 +720,17 @@ mod tests {
schedule.run(&mut world);
// Item should have CarriedBy + InventorySlot, no TilePosition
assert!(world.get::<TilePosition>(item).is_none(), "item should leave the ground");
let carried = world.get::<CarriedBy>(item).expect("item should have CarriedBy");
assert!(
world.get::<TilePosition>(item).is_none(),
"item should leave the ground"
);
let carried = world
.get::<CarriedBy>(item)
.expect("item should have CarriedBy");
assert_eq!(carried.0, player_sid);
let slot = world.get::<InventorySlot>(item).expect("item should have slot");
let slot = world
.get::<InventorySlot>(item)
.expect("item should have slot");
assert_eq!(slot.0, 0, "first item goes to slot 0");
}
@@ -697,7 +744,9 @@ mod tests {
let player = world
.spawn((PlayerCharacter, TilePosition::new(5, 5, 0)))
.id();
let player_sid = world.resource_mut::<crate::knowledge::EntityRegistry>().register(player);
let player_sid = world
.resource_mut::<crate::knowledge::EntityRegistry>()
.register(player);
// Spawn item already in inventory (no TilePosition)
let item = world
@@ -707,7 +756,9 @@ mod tests {
InventorySlot(0),
))
.id();
let item_sid = world.resource_mut::<crate::knowledge::EntityRegistry>().register(item);
let item_sid = world
.resource_mut::<crate::knowledge::EntityRegistry>()
.register(item);
// Issue Place verb
world.resource_mut::<InputQueue>().push(PlayerInput {
@@ -723,10 +774,19 @@ mod tests {
schedule.run(&mut world);
// Item should have TilePosition at player's location, no CarriedBy/InventorySlot
let pos = world.get::<TilePosition>(item).expect("item should be on ground");
assert_eq!(*pos, TilePosition::new(5, 5, 0), "placed at player position");
let pos = world
.get::<TilePosition>(item)
.expect("item should be on ground");
assert_eq!(
*pos,
TilePosition::new(5, 5, 0),
"placed at player position"
);
assert!(world.get::<CarriedBy>(item).is_none(), "CarriedBy removed");
assert!(world.get::<InventorySlot>(item).is_none(), "InventorySlot removed");
assert!(
world.get::<InventorySlot>(item).is_none(),
"InventorySlot removed"
);
}
#[test]
@@ -739,7 +799,9 @@ mod tests {
let player = world
.spawn((PlayerCharacter, TilePosition::new(5, 5, 0)))
.id();
let player_sid = world.resource_mut::<crate::knowledge::EntityRegistry>().register(player);
let player_sid = world
.resource_mut::<crate::knowledge::EntityRegistry>()
.register(player);
// Item already in slot 0
world.spawn((
@@ -750,12 +812,11 @@ mod tests {
// New item on the ground
let item2 = world
.spawn((
TilePosition::new(5, 4, 0),
ItemName("Token".into()),
))
.spawn((TilePosition::new(5, 4, 0), ItemName("Token".into())))
.id();
let item2_sid = world.resource_mut::<crate::knowledge::EntityRegistry>().register(item2);
let item2_sid = world
.resource_mut::<crate::knowledge::EntityRegistry>()
.register(item2);
world.resource_mut::<InputQueue>().push(PlayerInput {
tick: 0,
@@ -769,7 +830,9 @@ mod tests {
schedule.add_systems(process_player_input);
schedule.run(&mut world);
let slot = world.get::<InventorySlot>(item2).expect("item should have slot");
let slot = world
.get::<InventorySlot>(item2)
.expect("item should have slot");
assert_eq!(slot.0, 1, "second item goes to slot 1");
}
@@ -783,7 +846,9 @@ mod tests {
let player = world
.spawn((PlayerCharacter, TilePosition::new(5, 5, 0)))
.id();
let player_sid = world.resource_mut::<crate::knowledge::EntityRegistry>().register(player);
let player_sid = world
.resource_mut::<crate::knowledge::EntityRegistry>()
.register(player);
// Fill all 9 slots
for slot in 0..MAX_INVENTORY_SLOTS {
@@ -796,12 +861,11 @@ mod tests {
// Try to take another item
let item = world
.spawn((
TilePosition::new(5, 4, 0),
ItemName("Overflow".into()),
))
.spawn((TilePosition::new(5, 4, 0), ItemName("Overflow".into())))
.id();
let item_sid = world.resource_mut::<crate::knowledge::EntityRegistry>().register(item);
let item_sid = world
.resource_mut::<crate::knowledge::EntityRegistry>()
.register(item);
world.resource_mut::<InputQueue>().push(PlayerInput {
tick: 0,
@@ -816,8 +880,14 @@ mod tests {
schedule.run(&mut world);
// Item should still be on the ground
assert!(world.get::<TilePosition>(item).is_some(), "item stays on ground");
assert!(world.get::<CarriedBy>(item).is_none(), "no CarriedBy when full");
assert!(
world.get::<TilePosition>(item).is_some(),
"item stays on ground"
);
assert!(
world.get::<CarriedBy>(item).is_none(),
"no CarriedBy when full"
);
}
#[test]
@@ -831,15 +901,16 @@ mod tests {
let player = world
.spawn((PlayerCharacter, TilePosition::new(5, 5, 0)))
.id();
let player_sid = world.resource_mut::<crate::knowledge::EntityRegistry>().register(player);
let player_sid = world
.resource_mut::<crate::knowledge::EntityRegistry>()
.register(player);
let item = world
.spawn((
TilePosition::new(5, 4, 0),
ItemName("Manifest Copy".into()),
))
.spawn((TilePosition::new(5, 4, 0), ItemName("Manifest Copy".into())))
.id();
let item_sid = world.resource_mut::<crate::knowledge::EntityRegistry>().register(item);
let item_sid = world
.resource_mut::<crate::knowledge::EntityRegistry>()
.register(item);
let mut schedule = bevy_ecs::schedule::Schedule::default();
schedule.add_systems(process_player_input);
@@ -854,7 +925,10 @@ mod tests {
});
schedule.run(&mut world);
assert!(world.get::<TilePosition>(item).is_none(), "item off ground after Take");
assert!(
world.get::<TilePosition>(item).is_none(),
"item off ground after Take"
);
assert_eq!(world.get::<CarriedBy>(item).unwrap().0, player_sid);
assert_eq!(world.get::<InventorySlot>(item).unwrap().0, 0);
@@ -869,10 +943,22 @@ mod tests {
world.resource_mut::<SimulationTime>().tick = 1;
schedule.run(&mut world);
let pos = world.get::<TilePosition>(item).expect("item back on ground after Place");
assert_eq!(*pos, TilePosition::new(5, 5, 0), "placed at player position");
assert!(world.get::<CarriedBy>(item).is_none(), "CarriedBy removed after Place");
assert!(world.get::<InventorySlot>(item).is_none(), "InventorySlot removed after Place");
let pos = world
.get::<TilePosition>(item)
.expect("item back on ground after Place");
assert_eq!(
*pos,
TilePosition::new(5, 5, 0),
"placed at player position"
);
assert!(
world.get::<CarriedBy>(item).is_none(),
"CarriedBy removed after Place"
);
assert!(
world.get::<InventorySlot>(item).is_none(),
"InventorySlot removed after Place"
);
}
#[test]
+170 -54
View File
@@ -52,30 +52,100 @@ impl ObjectType {
pub fn verb_set(&self) -> &'static [VerbDef] {
match self {
Self::Readable => &[
VerbDef { kind: VerbKind::Read, label: "Read", priority: 1, close_only: true },
VerbDef { kind: VerbKind::Observe, label: "Observe", priority: 2, close_only: false },
VerbDef {
kind: VerbKind::Read,
label: "Read",
priority: 1,
close_only: true,
},
VerbDef {
kind: VerbKind::Observe,
label: "Observe",
priority: 2,
close_only: false,
},
],
Self::Container => &[
VerbDef { kind: VerbKind::Open, label: "Open", priority: 1, close_only: true },
VerbDef { kind: VerbKind::Search, label: "Search", priority: 2, close_only: true },
VerbDef { kind: VerbKind::Observe, label: "Observe", priority: 3, close_only: false },
VerbDef {
kind: VerbKind::Open,
label: "Open",
priority: 1,
close_only: true,
},
VerbDef {
kind: VerbKind::Search,
label: "Search",
priority: 2,
close_only: true,
},
VerbDef {
kind: VerbKind::Observe,
label: "Observe",
priority: 3,
close_only: false,
},
],
Self::Terminal => &[
VerbDef { kind: VerbKind::Use, label: "Use", priority: 1, close_only: true },
VerbDef { kind: VerbKind::Observe, label: "Observe", priority: 2, close_only: false },
VerbDef {
kind: VerbKind::Use,
label: "Use",
priority: 1,
close_only: true,
},
VerbDef {
kind: VerbKind::Observe,
label: "Observe",
priority: 2,
close_only: false,
},
],
Self::Door => &[
VerbDef { kind: VerbKind::Open, label: "Open", priority: 1, close_only: true },
VerbDef { kind: VerbKind::Close, label: "Close", priority: 2, close_only: true },
VerbDef { kind: VerbKind::Observe, label: "Observe", priority: 3, close_only: false },
VerbDef {
kind: VerbKind::Open,
label: "Open",
priority: 1,
close_only: true,
},
VerbDef {
kind: VerbKind::Close,
label: "Close",
priority: 2,
close_only: true,
},
VerbDef {
kind: VerbKind::Observe,
label: "Observe",
priority: 3,
close_only: false,
},
],
Self::Pickup => &[
VerbDef { kind: VerbKind::Take, label: "Take", priority: 1, close_only: true },
VerbDef { kind: VerbKind::Observe, label: "Observe", priority: 2, close_only: false },
VerbDef {
kind: VerbKind::Take,
label: "Take",
priority: 1,
close_only: true,
},
VerbDef {
kind: VerbKind::Observe,
label: "Observe",
priority: 2,
close_only: false,
},
],
Self::Furniture => &[
VerbDef { kind: VerbKind::Sit, label: "Sit", priority: 1, close_only: true },
VerbDef { kind: VerbKind::Observe, label: "Observe", priority: 2, close_only: false },
VerbDef {
kind: VerbKind::Sit,
label: "Sit",
priority: 1,
close_only: true,
},
VerbDef {
kind: VerbKind::Observe,
label: "Observe",
priority: 2,
close_only: false,
},
],
}
}
@@ -201,7 +271,10 @@ pub fn compute_nearby_interactions(
.to_stable(entity)
.map(|sid| sid.0)
.unwrap_or_else(|| {
tracing::error!(?entity, "entity in interaction range but not in EntityRegistry");
tracing::error!(
?entity,
"entity in interaction range but not in EntityRegistry"
);
entity.to_bits()
});
@@ -216,9 +289,7 @@ pub fn compute_nearby_interactions(
}
// Sort interactions by distance (nearest first)
buffer
.interactions
.sort_by_key(|a| a.distance);
buffer.interactions.sort_by_key(|a| a.distance);
}
/// Buffer for nearby interaction results, consumed by snapshot generation.
@@ -417,11 +488,7 @@ mod tests {
fn door_close_range_gets_open_close_observe() {
let mut world = setup_world();
spawn_player(&mut world, 5, 5);
world.spawn((
TilePosition::new(5, 6, 0),
Interactable,
ObjectType::Door,
));
world.spawn((TilePosition::new(5, 6, 0), Interactable, ObjectType::Door));
let mut schedule = bevy_ecs::schedule::Schedule::default();
schedule.add_systems(compute_nearby_interactions);
@@ -439,11 +506,7 @@ mod tests {
fn pickup_close_range_gets_take_and_observe() {
let mut world = setup_world();
spawn_player(&mut world, 5, 5);
world.spawn((
TilePosition::new(5, 6, 0),
Interactable,
ObjectType::Pickup,
));
world.spawn((TilePosition::new(5, 6, 0), Interactable, ObjectType::Pickup));
let mut schedule = bevy_ecs::schedule::Schedule::default();
schedule.add_systems(compute_nearby_interactions);
@@ -514,7 +577,10 @@ mod tests {
let buffer = read_buffer(&mut world);
assert_eq!(buffer.interactions.len(), 1);
assert_eq!(buffer.interactions[0].verbs.len(), 1);
assert_eq!(buffer.interactions[0].verbs[0].kind, VerbKind::ExamineObject);
assert_eq!(
buffer.interactions[0].verbs[0].kind,
VerbKind::ExamineObject
);
}
#[test]
@@ -593,7 +659,10 @@ mod tests {
let buffer = read_buffer(&mut world);
assert_eq!(buffer.interactions.len(), 2);
assert_eq!(buffer.interactions[0].distance, buffer.interactions[1].distance);
assert_eq!(
buffer.interactions[0].distance,
buffer.interactions[1].distance
);
}
#[test]
@@ -708,11 +777,7 @@ mod tests {
let mut world = setup_world();
spawn_player(&mut world, 5, 5);
// Distance 4 = mid range (> CLOSE_RANGE=2, <= MID_RANGE=5)
world.spawn((
TilePosition::new(5, 9, 0),
Interactable,
obj_type,
));
world.spawn((TilePosition::new(5, 9, 0), Interactable, obj_type));
let mut schedule = bevy_ecs::schedule::Schedule::default();
schedule.add_systems(compute_nearby_interactions);
@@ -720,16 +785,22 @@ mod tests {
let buffer = read_buffer(&mut world);
assert_eq!(
buffer.interactions.len(), 1,
"{:?} at mid range should produce 1 interaction", obj_type
buffer.interactions.len(),
1,
"{:?} at mid range should produce 1 interaction",
obj_type
);
assert_eq!(
buffer.interactions[0].verbs.len(), 1,
"{:?} at mid range should have exactly 1 verb (Observe)", obj_type
buffer.interactions[0].verbs.len(),
1,
"{:?} at mid range should have exactly 1 verb (Observe)",
obj_type
);
assert_eq!(
buffer.interactions[0].verbs[0].kind, VerbKind::Observe,
"{:?} at mid range verb should be Observe", obj_type
buffer.interactions[0].verbs[0].kind,
VerbKind::Observe,
"{:?} at mid range verb should be Observe",
obj_type
);
}
}
@@ -795,11 +866,16 @@ mod tests {
for obj_type in types {
for def in obj_type.verb_set() {
if def.kind == VerbKind::Observe {
assert!(!def.close_only, "{:?} Observe should be mid-range", obj_type);
assert!(
!def.close_only,
"{:?} Observe should be mid-range",
obj_type
);
} else {
assert!(
def.close_only,
"{:?} {:?} should be close-only", obj_type, def.kind
"{:?} {:?} should be close-only",
obj_type, def.kind
);
}
}
@@ -822,7 +898,9 @@ mod tests {
let verbs = obj_type.verb_set();
assert!(
verbs.len() <= 4,
"{:?} has {} verbs, D-057 max is 4", obj_type, verbs.len()
"{:?} has {} verbs, D-057 max is 4",
obj_type,
verbs.len()
);
}
}
@@ -832,7 +910,12 @@ mod tests {
// -----------------------------------------------------------------------
/// Spawn player with Stance component for sprint suppression tests.
fn spawn_player_with_stance(world: &mut World, x: i32, y: i32, stance: MovementStance) -> Entity {
fn spawn_player_with_stance(
world: &mut World,
x: i32,
y: i32,
stance: MovementStance,
) -> Entity {
world
.spawn((
PlayerCharacter,
@@ -854,21 +937,31 @@ mod tests {
schedule.run(&mut world);
let buffer = read_buffer(&mut world);
assert!(buffer.interactions.is_empty(), "sprint should suppress all interactions");
assert!(
buffer.interactions.is_empty(),
"sprint should suppress all interactions"
);
}
#[test]
fn sprint_suppresses_object_interactions() {
let mut world = setup_world();
spawn_player_with_stance(&mut world, 5, 5, MovementStance::Sprint);
world.spawn((TilePosition::new(5, 6, 0), Interactable, ObjectType::Terminal));
world.spawn((
TilePosition::new(5, 6, 0),
Interactable,
ObjectType::Terminal,
));
let mut schedule = bevy_ecs::schedule::Schedule::default();
schedule.add_systems(compute_nearby_interactions);
schedule.run(&mut world);
let buffer = read_buffer(&mut world);
assert!(buffer.interactions.is_empty(), "sprint should suppress object interactions");
assert!(
buffer.interactions.is_empty(),
"sprint should suppress object interactions"
);
}
#[test]
@@ -882,7 +975,11 @@ mod tests {
schedule.run(&mut world);
let buffer = read_buffer(&mut world);
assert_eq!(buffer.interactions.len(), 1, "Walk should allow interactions");
assert_eq!(
buffer.interactions.len(),
1,
"Walk should allow interactions"
);
}
#[test]
@@ -896,7 +993,11 @@ mod tests {
schedule.run(&mut world);
let buffer = read_buffer(&mut world);
assert_eq!(buffer.interactions.len(), 1, "Careful should allow interactions");
assert_eq!(
buffer.interactions.len(),
1,
"Careful should allow interactions"
);
}
#[test]
@@ -910,7 +1011,11 @@ mod tests {
schedule.run(&mut world);
let buffer = read_buffer(&mut world);
assert_eq!(buffer.interactions.len(), 1, "Crouch should allow interactions");
assert_eq!(
buffer.interactions.len(),
1,
"Crouch should allow interactions"
);
}
#[test]
@@ -925,7 +1030,11 @@ mod tests {
schedule.run(&mut world);
let buffer = read_buffer(&mut world);
assert_eq!(buffer.interactions.len(), 1, "no Stance component should allow interactions");
assert_eq!(
buffer.interactions.len(),
1,
"no Stance component should allow interactions"
);
}
#[test]
@@ -933,7 +1042,11 @@ mod tests {
let mut world = setup_world();
spawn_player_with_stance(&mut world, 5, 5, MovementStance::Sprint);
world.spawn((Npc, TilePosition::new(5, 6, 0), Interactable));
world.spawn((TilePosition::new(6, 5, 0), Interactable, ObjectType::Container));
world.spawn((
TilePosition::new(6, 5, 0),
Interactable,
ObjectType::Container,
));
world.spawn((TilePosition::new(4, 5, 0), Interactable));
let mut schedule = bevy_ecs::schedule::Schedule::default();
@@ -941,6 +1054,9 @@ mod tests {
schedule.run(&mut world);
let buffer = read_buffer(&mut world);
assert!(buffer.interactions.is_empty(), "sprint should suppress all 3 nearby entities");
assert!(
buffer.interactions.is_empty(),
"sprint should suppress all 3 nearby entities"
);
}
}
+2 -8
View File
@@ -38,12 +38,7 @@ pub struct InventorySlot(pub u8);
/// Find the next available inventory slot for a carrier.
/// Returns None if all 9 slots are occupied.
pub fn find_next_slot(occupied: &[u8]) -> Option<u8> {
for slot in 0..MAX_INVENTORY_SLOTS {
if !occupied.contains(&slot) {
return Some(slot);
}
}
None
(0..MAX_INVENTORY_SLOTS).find(|slot| !occupied.contains(slot))
}
/// Collect inventory items for a specific carrier (by StableId).
@@ -126,8 +121,7 @@ mod tests {
let player = world.spawn_empty().id();
let player_sid = world.resource_mut::<EntityRegistry>().register(player);
let mut query_state =
world.query::<(Entity, &CarriedBy, &ItemName, &InventorySlot)>();
let mut query_state = world.query::<(Entity, &CarriedBy, &ItemName, &InventorySlot)>();
// Can't use system params directly in tests — use world query
// Instead, verify the logic by spawning items and checking
+36 -10
View File
@@ -36,9 +36,18 @@ pub(crate) const ANOMALY_DELAY_TICKS: u64 = 90;
/// Hardcoded v0.1 sprint anomaly "double-take" lines.
/// Future: move to content pools with trigger="sprint_anomaly".
const ANOMALY_LINES: &[(&str, &str)] = &[
("sprint_anomaly_01", "Wait \u{2014} something wasn't right back there."),
("sprint_anomaly_02", "Hold on. That face... why were they there?"),
("sprint_anomaly_03", "Something's off. That wasn't where they should be."),
(
"sprint_anomaly_01",
"Wait \u{2014} something wasn't right back there.",
),
(
"sprint_anomaly_02",
"Hold on. That face... why were they there?",
),
(
"sprint_anomaly_03",
"Something's off. That wasn't where they should be.",
),
];
/// Tracks monologue state for cooldown and trigger detection.
@@ -148,7 +157,11 @@ pub fn process_sprint_anomaly_monologue(
time: Res<SimulationTime>,
mut rng: ResMut<SimRng>,
mut query: Query<
(&mut SprintAnomalyQueue, &mut MonologueBuffer, &mut MonologueState),
(
&mut SprintAnomalyQueue,
&mut MonologueBuffer,
&mut MonologueState,
),
With<PlayerCharacter>,
>,
) {
@@ -236,7 +249,7 @@ pub fn trigger_monologue(
let character = state.character.as_str();
let mut candidates: Vec<(&str, &str)> = Vec::new(); // (id, text)
for (_district_id, district) in &content.0.districts {
for district in content.0.districts.values() {
for pool in &district.monologue_pools {
if pool.character != character {
continue;
@@ -255,7 +268,7 @@ pub fn trigger_monologue(
if candidates.is_empty() {
// All lines for this trigger have been shown; allow repeats
for (_district_id, district) in &content.0.districts {
for district in content.0.districts.values() {
for pool in &district.monologue_pools {
if pool.character != character {
continue;
@@ -688,7 +701,11 @@ mod tests {
// All hardcoded v0.1 lines should have id prefix and non-empty text
assert!(!ANOMALY_LINES.is_empty());
for (id, text) in ANOMALY_LINES {
assert!(id.starts_with("sprint_anomaly_"), "id={} should start with sprint_anomaly_", id);
assert!(
id.starts_with("sprint_anomaly_"),
"id={} should start with sprint_anomaly_",
id
);
assert!(!text.is_empty(), "text for {} should be non-empty", id);
}
}
@@ -716,10 +733,16 @@ mod tests {
schedule.run(&mut world);
let mut buf_query = world.query::<&MonologueBuffer>();
assert!(buf_query.single(&world).unwrap().event.is_none(), "should not fire before delay");
assert!(
buf_query.single(&world).unwrap().event.is_none(),
"should not fire before delay"
);
let mut q_query = world.query::<&SprintAnomalyQueue>();
assert!(q_query.single(&world).unwrap().has_pending(), "still pending before delay");
assert!(
q_query.single(&world).unwrap().has_pending(),
"still pending before delay"
);
// Tick 90: delay elapsed — should fire
world.resource_mut::<SimulationTime>().tick = ANOMALY_DELAY_TICKS;
@@ -734,7 +757,10 @@ mod tests {
// Queue should be cleared
let mut q_query = world.query::<&SprintAnomalyQueue>();
assert!(!q_query.single(&world).unwrap().has_pending(), "queue cleared after fire");
assert!(
!q_query.single(&world).unwrap().has_pending(),
"queue cleared after fire"
);
// last_fired_tick should be updated
let mut state_query = world.query::<&MonologueState>();
+13 -3
View File
@@ -261,7 +261,12 @@ pub struct MoveIntent {
pub fn validate_movement(
mut commands: Commands,
walkability: Option<Res<WalkabilityMap>>,
mut movers: Query<(Entity, &MoveIntent, &mut TilePosition, Option<&TilePresence>)>,
mut movers: Query<(
Entity,
&MoveIntent,
&mut TilePosition,
Option<&TilePresence>,
)>,
stationary: Query<(Entity, &TilePosition, Option<&TilePresence>), Without<MoveIntent>>,
) {
let Some(map) = walkability else {
@@ -290,12 +295,17 @@ pub fn validate_movement(
} else if occupied.contains_key(&slot) {
tracing::trace!(
"Entity {:?} blocked by entity at {:?} (layer {:?})",
entity, target, layer
entity,
target,
layer
);
} else {
tracing::trace!(
"Entity {:?} moving from {:?} to {:?} (layer {:?})",
entity, *position, target, layer
entity,
*position,
target,
layer
);
// Free old layer slot, claim new one
occupied.remove(&(*position, layer));
+1 -4
View File
@@ -151,10 +151,7 @@ mod tests {
Npc,
TilePosition::new(0, 0, 0),
ComputedPath {
steps: vec![
TilePosition::new(1, 0, 0),
TilePosition::new(2, 0, 0),
],
steps: vec![TilePosition::new(1, 0, 0), TilePosition::new(2, 0, 0)],
current_index: 0,
},
MovementSpeed::new(3),
+7 -5
View File
@@ -102,7 +102,12 @@ pub fn compute_paths(
Some((path, _cost)) => {
// path includes start position; skip it
let steps: Vec<TilePosition> = path.into_iter().skip(1).collect();
tracing::trace!("Entity {:?}: path to {:?}, {} steps", entity, goal, steps.len());
tracing::trace!(
"Entity {:?}: path to {:?}, {} steps",
entity,
goal,
steps.len()
);
commands.entity(entity).insert(ComputedPath {
steps,
current_index: 0,
@@ -208,10 +213,7 @@ mod tests {
world.insert_resource(map);
let entity = world
.spawn((
TilePosition::new(5, 5, 0),
PathRequest { goal },
))
.spawn((TilePosition::new(5, 5, 0), PathRequest { goal }))
.id();
let mut schedule = bevy_ecs::schedule::Schedule::default();
+8 -2
View File
@@ -193,7 +193,7 @@ mod tests {
fn cooldown_stance_switch_mid_cooldown() {
let mut cd = PlayerMoveCooldown::default();
assert!(cd.try_move(MovementStance::Crouch)); // move at crouch speed
// Switch to sprint mid-cooldown
// Switch to sprint mid-cooldown
assert!(cd.try_move(MovementStance::Sprint)); // sprint allows every tick
}
@@ -239,7 +239,13 @@ mod tests {
fn movement_profile_as_ecs_component() {
let mut world = bevy_ecs::world::World::new();
let profile = MovementProfile::smuggler();
let entity = world.spawn((profile, profile.initial_stance(), PlayerMoveCooldown::default())).id();
let entity = world
.spawn((
profile,
profile.initial_stance(),
PlayerMoveCooldown::default(),
))
.id();
let stored = world.get::<MovementProfile>(entity).unwrap();
assert_eq!(stored.default_stance, MovementStance::Walk);
+48 -12
View File
@@ -115,7 +115,10 @@ mod tests {
#[test]
fn tick_to_minute_conversion() {
let time = SimulationTime { tick: 10, ..Default::default() };
let time = SimulationTime {
tick: 10,
..Default::default()
};
assert_eq!(time.game_minutes(), 1);
}
@@ -123,18 +126,30 @@ mod tests {
fn day_phase_boundaries() {
let time = SimulationTime::default();
assert_eq!(time.day_phase(), DayPhase::Morning);
let time = SimulationTime { tick: 360 * TICKS_PER_GAME_MINUTE, ..Default::default() };
let time = SimulationTime {
tick: 360 * TICKS_PER_GAME_MINUTE,
..Default::default()
};
assert_eq!(time.day_phase(), DayPhase::Afternoon);
let time = SimulationTime { tick: 720 * TICKS_PER_GAME_MINUTE, ..Default::default() };
let time = SimulationTime {
tick: 720 * TICKS_PER_GAME_MINUTE,
..Default::default()
};
assert_eq!(time.day_phase(), DayPhase::Evening);
let time = SimulationTime { tick: 1080 * TICKS_PER_GAME_MINUTE, ..Default::default() };
let time = SimulationTime {
tick: 1080 * TICKS_PER_GAME_MINUTE,
..Default::default()
};
assert_eq!(time.day_phase(), DayPhase::Night);
}
#[test]
fn paused_prevents_tick_advance() {
let mut world = bevy_ecs::world::World::new();
world.insert_resource(SimulationTime { tick_rate: TickRate::Paused, ..Default::default() });
world.insert_resource(SimulationTime {
tick_rate: TickRate::Paused,
..Default::default()
});
let mut schedule = bevy_ecs::schedule::Schedule::default();
schedule.add_systems(advance_tick);
schedule.run(&mut world);
@@ -154,7 +169,10 @@ mod tests {
#[test]
fn half_rate_advances_every_two_frames() {
let mut world = bevy_ecs::world::World::new();
world.insert_resource(SimulationTime { tick_rate: TickRate::Half, ..Default::default() });
world.insert_resource(SimulationTime {
tick_rate: TickRate::Half,
..Default::default()
});
let mut schedule = bevy_ecs::schedule::Schedule::default();
schedule.add_systems(advance_tick);
@@ -179,15 +197,24 @@ mod tests {
fn paused_helper_method() {
let time = SimulationTime::default();
assert!(!time.paused());
let time = SimulationTime { tick_rate: TickRate::Paused, ..Default::default() };
let time = SimulationTime {
tick_rate: TickRate::Paused,
..Default::default()
};
assert!(time.paused());
let time = SimulationTime { tick_rate: TickRate::Half, ..Default::default() };
let time = SimulationTime {
tick_rate: TickRate::Half,
..Default::default()
};
assert!(!time.paused());
}
#[test]
fn day_wraparound_at_midnight() {
let time = SimulationTime { tick: MINUTES_PER_DAY * TICKS_PER_GAME_MINUTE, ..Default::default() };
let time = SimulationTime {
tick: MINUTES_PER_DAY * TICKS_PER_GAME_MINUTE,
..Default::default()
};
assert_eq!(time.day_phase(), DayPhase::Morning);
assert_eq!(time.time_of_day_minutes(), 0);
assert_eq!(time.day(), 1);
@@ -195,7 +222,10 @@ mod tests {
#[test]
fn day_calculation() {
let time = SimulationTime { tick: 3 * MINUTES_PER_DAY * TICKS_PER_GAME_MINUTE + 100, ..Default::default() };
let time = SimulationTime {
tick: 3 * MINUTES_PER_DAY * TICKS_PER_GAME_MINUTE + 100,
..Default::default()
};
assert_eq!(time.day(), 3);
}
@@ -203,7 +233,10 @@ mod tests {
fn tick_rate_switch_mid_accumulation() {
// Half->Full with 0.5 remainder: Full should tick immediately (0.5 + 1.0 >= 1.0)
let mut world = bevy_ecs::world::World::new();
world.insert_resource(SimulationTime { tick_rate: TickRate::Half, ..Default::default() });
world.insert_resource(SimulationTime {
tick_rate: TickRate::Half,
..Default::default()
});
let mut schedule = bevy_ecs::schedule::Schedule::default();
schedule.add_systems(advance_tick);
@@ -233,7 +266,10 @@ mod tests {
#[test]
fn half_rate_no_drift_over_10000_frames() {
let mut world = bevy_ecs::world::World::new();
world.insert_resource(SimulationTime { tick_rate: TickRate::Half, ..Default::default() });
world.insert_resource(SimulationTime {
tick_rate: TickRate::Half,
..Default::default()
});
let mut schedule = bevy_ecs::schedule::Schedule::default();
schedule.add_systems(advance_tick);