feat(simulation): add follow mechanic (#241)

Follow verb on interaction dispatcher. FollowTarget component tracks
target entity, proximity ticks, and LOS-lost ticks. update_follow_state
system: observation events fire at double frequency while following,
NPC suspicion increases via stress when player within 2 tiles for 60+
ticks (configurable). Follow ends on LOS lost timeout, suspicion
threshold crossed, or player issues different action. FollowStateWire
emitted in ObserverSnapshot for client HUD.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-21 14:39:47 +01:00
co-authored by Claude Opus 4.6
parent f31be6f866
commit 0af86b1f0b
3 changed files with 1132 additions and 9 deletions
+16 -7
View File
@@ -208,9 +208,9 @@ pub fn compute_nearby_interactions(
let mut verbs = Vec::new();
if is_npc.is_some() {
// NPC verb logic — unchanged from #404
// NPC verb logic — Talk + ExamineNpc (#404), Follow (#241)
if is_close {
// Default priority: Talk first, Observe second.
// Default priority: Talk first, Observe second, Follow third.
// Observer adjusts priority for POI entities.
verbs.push(VerbOption {
kind: VerbKind::Talk,
@@ -224,8 +224,14 @@ pub fn compute_nearby_interactions(
priority: 2,
available: true,
});
verbs.push(VerbOption {
kind: VerbKind::Follow,
label: "Follow".into(),
priority: 3,
available: true,
});
} else {
// Mid range: only Examine NPC (Talk requires close range)
// Mid range: only Examine NPC (Talk + Follow require close range)
verbs.push(VerbOption {
kind: VerbKind::ExamineNpc,
label: "Examine NPC".into(),
@@ -346,7 +352,7 @@ mod tests {
// -----------------------------------------------------------------------
#[test]
fn npc_in_close_range_gets_talk_and_observe() {
fn npc_in_close_range_gets_talk_examine_follow() {
let mut world = setup_world();
spawn_player(&mut world, 5, 5);
world.spawn((Npc, TilePosition::new(5, 6, 0), Interactable));
@@ -357,11 +363,13 @@ mod tests {
let buffer = read_buffer(&mut world);
assert_eq!(buffer.interactions.len(), 1);
assert_eq!(buffer.interactions[0].verbs.len(), 2);
assert_eq!(buffer.interactions[0].verbs.len(), 3);
assert_eq!(buffer.interactions[0].verbs[0].kind, VerbKind::Talk);
assert_eq!(buffer.interactions[0].verbs[0].priority, 1);
assert_eq!(buffer.interactions[0].verbs[1].kind, VerbKind::ExamineNpc);
assert_eq!(buffer.interactions[0].verbs[1].priority, 2);
assert_eq!(buffer.interactions[0].verbs[2].kind, VerbKind::Follow);
assert_eq!(buffer.interactions[0].verbs[2].priority, 3);
}
#[test]
@@ -845,10 +853,11 @@ mod tests {
let buffer = read_buffer(&mut world);
assert_eq!(buffer.interactions.len(), 1);
// Should get NPC verbs (Talk + ExamineNpc), NOT Terminal verbs (Use + Observe)
assert_eq!(buffer.interactions[0].verbs.len(), 2);
// Should get NPC verbs (Talk + ExamineNpc + Follow), NOT Terminal verbs (Use + Observe)
assert_eq!(buffer.interactions[0].verbs.len(), 3);
assert_eq!(buffer.interactions[0].verbs[0].kind, VerbKind::Talk);
assert_eq!(buffer.interactions[0].verbs[1].kind, VerbKind::ExamineNpc);
assert_eq!(buffer.interactions[0].verbs[2].kind, VerbKind::Follow);
}
/// All ObjectType primary verbs are close_only (except Observe).