diff --git a/CHANGELOG.md b/CHANGELOG.md index dfc6545..8f79492 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,8 +14,8 @@ versioned release tag yet, so entries are organized as pre-alpha snapshots. - State of Mind and behavior-tendency backend expansion for issue #134: NPC heartbeats now track fear, confidence, and current goal, personality-derived tendencies now include social initiative, risk tolerance, idle radius, and - approach style, and model decision context receives a compact runtime state - card for more varied NPC behavior. + approach style, model decision context receives a compact runtime state card, + and Unity debug HUD DTOs can display the expanded runtime state. - Linux headless dedicated server build path for issue #36: Unity now has a `BuildLinuxDedicatedServer` editor build method, `tools/build-unity-linux-server.ps1` runs batchmode/nographics/server builds, and setup docs define prerequisites, diff --git a/Unity/Assets/_SecondSpawn/Scripts/AI/AgentContextDto.cs b/Unity/Assets/_SecondSpawn/Scripts/AI/AgentContextDto.cs index 96ccba0..33c5890 100644 --- a/Unity/Assets/_SecondSpawn/Scripts/AI/AgentContextDto.cs +++ b/Unity/Assets/_SecondSpawn/Scripts/AI/AgentContextDto.cs @@ -248,7 +248,10 @@ public sealed class FrameHeartbeatDto public string fallback_state; public string mood; public int stress; + public int fear; + public int confidence; public string dominant_need; + public string current_goal; public string last_trigger; public string last_plan_summary; } diff --git a/Unity/Assets/_SecondSpawn/Scripts/UI/HUDController.cs b/Unity/Assets/_SecondSpawn/Scripts/UI/HUDController.cs index 0bef359..c27987f 100644 --- a/Unity/Assets/_SecondSpawn/Scripts/UI/HUDController.cs +++ b/Unity/Assets/_SecondSpawn/Scripts/UI/HUDController.cs @@ -721,8 +721,9 @@ private string BuildAgentActivityText(AgentContextDto context) builder.AppendLine($"Offline {FormatSeconds(runtime.offline_seconds)} | Last {FormatTimestamp(runtime.last_activity_at)}"); if (body.heartbeat != null) { - builder.AppendLine($"Mind {Fallback(body.heartbeat.mood, "steady")} | Stress {body.heartbeat.stress}/10"); + builder.AppendLine($"Mind {Fallback(body.heartbeat.mood, "steady")} | Stress {body.heartbeat.stress}/10 | Fear {body.heartbeat.fear}/10 | Conf {body.heartbeat.confidence}/10"); builder.AppendLine($"Need {TrimForHud(Fallback(body.heartbeat.dominant_need, "preserve BodyTime"), 58)}"); + builder.AppendLine($"Goal {TrimForHud(Fallback(body.heartbeat.current_goal, "stay alive"), 58)}"); } return builder.ToString(); @@ -1358,7 +1359,9 @@ private void DrawStateOfMind(FrameHeartbeatDto heartbeat) return; } - GUILayout.Label($"Mind {Fallback(heartbeat.mood, "steady")} | Stress {heartbeat.stress}/10 | Need {TrimForHud(Fallback(heartbeat.dominant_need, "preserve BodyTime"), 56)}", _labelStyle); + GUILayout.Label($"Mind {Fallback(heartbeat.mood, "steady")} | Stress {heartbeat.stress}/10 | Fear {heartbeat.fear}/10 | Conf {heartbeat.confidence}/10", _labelStyle); + GUILayout.Label($"Need: {TrimForHud(Fallback(heartbeat.dominant_need, "preserve BodyTime"), 76)}", _wrapStyle); + GUILayout.Label($"Goal: {TrimForHud(Fallback(heartbeat.current_goal, "stay alive and preserve context"), 76)}", _wrapStyle); GUILayout.Label($"Plan: {TrimForHud(Fallback(heartbeat.last_plan_summary, heartbeat.last_action_summary, "No plan yet."), 92)}", _wrapStyle); } diff --git a/backend/nakama/tests/supabase_custom_auth.test.mjs b/backend/nakama/tests/supabase_custom_auth.test.mjs index 0ffa92e..4e88861 100644 --- a/backend/nakama/tests/supabase_custom_auth.test.mjs +++ b/backend/nakama/tests/supabase_custom_auth.test.mjs @@ -462,6 +462,59 @@ assert.equal(seededNpcs.npcs[7].body.visual_variant, 17); assert.equal(seededNpcs.npcs[8].body.visual_variant, 15); assert.ok(harness.storage.get(storageKey("user-1", "secondspawn_actor", "world_profile:npc-synthetic-sentinel-0101"))); +const cautiousTendencyProfile = JSON.parse(harness.registeredRpcs.get("secondspawn_actor_profile_get")( + { userId: "user-1", env: {} }, + harness.logger, + harness.nk, + JSON.stringify({ + actor_id: "npc-tendency-cautious", + display_name: "Cautious Tendency", + story: { role: "relay yard drifter" }, + characteristics: { courage: 2, discipline: 8, empathy: 7, curiosity: 1, aggression: 1, sociability: 1 } + }) +)); +const socialTendencyProfile = JSON.parse(harness.registeredRpcs.get("secondspawn_actor_profile_get")( + { userId: "user-1", env: {} }, + harness.logger, + harness.nk, + JSON.stringify({ + actor_id: "npc-tendency-social", + display_name: "Social Tendency", + story: { role: "relay yard drifter" }, + characteristics: { courage: 6, discipline: 3, empathy: 7, curiosity: 9, aggression: 1, sociability: 9 } + }) +)); +assert.ok(socialTendencyProfile.body.behavior_tendencies.talk_frequency > cautiousTendencyProfile.body.behavior_tendencies.talk_frequency); +assert.ok(socialTendencyProfile.body.behavior_tendencies.social_initiative > cautiousTendencyProfile.body.behavior_tendencies.social_initiative); +assert.ok(socialTendencyProfile.body.behavior_tendencies.idle_radius_meters > cautiousTendencyProfile.body.behavior_tendencies.idle_radius_meters); +assert.ok(cautiousTendencyProfile.body.behavior_tendencies.approach_style.length > 0); + +const malformedHeartbeatProfile = JSON.parse(harness.registeredRpcs.get("secondspawn_actor_profile_get")( + { userId: "user-1", env: {} }, + harness.logger, + harness.nk, + JSON.stringify({ + actor_id: "npc-malformed-heartbeat", + display_name: "Malformed Heartbeat", + heartbeat: { + mood: "", + stress: -5, + fear: 99, + confidence: -8, + dominant_need: "", + current_goal: "", + last_trigger: "", + last_plan_summary: "" + } + }) +)); +assert.equal(malformedHeartbeatProfile.body.heartbeat.mood, "steady"); +assert.equal(malformedHeartbeatProfile.body.heartbeat.stress, 0); +assert.equal(malformedHeartbeatProfile.body.heartbeat.fear, 10); +assert.equal(malformedHeartbeatProfile.body.heartbeat.confidence, 0); +assert.equal(malformedHeartbeatProfile.body.heartbeat.dominant_need, "preserve BodyTime"); +assert.equal(malformedHeartbeatProfile.body.heartbeat.current_goal, "stay alive and preserve useful context"); + const playerChatEvent = JSON.parse(harness.registeredRpcs.get("secondspawn_npc_player_chat_event")( { userId: "user-1", env: {} }, harness.logger,