Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,8 +149,12 @@ Recommended views:
- [x] Closed NPC conversation sessions now update the NPC relationship ledger
for the speaking player actor, giving later prompts a durable familiarity and
affinity signal instead of only a transcript.
- [ ] Real combat damage, enemy rewards, loot drops, quest progress, and player
time-loot from other users are not implemented yet.
- [x] Prototype quest state now lives in Nakama with validated accept,
progress, and reward-claim RPCs. The first demo quest, `Check The Ash
Underpass Signal`, requires local NPC confirmations before its BodyTime reward
can be claimed.
- [ ] Real combat damage, enemy rewards, loot drops, production quest content,
and player time-loot from other users are not implemented yet.

## Current Review Gate

Expand Down
72 changes: 72 additions & 0 deletions Unity/Assets/_SecondSpawn/Scripts/AI/AgentContextDto.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ public sealed class BodyProfileDto
public FrameHeartbeatDto heartbeat;
public AgentPolicyDto agent_policy;
public SoulProfileDto soul;
public QuestProgressDto[] quests;
public MemoryRecordDto[] memory;
public AgentRuntimeDto agent_runtime;
public AgentActivityRecordDto[] agent_activity;
Expand Down Expand Up @@ -259,6 +260,77 @@ public sealed class RewardClaimRequestDto
public string objective_id = "prototype-training-drone";
}

[Serializable]
public sealed class QuestListResponseDto
{
public QuestProgressDto[] quests;
public long active_count;
public long completed_count;
}

[Serializable]
public sealed class QuestAcceptRequestDto
{
public string quest_id = "prototype-ash-underpass-signal";
}

[Serializable]
public sealed class QuestAcceptResponseDto
{
public QuestProgressDto quest;
public QuestProgressDto[] quests;
}

[Serializable]
public sealed class QuestProgressRequestDto
{
public string quest_id = "prototype-ash-underpass-signal";
public string step_id;
public string actor_id;
public string source = "player_dialog";
public int amount = 1;
}

[Serializable]
public sealed class QuestProgressResponseDto
{
public QuestProgressDto quest;
public bool changed;
public string completed_step_id;
public bool ready_to_claim;
public QuestProgressDto[] quests;
}

[Serializable]
public sealed class QuestProgressDto
{
public string quest_id;
public string title;
public string giver_actor_id;
public string[] recommended_actor_ids;
public string summary;
public string objective;
public string reward_objective_id;
public string status;
public string accepted_at;
public string completed_at;
public string claimed_at;
public QuestStepProgressDto[] steps;
}

[Serializable]
public sealed class QuestStepProgressDto
{
public string step_id;
public string title;
public long required_count = 1;
public long count;
public bool completed;
public string completed_at;
public string last_actor_id;
public string last_source;
}

[Serializable]
public sealed class AgentPolicyDto
{
Expand Down
52 changes: 52 additions & 0 deletions Unity/Assets/_SecondSpawn/Scripts/AI/CharacterMemorySync.cs
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,58 @@ public IEnumerator ClaimPrototypeReward(string objectiveId)
yield return ApplyProfileToLocalPlayerWhenAvailable();
}

public IEnumerator AcceptPrototypeQuest(string questId)
{
if (!_preferNakama || !_gateway.HasNakamaSession)
{
Debug.LogWarning("[CharacterMemorySync] Quest acceptance requires an authenticated Nakama session.");
yield break;
}

QuestAcceptResponseDto response = null;
string error = null;
yield return _gateway.AcceptNakamaQuest(new QuestAcceptRequestDto
{
quest_id = string.IsNullOrWhiteSpace(questId) ? "prototype-ash-underpass-signal" : questId.Trim()
}, value => response = value, value => error = value);

if (response == null)
{
Debug.LogWarning($"[CharacterMemorySync] Quest accept failed: {error}");
yield break;
}

yield return Refresh();
}

public IEnumerator ProgressPrototypeQuest(string questId, string stepId, string actorId, string source = "player_dialog")
{
if (!_preferNakama || !_gateway.HasNakamaSession)
{
Debug.LogWarning("[CharacterMemorySync] Quest progress requires an authenticated Nakama session.");
yield break;
}

QuestProgressResponseDto response = null;
string error = null;
yield return _gateway.ProgressNakamaQuest(new QuestProgressRequestDto
{
quest_id = string.IsNullOrWhiteSpace(questId) ? "prototype-ash-underpass-signal" : questId.Trim(),
step_id = string.IsNullOrWhiteSpace(stepId) ? "ask_route_or_gate" : stepId.Trim(),
actor_id = string.IsNullOrWhiteSpace(actorId) ? "npc-route-courier-0244" : actorId.Trim(),
source = string.IsNullOrWhiteSpace(source) ? "player_dialog" : source.Trim(),
amount = 1
}, value => response = value, value => error = value);

if (response == null)
{
Debug.LogWarning($"[CharacterMemorySync] Quest progress failed: {error}");
yield break;
}

yield return Refresh();
}

private IEnumerator WaitForAuthAttempt()
{
const float maxWaitSeconds = 10f;
Expand Down
107 changes: 107 additions & 0 deletions Unity/Assets/_SecondSpawn/Scripts/AI/PrototypeNearbyNpcChatBox.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ public sealed class PrototypeNearbyNpcChatBox : MonoBehaviour
private string _status = "Nearby NPC chat ready";
private Vector2 _historyScrollPosition;
private bool _societyEventRpcAvailable = true;
private CharacterMemorySync _memorySync;
private string _focusedNpcActorId;
private string _focusedNpcDisplayName;
private PrototypeAgentBrain _focusedNpcBrain;
Expand Down Expand Up @@ -101,6 +102,7 @@ private static void AttachToGatewayOnSceneLoad()
private void Awake()
{
_gateway = GetComponent<SecondSpawnGatewayClient>();
_memorySync = GetComponent<CharacterMemorySync>();
}

private void Update()
Expand Down Expand Up @@ -278,6 +280,8 @@ private IEnumerator SendNearbyMessage(string message, List<string> explicitActor
yield break;
}

yield return TryProgressPrototypeQuestFromNpcChat(nearbyActorIds);

NpcSocietyTickResponseDto tickResponse = null;
error = null;
yield return _gateway.TickNpcSociety(new NpcSocietyTickRequestDto
Expand Down Expand Up @@ -323,6 +327,109 @@ private IEnumerator SendNearbyMessage(string message, List<string> explicitActor
_busy = false;
}

private IEnumerator TryProgressPrototypeQuestFromNpcChat(List<string> nearbyActorIds)
{
if (_gateway == null || nearbyActorIds == null || nearbyActorIds.Count == 0)
{
yield break;
}

var stepId = ResolvePrototypeQuestStepId(nearbyActorIds);
if (string.IsNullOrWhiteSpace(stepId))
{
yield break;
}

var actorId = ResolvePrototypeQuestActorId(nearbyActorIds, stepId);
if (string.IsNullOrWhiteSpace(actorId))
{
yield break;
}

QuestProgressResponseDto response = null;
string error = null;
yield return _gateway.ProgressNakamaQuest(new QuestProgressRequestDto
{
quest_id = "prototype-ash-underpass-signal",
step_id = stepId,
actor_id = actorId,
source = "player_dialog",
amount = 1
}, value => response = value, value => error = value);

if (response == null)
{
Debug.LogWarning($"[PrototypeNearbyNpcChatBox] Quest progress failed actor={actorId}, step={stepId}: {Shorten(error, 100)}");
yield break;
}

if (response.changed)
{
_status = response.ready_to_claim
? "Quest ready to claim: Check The Ash Underpass Signal."
: "Quest updated: Check The Ash Underpass Signal.";
Debug.Log($"[PrototypeNearbyNpcChatBox] Quest progress changed actor={actorId}, step={stepId}, ready={response.ready_to_claim}");
if (_memorySync != null)
{
yield return _memorySync.Refresh();
}
}
}

private static string ResolvePrototypeQuestStepId(List<string> actorIds)
{
foreach (var actorId in actorIds)
{
if (IsRouteOrGateQuestActor(actorId))
{
return "ask-route-or-gate";
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Use canonical quest step IDs in NPC chat progress

ResolvePrototypeQuestStepId emits ask-route-or-gate / confirm-second-source, but the server quest catalog and progress path use ask_route_or_gate / confirm_second_source (backend/nakama/modules/index.ts, prototypeQuestCatalog and requirePrototypeQuestStep). Because normalizeQuestStepId does not convert hyphens to underscores, these chat-triggered progress RPCs hit unknown prototype quest step, so the new nearby-NPC chat loop cannot advance the quest and players can be blocked from reaching the reward-claim state through this intended path.

Useful? React with 👍 / 👎.

Comment on lines +383 to +385
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Choose incomplete step before routing quest progress

ResolvePrototypeQuestStepId always prefers route/gate actors over second-source actors, regardless of current quest progress. When both actor groups are nearby and the first step is already complete, chat keeps sending progress for the completed first step; the server treats completed steps as a no-op, so the second step never advances until the player leaves route/gate proximity. This can stall quest completion in mixed-NPC areas.

Useful? React with 👍 / 👎.

}
}

foreach (var actorId in actorIds)
{
if (IsSecondSourceQuestActor(actorId))
{
return "confirm-second-source";
}
}

return "";
}

private static string ResolvePrototypeQuestActorId(List<string> actorIds, string stepId)
{
foreach (var actorId in actorIds)
{
if (stepId == "ask-route-or-gate" && IsRouteOrGateQuestActor(actorId))
{
return actorId;
}

if (stepId == "confirm-second-source" && IsSecondSourceQuestActor(actorId))
{
return actorId;
}
}

return "";
}

private static bool IsRouteOrGateQuestActor(string actorId)
{
return actorId == "npc-route-courier-0244" ||
actorId == "npc-gate-sentinel-0101" ||
actorId == "npc-gate-sentinel-0627";
}

private static bool IsSecondSourceQuestActor(string actorId)
{
return actorId == "npc-crossline-hunter-1058" ||
actorId == "npc-crossline-hunter-5104" ||
actorId == "npc-clinic-operator-0320" ||
actorId == "npc-scrap-warden-0441";
}

private static void ShowPlayerSpeechBubble(NetworkPlayer player, string message)
{
if (player == null || string.IsNullOrWhiteSpace(message))
Expand Down
15 changes: 15 additions & 0 deletions Unity/Assets/_SecondSpawn/Scripts/AI/SecondSpawnGatewayClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,21 @@ public IEnumerator ClaimNakamaReward(RewardClaimRequestDto request, Action<Agent
yield return SendNakamaRpc("secondspawn_reward_claim", request, onSuccess, onError);
}

public IEnumerator ListNakamaQuests(Action<QuestListResponseDto> onSuccess, Action<string> onError = null)
{
yield return SendNakamaRpc("secondspawn_quest_list", new EmptyPayload(), onSuccess, onError);
}

public IEnumerator AcceptNakamaQuest(QuestAcceptRequestDto request, Action<QuestAcceptResponseDto> onSuccess = null, Action<string> onError = null)
{
yield return SendNakamaRpc("secondspawn_quest_accept", request, onSuccess, onError);
}

public IEnumerator ProgressNakamaQuest(QuestProgressRequestDto request, Action<QuestProgressResponseDto> onSuccess = null, Action<string> onError = null)
{
yield return SendNakamaRpc("secondspawn_quest_progress", request, onSuccess, onError);
}

public IEnumerator UpdateNakamaSoul(UpdateSoulRequestDto request, Action<AgentContextDto> onSuccess = null, Action<string> onError = null)
{
yield return SendNakamaRpc("secondspawn_soul_update", request, onSuccess, onError);
Expand Down
Loading
Loading