From ea334131873beb40a43ac32a6434673b4409941c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=AB=98=E7=89=A9?= <253518756@qq.com> Date: Sun, 26 Apr 2026 14:27:48 +0800 Subject: [PATCH] 1 --- ..._AND_INLINE_FUNCTION_OPTIONS_2026-04-25.md | 80 ++ docs/experience/AGENT_UI_CHANGELOG.md | 8 + docs/experience/MOBILE_UI_DEV_EXPERIENCE.md | 12 + ...REATOR_AND_GAMEPLAY_FLOW_PRD_2026-04-20.md | 9 +- ...ON_SCENE_WORLD_MAP_PANEL_FIX_2026-04-25.md | 32 + ..._WORLD_ASSET_PROMPT_DEFAULTS_2026-04-24.md | 1 + ...NE_TASK_AND_ACT_EVENT_FIELDS_2026-04-25.md | 22 + ...T_EXTERNAL_GENERATION_STAGE2_2026-04-23.md | 4 +- ...AND_COMMUNITY_IMPLEMENTATION_2026-04-25.md | 7 + ...IELD_ANCHOR_CONTENT_REFACTOR_2026-04-25.md | 52 ++ ...ATOR_IMPLEMENTATION_PROGRESS_2026-04-20.md | 3 + .../src/contracts/customWorldAgentAnchors.ts | 9 +- .../shared/src/contracts/rpgAgentAnchors.ts | 66 +- .../shared/src/contracts/rpgContracts.test.ts | 2 +- .../src/contracts/rpgCreationFixtures.ts | 58 +- .../shared/src/contracts/rpgRuntimeChat.ts | 22 + packages/shared/src/contracts/runtime.ts | 26 + ...erver-auth-store-creation-doc-bad-ext.json | 28 + ...-auth-store-creation-doc-large-base64.json | 28 + ...api-server-auth-store-creation-doc-ok.json | 28 + .../server-rs/.data/auth-store.json | 56 ++ server-rs/crates/api-server/src/app.rs | 31 +- server-rs/crates/api-server/src/big_fish.rs | 8 +- .../api-server/src/character_visual_assets.rs | 32 +- .../crates/api-server/src/custom_world.rs | 5 +- .../api-server/src/custom_world_agent_turn.rs | 348 +++----- .../crates/api-server/src/custom_world_ai.rs | 36 +- .../src/custom_world_foundation_draft.rs | 197 +++-- .../api-server/src/prompt/agent_chat.rs | 64 +- .../api-server/src/prompt/character_visual.rs | 30 +- .../api-server/src/prompt/foundation_draft.rs | 9 +- server-rs/crates/api-server/src/puzzle.rs | 77 +- .../crates/api-server/src/runtime_chat.rs | 253 +++++- .../api-server/src/runtime_chat_prompt.rs | 105 ++- .../crates/api-server/src/runtime_profile.rs | 135 ++- .../crates/module-custom-world/src/lib.rs | 2 +- .../crates/shared-contracts/src/assets.rs | 2 - .../crates/shared-contracts/src/runtime.rs | 34 + server-rs/crates/spacetime-client/src/lib.rs | 7 +- .../crates/spacetime-client/src/mapper.rs | 95 +++ ...rofile_referral_invite_center_procedure.rs | 58 ++ .../src/module_bindings/mod.rs | 533 +++++++++++- .../profile_invite_code_table.rs | 194 +++++ .../profile_invite_code_type.rs | 71 ++ .../profile_membership_table.rs | 165 ++++ .../profile_recharge_order_table.rs | 165 ++++ .../profile_referral_relation_table.rs | 163 ++++ .../profile_referral_relation_type.rs | 77 ++ ..._profile_referral_invite_code_procedure.rs | 58 ++ ...e_referral_invite_center_get_input_type.rs | 23 + ...ral_invite_center_procedure_result_type.rs | 26 + ...me_referral_invite_center_snapshot_type.rs | 34 + .../runtime_referral_redeem_input_type.rs | 25 + ...e_referral_redeem_procedure_result_type.rs | 26 + .../runtime_referral_redeem_snapshot_type.rs | 28 + .../crates/spacetime-client/src/runtime.rs | 45 + .../spacetime-module/src/custom_world/mod.rs | 2 + server-rs/crates/spacetime-module/src/lib.rs | 2 + .../spacetime-module/src/runtime/profile.rs | 277 ++++++ src/components/CustomWorldEntityCatalog.tsx | 22 +- .../CustomWorldEntityEditorModal.test.tsx | 164 +++- src/components/CustomWorldNpcVisualEditor.tsx | 4 +- src/components/CustomWorldResultView.test.tsx | 67 +- .../characterAssetWorkflowPersistence.ts | 1 - ...omWorldAgentWorkspace.interaction.test.tsx | 16 +- .../CustomWorldAgentWorkspace.test.tsx | 11 +- .../GameCanvasEntityLayer.test.tsx | 17 + .../game-canvas/GameCanvasEntityLayer.tsx | 10 +- .../game-canvas/GameCanvasRuntime.tsx | 11 +- .../game-canvas/GameCanvasShared.tsx | 36 +- .../PlatformEntryFlowShellImpl.tsx | 2 + .../RpgCreationRoleAssetStudioModalImpl.tsx | 1 - .../useRoleVisualCandidateWorkflow.ts | 3 - .../RpgCreationEntityEditorShared.tsx | 670 ++++++++------- ...gEntryFlowShell.agent.interaction.test.tsx | 107 +-- src/components/rpg-entry/RpgEntryHomeView.tsx | 265 +++++- .../useRpgCreationEnterWorld.test.tsx | 2 +- .../useRpgEntryAgentDraftRestore.test.tsx | 2 +- .../RpgAdventurePanel.npcChat.test.tsx | 11 +- .../RpgAdventurePanel.test.tsx | 21 +- .../rpg-runtime-panels/RpgAdventurePanel.tsx | 161 ++-- .../RpgRuntimePanelRouter.tsx | 18 +- .../rpg-runtime-shell/RpgRuntimeShell.tsx | 5 +- .../RpgRuntimeStageRouter.tsx | 5 +- src/components/rpg-runtime-shell/types.ts | 8 +- src/data/customWorldLibrary.ts | 1 + src/hooks/combat/resolvedChoice.test.ts | 49 ++ src/hooks/combat/resolvedChoice.ts | 17 +- .../rpg-runtime-story/choiceActions.test.ts | 120 +++ src/hooks/rpg-runtime-story/choiceActions.ts | 19 + .../npcEncounterActions.test.ts | 390 ++++++--- .../useRpgRuntimeInteractionFlow.ts | 28 +- .../useRpgRuntimeNpcInteraction.ts | 475 ++++++++--- .../rpg-runtime-story/useRpgRuntimeStory.ts | 8 +- .../useRpgRuntimeStoryFlow.ts | 6 +- src/hooks/rpg-session/useRpgRuntimeSession.ts | 1 + src/index.css | 83 ++ src/services/aiService.ts | 23 +- ...customWorldAgentGenerationProgress.test.ts | 22 +- .../customWorldAgentGenerationProgress.ts | 145 +--- src/services/customWorldFoundationEntries.ts | 307 ++----- src/services/customWorldSceneActRuntime.ts | 127 ++- .../rpgCreationPreviewAdapter.test.ts | 2 +- src/services/rpg-entry/rpgProfileClient.ts | 31 + .../generated/ai_result_reference_table.ts | 27 + .../generated/ai_task_stage_table.ts | 35 + src/spacetime/generated/ai_task_table.ts | 39 + .../generated/ai_text_chunk_table.ts | 27 + .../generated/asset_entity_binding_table.ts | 24 + src/spacetime/generated/asset_object_table.ts | 35 + .../generated/auth_identity_table.ts | 22 + .../generated/auth_store_snapshot_table.ts | 17 + src/spacetime/generated/battle_state_table.ts | 56 ++ .../generated/big_fish_agent_message_table.ts | 29 + .../generated/big_fish_asset_slot_table.ts | 32 + .../big_fish_creation_session_table.ts | 33 + .../generated/big_fish_runtime_run_table.ts | 30 + .../generated/chapter_progression_table.ts | 41 + .../custom_world_agent_message_table.ts | 30 + .../custom_world_agent_operation_table.ts | 33 + .../custom_world_agent_session_table.ts | 44 + .../custom_world_draft_card_table.ts | 40 + .../generated/custom_world_profile_table.ts | 42 + .../generated/custom_world_session_table.ts | 34 + ...rofile_referral_invite_center_procedure.ts | 23 + src/spacetime/generated/index.ts | 798 ++++++++++++++++++ .../generated/inventory_slot_table.ts | 49 ++ src/spacetime/generated/npc_state_table.ts | 41 + .../generated/player_progression_table.ts | 29 + .../profile_dashboard_state_table.ts | 19 + .../generated/profile_invite_code_table.ts | 18 + .../generated/profile_membership_table.ts | 29 + .../generated/profile_played_world_table.ts | 25 + .../generated/profile_recharge_order_table.ts | 35 + .../profile_referral_relation_table.ts | 20 + .../generated/profile_save_archive_table.ts | 30 + .../generated/profile_wallet_ledger_table.ts | 26 + .../generated/puzzle_agent_message_table.ts | 29 + .../generated/puzzle_agent_session_table.ts | 32 + .../generated/puzzle_runtime_run_table.ts | 26 + .../generated/puzzle_work_profile_table.ts | 37 + src/spacetime/generated/quest_log_table.ts | 40 + src/spacetime/generated/quest_record_table.ts | 64 ++ ..._profile_referral_invite_code_procedure.ts | 23 + .../generated/refresh_session_table.ts | 24 + .../generated/runtime_setting_table.ts | 25 + .../generated/runtime_snapshot_table.ts | 22 + src/spacetime/generated/story_event_table.ts | 26 + .../generated/story_session_table.ts | 32 + .../generated/treasure_record_table.ts | 39 + src/spacetime/generated/types.ts | 75 ++ src/spacetime/generated/types/procedures.ts | 6 + src/spacetime/generated/user_account_table.ts | 26 + .../generated/user_browse_history_table.ts | 33 + src/types/story.ts | 10 +- 155 files changed, 8130 insertions(+), 1740 deletions(-) create mode 100644 docs/design/RPG_NPC_CHAT_HOSTILE_TERMINATION_AND_INLINE_FUNCTION_OPTIONS_2026-04-25.md create mode 100644 docs/technical/CREATION_SCENE_WORLD_MAP_PANEL_FIX_2026-04-25.md create mode 100644 docs/technical/RPG_AGENT_SINGLE_FIELD_ANCHOR_CONTENT_REFACTOR_2026-04-25.md create mode 100644 server-rs/crates/api-server/.codex-temp/api-server-auth-store-creation-doc-bad-ext.json create mode 100644 server-rs/crates/api-server/.codex-temp/api-server-auth-store-creation-doc-large-base64.json create mode 100644 server-rs/crates/api-server/.codex-temp/api-server-auth-store-creation-doc-ok.json create mode 100644 server-rs/crates/api-server/server-rs/.data/auth-store.json create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/get_profile_referral_invite_center_procedure.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/profile_invite_code_table.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/profile_invite_code_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/profile_membership_table.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/profile_recharge_order_table.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/profile_referral_relation_table.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/profile_referral_relation_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/redeem_profile_referral_invite_code_procedure.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/runtime_referral_invite_center_get_input_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/runtime_referral_invite_center_procedure_result_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/runtime_referral_invite_center_snapshot_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/runtime_referral_redeem_input_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/runtime_referral_redeem_procedure_result_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/runtime_referral_redeem_snapshot_type.rs create mode 100644 src/spacetime/generated/ai_result_reference_table.ts create mode 100644 src/spacetime/generated/ai_task_stage_table.ts create mode 100644 src/spacetime/generated/ai_task_table.ts create mode 100644 src/spacetime/generated/ai_text_chunk_table.ts create mode 100644 src/spacetime/generated/asset_entity_binding_table.ts create mode 100644 src/spacetime/generated/asset_object_table.ts create mode 100644 src/spacetime/generated/auth_identity_table.ts create mode 100644 src/spacetime/generated/auth_store_snapshot_table.ts create mode 100644 src/spacetime/generated/battle_state_table.ts create mode 100644 src/spacetime/generated/big_fish_agent_message_table.ts create mode 100644 src/spacetime/generated/big_fish_asset_slot_table.ts create mode 100644 src/spacetime/generated/big_fish_creation_session_table.ts create mode 100644 src/spacetime/generated/big_fish_runtime_run_table.ts create mode 100644 src/spacetime/generated/chapter_progression_table.ts create mode 100644 src/spacetime/generated/custom_world_agent_message_table.ts create mode 100644 src/spacetime/generated/custom_world_agent_operation_table.ts create mode 100644 src/spacetime/generated/custom_world_agent_session_table.ts create mode 100644 src/spacetime/generated/custom_world_draft_card_table.ts create mode 100644 src/spacetime/generated/custom_world_profile_table.ts create mode 100644 src/spacetime/generated/custom_world_session_table.ts create mode 100644 src/spacetime/generated/get_profile_referral_invite_center_procedure.ts create mode 100644 src/spacetime/generated/inventory_slot_table.ts create mode 100644 src/spacetime/generated/npc_state_table.ts create mode 100644 src/spacetime/generated/player_progression_table.ts create mode 100644 src/spacetime/generated/profile_dashboard_state_table.ts create mode 100644 src/spacetime/generated/profile_invite_code_table.ts create mode 100644 src/spacetime/generated/profile_membership_table.ts create mode 100644 src/spacetime/generated/profile_played_world_table.ts create mode 100644 src/spacetime/generated/profile_recharge_order_table.ts create mode 100644 src/spacetime/generated/profile_referral_relation_table.ts create mode 100644 src/spacetime/generated/profile_save_archive_table.ts create mode 100644 src/spacetime/generated/profile_wallet_ledger_table.ts create mode 100644 src/spacetime/generated/puzzle_agent_message_table.ts create mode 100644 src/spacetime/generated/puzzle_agent_session_table.ts create mode 100644 src/spacetime/generated/puzzle_runtime_run_table.ts create mode 100644 src/spacetime/generated/puzzle_work_profile_table.ts create mode 100644 src/spacetime/generated/quest_log_table.ts create mode 100644 src/spacetime/generated/quest_record_table.ts create mode 100644 src/spacetime/generated/redeem_profile_referral_invite_code_procedure.ts create mode 100644 src/spacetime/generated/refresh_session_table.ts create mode 100644 src/spacetime/generated/runtime_setting_table.ts create mode 100644 src/spacetime/generated/runtime_snapshot_table.ts create mode 100644 src/spacetime/generated/story_event_table.ts create mode 100644 src/spacetime/generated/story_session_table.ts create mode 100644 src/spacetime/generated/treasure_record_table.ts create mode 100644 src/spacetime/generated/user_account_table.ts create mode 100644 src/spacetime/generated/user_browse_history_table.ts diff --git a/docs/design/RPG_NPC_CHAT_HOSTILE_TERMINATION_AND_INLINE_FUNCTION_OPTIONS_2026-04-25.md b/docs/design/RPG_NPC_CHAT_HOSTILE_TERMINATION_AND_INLINE_FUNCTION_OPTIONS_2026-04-25.md new file mode 100644 index 00000000..f924e5d9 --- /dev/null +++ b/docs/design/RPG_NPC_CHAT_HOSTILE_TERMINATION_AND_INLINE_FUNCTION_OPTIONS_2026-04-25.md @@ -0,0 +1,80 @@ +# RPG NPC 聊天敌对中止与聊天内 Function 选项设计(2026-04-25) + +## 1. 目标 + +本次迭代调整运行时 NPC 聊天,让敌对角色聊天从固定五回合上限改为由模型按当前语境判定是否中止;好感度大于等于 0 的角色继续保持可持续聊天,不由模型强制结束。 + +同时,原本部分只在退出聊天后才出现的 NPC function 选项,需要进入聊天续写候选池。模型在生成聊天候选时要能看到可触发的 function 选项,并把它们改写成玩家可直接点击的动作文本。聊天中保留“换一换”能力,用于刷新下方候选。 + +## 2. 行为规则 + +1. 负好感或敌对 NPC 进入聊天后,不再设置固定 5 回合上限。 +2. 负好感或敌对 NPC 每轮回复后,模型必须判断本轮是否结束聊天。 +3. 敌对 NPC 判定时应偏向随时结束聊天并进入对峙,但必须结合玩家刚说的话、NPC 性格、当前剧情压力和对话历史。 +4. 好感度大于等于 0 且非敌对 NPC 不启用模型终止判定,玩家可一直聊天。 +5. 模型判定终止后,聊天面板不再继续提供聊天输入,只显示“继续”按钮,点击后沿用原流程继续生成冒险选项。 +6. 点击“退出聊天”不再直接收起聊天页,也不立即进入剧情推理;它会发送一条结束聊天的玩家输入,对方回复后同样只显示“继续”按钮。 +7. 正向 NPC 的退出聊天只是玩家主动收束,不代表模型强制中止,也不展示战斗/逃跑选项。 +8. 对负好感或敌对 NPC,在聊天终止后的后续流程仍沿用原敌对出口:继续推进后回到原有战斗或逃跑选择。 +9. 聊天候选中允许混入当前 NPC 可执行 function,例如交易、送礼、请求帮助、招募、接任务、交任务、开战、离开等。 +10. Function 候选进入聊天上下文时只作为可触发动作,不在 UI 中展示说明类文本。 +11. “换一换”在聊天态可用,用于在不推进对话的情况下改排/轮换当前候选;它不调用后端,不改变聊天历史。 + +## 3. 后端契约 + +`NpcChatTurnDirective` 增加: + +1. `terminationMode`:`none | hostile_model` +2. `isHostileChat`:当前聊天是否按敌对中止规则处理 +3. `functionOptions`:可进入聊天候选的 function 列表,包含 `functionId`、`actionText`、`detailText`、`action` + +`NpcChatTurnCompletionDirective` 增加: + +1. `forceExit`:本轮回复后是否关闭聊天输入 +2. `closingMode`:保留 `free | foreshadow_close` +3. `terminationReason`:`hostile_breakoff | player_exit | null` + +后端返回 `suggestions` 仍是字符串数组,前端按字符串生成 `npc_chat` 续写选项;新增 `functionSuggestions`,元素包含 `functionId` 与模型生成的 `actionText`,前端按对应 function 触发原 NPC action。 + +## 4. Prompt 规则 + +回复 prompt 需要明确: + +1. 敌对聊天可随时中止,NPC 更偏好结束谈判转入战斗或驱逐。 +2. 终止不等于在回复正文里直接执行战斗,只需要用台词把对话收束到对峙、威胁、驱逐、最后通牒或行动前一刻。 +3. 玩家主动退出聊天时,NPC 回复要对这次收束作出回应,并留下自然的后续入口。 + +建议 prompt 需要明确: + +1. 常规聊天候选继续生成玩家台词。 +2. Function 候选要根据提供的 function 列表,改写成玩家可直接点击的动作文本。 +3. 不输出规则说明,不把 functionId 暴露给玩家。 + +## 5. 前端流程 + +1. `enterNpcChat` 与每轮 `handleNpcChatTurn` 统一构造聊天可用 function 列表。 +2. 聊天中的普通候选仍触发 `npc_chat`,function 候选触发原 `handleNpcInteraction` 分流。 +3. `exitNpcChat` 改为调用 `handleNpcChatTurn`,输入文本为结束聊天意图,并携带 `player_exit` 指令。 +4. 收到终止结果后,当前 `StoryMoment` 保留 `dialogue`,移除 `npcChatState`,`options` 只保留 `buildContinueAdventureOption()`。 +5. 点击“继续”后沿用已有 deferred continue / story continue 逻辑进入下一阶段。 +6. 聊天态“换一换”只轮换当前 `options`,若 function 候选不足则补普通聊天兜底候选。 + +## 6. 追加规则:Function 标签与场景幕推进 + +1. 运行时选项按钮需要在动作文本前展示 function 短标签,例如 `npc_chat` 显示“聊天”,`npc_quest_accept` / `npc_quest_turn_in` 显示“任务”,`npc_gift` 显示“送礼”。 +2. 标签只承担识别用途,不展示 functionId,也不展示规则说明。 +3. NPC 聊天终止后点击“继续冒险”,不再重新请求剧情推理;如果当前场景还有下一幕,直接进入下一幕并展示该幕可用的冒险选项。 +4. 当当前场景已经到最后一幕,再点击“继续冒险”应展示所有相邻场景入口;选项文案按方向表达为“向东走,前往xxxx”“向南走,前往xxxx”等。 +5. 相邻场景选项继续使用 `idle_travel_next_scene`,并在 `runtimePayload.targetSceneId` 中携带目标场景,后续点击沿用现有地图跳转结算。 +6. 若没有场景幕数据,则继续使用当前可用选项作为兜底,不额外生成规则说明文案。 + +## 7. 验收 + +1. 负好感主 NPC 不再出现固定 `turnLimit: 5`。 +2. 敌对 NPC 每轮请求会向后端传 `terminationMode: hostile_model`。 +3. 模型返回 `forceExit: true` 后,聊天输入消失,只显示继续按钮。 +4. 好感度大于等于 0 的 NPC 聊天不传敌对中止模式。 +5. 点击退出聊天会新增玩家结束聊天气泡与 NPC 回复,而不是直接切走面板。 +6. 聊天态可看到并点击 function 候选,且“换一换”可改变候选顺序。 +7. 选项文字前出现中文 function 标签,且标签不改变原 actionText。 +8. 聊天结束后的“继续冒险”直接进入下一幕;最后一幕则展示多个相邻场景方向入口。 diff --git a/docs/experience/AGENT_UI_CHANGELOG.md b/docs/experience/AGENT_UI_CHANGELOG.md index e5d9e2e2..8fd466d6 100644 --- a/docs/experience/AGENT_UI_CHANGELOG.md +++ b/docs/experience/AGENT_UI_CHANGELOG.md @@ -199,4 +199,12 @@ --- +## 17. 2026-04-26 创作编辑器关闭确认弹窗亮色主题修正 + +- `RpgCreationEntityEditorShared.tsx` 的未保存关闭确认统一收口为 `CloseConfirmDialog`,弹窗只保留确认信息和两个动作,不新增说明文案。 +- `CloseConfirmDialog` 通过 `platform-close-confirm-dialog` 语义类接入平台主题 token;提示块使用 `--platform-warm-*`,确认按钮使用 `--platform-button-primary-*`,继续编辑按钮使用 `--platform-neutral-*`。 +- 后续新增关闭 / 退出确认面板时,不要继续复制 `text-amber-50`、`text-sky-50`、`bg-black/*` 这类深色 Tailwind 组合;优先复用语义类,避免亮色主题出现浅底白字和按钮文字不可读。 + +--- + _文档目的:交接给下一个 Agent 时,优先读本文件 + `UI_CODING_STANDARD.md`,再改 `uiAssets.ts` / `App.tsx` / `index.css`。_ diff --git a/docs/experience/MOBILE_UI_DEV_EXPERIENCE.md b/docs/experience/MOBILE_UI_DEV_EXPERIENCE.md index 92489688..515c95bd 100644 --- a/docs/experience/MOBILE_UI_DEV_EXPERIENCE.md +++ b/docs/experience/MOBILE_UI_DEV_EXPERIENCE.md @@ -97,6 +97,12 @@ - 当前仓库已进一步收口为: 不再提供右上角全局账号悬浮条,统一只保留页面内入口与独立账号面板。 +### 4.6 冒险主场景双方角色必须按画面中线镜像 +- 非滚动画面的交谈、预览、单体对峙态,主角和对面角色不能分别用左右边距、世界坐标和角色宽度重复推算。 +- 正确做法是先定义“角色容器中心距离画面中线”的统一间距,再让主角中心落在中线左侧、对面角色中心落在中线右侧。 +- 角色容器宽度、角色图片缩放和左右朝向必须各自独立处理,不能用额外 left inset 去修正角色图片,否则会破坏左右对称。 +- 自定义图片 NPC、模板角色和组合式 NPC 都要进入同一 112px 场景容器,再按各自素材锚点做场景缩放,保证视觉大小不漂移。 + ## 5. 队伍面板经验 ### 5.1 移动端成员列表不能太“卡片化” @@ -202,3 +208,9 @@ - 可扮演角色的形象预览容器统一使用 1:1 方形,入口选择轮播、角色资产工坊和结果页角色卡片都不能用纵向长卡片去拉伸预览图。 - 预览图片本身使用 `object-contain`,保证 AI 生成主形象、模板像素角色和运行时动画都在方形容器内完整显示,不裁切角色主体。 - 卡片可以在方形预览下方放角色名、称号、状态等信息,但这些文本区不能反向影响预览区比例。 +- 编辑角色弹窗也遵循同一规则:移动端不能用固定高度压扁预览区,预览容器应随宽度保持 `aspect-square`。 + +### 10.2 运行画面怪物锚点按视觉底边校准 +- 对战预览里主角和对手要沿画面中线成对出现,但纵向不能只共用一个 `bottom` 常量。 +- 怪物精灵帧的空白、体型和脚底位置差异很大,运行画面应按帧高分档下沉,让怪物视觉底边落在主角同一条地面线上。 +- 后续新增怪物资源时,先检查红圈标注的实际落点,再调整锚点分档或单怪物偏移,避免出现“悬在地面上方”的状态。 diff --git a/docs/prd/AI_NATIVE_SCENE_MULTI_ACT_CREATOR_AND_GAMEPLAY_FLOW_PRD_2026-04-20.md b/docs/prd/AI_NATIVE_SCENE_MULTI_ACT_CREATOR_AND_GAMEPLAY_FLOW_PRD_2026-04-20.md index 86a5f1bb..f4cf2a9a 100644 --- a/docs/prd/AI_NATIVE_SCENE_MULTI_ACT_CREATOR_AND_GAMEPLAY_FLOW_PRD_2026-04-20.md +++ b/docs/prd/AI_NATIVE_SCENE_MULTI_ACT_CREATOR_AND_GAMEPLAY_FLOW_PRD_2026-04-20.md @@ -214,7 +214,7 @@ - `name` - `description` - `imageSrc` - - `sceneNpcIds` + - `sceneNpcIds`(仅作为兼容字段,由多幕配置自动派生,不再作为创作者可编辑字段) - `connections` - `sceneChapterBlueprints` 对应的多幕配置 2. 场景配置面板中,开局场景必须复用普通场景同级的配置 UI,而不是继续保留一套缩水版表单。 @@ -224,7 +224,7 @@ 4. 除“初始所在场景”语义之外,不允许再因为它是开局场景而裁掉 NPC、连接、多幕、危险度等配置能力。 5. 为兼容现有数据,当前 `camp` 字段可以继续保留,但其承载的结构必须与普通场景对齐,不能再是阉割版场景结构。 6. 运行时编译时,开局场景也必须按普通场景规则参与: - - 场景 NPC 池编译 + - 多幕相遇 NPC 编译 - 场景连接编译 - 多幕蓝图读取 - 场景图片 / 残痕 / 预览数据生成 @@ -500,7 +500,7 @@ type NpcChatTurnResult = { 1. “场景图片”不再作为场景详情页里的独立字段展示,创作者只能通过每一幕的“配置背景”入口管理视觉。 2. “场景内 NPC”不再作为场景详情页里的独立字段展示,创作者只能通过每一幕角色槽位配置相遇 NPC。 -3. 为兼容现有运行时与旧数据结构,场景对象上的 `imageSrc / sceneNpcIds` 仍然保留,但必须由多幕配置自动回填,前台不再暴露单独编辑控件。 +3. 为兼容现有运行时与旧数据结构,场景对象上的 `imageSrc / sceneNpcIds` 仍然保留,但必须由多幕配置自动回填,前台不再暴露单独编辑控件,且不能再用 `sceneNpcIds` 限制每幕可选角色。 多幕区块至少展示: @@ -553,7 +553,7 @@ type NpcChatTurnResult = { NPC 配置面板必须支持: 1. 从当前世界的 `playableNpcs + storyNpcs` 中选择角色 -2. 只展示与当前场景相关的优先推荐角色 +2. 当前场景相关角色只能作为排序或推荐依据,不能过滤掉其他世界角色 3. 以 `3` 个固定槽位进行配置,而不是长列表表单 4. 第一槽位明确标记为“主角色” 5. 允许同一角色出现在多个不同幕 @@ -566,6 +566,7 @@ NPC 配置面板必须支持: 3. 不允许把不存在于当前世界角色池中的 id 写入幕配置。 4. 若主角色未与当前场景或线程建立任何关联,给出发布警告。 5. 存储时继续落到 `encounterNpcIds` 有序数组,槽位从左到右按顺序压缩写入。 +6. `sceneNpcIds` 不再作为创作者字段,也不再作为幕角色选择范围;保存时只从所有幕的 `encounterNpcIds` 自动派生兼容值。 ## 7.6 幕预览 diff --git a/docs/technical/CREATION_SCENE_WORLD_MAP_PANEL_FIX_2026-04-25.md b/docs/technical/CREATION_SCENE_WORLD_MAP_PANEL_FIX_2026-04-25.md new file mode 100644 index 00000000..33319ba1 --- /dev/null +++ b/docs/technical/CREATION_SCENE_WORLD_MAP_PANEL_FIX_2026-04-25.md @@ -0,0 +1,32 @@ +# 创作页场景世界地图面板修复设计(2026-04-25) + +## 背景 + +创作结果页进入“场景”编辑面板后,底部“查看世界地图”弹出的面板存在两个问题: + +1. 面板仍使用偏运行时的深色地图容器,放在浅色创作页主题下时配色割裂,节点文字与背景层次也不稳定。 +2. 地图只按传入的地标列表渲染,普通场景编辑时容易漏掉开局场景,无法形成完整“世界地图”视角。 + +## 落地范围 + +- `src/components/rpg-creation-editor/RpgCreationEntityEditorShared.tsx` +- `src/components/CustomWorldEntityEditorModal.test.tsx` + +## 设计约束 + +1. 不新增说明类大段 UI 文案,只保留必要的节点名、方向标签和空状态。 +2. 地图面板继续作为独立弹窗,不在当前场景连接面板下方展开。 +3. 地图数据必须使用当前编辑中的草稿状态: + - 普通场景编辑:开局场景 + 已保存场景列表,并用当前 `draft` 替换正在编辑的场景。 + - 新增普通场景:开局场景 + 已保存场景列表 + 当前 `draft`。 + - 开局场景编辑:当前 `draft` 开局场景 + 已保存场景列表。 +4. 地图节点要标记当前编辑场景,连接线要展示方向短标签,避免用户只能看到无语义的线。 +5. 配色使用 `platform-*` 主题变量,适配浅色与深色创作页主题。 + +## 验收点 + +1. 在普通场景编辑器点击“查看世界地图”后,弹窗中能同时看到开局场景和当前场景。 +2. 未保存的场景连接关系会立刻体现在地图弹窗里。 +3. 当前编辑场景节点有明确高亮。 +4. 地图容器和节点不再固定为深色运行时风格。 +5. 相关前端测试覆盖普通场景与开局场景两条入口。 diff --git a/docs/technical/CUSTOM_WORLD_ASSET_PROMPT_DEFAULTS_2026-04-24.md b/docs/technical/CUSTOM_WORLD_ASSET_PROMPT_DEFAULTS_2026-04-24.md index 64b65cd6..d98c0092 100644 --- a/docs/technical/CUSTOM_WORLD_ASSET_PROMPT_DEFAULTS_2026-04-24.md +++ b/docs/technical/CUSTOM_WORLD_ASSET_PROMPT_DEFAULTS_2026-04-24.md @@ -41,6 +41,7 @@ - 角色主图:`server-rs/crates/api-server/src/custom_world_asset_prompts.rs` - `build_character_visual_prompt` - 内部使用 `build_master_prompt` + - 只拼入用户可见的 `promptText` / `visualPromptText`,不再拼入 `characterBriefText` 或角色摘要字段。 - 角色动作视频:`server-rs/crates/api-server/src/custom_world_asset_prompts.rs` - `build_character_animation_prompt` - 图生视频分支使用 `build_video_action_prompt` diff --git a/docs/technical/CUSTOM_WORLD_DRAFT_SCENE_TASK_AND_ACT_EVENT_FIELDS_2026-04-25.md b/docs/technical/CUSTOM_WORLD_DRAFT_SCENE_TASK_AND_ACT_EVENT_FIELDS_2026-04-25.md index 90e89728..3f1a59ab 100644 --- a/docs/technical/CUSTOM_WORLD_DRAFT_SCENE_TASK_AND_ACT_EVENT_FIELDS_2026-04-25.md +++ b/docs/technical/CUSTOM_WORLD_DRAFT_SCENE_TASK_AND_ACT_EVENT_FIELDS_2026-04-25.md @@ -20,6 +20,7 @@ - `eventDescription: string` - 描述当前幕正在发生的事件。 - 必须强绑定 `oppositeNpcId` / `primaryNpcId` 所指角色,写清该角色的行动、阻碍、试探、求助或冲突。 + - 三幕默认遵循戏剧曲线:第一幕铺垫并露出异常,第二幕让阻碍或立场冲突升级,第三幕进入高潮、关键抉择或直接后果。 - 默认生成兜底规则:`第N幕中,玩家在当前场景遭遇/处理与某角色直接相关的事件,并推动当前场景问题升级或转向。` 兼容字段: @@ -43,12 +44,33 @@ - `camp.sceneTaskDescription` 默认生成开局场景核心任务。 - `landmarks[*].sceneTaskDescription` 默认生成关键场景核心任务。 - `actEventDescriptions` 恰好 3 条,对应每一幕事件。 + - `actEventDescriptions[0] / [1] / [2]` 必须分别承担铺垫、冲突、高潮,不允许三条只是同一事件的近义复述。 + - `actBackgroundPromptTexts[n]` 必须基于同序号幕事件和相关角色写出画面主体、站位空间、冲突痕迹与氛围,不能只用场景名或幕标题拼接。 3. 后端合成 `sceneChapterBlueprints` 时把这些源字段落到: - `sceneChapterBlueprints[*].sceneTaskDescription` - `sceneChapterBlueprints[*].acts[*].oppositeNpcId` - `sceneChapterBlueprints[*].acts[*].eventDescription` 4. 若 LLM 遗漏字段,归一化阶段用场景描述、入口钩子、角色名单生成中文默认值,保证草稿阶段字段非空。 5. 前端类型与归一化逻辑必须允许读取这些字段,旧草稿缺字段时仍自动补默认值。 +6. 幕信息编辑界面必须直接展示 `eventDescription`,并在保存时保留 `sceneTaskDescription / oppositeNpcId / eventDescription / backgroundPromptText`,避免旧草稿经前端编辑后丢失后端生成字段。 + +## 幕配置预览标识 + +1. 幕配置预览图里只保留简洁角色点位标识,不新增说明类文案。 +2. 主角固定在画面左侧可站立区域,对面角色槽位固定在画面右侧可站立区域: + - 主角色槽位位于画面中右侧,作为当前幕的主要对峙对象。 + - 第二、第三角色槽位位于右侧上、下两个辅助位置,形成清晰的纵向层次。 +3. 每个槽位使用圆形短标识表达序号:`主 / 2 / 3`,旁边只展示角色名或“添加角色”。 +4. 标识必须有高对比底色、描边和轻微阴影,避免在浅色天空、地面纹理或深色背景上丢失。 +5. 空槽位仍然可点击,但只能显示 `+` 与短标签,不能显示大段规则说明。 + +## 对话选项差异要求 + +运行时 NPC 聊天每轮生成的 3 个对话选项必须导向不同氛围和好感结果: + +1. 第一条为温和共情或愿意倾听,通常让气氛缓和并更容易带来好感上升。 +2. 第二条为冷静追问或试探,通常保持中性但推进情报。 +3. 第三条为施压、质疑或立场冲突,通常让气氛变紧,可能带来好感下降或代价。 ## 非目标 diff --git a/docs/technical/M6_CHARACTER_VISUAL_ASSET_EXTERNAL_GENERATION_STAGE2_2026-04-23.md b/docs/technical/M6_CHARACTER_VISUAL_ASSET_EXTERNAL_GENERATION_STAGE2_2026-04-23.md index 13cf4889..90779984 100644 --- a/docs/technical/M6_CHARACTER_VISUAL_ASSET_EXTERNAL_GENERATION_STAGE2_2026-04-23.md +++ b/docs/technical/M6_CHARACTER_VISUAL_ASSET_EXTERNAL_GENERATION_STAGE2_2026-04-23.md @@ -88,13 +88,13 @@ Rust Stage 2 不再使用“LLM 先摘要再拼 SVG”的链路。 新的生成 prompt 口径: -1. 以请求里的 `promptText + characterBriefText` 组装正式主图 prompt +1. 以请求里的 `promptText` 组装正式主图 prompt;主形象不再拼入 `characterBriefText`,避免角色名、身份摘要、参考模板等非形象描述干扰体型、视角和风格约束 2. 约束必须覆盖: 1. 单人 2. 右向斜侧身 3. 1:1 正方形画布 4. 纯绿色绿幕 - 5. 3 到 4 头身 + 5. 1 到 1.5 头身 6. 像素动作角色 7. 不要扩写复杂背景 3. 主目标是与旧 Node `buildNpcVisualPrompt` 生成出的正式约束保持同方向 diff --git a/docs/technical/MY_TAB_REFERRAL_AND_COMMUNITY_IMPLEMENTATION_2026-04-25.md b/docs/technical/MY_TAB_REFERRAL_AND_COMMUNITY_IMPLEMENTATION_2026-04-25.md index 155e20af..f567c534 100644 --- a/docs/technical/MY_TAB_REFERRAL_AND_COMMUNITY_IMPLEMENTATION_2026-04-25.md +++ b/docs/technical/MY_TAB_REFERRAL_AND_COMMUNITY_IMPLEMENTATION_2026-04-25.md @@ -64,6 +64,13 @@ 返回绑定后的邀请中心状态与本次奖励发放结果。 +## 落地状态 + +- `server-rs/crates/spacetime-module` 已新增邀请码与邀请关系表,邀请中心读取和填码绑定均通过 SpacetimeDB procedure 执行。 +- `server-rs/crates/api-server` 已挂接 `/api/runtime/profile/referrals/*` 与 `/api/profile/referrals/*` 两组路由。 +- 前端“我的”Tab 三个快捷入口均打开独立弹窗,玩家社区先使用空白二维码占位。 +- 复制邀请会复制邀请码和邀请链接;填码成功后刷新个人看板积分。 + ## 前端交互 - 三个入口继续放在“我的”Tab 常用功能区,不新增页面。 diff --git a/docs/technical/RPG_AGENT_SINGLE_FIELD_ANCHOR_CONTENT_REFACTOR_2026-04-25.md b/docs/technical/RPG_AGENT_SINGLE_FIELD_ANCHOR_CONTENT_REFACTOR_2026-04-25.md new file mode 100644 index 00000000..0b8b7d98 --- /dev/null +++ b/docs/technical/RPG_AGENT_SINGLE_FIELD_ANCHOR_CONTENT_REFACTOR_2026-04-25.md @@ -0,0 +1,52 @@ +# RPG Agent 单字段锚点结构重构方案 + +## 背景 + +当前 RPG 创作 Agent 的 `anchorContent` 把每个锚点继续拆成多个子字段,例如 `worldPromise.hook / differentiator / desiredExperience`、`playerFantasy.playerRole / corePursuit / fearOfLoss`。这些子字段在语义上会互相重叠,后续基础设定展示又会把子字段用分隔符连接成“标签”,导致同一内容在一个锚点内或跨锚点重复出现。 + +本次重构将 `anchorContent` 收束为“每个锚点一个字段”。子字段不再作为数据结构保存,只保留为 prompt 中的生成关注点。 + +## 新数据结构 + +`anchorContent` 保留原有 8 个键,便于上下游和旧存档兼容,但每个键的值统一为 `string | null`: + +```ts +type RpgCreationAnchorContent = { + worldPromise: string | null; + playerFantasy: string | null; + themeBoundary: string | null; + playerEntryPoint: string | null; + coreConflict: string | null; + keyRelationships: string | null; + hiddenLines: string | null; + iconicElements: string | null; +}; +``` + +## 生成口径 + +1. Agent 每轮仍然输出完整 `nextAnchorContent`,并覆盖上一版。 +2. 每个锚点只输出一段凝练中文,不再输出对象或数组。 +3. 旧的子字段关注点进入 prompt 约束: + - `worldPromise` 关注世界钩子、差异点、玩家体验。 + - `playerFantasy` 关注玩家身份、核心追求、失去风险。 + - `themeBoundary` 关注主题气质、美术方向、禁用方向。 + - `playerEntryPoint` 关注开局身份、开局问题、行动动机。 + - `coreConflict` 关注表层冲突、隐藏危机、首次触发点。 + - `keyRelationships` 关注关键人物关系、关系类型、代价或秘密。 + - `hiddenLines` 关注隐藏真相、误导线索、揭示节奏。 + - `iconicElements` 关注标志意象、组织/物件、硬规则。 + +## 兼容策略 + +1. 后端读取旧 `anchorContent` 时,允许把旧对象/数组压缩成字符串。 +2. 后端新写回永远写单字段字符串结构。 +3. 前端契约和展示只按字符串字段读取,不再把子字段连接成标签。 +4. 草稿生成、`creatorIntent`、`anchorPack` 均从单字段锚点派生。 + +## 验收点 + +1. Agent 对话后 `anchorContent` 的 8 个锚点值均为字符串或 `null`。 +2. 基础设定面板不再因为子字段连接产生重复标签。 +3. 旧存档中的对象结构仍可被读取并压缩展示。 +4. `draft_foundation` 生成种子直接使用 8 个单字段锚点。 diff --git a/docs/technical/SCENE_MULTI_ACT_CREATOR_IMPLEMENTATION_PROGRESS_2026-04-20.md b/docs/technical/SCENE_MULTI_ACT_CREATOR_IMPLEMENTATION_PROGRESS_2026-04-20.md index 828071c8..b6fd583a 100644 --- a/docs/technical/SCENE_MULTI_ACT_CREATOR_IMPLEMENTATION_PROGRESS_2026-04-20.md +++ b/docs/technical/SCENE_MULTI_ACT_CREATOR_IMPLEMENTATION_PROGRESS_2026-04-20.md @@ -90,6 +90,9 @@ 前排主角色与玩家角色保持同一 y 轴;后排两个角色改为同一列、x 轴对齐并上下分布,且后排整体 y 轴中点与前排主角色一致 9. 新增幕默认只带 1 个主角色,后续槽位由创作者按需补充 10. 小预览里的名字已移动到角色头顶,角色渲染不再带方形底板,避免遮挡场景背景 +11. 幕预览复用真实游戏壳时隐藏左上角角色等级徽标,退出入口固定在上方画面区域底部居中,并使用“结束预览”作为操作文案 +12. 创作侧场景列表封面、多幕配置卡片、配置背景弹层统一读取同一张场景显示图;在任一幕保存背景时同步回全部幕背景字段和场景兼容图,避免同一场景在不同层级出现不同预览图 +13. 场景角色预览图背景改用平台主题变量,亮色主题下不再保留深色预览底 ## 2.6 负好感主角色有限聊天闭环 diff --git a/packages/shared/src/contracts/customWorldAgentAnchors.ts b/packages/shared/src/contracts/customWorldAgentAnchors.ts index 67d7793a..fe5fb6eb 100644 --- a/packages/shared/src/contracts/customWorldAgentAnchors.ts +++ b/packages/shared/src/contracts/customWorldAgentAnchors.ts @@ -4,13 +4,6 @@ */ export type { + RpgCreationAnchorText as AnchorTextValue, RpgCreationAnchorContent as EightAnchorContent, - RpgCreationCoreConflictValue as CoreConflictValue, - RpgCreationHiddenLineValue as HiddenLineValue, - RpgCreationIconicElementValue as IconicElementValue, - RpgCreationKeyRelationshipValue as KeyRelationshipValue, - RpgCreationPlayerEntryPointValue as PlayerEntryPointValue, - RpgCreationPlayerFantasyValue as PlayerFantasyValue, - RpgCreationThemeBoundaryValue as ThemeBoundaryValue, - RpgCreationWorldPromiseValue as WorldPromiseValue, } from './rpgAgentAnchors'; diff --git a/packages/shared/src/contracts/rpgAgentAnchors.ts b/packages/shared/src/contracts/rpgAgentAnchors.ts index 9f4f3e41..8cd3c084 100644 --- a/packages/shared/src/contracts/rpgAgentAnchors.ts +++ b/packages/shared/src/contracts/rpgAgentAnchors.ts @@ -1,63 +1,17 @@ /** * RPG 创作八锚点契约。 - * 这一层只描述“创作意图采集态”的结构,不混入 session 或结果页字段。 + * 每个锚点只保留一段凝练文本;细分关注点由 Agent prompt 负责,不再进入存储结构。 */ -export interface RpgCreationWorldPromiseValue { - hook: string; - differentiator: string; - desiredExperience: string; -} - -export interface RpgCreationPlayerFantasyValue { - playerRole: string; - corePursuit: string; - fearOfLoss: string; -} - -export interface RpgCreationThemeBoundaryValue { - toneKeywords: string[]; - aestheticDirectives: string[]; - forbiddenDirectives: string[]; -} - -export interface RpgCreationPlayerEntryPointValue { - openingIdentity: string; - openingProblem: string; - entryMotivation: string; -} - -export interface RpgCreationCoreConflictValue { - surfaceConflicts: string[]; - hiddenCrisis: string; - firstTouchedConflict: string; -} - -export interface RpgCreationKeyRelationshipValue { - pairs: string; - relationshipType: string; - secretOrCost: string; -} - -export interface RpgCreationHiddenLineValue { - hiddenTruths: string[]; - misdirectionHints: string[]; - revealPacing: string; -} - -export interface RpgCreationIconicElementValue { - iconicMotifs: string[]; - institutionsOrArtifacts: string[]; - hardRules: string[]; -} +export type RpgCreationAnchorText = string | null; export interface RpgCreationAnchorContent { - worldPromise: RpgCreationWorldPromiseValue | null; - playerFantasy: RpgCreationPlayerFantasyValue | null; - themeBoundary: RpgCreationThemeBoundaryValue | null; - playerEntryPoint: RpgCreationPlayerEntryPointValue | null; - coreConflict: RpgCreationCoreConflictValue | null; - keyRelationships: RpgCreationKeyRelationshipValue[]; - hiddenLines: RpgCreationHiddenLineValue | null; - iconicElements: RpgCreationIconicElementValue | null; + worldPromise: RpgCreationAnchorText; + playerFantasy: RpgCreationAnchorText; + themeBoundary: RpgCreationAnchorText; + playerEntryPoint: RpgCreationAnchorText; + coreConflict: RpgCreationAnchorText; + keyRelationships: RpgCreationAnchorText; + hiddenLines: RpgCreationAnchorText; + iconicElements: RpgCreationAnchorText; } diff --git a/packages/shared/src/contracts/rpgContracts.test.ts b/packages/shared/src/contracts/rpgContracts.test.ts index 930d7e2a..8278d2a0 100644 --- a/packages/shared/src/contracts/rpgContracts.test.ts +++ b/packages/shared/src/contracts/rpgContracts.test.ts @@ -32,7 +32,7 @@ describe('RPG 创作共享契约 fixture', () => { const anchors = createRpgCreationAnchorContentFixture(); const draftProfile = createRpgAgentFoundationDraftProfileFixture(); - expect(anchors.worldPromise?.hook).toContain('旧航路群岛'); + expect(anchors.worldPromise).toContain('旧航路群岛'); expect(draftProfile.worldHook).toContain('旧航路群岛'); expect(draftProfile.playableNpcs).toHaveLength(1); expect(draftProfile.storyNpcs).toHaveLength(1); diff --git a/packages/shared/src/contracts/rpgCreationFixtures.ts b/packages/shared/src/contracts/rpgCreationFixtures.ts index c8a28d55..ba5e90ca 100644 --- a/packages/shared/src/contracts/rpgCreationFixtures.ts +++ b/packages/shared/src/contracts/rpgCreationFixtures.ts @@ -32,48 +32,22 @@ function cloneFixture(value: T): T { */ export function createRpgCreationAnchorContentFixture(): RpgCreationAnchorContent { return cloneFixture({ - worldPromise: { - hook: '被海雾吞没的旧航路群岛', - differentiator: '灯塔与禁航令共同决定谁能活着穿过去。', - desiredExperience: '压抑、悬疑、潮湿', - }, - playerFantasy: { - playerRole: '玩家回到群岛调查沉船真相。', - corePursuit: '找出失控航路背后的真相。', - fearOfLoss: '失去最后一个还能对上旧案的人。', - }, - themeBoundary: { - toneKeywords: ['压抑', '潮湿', '悬疑'], - aestheticDirectives: ['旧灯塔', '潮雾', '断裂航路'], - forbiddenDirectives: ['不要出现现代枪械'], - }, - playerEntryPoint: { - openingIdentity: '被迫返乡的失职守灯人', - openingProblem: '首夜就有陌生船只闯入禁航区。', - entryMotivation: '查清沉船夜里被谁改动了灯册。', - }, - coreConflict: { - surfaceConflicts: ['守灯会与航运公会争夺旧航路控制权'], - hiddenCrisis: '沉船夜的航灯与灯册被人动过手脚。', - firstTouchedConflict: '玩家开局就会撞上新的封航命令。', - }, - keyRelationships: [ - { - pairs: '玩家 / 沈砺', - relationshipType: '旧友兼潜在背叛者', - secretOrCost: '沈砺暗地里在替沉船商盟引路。', - }, - ], - hiddenLines: { - hiddenTruths: ['沉船夜的真实失误并不是单纯天灾。'], - misdirectionHints: ['所有人都会先把问题推给潮雾本身。'], - revealPacing: '第一章露出痕迹,第二章才让玩家摸到灯册线。', - }, - iconicElements: { - iconicMotifs: ['会移动的海雾'], - institutionsOrArtifacts: ['回潮旧灯塔', '封灯令', '旧潮图'], - hardRules: ['禁航信号一旦点亮,任何船都必须退航。'], - }, + worldPromise: + '被海雾吞没的旧航路群岛,灯塔与禁航令共同决定谁能活着穿过去,体验压抑、悬疑、潮湿。', + playerFantasy: + '玩家回到群岛调查沉船真相,核心追求是找出失控航路背后的真相,风险是失去最后一个还能对上旧案的人。', + themeBoundary: + '压抑、潮湿、悬疑;旧灯塔、潮雾、断裂航路;不要出现现代枪械。', + playerEntryPoint: + '玩家是被迫返乡的失职守灯人,首夜就有陌生船只闯入禁航区,动机是查清沉船夜里被谁改动了灯册。', + coreConflict: + '守灯会与航运公会争夺旧航路控制权,沉船夜的航灯与灯册被人动过手脚,玩家开局会撞上新的封航命令。', + keyRelationships: + '玩家与沈砺是旧友兼潜在背叛者,沈砺暗地里在替沉船商盟引路。', + hiddenLines: + '沉船夜的真实失误并不是单纯天灾;所有人都会先把问题推给潮雾本身;第一章露出痕迹,第二章才让玩家摸到灯册线。', + iconicElements: + '会移动的海雾、回潮旧灯塔、封灯令、旧潮图;禁航信号一旦点亮,任何船都必须退航。', } satisfies RpgCreationAnchorContent); } diff --git a/packages/shared/src/contracts/rpgRuntimeChat.ts b/packages/shared/src/contracts/rpgRuntimeChat.ts index a08dff5c..57ae004a 100644 --- a/packages/shared/src/contracts/rpgRuntimeChat.ts +++ b/packages/shared/src/contracts/rpgRuntimeChat.ts @@ -8,6 +8,17 @@ export type NpcChatTurnLimitReason = 'negative_affinity'; export type NpcChatTurnClosingMode = 'free' | 'foreshadow_close'; +export type NpcChatTurnTerminationMode = 'none' | 'hostile_model'; + +export type NpcChatTurnTerminationReason = 'hostile_breakoff' | 'player_exit'; + +export type NpcChatFunctionOption = { + functionId: string; + actionText: string; + detailText?: string | null; + action?: string | null; +}; + export type NpcChatTurnDirective = { sceneActId?: string | null; turnLimit?: number | null; @@ -15,6 +26,10 @@ export type NpcChatTurnDirective = { limitReason?: NpcChatTurnLimitReason | null; closingMode?: NpcChatTurnClosingMode | null; forceExitAfterTurn?: boolean; + terminationMode?: NpcChatTurnTerminationMode | null; + terminationReason?: NpcChatTurnTerminationReason | null; + isHostileChat?: boolean; + functionOptions?: NpcChatFunctionOption[]; }; export type NpcChatTurnCompletionDirective = { @@ -22,6 +37,7 @@ export type NpcChatTurnCompletionDirective = { remainingTurns?: number | null; forceExit?: boolean; closingMode?: NpcChatTurnClosingMode; + terminationReason?: NpcChatTurnTerminationReason | null; }; export type CharacterChatReplyRequest< @@ -133,11 +149,17 @@ export type NpcChatPendingQuestOffer = { introText?: string; }; +export type NpcChatFunctionSuggestion = { + functionId: string; + actionText: string; +}; + export type NpcChatTurnResult = { npcReply: string; affinityDelta: number; affinityText: string; suggestions: string[]; + functionSuggestions?: NpcChatFunctionSuggestion[]; pendingQuestOffer?: NpcChatPendingQuestOffer | null; chatDirective?: NpcChatTurnCompletionDirective | null; }; diff --git a/packages/shared/src/contracts/runtime.ts b/packages/shared/src/contracts/runtime.ts index 039eea6f..e446da25 100644 --- a/packages/shared/src/contracts/runtime.ts +++ b/packages/shared/src/contracts/runtime.ts @@ -131,6 +131,32 @@ export type CreateProfileRechargeOrderResponse = { center: ProfileRechargeCenterResponse; }; +export type ProfileReferralInviteCenterResponse = { + inviteCode: string; + inviteLinkPath: string; + invitedCount: number; + rewardedInviteCount: number; + todayInviterRewardCount: number; + todayInviterRewardRemaining: number; + rewardPoints: number; + hasRedeemedCode: boolean; + boundInviterUserId: string | null; + boundAt: string | null; + updatedAt: string; +}; + +export type RedeemProfileReferralInviteCodeRequest = { + inviteCode: string; +}; + +export type RedeemProfileReferralInviteCodeResponse = { + center: ProfileReferralInviteCenterResponse; + inviteeRewardGranted: boolean; + inviterRewardGranted: boolean; + inviteeBalanceAfter: number; + inviterBalanceAfter: number; +}; + export type ProfilePlayedWorkSummary = { worldKey: string; ownerUserId: string | null; diff --git a/server-rs/crates/api-server/.codex-temp/api-server-auth-store-creation-doc-bad-ext.json b/server-rs/crates/api-server/.codex-temp/api-server-auth-store-creation-doc-bad-ext.json new file mode 100644 index 00000000..60a03753 --- /dev/null +++ b/server-rs/crates/api-server/.codex-temp/api-server-auth-store-creation-doc-bad-ext.json @@ -0,0 +1,28 @@ +{ + "next_user_id": 2, + "users_by_username": { + "phone_00000002": { + "user": { + "id": "user_00000001", + "public_user_code": "SY-00000001", + "username": "phone_00000002", + "display_name": "138****8111", + "phone_number_masked": "138****8111", + "login_method": "Phone", + "binding_status": "Active", + "wechat_bound": false, + "token_version": 1 + }, + "password_hash": "$argon2id$v=19$m=19456,t=2,p=1$qnArSgOrZvcQxap4KAMMnA$+K+gQgf7h0jQibJLuvAlOeHnNNYutTvLVDAyo1hqS/o", + "password_login_enabled": false, + "phone_number": "+8613800138111" + } + }, + "phone_to_user_id": { + "+8613800138111": "user_00000001" + }, + "sessions_by_id": {}, + "session_id_by_refresh_token_hash": {}, + "wechat_identity_by_provider_uid": {}, + "user_id_by_provider_union_id": {} +} \ No newline at end of file diff --git a/server-rs/crates/api-server/.codex-temp/api-server-auth-store-creation-doc-large-base64.json b/server-rs/crates/api-server/.codex-temp/api-server-auth-store-creation-doc-large-base64.json new file mode 100644 index 00000000..c89a57a3 --- /dev/null +++ b/server-rs/crates/api-server/.codex-temp/api-server-auth-store-creation-doc-large-base64.json @@ -0,0 +1,28 @@ +{ + "next_user_id": 2, + "users_by_username": { + "phone_00000002": { + "user": { + "id": "user_00000001", + "public_user_code": "SY-00000001", + "username": "phone_00000002", + "display_name": "138****8112", + "phone_number_masked": "138****8112", + "login_method": "Phone", + "binding_status": "Active", + "wechat_bound": false, + "token_version": 1 + }, + "password_hash": "$argon2id$v=19$m=19456,t=2,p=1$0HR2g/fKOw9EFHz7BuYtGg$cpXb5KBwbEXPxPJHA4Bk1U7NtM97GhGTq7VK6jCJ+lA", + "password_login_enabled": false, + "phone_number": "+8613800138112" + } + }, + "phone_to_user_id": { + "+8613800138112": "user_00000001" + }, + "sessions_by_id": {}, + "session_id_by_refresh_token_hash": {}, + "wechat_identity_by_provider_uid": {}, + "user_id_by_provider_union_id": {} +} \ No newline at end of file diff --git a/server-rs/crates/api-server/.codex-temp/api-server-auth-store-creation-doc-ok.json b/server-rs/crates/api-server/.codex-temp/api-server-auth-store-creation-doc-ok.json new file mode 100644 index 00000000..14249074 --- /dev/null +++ b/server-rs/crates/api-server/.codex-temp/api-server-auth-store-creation-doc-ok.json @@ -0,0 +1,28 @@ +{ + "next_user_id": 2, + "users_by_username": { + "phone_00000002": { + "user": { + "id": "user_00000001", + "public_user_code": "SY-00000001", + "username": "phone_00000002", + "display_name": "138****8110", + "phone_number_masked": "138****8110", + "login_method": "Phone", + "binding_status": "Active", + "wechat_bound": false, + "token_version": 1 + }, + "password_hash": "$argon2id$v=19$m=19456,t=2,p=1$fEeSrVyialDeb8rarDSpdA$HFihZiuCOyaz8F5iNukmobeiHI/EpYWdeQzhbIYR4zk", + "password_login_enabled": false, + "phone_number": "+8613800138110" + } + }, + "phone_to_user_id": { + "+8613800138110": "user_00000001" + }, + "sessions_by_id": {}, + "session_id_by_refresh_token_hash": {}, + "wechat_identity_by_provider_uid": {}, + "user_id_by_provider_union_id": {} +} \ No newline at end of file diff --git a/server-rs/crates/api-server/server-rs/.data/auth-store.json b/server-rs/crates/api-server/server-rs/.data/auth-store.json new file mode 100644 index 00000000..b374bfbc --- /dev/null +++ b/server-rs/crates/api-server/server-rs/.data/auth-store.json @@ -0,0 +1,56 @@ +{ + "next_user_id": 2, + "users_by_username": { + "phone_00000002": { + "user": { + "id": "user_00000001", + "public_user_code": "SY-00000001", + "username": "phone_00000002", + "display_name": "138****8000", + "phone_number_masked": "138****8000", + "login_method": "Phone", + "binding_status": "Active", + "wechat_bound": false, + "token_version": 1 + }, + "password_hash": "$argon2id$v=19$m=19456,t=2,p=1$hoXmK/LzABj2QfWZSO3SNA$Qg71V2iZCPyLOsoQLffiCv3KPkWVNSAsP6IooTIXi/w", + "password_login_enabled": false, + "phone_number": "+8613800138000" + } + }, + "phone_to_user_id": { + "+8613800138000": "user_00000001" + }, + "sessions_by_id": { + "usess_52522126b58d40e3b9e503808dd11e2c": { + "session": { + "session_id": "usess_52522126b58d40e3b9e503808dd11e2c", + "user_id": "user_00000001", + "refresh_token_hash": "f42140526caea3e4a9f533bcc2d8799feae4f96769ea975ef771b1ae11e4dbe9", + "issued_by_provider": "Phone", + "client_info": { + "client_type": "web_browser", + "client_runtime": "unknown", + "client_platform": "unknown", + "client_instance_id": null, + "device_fingerprint": null, + "device_display_name": "未知设备 / 未知客户端", + "mini_program_app_id": null, + "mini_program_env": null, + "user_agent": null, + "ip": null + }, + "expires_at": "2026-05-25T15:41:01.0856147Z", + "revoked_at": null, + "created_at": "2026-04-25T15:41:01.0856147Z", + "updated_at": "2026-04-25T15:41:01.0856147Z", + "last_seen_at": "2026-04-25T15:41:01.0856147Z" + } + } + }, + "session_id_by_refresh_token_hash": { + "f42140526caea3e4a9f533bcc2d8799feae4f96769ea975ef771b1ae11e4dbe9": "usess_52522126b58d40e3b9e503808dd11e2c" + }, + "wechat_identity_by_provider_uid": {}, + "user_id_by_provider_union_id": {} +} \ No newline at end of file diff --git a/server-rs/crates/api-server/src/app.rs b/server-rs/crates/api-server/src/app.rs index c3ca91df..0e75465d 100644 --- a/server-rs/crates/api-server/src/app.rs +++ b/server-rs/crates/api-server/src/app.rs @@ -95,7 +95,8 @@ use crate::{ runtime_inventory::get_runtime_inventory_state, runtime_profile::{ create_profile_recharge_order, get_profile_dashboard, get_profile_play_stats, - get_profile_recharge_center, get_profile_wallet_ledger, + get_profile_recharge_center, get_profile_referral_invite_center, get_profile_wallet_ledger, + redeem_profile_referral_invite_code, }, runtime_save::{ delete_runtime_snapshot, get_runtime_snapshot, list_profile_save_archives, @@ -820,6 +821,34 @@ pub fn build_router(state: AppState) -> Router { require_bearer_auth, )), ) + .route( + "/api/runtime/profile/referrals/invite-center", + get(get_profile_referral_invite_center).route_layer(middleware::from_fn_with_state( + state.clone(), + require_bearer_auth, + )), + ) + .route( + "/api/profile/referrals/invite-center", + get(get_profile_referral_invite_center).route_layer(middleware::from_fn_with_state( + state.clone(), + require_bearer_auth, + )), + ) + .route( + "/api/runtime/profile/referrals/redeem-code", + post(redeem_profile_referral_invite_code).route_layer(middleware::from_fn_with_state( + state.clone(), + require_bearer_auth, + )), + ) + .route( + "/api/profile/referrals/redeem-code", + post(redeem_profile_referral_invite_code).route_layer(middleware::from_fn_with_state( + state.clone(), + require_bearer_auth, + )), + ) .route( "/api/runtime/profile/play-stats", get(get_profile_play_stats).route_layer(middleware::from_fn_with_state( diff --git a/server-rs/crates/api-server/src/big_fish.rs b/server-rs/crates/api-server/src/big_fish.rs index dbc9acee..fb874774 100644 --- a/server-rs/crates/api-server/src/big_fish.rs +++ b/server-rs/crates/api-server/src/big_fish.rs @@ -434,13 +434,7 @@ pub async fn execute_big_fish_action( let now = current_utc_micros(); let session = match payload.action.trim() { "big_fish_compile_draft" => { - compile_big_fish_draft_with_all_assets( - &state, - session_id, - owner_user_id, - now, - ) - .await + compile_big_fish_draft_with_all_assets(&state, session_id, owner_user_id, now).await } "big_fish_generate_level_main_image" => { let asset_url = generate_big_fish_formal_asset( diff --git a/server-rs/crates/api-server/src/character_visual_assets.rs b/server-rs/crates/api-server/src/character_visual_assets.rs index bdca333f..88657eb7 100644 --- a/server-rs/crates/api-server/src/character_visual_assets.rs +++ b/server-rs/crates/api-server/src/character_visual_assets.rs @@ -74,14 +74,9 @@ pub async fn generate_character_visual( // 旧资产工坊接口没有显式 Bearer 头,Rust 兼容层先使用工具用户归属,避免破坏现有前端调用。 let owner_user_id = "asset-tool".to_string(); let task_id = generate_ai_task_id(current_utc_micros()); - let prompt = build_character_visual_prompt( - payload.prompt_text.as_str(), - payload.character_brief_text.as_deref(), - ); - let fallback_prompt = build_fallback_moderation_safe_character_visual_prompt( - payload.prompt_text.as_str(), - payload.character_brief_text.as_deref(), - ); + let prompt = build_character_visual_prompt(payload.prompt_text.as_str()); + let fallback_prompt = + build_fallback_moderation_safe_character_visual_prompt(payload.prompt_text.as_str()); let character_id = normalize_required_text(payload.character_id.as_str(), "character"); let model = normalize_required_text(payload.image_model.as_str(), CHARACTER_VISUAL_MODEL); let size = normalize_required_text(payload.size.as_str(), "1024*1024"); @@ -296,27 +291,20 @@ pub(crate) async fn generate_character_primary_visual_for_profile( owner_user_id: &str, character_id: &str, prompt_text: &str, - character_brief_text: Option<&str>, ) -> Result { let payload = CharacterVisualGenerateRequest { character_id: character_id.to_string(), source_mode: shared_contracts::assets::CharacterVisualSourceMode::TextToImage, prompt_text: prompt_text.to_string(), - character_brief_text: character_brief_text.map(ToOwned::to_owned), reference_image_data_urls: Vec::new(), candidate_count: 1, image_model: CHARACTER_VISUAL_MODEL.to_string(), size: "1024*1024".to_string(), }; let task_id = generate_ai_task_id(current_utc_micros()); - let prompt = build_character_visual_prompt( - payload.prompt_text.as_str(), - payload.character_brief_text.as_deref(), - ); - let fallback_prompt = build_fallback_moderation_safe_character_visual_prompt( - payload.prompt_text.as_str(), - payload.character_brief_text.as_deref(), - ); + let prompt = build_character_visual_prompt(payload.prompt_text.as_str()); + let fallback_prompt = + build_fallback_moderation_safe_character_visual_prompt(payload.prompt_text.as_str()); let character_id = normalize_required_text(payload.character_id.as_str(), "character"); let model = normalize_required_text(payload.image_model.as_str(), CHARACTER_VISUAL_MODEL); let size = normalize_required_text(payload.size.as_str(), "1024*1024"); @@ -2068,7 +2056,7 @@ mod tests { #[test] fn build_character_visual_prompt_keeps_generation_constraints() { - let prompt = build_character_visual_prompt("潮雾港向导", Some("旧港守望者")); + let prompt = build_character_visual_prompt("潮雾港向导"); assert!(prompt.contains("潮雾港向导")); assert!(prompt.contains("右向斜侧身")); @@ -2077,10 +2065,8 @@ mod tests { #[test] fn fallback_character_visual_prompt_removes_risky_specific_names() { - let prompt = build_fallback_moderation_safe_character_visual_prompt( - "艾瑞克,银发剑士,红色长披风", - Some("某知名设定参考"), - ); + let prompt = + build_fallback_moderation_safe_character_visual_prompt("艾瑞克,银发剑士,红色长披风"); assert!(prompt.contains("原创")); assert!(prompt.contains("不参考任何现有")); diff --git a/server-rs/crates/api-server/src/custom_world.rs b/server-rs/crates/api-server/src/custom_world.rs index ca34ed4d..ba95f991 100644 --- a/server-rs/crates/api-server/src/custom_world.rs +++ b/server-rs/crates/api-server/src/custom_world.rs @@ -1613,7 +1613,6 @@ async fn generate_draft_foundation_role_visuals( task_owner_user_id.as_str(), role_ref.role_id.as_str(), role_ref.prompt.as_str(), - Some(role_ref.name.as_str()), ) .await }; @@ -2429,8 +2428,8 @@ fn log_custom_world_publish_gate_diagnostics( has_draft_profile = session.draft_profile.as_object().map(|value| !value.is_empty()).unwrap_or(false), has_result_preview = session.result_preview.is_some(), preview_source = session.result_preview.as_ref().and_then(|value| value.get("source")).and_then(serde_json::Value::as_str).unwrap_or(""), - has_world_hook = has_custom_world_publish_text(profile, &["worldHook", "creatorIntent.worldHook", "anchorContent.worldPromise.hook", "settingText"]), - has_player_premise = has_custom_world_publish_text(profile, &["playerPremise", "creatorIntent.playerPremise", "anchorContent.playerEntryPoint.openingIdentity", "anchorContent.playerEntryPoint.openingProblem", "anchorContent.playerEntryPoint.entryMotivation"]), + has_world_hook = has_custom_world_publish_text(profile, &["worldHook", "creatorIntent.worldHook", "anchorContent.worldPromise", "anchorContent.worldPromise.hook", "settingText"]), + has_player_premise = has_custom_world_publish_text(profile, &["playerPremise", "creatorIntent.playerPremise", "anchorContent.playerEntryPoint", "anchorContent.playerEntryPoint.openingIdentity", "anchorContent.playerEntryPoint.openingProblem", "anchorContent.playerEntryPoint.entryMotivation"]), has_core_conflicts = has_custom_world_non_empty_text_array(profile, "coreConflicts"), has_main_chapter = has_custom_world_array(profile, "chapters") || has_custom_world_array(profile, "sceneChapterBlueprints") || has_custom_world_array(profile, "sceneChapters"), has_scene_act = has_custom_world_scene_act(profile), diff --git a/server-rs/crates/api-server/src/custom_world_agent_turn.rs b/server-rs/crates/api-server/src/custom_world_agent_turn.rs index aa94ca96..37ba251d 100644 --- a/server-rs/crates/api-server/src/custom_world_agent_turn.rs +++ b/server-rs/crates/api-server/src/custom_world_agent_turn.rs @@ -99,113 +99,25 @@ pub(crate) struct PromptDynamicStateInference { judgement_summary: Option, } -#[derive(Clone, Debug, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -struct WorldPromiseValue { - #[serde(default)] - hook: String, - #[serde(default)] - differentiator: String, - #[serde(default)] - desired_experience: String, -} - -#[derive(Clone, Debug, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -struct PlayerFantasyValue { - #[serde(default)] - player_role: String, - #[serde(default)] - core_pursuit: String, - #[serde(default)] - fear_of_loss: String, -} - -#[derive(Clone, Debug, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -struct ThemeBoundaryValue { - #[serde(default)] - tone_keywords: Vec, - #[serde(default)] - aesthetic_directives: Vec, - #[serde(default)] - forbidden_directives: Vec, -} - -#[derive(Clone, Debug, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -struct PlayerEntryPointValue { - #[serde(default)] - opening_identity: String, - #[serde(default)] - opening_problem: String, - #[serde(default)] - entry_motivation: String, -} - -#[derive(Clone, Debug, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -struct CoreConflictValue { - #[serde(default)] - surface_conflicts: Vec, - #[serde(default)] - hidden_crisis: String, - #[serde(default)] - first_touched_conflict: String, -} - -#[derive(Clone, Debug, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -struct KeyRelationshipValue { - #[serde(default)] - pairs: String, - #[serde(default)] - relationship_type: String, - #[serde(default)] - secret_or_cost: String, -} - -#[derive(Clone, Debug, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -struct HiddenLineValue { - #[serde(default)] - hidden_truths: Vec, - #[serde(default)] - misdirection_hints: Vec, - #[serde(default)] - reveal_pacing: String, -} - -#[derive(Clone, Debug, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -struct IconicElementValue { - #[serde(default)] - iconic_motifs: Vec, - #[serde(default)] - institutions_or_artifacts: Vec, - #[serde(default)] - hard_rules: Vec, -} - #[derive(Clone, Debug, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct EightAnchorContent { #[serde(default)] - world_promise: Option, + world_promise: Option, #[serde(default)] - player_fantasy: Option, + player_fantasy: Option, #[serde(default)] - theme_boundary: Option, + theme_boundary: Option, #[serde(default)] - player_entry_point: Option, + player_entry_point: Option, #[serde(default)] - core_conflict: Option, + core_conflict: Option, #[serde(default)] - key_relationships: Vec, + key_relationships: Option, #[serde(default)] - hidden_lines: Option, + hidden_lines: Option, #[serde(default)] - iconic_elements: Option, + iconic_elements: Option, } #[derive(Clone, Debug, Default, Serialize, Deserialize)] @@ -814,137 +726,127 @@ fn build_chat_history(messages: &[CustomWorldAgentMessageRecord]) -> Vec EightAnchorContent { - serde_json::from_value::(value.clone()).unwrap_or_default() + // Agent session 的新结构要求每个锚点只保存一段文本;这里兼容旧对象/数组存档, + // 读取时压缩成单字段,写回时仍由 EightAnchorContent 序列化为新结构。 + EightAnchorContent { + world_promise: normalize_anchor_text(value.get("worldPromise")), + player_fantasy: normalize_anchor_text(value.get("playerFantasy")), + theme_boundary: normalize_anchor_text(value.get("themeBoundary")), + player_entry_point: normalize_anchor_text(value.get("playerEntryPoint")), + core_conflict: normalize_anchor_text(value.get("coreConflict")), + key_relationships: normalize_anchor_text(value.get("keyRelationships")), + hidden_lines: normalize_anchor_text(value.get("hiddenLines")), + iconic_elements: normalize_anchor_text(value.get("iconicElements")), + } +} + +fn normalize_anchor_text(value: Option<&JsonValue>) -> Option { + let normalized = compact_json_anchor_text(value?)?; + Some(clamp_text(normalized.as_str(), 180)) +} + +fn compact_json_anchor_text(value: &JsonValue) -> Option { + match value { + JsonValue::Null => None, + JsonValue::String(text) => { + let normalized = text.split_whitespace().collect::>().join(" "); + (!normalized.trim().is_empty()).then_some(normalized.trim().to_string()) + } + JsonValue::Array(items) => { + let values = items + .iter() + .filter_map(compact_json_anchor_text) + .collect::>(); + let compacted = dedupe_string_list(values, 8).join(";"); + (!compacted.trim().is_empty()).then_some(compacted) + } + JsonValue::Object(object) => { + let values = object + .values() + .filter_map(compact_json_anchor_text) + .collect::>(); + let compacted = dedupe_string_list(values, 8).join(";"); + (!compacted.trim().is_empty()).then_some(compacted) + } + JsonValue::Bool(value) => Some(value.to_string()), + JsonValue::Number(value) => Some(value.to_string()), + } +} + +fn split_anchor_phrases(value: Option<&str>) -> Vec { + value + .unwrap_or_default() + .split([';', ';', '、', ',', ',', '\n']) + .map(str::trim) + .filter(|item| !item.is_empty()) + .map(str::to_string) + .collect() } fn build_creator_intent_from_eight_anchor_content( anchor_content: &EightAnchorContent, ) -> CreatorIntentRecord { - let key_characters = anchor_content + let key_relationship_text = anchor_content .key_relationships - .iter() - .enumerate() - .map(|(index, entry)| { - let (lead_name, relation_to_player) = split_relationship_pair(entry.pairs.as_str()); - CreatorCharacterSeedRecord { - id: format!("creator-character-{}", index + 1), - name: if lead_name.is_empty() { - format!("关键人物{}", index + 1) - } else { - lead_name - }, - role: entry.relationship_type.clone(), - public_mask: String::new(), - hidden_hook: entry.secret_or_cost.clone(), - relation_to_player, - notes: String::new(), - } - }) - .collect::>(); - - let core_conflicts = anchor_content - .core_conflict - .as_ref() - .map(|value| { - value - .surface_conflicts - .iter() - .cloned() - .chain( - (!value.hidden_crisis.trim().is_empty()).then_some(value.hidden_crisis.clone()), - ) - .collect::>() - }) - .unwrap_or_default(); + .as_deref() + .unwrap_or_default() + .trim() + .to_string(); + let key_characters = if key_relationship_text.is_empty() { + Vec::new() + } else { + let (lead_name, relation_to_player) = + split_relationship_pair(key_relationship_text.as_str()); + vec![CreatorCharacterSeedRecord { + id: "creator-character-1".to_string(), + name: if lead_name.is_empty() { + "关键人物1".to_string() + } else { + lead_name + }, + role: key_relationship_text.clone(), + public_mask: String::new(), + hidden_hook: key_relationship_text.clone(), + relation_to_player, + notes: String::new(), + }] + }; CreatorIntentRecord { source_mode: "freeform".to_string(), raw_setting_text: compact_lines([ - anchor_content - .world_promise - .as_ref() - .map(|value| value.differentiator.as_str()), - anchor_content - .player_fantasy - .as_ref() - .map(|value| value.core_pursuit.as_str()), - anchor_content - .hidden_lines - .as_ref() - .and_then(|value| value.hidden_truths.first().map(String::as_str)), + anchor_content.world_promise.as_deref(), + anchor_content.player_fantasy.as_deref(), + anchor_content.hidden_lines.as_deref(), ]), - world_hook: compact_lines([ - anchor_content - .world_promise - .as_ref() - .map(|value| value.hook.as_str()), - anchor_content - .world_promise - .as_ref() - .map(|value| value.differentiator.as_str()), - ]), - theme_keywords: anchor_content - .theme_boundary - .as_ref() - .map(|value| value.tone_keywords.clone()) - .unwrap_or_default(), - tone_directives: anchor_content - .theme_boundary - .as_ref() - .map(|value| value.aesthetic_directives.clone()) - .unwrap_or_default(), + world_hook: anchor_content.world_promise.clone().unwrap_or_default(), + theme_keywords: split_anchor_phrases(anchor_content.theme_boundary.as_deref()), + tone_directives: split_anchor_phrases(anchor_content.theme_boundary.as_deref()), player_premise: compact_lines([ - anchor_content - .player_fantasy - .as_ref() - .map(|value| value.player_role.as_str()), - anchor_content - .player_entry_point - .as_ref() - .map(|value| value.opening_identity.as_str()), + anchor_content.player_fantasy.as_deref(), + anchor_content.player_entry_point.as_deref(), ]), - opening_situation: compact_lines([ - anchor_content - .player_entry_point - .as_ref() - .map(|value| value.opening_problem.as_str()), - anchor_content - .player_entry_point - .as_ref() - .map(|value| value.entry_motivation.as_str()), - ]), - core_conflicts: dedupe_string_list(core_conflicts, 6), + opening_situation: anchor_content + .player_entry_point + .clone() + .unwrap_or_default(), + core_conflicts: dedupe_string_list( + split_anchor_phrases(anchor_content.core_conflict.as_deref()), + 6, + ), key_characters, key_landmarks: Vec::new(), iconic_elements: dedupe_string_list( - anchor_content - .iconic_elements - .as_ref() - .map(|value| { - value - .iconic_motifs - .iter() - .cloned() - .chain(value.institutions_or_artifacts.iter().cloned()) - .collect::>() - }) - .unwrap_or_default(), + split_anchor_phrases(anchor_content.iconic_elements.as_deref()), 8, ), forbidden_directives: dedupe_string_list( - anchor_content - .theme_boundary - .as_ref() - .map(|value| value.forbidden_directives.clone()) - .unwrap_or_default() + split_anchor_phrases(anchor_content.theme_boundary.as_deref()) .into_iter() - .chain( - anchor_content - .iconic_elements - .as_ref() - .map(|value| value.hard_rules.clone()) - .unwrap_or_default(), - ) + .chain(split_anchor_phrases( + anchor_content.iconic_elements.as_deref(), + )) + .filter(|value| contains_any(value, &["避免", "不要", "禁止", "不能", "硬规则"])) .collect::>(), 8, ), @@ -1370,36 +1272,12 @@ fn detect_drift_risk( let filled_count = [ anchor_content.world_promise.is_some(), anchor_content.player_fantasy.is_some(), - anchor_content - .theme_boundary - .as_ref() - .map(|value| { - !value.tone_keywords.is_empty() - || !value.aesthetic_directives.is_empty() - || !value.forbidden_directives.is_empty() - }) - .unwrap_or(false), + anchor_content.theme_boundary.is_some(), anchor_content.player_entry_point.is_some(), anchor_content.core_conflict.is_some(), - !anchor_content.key_relationships.is_empty(), - anchor_content - .hidden_lines - .as_ref() - .map(|value| { - !value.hidden_truths.is_empty() - || !value.misdirection_hints.is_empty() - || !value.reveal_pacing.trim().is_empty() - }) - .unwrap_or(false), - anchor_content - .iconic_elements - .as_ref() - .map(|value| { - !value.iconic_motifs.is_empty() - || !value.institutions_or_artifacts.is_empty() - || !value.hard_rules.is_empty() - }) - .unwrap_or(false), + anchor_content.key_relationships.is_some(), + anchor_content.hidden_lines.is_some(), + anchor_content.iconic_elements.is_some(), ] .iter() .filter(|value| **value) diff --git a/server-rs/crates/api-server/src/custom_world_ai.rs b/server-rs/crates/api-server/src/custom_world_ai.rs index 4f3c47c8..5dda2b94 100644 --- a/server-rs/crates/api-server/src/custom_world_ai.rs +++ b/server-rs/crates/api-server/src/custom_world_ai.rs @@ -2548,26 +2548,24 @@ mod tests { name: Some("礁石神殿".to_string()), description: Some("古老礁石上的半沉神殿。".to_string()), }; - let manual_prompt = build_custom_world_scene_image_prompt( - SceneImagePromptParams { - profile: SceneImagePromptProfile { - name: profile_input.name.as_deref().unwrap_or_default(), - subtitle: profile_input.subtitle.as_deref().unwrap_or_default(), - tone: profile_input.tone.as_deref().unwrap_or_default(), - player_goal: profile_input.player_goal.as_deref().unwrap_or_default(), - summary: profile_input.summary.as_deref().unwrap_or_default(), - setting_text: profile_input.setting_text.as_deref().unwrap_or_default(), - }, - landmark: SceneImagePromptLandmark { - name: landmark.name.as_deref().unwrap_or_default(), - description: landmark.description.as_deref().unwrap_or_default(), - }, - user_prompt, - has_reference_image: false, - fallback_landmark_name: Some("礁石神殿"), - fallback_world_name: "雾海群岛", + let manual_prompt = build_custom_world_scene_image_prompt(SceneImagePromptParams { + profile: SceneImagePromptProfile { + name: profile_input.name.as_deref().unwrap_or_default(), + subtitle: profile_input.subtitle.as_deref().unwrap_or_default(), + tone: profile_input.tone.as_deref().unwrap_or_default(), + player_goal: profile_input.player_goal.as_deref().unwrap_or_default(), + summary: profile_input.summary.as_deref().unwrap_or_default(), + setting_text: profile_input.setting_text.as_deref().unwrap_or_default(), }, - ); + landmark: SceneImagePromptLandmark { + name: landmark.name.as_deref().unwrap_or_default(), + description: landmark.description.as_deref().unwrap_or_default(), + }, + user_prompt, + has_reference_image: false, + fallback_landmark_name: Some("礁石神殿"), + fallback_world_name: "雾海群岛", + }); let normalized = normalize_scene_image_request(CustomWorldSceneImageRequest { profile_id: Some("profile_001".to_string()), diff --git a/server-rs/crates/api-server/src/custom_world_foundation_draft.rs b/server-rs/crates/api-server/src/custom_world_foundation_draft.rs index a15d6c94..64b57249 100644 --- a/server-rs/crates/api-server/src/custom_world_foundation_draft.rs +++ b/server-rs/crates/api-server/src/custom_world_foundation_draft.rs @@ -933,7 +933,7 @@ fn build_scene_act_blueprint_from_landmark( .map(String::as_str) .map(str::trim) .filter(|value| !value.is_empty()) - .unwrap_or(""); + .map(ToOwned::to_owned); let opposite_npc_id = scene_npc_names.first().cloned().unwrap_or_default(); let event_description = act_events .get(act_index) @@ -944,13 +944,20 @@ fn build_scene_act_blueprint_from_landmark( .unwrap_or_else(|| { build_default_act_event_description(scene_summary, opposite_npc_id.as_str(), act_index) }); - // 缺失时保留空值,让后续生图前校验暴露底稿质量问题。 + let background_prompt = prompt.unwrap_or_else(|| { + build_default_act_background_prompt( + scene_summary, + opposite_npc_id.as_str(), + event_description.as_str(), + act_index, + ) + }); json!({ "id": format!("{}-act-{}", scene_id, act_index + 1), "sceneId": scene_id, "title": act_title, "summary": scene_summary, - "backgroundPromptText": prompt, + "backgroundPromptText": background_prompt, "encounterNpcIds": scene_npc_names, "primaryNpcId": opposite_npc_id, "oppositeNpcId": opposite_npc_id, @@ -982,11 +989,46 @@ fn build_default_act_event_description( } else { scene_summary.trim() }; + match act_index { + 0 => format!( + "第1幕中,{}先露出与{}有关的异常线索,玩家必须确认局势入口。", + role_text, scene_text + ), + 1 => format!( + "第2幕中,{}的立场或阻碍让{}升级,玩家必须在压力下作出判断。", + role_text, scene_text + ), + _ => format!( + "第3幕中,{}把{}推向高潮,玩家必须面对关键抉择或直接后果。", + role_text, scene_text + ), + } +} + +fn build_default_act_background_prompt( + scene_summary: &str, + opposite_npc_id: &str, + event_description: &str, + act_index: usize, +) -> String { + let role_text = if opposite_npc_id.trim().is_empty() { + "当前场景关键角色" + } else { + opposite_npc_id.trim() + }; + let scene_text = if scene_summary.trim().is_empty() { + "场景内的主线压力" + } else { + scene_summary.trim() + }; + let phase_text = match act_index { + 0 => "铺垫阶段", + 1 => "冲突升级阶段", + _ => "高潮阶段", + }; + // 中文注释:幕背景默认值直接吃同幕事件和角色,避免前端再拼规则说明句。 format!( - "第{}幕中,玩家与{}正面接触,围绕{}处理一件会改变局势走向的事件。", - act_index + 1, - role_text, - scene_text, + "{scene_text}的{phase_text}画面,{role_text}与玩家隔着可站立空间形成对峙,环境里保留“{event_description}”的冲突痕迹与清晰氛围。" ) } @@ -1080,11 +1122,16 @@ fn normalize_framework_shape(framework: &mut JsonValue, setting_text: &str) { JsonValue::Array( (0..3) .map(|index| { - JsonValue::String(format!( - "{}第{}幕,{},画面保留玩家站位、近景可交互物件与远景世界压力。", - camp_name, - index + 1, - camp_description, + let event_description = build_default_act_event_description( + camp_description.as_str(), + "开局关键角色", + index, + ); + JsonValue::String(build_default_act_background_prompt( + camp_description.as_str(), + "开局关键角色", + event_description.as_str(), + index, )) }) .collect(), @@ -1382,17 +1429,12 @@ fn normalize_scene_act_blueprint(act: JsonValue, index: usize) -> JsonValue { .unwrap_or_else(|| "当前幕推动场景内的主线压力。".to_string()); object.insert("title".to_string(), JsonValue::String(title.clone())); object.insert("summary".to_string(), JsonValue::String(summary.clone())); - let background_prompt = object + let raw_background_prompt = object .get("backgroundPromptText") .and_then(JsonValue::as_str) .map(str::trim) .filter(|value| !value.is_empty()) - .map(ToOwned::to_owned) - .unwrap_or_default(); - object.insert( - "backgroundPromptText".to_string(), - JsonValue::String(background_prompt), - ); + .map(ToOwned::to_owned); let encounter_npc_ids = object .get("encounterNpcIds") .and_then(JsonValue::as_array) @@ -1434,6 +1476,18 @@ fn normalize_scene_act_blueprint(act: JsonValue, index: usize) -> JsonValue { .unwrap_or_else(|| { build_default_act_event_description(summary.as_str(), opposite_npc_id.as_str(), index) }); + let background_prompt = raw_background_prompt.unwrap_or_else(|| { + build_default_act_background_prompt( + summary.as_str(), + opposite_npc_id.as_str(), + event_description.as_str(), + index, + ) + }); + object.insert( + "backgroundPromptText".to_string(), + JsonValue::String(background_prompt), + ); object.insert( "encounterNpcIds".to_string(), JsonValue::Array(encounter_npc_ids), @@ -1468,15 +1522,25 @@ fn build_fallback_scene_act() -> JsonValue { } fn build_fallback_scene_act_with_index(index: usize) -> JsonValue { + let event_description = build_default_act_event_description( + "玩家被推入第一波局势,必须先确认站位、威胁和下一步追查方向。", + "", + index, + ); json!({ "id": format!("scene-act-{}", index + 1), "title": if index == 0 { "开场场景幕".to_string() } else { format!("第{}幕", index + 1) }, "summary": "玩家被推入第一波局势,必须先确认站位、威胁和下一步追查方向。", - "backgroundPromptText": "", + "backgroundPromptText": build_default_act_background_prompt( + "玩家被推入第一波局势,必须先确认站位、威胁和下一步追查方向。", + "", + event_description.as_str(), + index, + ), "encounterNpcIds": [], "primaryNpcId": "", "oppositeNpcId": "", - "eventDescription": build_default_act_event_description("玩家被推入第一波局势,必须先确认站位、威胁和下一步追查方向。", "", index), + "eventDescription": event_description, }) } @@ -1489,12 +1553,21 @@ fn derive_world_name( session .anchor_content .get("worldPromise") - .and_then(JsonValue::as_object) - .and_then(|entry| entry.get("hook")) .and_then(JsonValue::as_str) .map(str::trim) .filter(|value| !value.is_empty()) .map(ToOwned::to_owned) + .or_else(|| { + session + .anchor_content + .get("worldPromise") + .and_then(JsonValue::as_object) + .and_then(|entry| entry.get("hook")) + .and_then(JsonValue::as_str) + .map(str::trim) + .filter(|value| !value.is_empty()) + .map(ToOwned::to_owned) + }) }) .unwrap_or_else(|| "未命名世界草稿".to_string()) } @@ -1508,12 +1581,21 @@ fn derive_world_hook( session .anchor_content .get("worldPromise") - .and_then(JsonValue::as_object) - .and_then(|entry| entry.get("hook")) .and_then(JsonValue::as_str) .map(str::trim) .filter(|value| !value.is_empty()) .map(ToOwned::to_owned) + .or_else(|| { + session + .anchor_content + .get("worldPromise") + .and_then(JsonValue::as_object) + .and_then(|entry| entry.get("hook")) + .and_then(JsonValue::as_str) + .map(str::trim) + .filter(|value| !value.is_empty()) + .map(ToOwned::to_owned) + }) }) .unwrap_or_else(|| { "这个世界正被一条持续扩大的主线危机推向失衡,而玩家会被卷入其中心。".to_string() @@ -1529,28 +1611,37 @@ fn derive_player_premise( session .anchor_content .get("playerEntryPoint") - .and_then(JsonValue::as_object) - .map(|entry| { - let identity = entry - .get("openingIdentity") - .and_then(JsonValue::as_str) - .map(str::trim) - .unwrap_or_default(); - let problem = entry - .get("openingProblem") - .and_then(JsonValue::as_str) - .map(str::trim) - .unwrap_or_default(); - let motivation = entry - .get("entryMotivation") - .and_then(JsonValue::as_str) - .map(str::trim) - .unwrap_or_default(); - [identity, problem, motivation] - .into_iter() - .filter(|value| !value.is_empty()) - .collect::>() - .join(";") + .and_then(JsonValue::as_str) + .map(str::trim) + .filter(|value| !value.is_empty()) + .map(ToOwned::to_owned) + .or_else(|| { + session + .anchor_content + .get("playerEntryPoint") + .and_then(JsonValue::as_object) + .map(|entry| { + let identity = entry + .get("openingIdentity") + .and_then(JsonValue::as_str) + .map(str::trim) + .unwrap_or_default(); + let problem = entry + .get("openingProblem") + .and_then(JsonValue::as_str) + .map(str::trim) + .unwrap_or_default(); + let motivation = entry + .get("entryMotivation") + .and_then(JsonValue::as_str) + .map(str::trim) + .unwrap_or_default(); + [identity, problem, motivation] + .into_iter() + .filter(|value| !value.is_empty()) + .collect::>() + .join(";") + }) }) .filter(|value| !value.trim().is_empty()) }) @@ -1740,7 +1831,7 @@ mod tests { } #[test] - fn normalize_scene_act_keeps_missing_background_prompt_empty() { + fn normalize_scene_act_fills_missing_background_prompt_from_event() { let act = normalize_scene_act_blueprint( json!({ "title": "第1幕", @@ -1749,7 +1840,15 @@ mod tests { 0, ); - assert_eq!(act.get("backgroundPromptText"), Some(&json!(""))); + assert!( + act.get("backgroundPromptText") + .and_then(JsonValue::as_str) + .is_some_and(|value| { + value.contains("铺垫阶段") + && value.contains("玩家进入雾港码头") + && value.contains("冲突痕迹") + }) + ); assert!( act.get("eventDescription") .and_then(JsonValue::as_str) diff --git a/server-rs/crates/api-server/src/prompt/agent_chat.rs b/server-rs/crates/api-server/src/prompt/agent_chat.rs index 0741f23b..8c1ad350 100644 --- a/server-rs/crates/api-server/src/prompt/agent_chat.rs +++ b/server-rs/crates/api-server/src/prompt/agent_chat.rs @@ -182,50 +182,28 @@ pub(crate) const OUTPUT_CONTRACT_REMINDER: &str = r#"请严格按以下 JSON 结 "replyText": "", "progressPercent": 0, "nextAnchorContent": { - "worldPromise": { - "hook": "", - "differentiator": "", - "desiredExperience": "" - }, - "playerFantasy": { - "playerRole": "", - "corePursuit": "", - "fearOfLoss": "" - }, - "themeBoundary": { - "toneKeywords": [], - "aestheticDirectives": [], - "forbiddenDirectives": [] - }, - "playerEntryPoint": { - "openingIdentity": "", - "openingProblem": "", - "entryMotivation": "" - }, - "coreConflict": { - "surfaceConflicts": [], - "hiddenCrisis": "", - "firstTouchedConflict": "" - }, - "keyRelationships": [ - { - "pairs": "", - "relationshipType": "", - "secretOrCost": "" - } - ], - "hiddenLines": { - "hiddenTruths": [], - "misdirectionHints": [], - "revealPacing": "" - }, - "iconicElements": { - "iconicMotifs": [], - "institutionsOrArtifacts": [], - "hardRules": [] - } + "worldPromise": "", + "playerFantasy": "", + "themeBoundary": "", + "playerEntryPoint": "", + "coreConflict": "", + "keyRelationships": "", + "hiddenLines": "", + "iconicElements": "" } -}"#; +} + +nextAnchorContent 的 8 个锚点每个都只能是一个字符串或 null,不允许输出对象或数组。 +请把每个锚点写成一段凝练中文: +- worldPromise 关注世界钩子、差异点、玩家体验。 +- playerFantasy 关注玩家身份、核心追求、失去风险。 +- themeBoundary 关注主题气质、美术方向、禁用方向。 +- playerEntryPoint 关注开局身份、开局问题、行动动机。 +- coreConflict 关注表层冲突、隐藏危机、首次触发点。 +- keyRelationships 关注关键人物关系、关系类型、代价或秘密。 +- hiddenLines 关注隐藏真相、误导线索、揭示节奏。 +- iconicElements 关注标志意象、组织/物件、硬规则。 +"#; pub(crate) fn render_dynamic_state_context(dynamic_state: &PromptDynamicState) -> String { format!( diff --git a/server-rs/crates/api-server/src/prompt/character_visual.rs b/server-rs/crates/api-server/src/prompt/character_visual.rs index ffc3394a..78f3fde6 100644 --- a/server-rs/crates/api-server/src/prompt/character_visual.rs +++ b/server-rs/crates/api-server/src/prompt/character_visual.rs @@ -1,27 +1,13 @@ -/// 自定义世界角色主图提示词脚本。 -pub(crate) fn build_character_visual_prompt( - prompt_text: &str, - character_brief_text: Option<&str>, -) -> String { - let character_brief = [character_brief_text.unwrap_or_default(), prompt_text] - .into_iter() - .map(str::trim) - .filter(|value| !value.is_empty()) - .collect::>() - .join("\n"); - - build_master_prompt(character_brief.as_str()) +/// 自定义世界角色主图提示词脚本。 +pub(crate) fn build_character_visual_prompt(prompt_text: &str) -> String { + build_master_prompt(prompt_text.trim()) } /// 角色主图被供应商内容审核拦截时使用的安全兜底提示词。 /// /// 这里刻意不继续携带角色姓名、作品名和长设定文本,避免把可疑专名原样送回上游导致连续失败。 -pub(crate) fn build_fallback_moderation_safe_character_visual_prompt( - prompt_text: &str, - character_brief_text: Option<&str>, -) -> String { - let source = [character_brief_text.unwrap_or_default(), prompt_text].join(" "); - let archetype = resolve_original_role_archetype(source.as_str()); +pub(crate) fn build_fallback_moderation_safe_character_visual_prompt(prompt_text: &str) -> String { + let archetype = resolve_original_role_archetype(prompt_text); build_master_prompt( [ @@ -61,11 +47,11 @@ fn resolve_original_role_archetype(source: &str) -> &'static str { /// 角色主图统一提示词骨架,迁移自旧共享 qwenSprite 主链。 fn build_master_prompt(character_brief: &str) -> String { [ - "单人,2D 横版游戏角色标准设定图,主体完整可见,底部轮廓完整,身体比例稳定,轮廓清楚,细节精致,设计感足,适合后续制作 sprite sheet 动画。".to_string(), + "单人,2D像素角色形象,头身比必须控制在 1.5 到 2 头身,主体完整可见,底部轮廓完整,身体比例稳定,轮廓清楚,适合后续制作 sprite sheet 动画。".to_string(), "视角要求:角色采用横版动作素材常用的右向斜侧身站姿,身体整体朝右,但保留少量正面信息,能读到面部轮廓与胸肩结构,不是完全 90 度纯右视图,也不是正面立绘。".to_string(), "主体要求:画面中只保留单个角色主体,不要额外人物、动物、召唤物、载具或陪体。".to_string(), "画面要求:1:1 正方形画布,画面中心构图,角色主体完整置于画面中央,不要裁切主体顶部和底部,不要镜头透视,不要特写。背景固定为纯绿色绿幕,只作为抠像底色,不出现建筑、室内布景、风景、地面道具、漂浮物、烟雾叙事元素、文字或其他角色以外的场景内容。".to_string(), - "风格要求:横版像素角色,头身比必须控制在 1 到 1.5 头身。使用深色清楚轮廓、稳定剪影、有限大色块和硬朗边缘,不要柔和厚涂插画感,发型、服装、配饰优先形成醒目可读的像素级识别点。".to_string(), + "风格要求:横版像素角色,细节精致,设计感足。使用深色清楚轮廓、稳定剪影、有限大色块和硬朗边缘,不要柔和厚涂插画感,发型、服装、配饰优先形成醒目可读的像素级识别点。".to_string(), "如果角色形象设定没有明确要求非人身体结构,默认优先使用人类或类人动作角色骨架。\ 默认将角色形象设定作用在角色自身的服装剪裁、材质、纹样、饰品、发光细节上。".to_string(), "角色形象设定:".to_string(), @@ -103,8 +89,6 @@ pub(crate) fn build_character_visual_negative_prompt() -> String { "文字", "水印", "UI 元素", - "软萌 Q版大头贴", - "儿童绘本风", "厚涂插画感", "低对比柔边", ] diff --git a/server-rs/crates/api-server/src/prompt/foundation_draft.rs b/server-rs/crates/api-server/src/prompt/foundation_draft.rs index 0815e998..a4e77d82 100644 --- a/server-rs/crates/api-server/src/prompt/foundation_draft.rs +++ b/server-rs/crates/api-server/src/prompt/foundation_draft.rs @@ -36,8 +36,8 @@ pub(crate) fn build_custom_world_framework_prompt(setting_text: &str) -> String "- templateWorldType 只是系统兼容字段,不代表正文应当引用的世界名称。".to_string(), "- camp 必须表示玩家开局时的落脚处,更接近归舍、住处、栖居、前哨居所这类“家/归处”的概念。".to_string(), "- camp.sceneTaskDescription 必须描述玩家首次进入开局场景时要完成的核心任务,会作为游戏章节任务生成上下文,控制在 24 到 56 个汉字内。".to_string(), - "- camp.actBackgroundPromptTexts 必须恰好 3 条,分别对应开局场景第 1/2/3 幕背景图画面内容描述;每条都必须可直接交给生图模型,控制在 40 到 90 个汉字内。".to_string(), - "- camp.actEventDescriptions 必须恰好 3 条,分别描述每一幕发生的事件;事件必须和当前幕对面的角色强相关,控制在 24 到 56 个汉字内。".to_string(), + "- camp.actEventDescriptions 必须恰好 3 条,分别描述每一幕发生的事件;第 1 幕负责铺垫,第 2 幕必须让冲突升级,第 3 幕必须形成高潮或关键抉择;事件必须和当前幕对面的角色强相关,控制在 24 到 56 个汉字内。".to_string(), + "- camp.actBackgroundPromptTexts 必须恰好 3 条,分别对应第 1/2/3 幕背景图画面内容描述;每条必须基于同序号 actEventDescriptions 和相关角色写出画面主体、站位空间、冲突痕迹与氛围,能直接交给生图模型,控制在 40 到 90 个汉字内。".to_string(), "- 不要输出 playableNpcs、storyNpcs、landmarks、items,也不要输出任何角色和地图细节。".to_string(), "- majorFactions 保持 2 到 3 个,coreConflicts 保持 2 到 3 个。".to_string(), "- 世界设定必须直接源自玩家输入,不要脱离主题乱扩写。".to_string(), @@ -199,9 +199,9 @@ pub(crate) fn build_custom_world_landmark_seed_batch_prompt( "- 每个地点只保留:name、description、visualDescription、sceneTaskDescription、actBackgroundPromptTexts、actEventDescriptions。".to_string(), "- sceneTaskDescription 必须描述玩家首次进入该场景时要完成的核心任务,会作为游戏章节任务生成上下文,控制在 24 到 56 个汉字内。".to_string(), "- visualDescription 是打开场景背景图像生成面板时默认填入的场景描述,必须具体到画面主体、远近景层次、地面可站立区域和氛围识别点,控制在 32 到 80 个汉字内。".to_string(), - "- actBackgroundPromptTexts 必须恰好 3 条,分别对应这个场景章节的第 1/2/3 幕背景图画面内容描述;每条都必须是大模型根据当前地点、主线阶段和可出场角色直接写出的画面描述,控制在 40 到 90 个汉字内。".to_string(), + "- actEventDescriptions 必须恰好 3 条,分别描述每一幕发生的事件;第 1 幕负责铺垫,第 2 幕必须让冲突升级,第 3 幕必须形成高潮或关键抉择;事件必须和当前幕对面的角色强相关,控制在 24 到 56 个汉字内。".to_string(), + "- actBackgroundPromptTexts 必须恰好 3 条,分别对应这个场景章节的第 1/2/3 幕背景图画面内容描述;每条都必须基于同序号 actEventDescriptions、当前地点和可出场角色直接写出画面主体、站位空间、冲突痕迹与氛围,控制在 40 到 90 个汉字内。".to_string(), "- actBackgroundPromptTexts 禁止使用“某某第1幕背景;玩家会在……”这类标题、摘要、规则句拼接格式;必须像可直接交给生图模型的自然画面描述。".to_string(), - "- actEventDescriptions 必须恰好 3 条,分别描述每一幕发生的事件;事件必须和当前幕对面的角色强相关,控制在 24 到 56 个汉字内。".to_string(), "- description 控制在 12 到 24 个汉字内。".to_string(), "- 所有生成文本都必须使用中文。".to_string(), "- 返回前自检:必须是一个能被 JSON.parse 直接解析的单个 JSON 对象。".to_string(), @@ -258,6 +258,7 @@ pub(crate) fn build_custom_world_landmark_network_batch_prompt( "要求:".to_string(), "- 必须只补全本批场景,name 必须与本批场景完全一致,不得增删改名。".to_string(), "- sceneNpcNames 只能引用上方可用场景角色名单中的名字,每个地点 1 到 3 个。".to_string(), + "- sceneNpcNames 的第一位会成为每幕对面主角色;三幕事件和幕背景必须围绕这个角色的行动、阻碍、试探或求助展开。".to_string(), "- connectedLandmarkNames 优先引用已知关键场景名称,每个地点 1 到 3 个。".to_string(), "- entryHook 控制在 16 到 36 个汉字内。".to_string(), "- 所有生成文本都必须使用中文。".to_string(), diff --git a/server-rs/crates/api-server/src/puzzle.rs b/server-rs/crates/api-server/src/puzzle.rs index c1ba06b7..8e9c120b 100644 --- a/server-rs/crates/api-server/src/puzzle.rs +++ b/server-rs/crates/api-server/src/puzzle.rs @@ -47,13 +47,14 @@ use spacetime_client::{ PuzzleAgentMessageRecord, PuzzleAgentMessageSubmitRecordInput, PuzzleAgentSessionCreateRecordInput, PuzzleAgentSessionRecord, PuzzleAgentSuggestedActionRecord, PuzzleAnchorItemRecord, PuzzleAnchorPackRecord, - PuzzleCreatorIntentRecord, PuzzleGeneratedImageCandidateRecord, - PuzzleGeneratedImagesSaveRecordInput, PuzzlePublishRecordInput, PuzzleResultDraftRecord, - PuzzleResultPreviewBlockerRecord, PuzzleResultPreviewFindingRecord, PuzzleResultPreviewRecord, - PuzzleBoardRecord, PuzzleCellPositionRecord, PuzzleMergedGroupRecord, PuzzlePieceStateRecord, - PuzzleRunDragRecordInput, PuzzleRunRecord, PuzzleRunStartRecordInput, PuzzleRunSwapRecordInput, - PuzzleRuntimeLevelRecord, PuzzleSelectCoverImageRecordInput, PuzzleWorkProfileRecord, - PuzzleWorkUpsertRecordInput, SpacetimeClientError, + PuzzleBoardRecord, PuzzleCellPositionRecord, PuzzleCreatorIntentRecord, + PuzzleGeneratedImageCandidateRecord, PuzzleGeneratedImagesSaveRecordInput, + PuzzleMergedGroupRecord, PuzzlePieceStateRecord, PuzzlePublishRecordInput, + PuzzleResultDraftRecord, PuzzleResultPreviewBlockerRecord, PuzzleResultPreviewFindingRecord, + PuzzleResultPreviewRecord, PuzzleRunDragRecordInput, PuzzleRunRecord, + PuzzleRunStartRecordInput, PuzzleRunSwapRecordInput, PuzzleRuntimeLevelRecord, + PuzzleSelectCoverImageRecordInput, PuzzleWorkProfileRecord, PuzzleWorkUpsertRecordInput, + SpacetimeClientError, }; use std::convert::Infallible; use tokio::time::sleep; @@ -1639,7 +1640,10 @@ async fn generate_puzzle_image_candidates( let mut items = Vec::with_capacity(generated.images.len()); for (index, image) in generated.images.into_iter().enumerate() { - let candidate_id = format!("{session_id}-candidate-{}", candidate_start_index + index + 1); + let candidate_id = format!( + "{session_id}-candidate-{}", + candidate_start_index + index + 1 + ); let asset = persist_puzzle_generated_asset( state, owner_user_id, @@ -1690,10 +1694,12 @@ async fn build_local_next_puzzle_run( })) })?; if current_level.status != "cleared" { - return Err(AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ - "provider": PUZZLE_RUNTIME_PROVIDER, - "message": "current level is not cleared", - }))); + return Err( + AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ + "provider": PUZZLE_RUNTIME_PROVIDER, + "message": "current level is not cleared", + })), + ); } if let Some(gallery_item) = resolve_gallery_next_puzzle_work(state, &run).await? { @@ -1702,10 +1708,12 @@ async fn build_local_next_puzzle_run( let source_session_id = payload.source_session_id.unwrap_or_default(); if source_session_id.trim().is_empty() { - return Err(AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ - "provider": PUZZLE_RUNTIME_PROVIDER, - "message": "sourceSessionId is required when gallery has no next puzzle work", - }))); + return Err( + AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ + "provider": PUZZLE_RUNTIME_PROVIDER, + "message": "sourceSessionId is required when gallery has no next puzzle work", + })), + ); } let session = state .spacetime_client() @@ -1767,14 +1775,23 @@ async fn build_local_next_puzzle_run( let candidate = updated_session .draft .as_ref() - .and_then(|draft| draft.candidates.iter().find(|candidate| !candidate.image_src.is_empty())) + .and_then(|draft| { + draft + .candidates + .iter() + .find(|candidate| !candidate.image_src.is_empty()) + }) .ok_or_else(|| { AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({ "provider": PUZZLE_RUNTIME_PROVIDER, "message": "现场生成后没有可用候选图", })) })?; - Ok(build_next_run_from_candidate(run, &updated_session, candidate)) + Ok(build_next_run_from_candidate( + run, + &updated_session, + candidate, + )) } async fn resolve_gallery_next_puzzle_work( @@ -1788,7 +1805,10 @@ async fn resolve_gallery_next_puzzle_work( .map_err(map_puzzle_client_error)?; Ok(items.into_iter().find(|item| { item.publication_status == "published" - && item.cover_image_src.as_ref().is_some_and(|value| !value.is_empty()) + && item + .cover_image_src + .as_ref() + .is_some_and(|value| !value.is_empty()) && !run.played_profile_ids.contains(&item.profile_id) })) } @@ -1836,7 +1856,9 @@ fn build_next_run_from_candidate( .map(|draft| format!("{} · 候选 {}", draft.level_name, level_index)) .unwrap_or_else(|| format!("候选拼图 {level_index}")), "当前草稿".to_string(), - draft.map(|draft| draft.theme_tags.clone()).unwrap_or_default(), + draft + .map(|draft| draft.theme_tags.clone()) + .unwrap_or_default(), Some(candidate.image_src.clone()), ) } @@ -1893,13 +1915,14 @@ fn build_local_puzzle_board(grid_size: u32) -> PuzzleBoardRecord { } let pieces = (0..total) .map(|index| { - let current = positions - .get(index as usize) - .cloned() - .unwrap_or(PuzzleCellPositionRecord { - row: index / grid_size, - col: index % grid_size, - }); + let current = + positions + .get(index as usize) + .cloned() + .unwrap_or(PuzzleCellPositionRecord { + row: index / grid_size, + col: index % grid_size, + }); PuzzlePieceStateRecord { piece_id: format!("piece-{index}"), correct_row: index / grid_size, diff --git a/server-rs/crates/api-server/src/runtime_chat.rs b/server-rs/crates/api-server/src/runtime_chat.rs index cbc1dcfd..85af42ed 100644 --- a/server-rs/crates/api-server/src/runtime_chat.rs +++ b/server-rs/crates/api-server/src/runtime_chat.rs @@ -71,7 +71,7 @@ pub async fn stream_runtime_npc_chat_turn( let llm_result = generate_llm_npc_chat_turn(&state, &request_context, &payload, &npc_name).await; - let (mut body, npc_reply, suggestions) = match llm_result { + let (mut body, npc_reply, suggestions, function_suggestions, force_exit) = match llm_result { Some(result) => result, None => { let npc_reply = build_deterministic_npc_reply( @@ -79,11 +79,21 @@ pub async fn stream_runtime_npc_chat_turn( player_message, payload.npc_initiates_conversation, ); - let suggestions = if should_force_chat_exit(payload.chat_directive.as_ref()) { + let force_exit = should_force_chat_exit(payload.chat_directive.as_ref()) + || should_hostile_chat_breakoff_deterministically( + player_message, + payload.chat_directive.as_ref(), + ); + let suggestions = if force_exit { Vec::new() } else { build_deterministic_chat_suggestions(npc_name.as_str(), player_message) }; + let function_suggestions = if force_exit { + Vec::new() + } else { + build_fallback_function_suggestions(payload.chat_directive.as_ref()) + }; let mut body = String::new(); append_sse_event( &request_context, @@ -91,7 +101,13 @@ pub async fn stream_runtime_npc_chat_turn( "reply_delta", &json!({ "text": npc_reply }), )?; - (body, npc_reply, suggestions) + ( + body, + npc_reply, + suggestions, + function_suggestions, + force_exit, + ) } }; @@ -103,8 +119,9 @@ pub async fn stream_runtime_npc_chat_turn( "affinityDelta": affinity_delta, "affinityText": describe_affinity_shift(affinity_delta), "suggestions": suggestions, + "functionSuggestions": function_suggestions, "pendingQuestOffer": null, - "chatDirective": build_completion_directive(payload.chat_directive.as_ref()), + "chatDirective": build_completion_directive(payload.chat_directive.as_ref(), force_exit), }); append_sse_event(&request_context, &mut body, "complete", &complete_payload)?; @@ -117,7 +134,7 @@ async fn generate_llm_npc_chat_turn( request_context: &RequestContext, payload: &NpcChatTurnRequest, npc_name: &str, -) -> Option<(String, String, Vec)> { +) -> Option<(String, String, Vec, Vec, bool)> { let llm_client = state.llm_client()?; let character = payload .character @@ -169,7 +186,7 @@ async fn generate_llm_npc_chat_turn( }); if should_force_chat_exit(payload.chat_directive.as_ref()) { - return Some((body, npc_reply, Vec::new())); + return Some((body, npc_reply, Vec::new(), Vec::new(), true)); } let suggestion_prompt = @@ -180,15 +197,37 @@ async fn generate_llm_npc_chat_turn( ]); suggestion_request.max_tokens = Some(200); suggestion_request.enable_web_search = state.config.rpg_llm_web_search_enabled; - let suggestions = llm_client + let suggestion_text = llm_client .request_text(suggestion_request) .await .ok() - .map(|response| parse_line_list_content(response.content.as_str(), 3)) - .filter(|items| items.len() == 3) - .unwrap_or_else(|| build_fallback_npc_chat_suggestions(payload.player_message.as_str())); + .map(|response| response.content) + .unwrap_or_default(); + let (mut suggestions, mut function_suggestions, should_end_chat) = + parse_npc_chat_suggestion_resolution( + suggestion_text.as_str(), + payload.chat_directive.as_ref(), + ); + let force_exit = should_end_chat + || should_hostile_chat_breakoff_deterministically( + payload.player_message.as_str(), + payload.chat_directive.as_ref(), + ); - Some((body, npc_reply, suggestions)) + if force_exit { + suggestions.clear(); + function_suggestions.clear(); + } else if suggestions.is_empty() { + suggestions = build_fallback_npc_chat_suggestions(payload.player_message.as_str()); + } + + Some(( + body, + npc_reply, + suggestions, + function_suggestions, + force_exit, + )) } fn build_deterministic_npc_reply( @@ -206,12 +245,12 @@ fn build_deterministic_npc_reply( fn build_deterministic_chat_suggestions(npc_name: &str, player_message: &str) -> Vec { // 建议只承载玩家可点选的行动意图,不在 UI 里额外塞说明文案。 vec![ - format!("继续询问{npc_name}的近况"), - "追问这里发生了什么".to_string(), + format!("{npc_name},我想先听你说"), + "这件事哪里不对劲".to_string(), if player_message.contains('帮') || player_message.contains('忙') { - "请对方说清需要什么帮助".to_string() + "先别绕,说清代价".to_string() } else { - "换个轻松的话题".to_string() + "你是不是还瞒着我".to_string() }, ] } @@ -225,33 +264,164 @@ fn build_fallback_npc_chat_suggestions(player_message: &str) -> Vec { }; vec![ - "你刚才那句是什么意思".to_string(), + "我愿意先听你说完".to_string(), format!("这事和{topic}有关吗"), - "你愿意再说清楚点吗".to_string(), + "你别再避重就轻".to_string(), ] } -fn build_completion_directive(chat_directive: Option<&Value>) -> Value { +fn build_fallback_function_suggestions(chat_directive: Option<&Value>) -> Vec { + read_function_options(chat_directive) + .into_iter() + .filter(|option| { + read_string_field(option, "functionId") + .as_deref() + .is_some_and(|function_id| function_id != "npc_chat") + }) + .take(2) + .filter_map(|option| { + let function_id = read_string_field(option, "functionId")?; + let action_text = read_string_field(option, "actionText")?; + Some(json!({ + "functionId": function_id, + "actionText": action_text, + })) + }) + .collect() +} + +fn build_completion_directive(chat_directive: Option<&Value>, force_exit: bool) -> Value { let Some(directive) = chat_directive else { return Value::Null; }; let closing_mode = read_string_field(directive, "closingMode") .filter(|value| value == "foreshadow_close") .unwrap_or_else(|| "free".to_string()); - let force_exit = closing_mode == "foreshadow_close" + let force_exit = force_exit + || closing_mode == "foreshadow_close" || directive .get("forceExitAfterTurn") .and_then(Value::as_bool) .unwrap_or(false); + let termination_reason = if force_exit { + read_string_field(directive, "terminationReason") + .filter(|value| value == "player_exit" || value == "hostile_breakoff") + .or_else(|| { + if is_hostile_model_chat(chat_directive) { + Some("hostile_breakoff".to_string()) + } else { + None + } + }) + } else { + None + }; json!({ "turnLimit": directive.get("turnLimit").cloned().unwrap_or(Value::Null), "remainingTurns": directive.get("remainingTurns").cloned().unwrap_or(Value::Null), "forceExit": force_exit, - "closingMode": closing_mode, + "closingMode": if force_exit { "foreshadow_close" } else { closing_mode.as_str() }, + "terminationReason": termination_reason, }) } +fn parse_npc_chat_suggestion_resolution( + text: &str, + chat_directive: Option<&Value>, +) -> (Vec, Vec, bool) { + let normalized = text.trim(); + if normalized.is_empty() { + return ( + Vec::new(), + build_fallback_function_suggestions(chat_directive), + false, + ); + } + + if let Ok(value) = serde_json::from_str::(normalized) { + let should_end_chat = value + .get("shouldEndChat") + .and_then(Value::as_bool) + .unwrap_or(false) + && is_hostile_model_chat(chat_directive); + let suggestions = value + .get("suggestions") + .and_then(Value::as_array) + .map(|items| { + items + .iter() + .filter_map(Value::as_str) + .map(str::trim) + .filter(|item| !item.is_empty()) + .map(ToOwned::to_owned) + .take(3) + .collect::>() + }) + .unwrap_or_default(); + let function_suggestions = + parse_function_suggestions(value.get("functionSuggestions"), chat_directive); + + return (suggestions, function_suggestions, should_end_chat); + } + + ( + parse_line_list_content(normalized, 3), + build_fallback_function_suggestions(chat_directive), + false, + ) +} + +fn parse_function_suggestions(value: Option<&Value>, chat_directive: Option<&Value>) -> Vec { + let allowed_options = read_function_options(chat_directive); + let allowed_ids = allowed_options + .iter() + .filter_map(|item| read_string_field(item, "functionId")) + .collect::>(); + let mut used_ids: Vec = Vec::new(); + + value + .and_then(Value::as_array) + .into_iter() + .flatten() + .filter_map(|item| { + let function_id = read_string_field(item, "functionId")?; + if function_id == "npc_chat" { + return None; + } + if !allowed_ids.is_empty() && !allowed_ids.contains(&function_id) { + return None; + } + if used_ids.contains(&function_id) { + return None; + } + let fallback_text = allowed_options + .iter() + .find(|option| { + read_string_field(option, "functionId").as_deref() == Some(function_id.as_str()) + }) + .and_then(|option| read_string_field(option, "actionText")); + let action_text = read_string_field(item, "actionText") + .or(fallback_text) + .filter(|text| !text.trim().is_empty())?; + used_ids.push(function_id.clone()); + Some(json!({ + "functionId": function_id, + "actionText": action_text, + })) + }) + .take(3) + .collect() +} + +fn read_function_options(chat_directive: Option<&Value>) -> Vec<&Value> { + chat_directive + .and_then(|directive| directive.get("functionOptions")) + .and_then(Value::as_array) + .map(|items| items.iter().collect::>()) + .unwrap_or_default() +} + fn read_string_field(value: &Value, field: &str) -> Option { value .get(field) @@ -268,18 +438,61 @@ fn read_number_field(value: &Value, field: &str) -> Option { .filter(|number| number.is_finite()) } +fn read_bool_field(value: &Value, field: &str) -> Option { + value.get(field).and_then(Value::as_bool) +} + fn should_force_chat_exit(chat_directive: Option<&Value>) -> bool { let Some(directive) = chat_directive else { return false; }; read_string_field(directive, "closingMode").as_deref() == Some("foreshadow_close") + || read_string_field(directive, "terminationReason").as_deref() == Some("player_exit") || directive .get("forceExitAfterTurn") .and_then(Value::as_bool) .unwrap_or(false) } +fn is_hostile_model_chat(chat_directive: Option<&Value>) -> bool { + let Some(directive) = chat_directive else { + return false; + }; + + read_string_field(directive, "terminationMode").as_deref() == Some("hostile_model") + || read_bool_field(directive, "isHostileChat").unwrap_or(false) +} + +fn should_hostile_chat_breakoff_deterministically( + player_message: &str, + chat_directive: Option<&Value>, +) -> bool { + if !is_hostile_model_chat(chat_directive) { + return false; + } + + let Some(directive) = chat_directive else { + return false; + }; + + if read_string_field(directive, "terminationReason").as_deref() == Some("player_exit") { + return true; + } + + let hostile_break_words = [ + "动手", + "开战", + "拔刀", + "杀", + "滚", + "闭嘴", + "少废话", + "别挡路", + ]; + count_keyword_matches(player_message, &hostile_break_words) > 0 +} + fn normalize_required_text(value: &str) -> Option { let normalized = value.trim(); if normalized.is_empty() { diff --git a/server-rs/crates/api-server/src/runtime_chat_prompt.rs b/server-rs/crates/api-server/src/runtime_chat_prompt.rs index 393a6a8b..161bf93d 100644 --- a/server-rs/crates/api-server/src/runtime_chat_prompt.rs +++ b/server-rs/crates/api-server/src/runtime_chat_prompt.rs @@ -6,10 +6,16 @@ pub(crate) const NPC_CHAT_TURN_REPLY_SYSTEM_PROMPT: &str = r#"你是角色扮演 - 如果这是第一次真正接触中的首轮回复,第一句必须先用自然招呼或开场判断起手,不能写成第三人称占位旁白。 回复长度控制在 1 到 3 句,必须紧接玩家刚说的话,自然推进气氛、情报或关系。"#; -pub(crate) const NPC_CHAT_TURN_SUGGESTION_SYSTEM_PROMPT: &str = r#"你要为玩家生成下一轮可直接点击的 3 条聊天续写候选。 -只输出纯文本,共 3 行,每行 1 条。 -不要加编号、项目符号、Markdown、JSON 或额外说明。 -三条候选必须明显不同,分别体现继续追问、表达态度、轻微拉近关系这三种不同方向。"#; +pub(crate) const NPC_CHAT_TURN_SUGGESTION_SYSTEM_PROMPT: &str = r#"你要为 RPG NPC 聊天生成下一步候选,并判断敌对聊天是否已经收束。 +只输出 JSON,不要输出 Markdown 或解释。 +JSON 结构: +{"shouldEndChat":false,"terminationReason":null,"suggestions":["温和共情台词","冷静追问台词","施压质疑台词"],"functionSuggestions":[{"functionId":"...","actionText":"玩家动作文本"}]} +- suggestions 是玩家下一轮可直接说出口的中文短句,每条 20 字以内;三条必须按顺序导向不同氛围和好感结果。 +- suggestions 第 1 条温和共情,通常让气氛缓和、好感上升;第 2 条冷静追问或试探,通常保持中性但推进情报;第 3 条施压、质疑或立场冲突,通常让气氛变紧、好感下降或付出代价。 +- functionSuggestions 只能从用户提示提供的 functionOptions 中挑选,不要发明 functionId。 +- functionSuggestions 的 actionText 必须像玩家可点击动作,不暴露 functionId,不写规则说明。 +- 非敌对聊天 shouldEndChat 必须为 false。 +- 敌对聊天可以随时 shouldEndChat=true,且敌对 NPC 更偏好在话不投机、被威胁、玩家退出、底线被触碰时结束聊天。"#; #[derive(Debug)] pub(crate) struct NpcChatTurnPromptInput<'a> { @@ -71,6 +77,17 @@ pub(crate) fn build_npc_chat_turn_reply_prompt(payload: &NpcChatTurnPromptInput< let closing_mode = chat_directive.and_then(|record| read_string(record.get("closingMode"))); let is_limited_negative_affinity_chat = limit_reason.as_deref() == Some("negative_affinity") && turn_limit > 0.0; + let is_hostile_model_chat = chat_directive + .and_then(|record| read_string(record.get("terminationMode"))) + .as_deref() + == Some("hostile_model") + || chat_directive + .and_then(|record| read_bool(record.get("isHostileChat"))) + .unwrap_or(false); + let is_player_exit_turn = chat_directive + .and_then(|record| read_string(record.get("terminationReason"))) + .as_deref() + == Some("player_exit"); let is_foreshadow_close_turn = closing_mode.as_deref() == Some("foreshadow_close") || chat_directive .and_then(|record| read_bool(record.get("forceExitAfterTurn"))) @@ -142,6 +159,21 @@ pub(crate) fn build_npc_chat_turn_reply_prompt(payload: &NpcChatTurnPromptInput< } else { None }, + if is_hostile_model_chat { + Some("当前是敌对或负好感聊天。对方不受固定回合限制,但随时可能不耐烦、结束谈话并把局势推向战斗或驱逐。".to_string()) + } else { + None + }, + if is_hostile_model_chat { + Some("敌对角色更偏好短促、戒备、带威胁的回应;如果玩家逼问、挑衅、退场或话题触到底线,回复应自然收束到对峙前一刻。".to_string()) + } else { + None + }, + if is_player_exit_turn { + Some("玩家正在主动结束这轮聊天。请对这个收束动作作出回应,并留下自然的下一步入口。回复后聊天会结束。".to_string()) + } else { + None + }, if is_limited_negative_affinity_chat { Some(format!( "在你回复完这一轮之后,还剩 {} 轮可以继续聊。", @@ -205,6 +237,22 @@ pub(crate) fn build_npc_chat_turn_suggestion_prompt( payload.dialogue }; let combat_context_block = payload.combat_context.and_then(describe_npc_combat_context); + let chat_directive = payload.chat_directive.and_then(as_record); + let is_hostile_model_chat = chat_directive + .and_then(|record| read_string(record.get("terminationMode"))) + .as_deref() + == Some("hostile_model") + || chat_directive + .and_then(|record| read_bool(record.get("isHostileChat"))) + .unwrap_or(false); + let is_player_exit_turn = chat_directive + .and_then(|record| read_string(record.get("terminationReason"))) + .as_deref() + == Some("player_exit"); + let function_options_block = chat_directive + .and_then(|record| record.get("functionOptions")) + .map(describe_function_options) + .filter(|text| !text.trim().is_empty()); [ Some(build_npc_dialogue_prompt_base(payload)), @@ -213,11 +261,22 @@ pub(crate) fn build_npc_chat_turn_suggestion_prompt( encounter.npc_name.as_str(), )), combat_context_block, + function_options_block, Some(format!("玩家刚刚说:{}", payload.player_message)), Some(format!("NPC 刚刚回复:{npc_reply}")), - Some("请围绕刚刚这轮对话,为玩家生成 3 条下一轮可以直接说出口的中文接话短句。".to_string()), - Some("每条都必须像玩家台词,不能写成行为描述、语气说明或策略建议。".to_string()), - Some("每条都必须控制在 20 个字以内,不要加序号、引号、括号或解释。".to_string()), + if is_hostile_model_chat { + Some("这是敌对或负好感聊天。你需要判断这轮是否应该结束聊天;敌对角色更偏好随时终止并转入对峙。".to_string()) + } else { + Some("这是非敌对聊天,shouldEndChat 必须为 false。".to_string()) + }, + if is_player_exit_turn { + Some("玩家已经选择结束聊天,shouldEndChat 必须为 true,terminationReason 必须为 player_exit。".to_string()) + } else { + None + }, + Some("suggestions 必须按顺序生成三种明显不同的玩家台词:温和共情、冷静追问或试探、施压质疑;不要给出同一种态度的近义句。".to_string()), + Some("functionSuggestions 从 functionOptions 中挑可触发动作并改写 actionText。".to_string()), + Some("只输出 JSON:{\"shouldEndChat\":false,\"terminationReason\":null,\"suggestions\":[\"...\"],\"functionSuggestions\":[{\"functionId\":\"...\",\"actionText\":\"...\"}]}".to_string()), ] .into_iter() .flatten() @@ -226,6 +285,38 @@ pub(crate) fn build_npc_chat_turn_suggestion_prompt( .join("\n\n") } +fn describe_function_options(value: &Value) -> String { + let lines = value + .as_array() + .map(|items| { + items + .iter() + .take(8) + .filter_map(|item| { + let record = as_record(item)?; + let function_id = read_string(record.get("functionId"))?; + let action_text = read_string(record.get("actionText"))?; + let detail_text = read_string(record.get("detailText")); + let action = read_string(record.get("action")); + Some(format!( + "- functionId: {function_id}; actionText: {action_text}; action: {}; detail: {}", + action.unwrap_or_else(|| "unknown".to_string()), + detail_text.unwrap_or_else(|| "无".to_string()), + )) + }) + .collect::>() + }) + .unwrap_or_default(); + + if lines.is_empty() { + return String::new(); + } + + let mut result = vec!["当前聊天中可改写为动作候选的 functionOptions:".to_string()]; + result.extend(lines); + result.join("\n") +} + fn build_npc_dialogue_prompt_base(payload: &NpcChatTurnPromptInput<'_>) -> String { let encounter = describe_encounter(payload.encounter); diff --git a/server-rs/crates/api-server/src/runtime_profile.rs b/server-rs/crates/api-server/src/runtime_profile.rs index 3989d6f0..07b6198b 100644 --- a/server-rs/crates/api-server/src/runtime_profile.rs +++ b/server-rs/crates/api-server/src/runtime_profile.rs @@ -7,15 +7,18 @@ use axum::{ use module_runtime::{ PROFILE_RECHARGE_PAYMENT_CHANNEL_MOCK, RuntimeProfileMembershipBenefitRecord, RuntimeProfileRechargeCenterRecord, RuntimeProfileRechargeOrderRecord, - RuntimeProfileRechargeProductRecord, + RuntimeProfileRechargeProductRecord, RuntimeReferralInviteCenterRecord, + RuntimeReferralRedeemRecord, }; use serde_json::{Value, json}; use shared_contracts::runtime::{ CreateProfileRechargeOrderRequest, CreateProfileRechargeOrderResponse, ProfileDashboardSummaryResponse, ProfileMembershipBenefitResponse, ProfileMembershipResponse, ProfilePlayStatsResponse, ProfilePlayedWorkSummaryResponse, ProfileRechargeCenterResponse, - ProfileRechargeOrderResponse, ProfileRechargeProductResponse, ProfileWalletLedgerEntryResponse, - ProfileWalletLedgerResponse, + ProfileRechargeOrderResponse, ProfileRechargeProductResponse, + ProfileReferralInviteCenterResponse, ProfileWalletLedgerEntryResponse, + ProfileWalletLedgerResponse, RedeemProfileReferralInviteCodeRequest, + RedeemProfileReferralInviteCodeResponse, }; use spacetime_client::SpacetimeClientError; use time::OffsetDateTime; @@ -146,6 +149,54 @@ pub async fn create_profile_recharge_order( )) } +pub async fn get_profile_referral_invite_center( + State(state): State, + Extension(request_context): Extension, + Extension(authenticated): Extension, +) -> Result, Response> { + let user_id = authenticated.claims().user_id().to_string(); + let record = state + .spacetime_client() + .get_profile_referral_invite_center(user_id) + .await + .map_err(|error| { + runtime_profile_error_response( + &request_context, + map_runtime_profile_client_error(error), + ) + })?; + + Ok(json_success_body( + Some(&request_context), + build_profile_referral_invite_center_response(record), + )) +} + +pub async fn redeem_profile_referral_invite_code( + State(state): State, + Extension(request_context): Extension, + Extension(authenticated): Extension, + Json(payload): Json, +) -> Result, Response> { + let user_id = authenticated.claims().user_id().to_string(); + let updated_at_micros = OffsetDateTime::now_utc().unix_timestamp_nanos() / 1_000; + let record = state + .spacetime_client() + .redeem_profile_referral_invite_code(user_id, payload.invite_code, updated_at_micros as i64) + .await + .map_err(|error| { + runtime_profile_error_response( + &request_context, + map_runtime_profile_client_error(error), + ) + })?; + + Ok(json_success_body( + Some(&request_context), + build_redeem_profile_referral_invite_code_response(record), + )) +} + pub async fn get_profile_play_stats( State(state): State, Extension(request_context): Extension, @@ -284,6 +335,36 @@ fn build_profile_recharge_order_response( } } +fn build_profile_referral_invite_center_response( + record: RuntimeReferralInviteCenterRecord, +) -> ProfileReferralInviteCenterResponse { + ProfileReferralInviteCenterResponse { + invite_code: record.invite_code, + invite_link_path: record.invite_link_path, + invited_count: record.invited_count, + rewarded_invite_count: record.rewarded_invite_count, + today_inviter_reward_count: record.today_inviter_reward_count, + today_inviter_reward_remaining: record.today_inviter_reward_remaining, + reward_points: record.reward_points, + has_redeemed_code: record.has_redeemed_code, + bound_inviter_user_id: record.bound_inviter_user_id, + bound_at: record.bound_at, + updated_at: record.updated_at, + } +} + +fn build_redeem_profile_referral_invite_code_response( + record: RuntimeReferralRedeemRecord, +) -> RedeemProfileReferralInviteCodeResponse { + RedeemProfileReferralInviteCodeResponse { + center: build_profile_referral_invite_center_response(record.center), + invitee_reward_granted: record.invitee_reward_granted, + inviter_reward_granted: record.inviter_reward_granted, + invitee_balance_after: record.invitee_balance_after, + inviter_balance_after: record.inviter_balance_after, + } +} + #[cfg(test)] mod tests { use axum::{ @@ -391,6 +472,43 @@ mod tests { assert_eq!(response.status(), StatusCode::UNAUTHORIZED); } + #[tokio::test] + async fn profile_referral_invite_center_requires_authentication() { + let app = build_router(AppState::new(AppConfig::default()).expect("state should build")); + + let response = app + .oneshot( + Request::builder() + .method("GET") + .uri("/api/profile/referrals/invite-center") + .body(Body::empty()) + .expect("request should build"), + ) + .await + .expect("request should succeed"); + + assert_eq!(response.status(), StatusCode::UNAUTHORIZED); + } + + #[tokio::test] + async fn profile_referral_redeem_code_requires_authentication() { + let app = build_router(AppState::new(AppConfig::default()).expect("state should build")); + + let response = app + .oneshot( + Request::builder() + .method("POST") + .uri("/api/profile/referrals/redeem-code") + .header("content-type", "application/json") + .body(Body::from(r#"{"inviteCode":"SY12345678"}"#)) + .expect("request should build"), + ) + .await + .expect("request should succeed"); + + assert_eq!(response.status(), StatusCode::UNAUTHORIZED); + } + #[tokio::test] async fn profile_dashboard_compat_route_matches_main_route_error_shape() { assert_compat_route_matches_main_route_error_shape( @@ -479,16 +597,7 @@ mod tests { } async fn seed_authenticated_state() -> AppState { - let state = AppState::new(AppConfig::default()).expect("state should build"); - state - .password_entry_service() - .execute(module_auth::PasswordEntryInput { - username: "runtime_profile_user".to_string(), - password: "secret123".to_string(), - }) - .await - .expect("seed login should succeed"); - state + AppState::new(AppConfig::default()).expect("state should build") } fn issue_access_token(state: &AppState) -> String { diff --git a/server-rs/crates/module-custom-world/src/lib.rs b/server-rs/crates/module-custom-world/src/lib.rs index 9d8fe120..14f50b85 100644 --- a/server-rs/crates/module-custom-world/src/lib.rs +++ b/server-rs/crates/module-custom-world/src/lib.rs @@ -1418,7 +1418,7 @@ pub fn build_custom_world_published_profile_compile_snapshot( } pub fn empty_agent_anchor_content_json() -> String { - r#"{"worldPromise":null,"playerFantasy":null,"themeBoundary":null,"playerEntryPoint":null,"coreConflict":null,"keyRelationships":[],"hiddenLines":null,"iconicElements":null}"#.to_string() + r#"{"worldPromise":null,"playerFantasy":null,"themeBoundary":null,"playerEntryPoint":null,"coreConflict":null,"keyRelationships":null,"hiddenLines":null,"iconicElements":null}"#.to_string() } pub fn empty_agent_creator_intent_readiness_json() -> String { diff --git a/server-rs/crates/shared-contracts/src/assets.rs b/server-rs/crates/shared-contracts/src/assets.rs index ee3cd538..a39915c8 100644 --- a/server-rs/crates/shared-contracts/src/assets.rs +++ b/server-rs/crates/shared-contracts/src/assets.rs @@ -99,8 +99,6 @@ pub struct CharacterVisualGenerateRequest { pub source_mode: CharacterVisualSourceMode, pub prompt_text: String, #[serde(default)] - pub character_brief_text: Option, - #[serde(default)] pub reference_image_data_urls: Vec, pub candidate_count: u32, pub image_model: String, diff --git a/server-rs/crates/shared-contracts/src/runtime.rs b/server-rs/crates/shared-contracts/src/runtime.rs index b34c514d..6f36e87e 100644 --- a/server-rs/crates/shared-contracts/src/runtime.rs +++ b/server-rs/crates/shared-contracts/src/runtime.rs @@ -5,6 +5,8 @@ pub const RUNTIME_PLATFORM_THEME_DARK: &str = "dark"; pub const SAVE_SNAPSHOT_VERSION: u32 = 2; pub const PROFILE_WALLET_LEDGER_SOURCE_TYPE_SNAPSHOT_SYNC: &str = "snapshot_sync"; pub const PROFILE_WALLET_LEDGER_SOURCE_TYPE_POINTS_RECHARGE: &str = "points_recharge"; +pub const PROFILE_WALLET_LEDGER_SOURCE_TYPE_INVITE_INVITER_REWARD: &str = "invite_inviter_reward"; +pub const PROFILE_WALLET_LEDGER_SOURCE_TYPE_INVITE_INVITEE_REWARD: &str = "invite_invitee_reward"; pub const BROWSE_HISTORY_THEME_MODE_MARTIAL: &str = "martial"; pub const BROWSE_HISTORY_THEME_MODE_ARCANE: &str = "arcane"; pub const BROWSE_HISTORY_THEME_MODE_MACHINA: &str = "machina"; @@ -220,6 +222,38 @@ pub struct CreateProfileRechargeOrderResponse { pub center: ProfileRechargeCenterResponse, } +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct ProfileReferralInviteCenterResponse { + pub invite_code: String, + pub invite_link_path: String, + pub invited_count: u32, + pub rewarded_invite_count: u32, + pub today_inviter_reward_count: u32, + pub today_inviter_reward_remaining: u32, + pub reward_points: u64, + pub has_redeemed_code: bool, + pub bound_inviter_user_id: Option, + pub bound_at: Option, + pub updated_at: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct RedeemProfileReferralInviteCodeRequest { + pub invite_code: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct RedeemProfileReferralInviteCodeResponse { + pub center: ProfileReferralInviteCenterResponse, + pub invitee_reward_granted: bool, + pub inviter_reward_granted: bool, + pub invitee_balance_after: u64, + pub inviter_balance_after: u64, +} + #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct ProfilePlayedWorkSummaryResponse { diff --git a/server-rs/crates/spacetime-client/src/lib.rs b/server-rs/crates/spacetime-client/src/lib.rs index bcaaf9b4..94e722cb 100644 --- a/server-rs/crates/spacetime-client/src/lib.rs +++ b/server-rs/crates/spacetime-client/src/lib.rs @@ -121,7 +121,8 @@ use module_runtime::{ RuntimeBrowseHistoryRecord, RuntimePlatformTheme as DomainRuntimePlatformTheme, RuntimeProfileDashboardRecord, RuntimeProfilePlayStatsRecord, RuntimeProfileRechargeCenterRecord, RuntimeProfileRechargeOrderRecord, - RuntimeProfileSaveArchiveRecord, RuntimeProfileWalletLedgerEntryRecord, RuntimeSettingsRecord, + RuntimeProfileSaveArchiveRecord, RuntimeProfileWalletLedgerEntryRecord, + RuntimeReferralInviteCenterRecord, RuntimeReferralRedeemRecord, RuntimeSettingsRecord, RuntimeSnapshotRecord, build_runtime_browse_history_clear_input, build_runtime_browse_history_list_input, build_runtime_browse_history_record, build_runtime_browse_history_sync_input, build_runtime_profile_dashboard_get_input, @@ -132,7 +133,9 @@ use module_runtime::{ build_runtime_profile_save_archive_list_input, build_runtime_profile_save_archive_record, build_runtime_profile_save_archive_resume_input, build_runtime_profile_wallet_ledger_entry_record, - build_runtime_profile_wallet_ledger_list_input, build_runtime_setting_get_input, + build_runtime_profile_wallet_ledger_list_input, build_runtime_referral_invite_center_get_input, + build_runtime_referral_invite_center_record, build_runtime_referral_redeem_input, + build_runtime_referral_redeem_record, build_runtime_setting_get_input, build_runtime_setting_record, build_runtime_setting_upsert_input, build_runtime_snapshot_delete_input, build_runtime_snapshot_get_input, build_runtime_snapshot_record, build_runtime_snapshot_upsert_input, diff --git a/server-rs/crates/spacetime-client/src/mapper.rs b/server-rs/crates/spacetime-client/src/mapper.rs index 33b00d92..539826cc 100644 --- a/server-rs/crates/spacetime-client/src/mapper.rs +++ b/server-rs/crates/spacetime-client/src/mapper.rs @@ -139,6 +139,26 @@ impl From } } +impl From + for RuntimeReferralInviteCenterGetInput +{ + fn from(input: module_runtime::RuntimeReferralInviteCenterGetInput) -> Self { + Self { + user_id: input.user_id, + } + } +} + +impl From for RuntimeReferralRedeemInput { + fn from(input: module_runtime::RuntimeReferralRedeemInput) -> Self { + Self { + user_id: input.user_id, + invite_code: input.invite_code, + updated_at_micros: input.updated_at_micros, + } + } +} + impl From for RuntimeProfilePlayStatsGetInput { fn from(input: module_runtime::RuntimeProfilePlayStatsGetInput) -> Self { Self { @@ -675,6 +695,50 @@ pub(crate) fn map_runtime_profile_recharge_order_procedure_result( )) } +pub(crate) fn map_runtime_referral_invite_center_procedure_result( + result: RuntimeReferralInviteCenterProcedureResult, +) -> Result { + if !result.ok { + return Err(SpacetimeClientError::Procedure( + result + .error_message + .unwrap_or_else(|| "SpacetimeDB procedure 返回未知错误".to_string()), + )); + } + + let snapshot = result.record.ok_or_else(|| { + SpacetimeClientError::Procedure( + "SpacetimeDB procedure 未返回 referral invite center 快照".to_string(), + ) + })?; + + Ok(build_runtime_referral_invite_center_record( + map_runtime_referral_invite_center_snapshot(snapshot), + )) +} + +pub(crate) fn map_runtime_referral_redeem_procedure_result( + result: RuntimeReferralRedeemProcedureResult, +) -> Result { + if !result.ok { + return Err(SpacetimeClientError::Procedure( + result + .error_message + .unwrap_or_else(|| "SpacetimeDB procedure 返回未知错误".to_string()), + )); + } + + let snapshot = result.record.ok_or_else(|| { + SpacetimeClientError::Procedure( + "SpacetimeDB procedure 未返回 referral redeem 快照".to_string(), + ) + })?; + + Ok(build_runtime_referral_redeem_record( + map_runtime_referral_redeem_snapshot(snapshot), + )) +} + pub(crate) fn map_runtime_profile_play_stats_procedure_result( result: RuntimeProfilePlayStatsProcedureResult, ) -> Result { @@ -1513,6 +1577,37 @@ pub(crate) fn map_runtime_profile_recharge_order_snapshot( } } +pub(crate) fn map_runtime_referral_invite_center_snapshot( + snapshot: RuntimeReferralInviteCenterSnapshot, +) -> module_runtime::RuntimeReferralInviteCenterSnapshot { + module_runtime::RuntimeReferralInviteCenterSnapshot { + user_id: snapshot.user_id, + invite_code: snapshot.invite_code, + invite_link_path: snapshot.invite_link_path, + invited_count: snapshot.invited_count, + rewarded_invite_count: snapshot.rewarded_invite_count, + today_inviter_reward_count: snapshot.today_inviter_reward_count, + today_inviter_reward_remaining: snapshot.today_inviter_reward_remaining, + reward_points: snapshot.reward_points, + has_redeemed_code: snapshot.has_redeemed_code, + bound_inviter_user_id: snapshot.bound_inviter_user_id, + bound_at_micros: snapshot.bound_at_micros, + updated_at_micros: snapshot.updated_at_micros, + } +} + +pub(crate) fn map_runtime_referral_redeem_snapshot( + snapshot: RuntimeReferralRedeemSnapshot, +) -> module_runtime::RuntimeReferralRedeemSnapshot { + module_runtime::RuntimeReferralRedeemSnapshot { + center: map_runtime_referral_invite_center_snapshot(snapshot.center), + invitee_reward_granted: snapshot.invitee_reward_granted, + inviter_reward_granted: snapshot.inviter_reward_granted, + invitee_balance_after: snapshot.invitee_balance_after, + inviter_balance_after: snapshot.inviter_balance_after, + } +} + pub(crate) fn map_runtime_profile_played_world_snapshot( snapshot: RuntimeProfilePlayedWorldSnapshot, ) -> module_runtime::RuntimeProfilePlayedWorldSnapshot { diff --git a/server-rs/crates/spacetime-client/src/module_bindings/get_profile_referral_invite_center_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/get_profile_referral_invite_center_procedure.rs new file mode 100644 index 00000000..1bc6fc86 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/get_profile_referral_invite_center_procedure.rs @@ -0,0 +1,58 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{ + self as __sdk, + __lib, + __sats, + __ws, +}; + +use super::runtime_referral_invite_center_get_input_type::RuntimeReferralInviteCenterGetInput; +use super::runtime_referral_invite_center_procedure_result_type::RuntimeReferralInviteCenterProcedureResult; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] + struct GetProfileReferralInviteCenterArgs { + pub input: RuntimeReferralInviteCenterGetInput, +} + + +impl __sdk::InModule for GetProfileReferralInviteCenterArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `get_profile_referral_invite_center`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait get_profile_referral_invite_center { + fn get_profile_referral_invite_center(&self, input: RuntimeReferralInviteCenterGetInput, +) { + self.get_profile_referral_invite_center_then(input, |_, _| {}); + } + + fn get_profile_referral_invite_center_then( + &self, + input: RuntimeReferralInviteCenterGetInput, + + __callback: impl FnOnce(&super::ProcedureEventContext, Result) + Send + 'static, + ); +} + +impl get_profile_referral_invite_center for super::RemoteProcedures { + fn get_profile_referral_invite_center_then( + &self, + input: RuntimeReferralInviteCenterGetInput, + + __callback: impl FnOnce(&super::ProcedureEventContext, Result) + Send + 'static, + ) { + self.imp.invoke_procedure_with_callback::<_, RuntimeReferralInviteCenterProcedureResult>( + "get_profile_referral_invite_center", + GetProfileReferralInviteCenterArgs { input, }, + __callback, + ); + } +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/mod.rs b/server-rs/crates/spacetime-client/src/module_bindings/mod.rs index 6d3049a4..10e41e4b 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/mod.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/mod.rs @@ -189,9 +189,11 @@ pub mod player_progression_grant_source_type; pub mod player_progression_procedure_result_type; pub mod player_progression_snapshot_type; pub mod profile_dashboard_state_type; +pub mod profile_invite_code_type; pub mod profile_membership_type; pub mod profile_played_world_type; pub mod profile_recharge_order_type; +pub mod profile_referral_relation_type; pub mod profile_save_archive_type; pub mod profile_wallet_ledger_type; pub mod puzzle_agent_message_finalize_input_type; @@ -305,6 +307,12 @@ pub mod runtime_profile_wallet_ledger_entry_snapshot_type; pub mod runtime_profile_wallet_ledger_list_input_type; pub mod runtime_profile_wallet_ledger_procedure_result_type; pub mod runtime_profile_wallet_ledger_source_type_type; +pub mod runtime_referral_invite_center_get_input_type; +pub mod runtime_referral_invite_center_procedure_result_type; +pub mod runtime_referral_invite_center_snapshot_type; +pub mod runtime_referral_redeem_input_type; +pub mod runtime_referral_redeem_procedure_result_type; +pub mod runtime_referral_redeem_snapshot_type; pub mod runtime_setting_type; pub mod runtime_setting_get_input_type; pub mod runtime_setting_procedure_result_type; @@ -359,7 +367,52 @@ pub mod unpublish_custom_world_profile_reducer; pub mod upsert_chapter_progression_reducer; pub mod upsert_custom_world_profile_reducer; pub mod upsert_npc_state_reducer; +pub mod ai_result_reference_table; +pub mod ai_task_table; +pub mod ai_task_stage_table; +pub mod ai_text_chunk_table; +pub mod asset_entity_binding_table; +pub mod asset_object_table; +pub mod auth_identity_table; +pub mod auth_store_snapshot_table; +pub mod battle_state_table; +pub mod big_fish_agent_message_table; +pub mod big_fish_asset_slot_table; +pub mod big_fish_creation_session_table; +pub mod big_fish_runtime_run_table; +pub mod chapter_progression_table; +pub mod custom_world_agent_message_table; +pub mod custom_world_agent_operation_table; +pub mod custom_world_agent_session_table; +pub mod custom_world_draft_card_table; pub mod custom_world_gallery_entry_table; +pub mod custom_world_profile_table; +pub mod custom_world_session_table; +pub mod inventory_slot_table; +pub mod npc_state_table; +pub mod player_progression_table; +pub mod profile_dashboard_state_table; +pub mod profile_invite_code_table; +pub mod profile_membership_table; +pub mod profile_played_world_table; +pub mod profile_recharge_order_table; +pub mod profile_referral_relation_table; +pub mod profile_save_archive_table; +pub mod profile_wallet_ledger_table; +pub mod puzzle_agent_message_table; +pub mod puzzle_agent_session_table; +pub mod puzzle_runtime_run_table; +pub mod puzzle_work_profile_table; +pub mod quest_log_table; +pub mod quest_record_table; +pub mod refresh_session_table; +pub mod runtime_setting_table; +pub mod runtime_snapshot_table; +pub mod story_event_table; +pub mod story_session_table; +pub mod treasure_record_table; +pub mod user_account_table; +pub mod user_browse_history_table; pub mod advance_puzzle_next_level_procedure; pub mod append_ai_text_chunk_and_return_procedure; pub mod apply_chapter_progression_ledger_entry_and_return_procedure; @@ -409,6 +462,7 @@ pub mod get_player_progression_or_default_procedure; pub mod get_profile_dashboard_procedure; pub mod get_profile_play_stats_procedure; pub mod get_profile_recharge_center_procedure; +pub mod get_profile_referral_invite_center_procedure; pub mod get_puzzle_agent_session_procedure; pub mod get_puzzle_gallery_detail_procedure; pub mod get_puzzle_run_procedure; @@ -432,6 +486,7 @@ pub mod publish_big_fish_game_procedure; pub mod publish_custom_world_profile_and_return_procedure; pub mod publish_custom_world_world_procedure; pub mod publish_puzzle_work_procedure; +pub mod redeem_profile_referral_invite_code_procedure; pub mod resolve_combat_action_and_return_procedure; pub mod resolve_npc_battle_interaction_and_return_procedure; pub mod resolve_npc_interaction_and_return_procedure; @@ -636,9 +691,11 @@ pub use player_progression_grant_source_type::PlayerProgressionGrantSource; pub use player_progression_procedure_result_type::PlayerProgressionProcedureResult; pub use player_progression_snapshot_type::PlayerProgressionSnapshot; pub use profile_dashboard_state_type::ProfileDashboardState; +pub use profile_invite_code_type::ProfileInviteCode; pub use profile_membership_type::ProfileMembership; pub use profile_played_world_type::ProfilePlayedWorld; pub use profile_recharge_order_type::ProfileRechargeOrder; +pub use profile_referral_relation_type::ProfileReferralRelation; pub use profile_save_archive_type::ProfileSaveArchive; pub use profile_wallet_ledger_type::ProfileWalletLedger; pub use puzzle_agent_message_finalize_input_type::PuzzleAgentMessageFinalizeInput; @@ -752,6 +809,12 @@ pub use runtime_profile_wallet_ledger_entry_snapshot_type::RuntimeProfileWalletL pub use runtime_profile_wallet_ledger_list_input_type::RuntimeProfileWalletLedgerListInput; pub use runtime_profile_wallet_ledger_procedure_result_type::RuntimeProfileWalletLedgerProcedureResult; pub use runtime_profile_wallet_ledger_source_type_type::RuntimeProfileWalletLedgerSourceType; +pub use runtime_referral_invite_center_get_input_type::RuntimeReferralInviteCenterGetInput; +pub use runtime_referral_invite_center_procedure_result_type::RuntimeReferralInviteCenterProcedureResult; +pub use runtime_referral_invite_center_snapshot_type::RuntimeReferralInviteCenterSnapshot; +pub use runtime_referral_redeem_input_type::RuntimeReferralRedeemInput; +pub use runtime_referral_redeem_procedure_result_type::RuntimeReferralRedeemProcedureResult; +pub use runtime_referral_redeem_snapshot_type::RuntimeReferralRedeemSnapshot; pub use runtime_setting_type::RuntimeSetting; pub use runtime_setting_get_input_type::RuntimeSettingGetInput; pub use runtime_setting_procedure_result_type::RuntimeSettingProcedureResult; @@ -782,7 +845,52 @@ pub use treasure_resolve_input_type::TreasureResolveInput; pub use unequip_inventory_item_input_type::UnequipInventoryItemInput; pub use user_account_type::UserAccount; pub use user_browse_history_type::UserBrowseHistory; +pub use ai_result_reference_table::*; +pub use ai_task_table::*; +pub use ai_task_stage_table::*; +pub use ai_text_chunk_table::*; +pub use asset_entity_binding_table::*; +pub use asset_object_table::*; +pub use auth_identity_table::*; +pub use auth_store_snapshot_table::*; +pub use battle_state_table::*; +pub use big_fish_agent_message_table::*; +pub use big_fish_asset_slot_table::*; +pub use big_fish_creation_session_table::*; +pub use big_fish_runtime_run_table::*; +pub use chapter_progression_table::*; +pub use custom_world_agent_message_table::*; +pub use custom_world_agent_operation_table::*; +pub use custom_world_agent_session_table::*; +pub use custom_world_draft_card_table::*; pub use custom_world_gallery_entry_table::*; +pub use custom_world_profile_table::*; +pub use custom_world_session_table::*; +pub use inventory_slot_table::*; +pub use npc_state_table::*; +pub use player_progression_table::*; +pub use profile_dashboard_state_table::*; +pub use profile_invite_code_table::*; +pub use profile_membership_table::*; +pub use profile_played_world_table::*; +pub use profile_recharge_order_table::*; +pub use profile_referral_relation_table::*; +pub use profile_save_archive_table::*; +pub use profile_wallet_ledger_table::*; +pub use puzzle_agent_message_table::*; +pub use puzzle_agent_session_table::*; +pub use puzzle_runtime_run_table::*; +pub use puzzle_work_profile_table::*; +pub use quest_log_table::*; +pub use quest_record_table::*; +pub use refresh_session_table::*; +pub use runtime_setting_table::*; +pub use runtime_snapshot_table::*; +pub use story_event_table::*; +pub use story_session_table::*; +pub use treasure_record_table::*; +pub use user_account_table::*; +pub use user_browse_history_table::*; pub use accept_quest_reducer::accept_quest; pub use acknowledge_quest_completion_reducer::acknowledge_quest_completion; pub use apply_chapter_progression_ledger_entry_reducer::apply_chapter_progression_ledger_entry; @@ -856,6 +964,7 @@ pub use get_player_progression_or_default_procedure::get_player_progression_or_d pub use get_profile_dashboard_procedure::get_profile_dashboard; pub use get_profile_play_stats_procedure::get_profile_play_stats; pub use get_profile_recharge_center_procedure::get_profile_recharge_center; +pub use get_profile_referral_invite_center_procedure::get_profile_referral_invite_center; pub use get_puzzle_agent_session_procedure::get_puzzle_agent_session; pub use get_puzzle_gallery_detail_procedure::get_puzzle_gallery_detail; pub use get_puzzle_run_procedure::get_puzzle_run; @@ -879,6 +988,7 @@ pub use publish_big_fish_game_procedure::publish_big_fish_game; pub use publish_custom_world_profile_and_return_procedure::publish_custom_world_profile_and_return; pub use publish_custom_world_world_procedure::publish_custom_world_world; pub use publish_puzzle_work_procedure::publish_puzzle_work; +pub use redeem_profile_referral_invite_code_procedure::redeem_profile_referral_invite_code; pub use resolve_combat_action_and_return_procedure::resolve_combat_action_and_return; pub use resolve_npc_battle_interaction_and_return_procedure::resolve_npc_battle_interaction_and_return; pub use resolve_npc_interaction_and_return_procedure::resolve_npc_interaction_and_return; @@ -1154,7 +1264,52 @@ fn args_bsatn(&self) -> Result, __sats::bsatn::EncodeError> { #[allow(non_snake_case)] #[doc(hidden)] pub struct DbUpdate { - custom_world_gallery_entry: __sdk::TableUpdate, + ai_result_reference: __sdk::TableUpdate, + ai_task: __sdk::TableUpdate, + ai_task_stage: __sdk::TableUpdate, + ai_text_chunk: __sdk::TableUpdate, + asset_entity_binding: __sdk::TableUpdate, + asset_object: __sdk::TableUpdate, + auth_identity: __sdk::TableUpdate, + auth_store_snapshot: __sdk::TableUpdate, + battle_state: __sdk::TableUpdate, + big_fish_agent_message: __sdk::TableUpdate, + big_fish_asset_slot: __sdk::TableUpdate, + big_fish_creation_session: __sdk::TableUpdate, + big_fish_runtime_run: __sdk::TableUpdate, + chapter_progression: __sdk::TableUpdate, + custom_world_agent_message: __sdk::TableUpdate, + custom_world_agent_operation: __sdk::TableUpdate, + custom_world_agent_session: __sdk::TableUpdate, + custom_world_draft_card: __sdk::TableUpdate, + custom_world_gallery_entry: __sdk::TableUpdate, + custom_world_profile: __sdk::TableUpdate, + custom_world_session: __sdk::TableUpdate, + inventory_slot: __sdk::TableUpdate, + npc_state: __sdk::TableUpdate, + player_progression: __sdk::TableUpdate, + profile_dashboard_state: __sdk::TableUpdate, + profile_invite_code: __sdk::TableUpdate, + profile_membership: __sdk::TableUpdate, + profile_played_world: __sdk::TableUpdate, + profile_recharge_order: __sdk::TableUpdate, + profile_referral_relation: __sdk::TableUpdate, + profile_save_archive: __sdk::TableUpdate, + profile_wallet_ledger: __sdk::TableUpdate, + puzzle_agent_message: __sdk::TableUpdate, + puzzle_agent_session: __sdk::TableUpdate, + puzzle_runtime_run: __sdk::TableUpdate, + puzzle_work_profile: __sdk::TableUpdate, + quest_log: __sdk::TableUpdate, + quest_record: __sdk::TableUpdate, + refresh_session: __sdk::TableUpdate, + runtime_setting: __sdk::TableUpdate, + runtime_snapshot: __sdk::TableUpdate, + story_event: __sdk::TableUpdate, + story_session: __sdk::TableUpdate, + treasure_record: __sdk::TableUpdate, + user_account: __sdk::TableUpdate, + user_browse_history: __sdk::TableUpdate, } @@ -1165,7 +1320,52 @@ impl TryFrom<__ws::v2::TransactionUpdate> for DbUpdate { for table_update in __sdk::transaction_update_iter_table_updates(raw) { match &table_update.table_name[..] { - "custom_world_gallery_entry" => db_update.custom_world_gallery_entry.append(custom_world_gallery_entry_table::parse_table_update(table_update)?), + "ai_result_reference" => db_update.ai_result_reference.append(ai_result_reference_table::parse_table_update(table_update)?), + "ai_task" => db_update.ai_task.append(ai_task_table::parse_table_update(table_update)?), + "ai_task_stage" => db_update.ai_task_stage.append(ai_task_stage_table::parse_table_update(table_update)?), + "ai_text_chunk" => db_update.ai_text_chunk.append(ai_text_chunk_table::parse_table_update(table_update)?), + "asset_entity_binding" => db_update.asset_entity_binding.append(asset_entity_binding_table::parse_table_update(table_update)?), + "asset_object" => db_update.asset_object.append(asset_object_table::parse_table_update(table_update)?), + "auth_identity" => db_update.auth_identity.append(auth_identity_table::parse_table_update(table_update)?), + "auth_store_snapshot" => db_update.auth_store_snapshot.append(auth_store_snapshot_table::parse_table_update(table_update)?), + "battle_state" => db_update.battle_state.append(battle_state_table::parse_table_update(table_update)?), + "big_fish_agent_message" => db_update.big_fish_agent_message.append(big_fish_agent_message_table::parse_table_update(table_update)?), + "big_fish_asset_slot" => db_update.big_fish_asset_slot.append(big_fish_asset_slot_table::parse_table_update(table_update)?), + "big_fish_creation_session" => db_update.big_fish_creation_session.append(big_fish_creation_session_table::parse_table_update(table_update)?), + "big_fish_runtime_run" => db_update.big_fish_runtime_run.append(big_fish_runtime_run_table::parse_table_update(table_update)?), + "chapter_progression" => db_update.chapter_progression.append(chapter_progression_table::parse_table_update(table_update)?), + "custom_world_agent_message" => db_update.custom_world_agent_message.append(custom_world_agent_message_table::parse_table_update(table_update)?), + "custom_world_agent_operation" => db_update.custom_world_agent_operation.append(custom_world_agent_operation_table::parse_table_update(table_update)?), + "custom_world_agent_session" => db_update.custom_world_agent_session.append(custom_world_agent_session_table::parse_table_update(table_update)?), + "custom_world_draft_card" => db_update.custom_world_draft_card.append(custom_world_draft_card_table::parse_table_update(table_update)?), + "custom_world_gallery_entry" => db_update.custom_world_gallery_entry.append(custom_world_gallery_entry_table::parse_table_update(table_update)?), + "custom_world_profile" => db_update.custom_world_profile.append(custom_world_profile_table::parse_table_update(table_update)?), + "custom_world_session" => db_update.custom_world_session.append(custom_world_session_table::parse_table_update(table_update)?), + "inventory_slot" => db_update.inventory_slot.append(inventory_slot_table::parse_table_update(table_update)?), + "npc_state" => db_update.npc_state.append(npc_state_table::parse_table_update(table_update)?), + "player_progression" => db_update.player_progression.append(player_progression_table::parse_table_update(table_update)?), + "profile_dashboard_state" => db_update.profile_dashboard_state.append(profile_dashboard_state_table::parse_table_update(table_update)?), + "profile_invite_code" => db_update.profile_invite_code.append(profile_invite_code_table::parse_table_update(table_update)?), + "profile_membership" => db_update.profile_membership.append(profile_membership_table::parse_table_update(table_update)?), + "profile_played_world" => db_update.profile_played_world.append(profile_played_world_table::parse_table_update(table_update)?), + "profile_recharge_order" => db_update.profile_recharge_order.append(profile_recharge_order_table::parse_table_update(table_update)?), + "profile_referral_relation" => db_update.profile_referral_relation.append(profile_referral_relation_table::parse_table_update(table_update)?), + "profile_save_archive" => db_update.profile_save_archive.append(profile_save_archive_table::parse_table_update(table_update)?), + "profile_wallet_ledger" => db_update.profile_wallet_ledger.append(profile_wallet_ledger_table::parse_table_update(table_update)?), + "puzzle_agent_message" => db_update.puzzle_agent_message.append(puzzle_agent_message_table::parse_table_update(table_update)?), + "puzzle_agent_session" => db_update.puzzle_agent_session.append(puzzle_agent_session_table::parse_table_update(table_update)?), + "puzzle_runtime_run" => db_update.puzzle_runtime_run.append(puzzle_runtime_run_table::parse_table_update(table_update)?), + "puzzle_work_profile" => db_update.puzzle_work_profile.append(puzzle_work_profile_table::parse_table_update(table_update)?), + "quest_log" => db_update.quest_log.append(quest_log_table::parse_table_update(table_update)?), + "quest_record" => db_update.quest_record.append(quest_record_table::parse_table_update(table_update)?), + "refresh_session" => db_update.refresh_session.append(refresh_session_table::parse_table_update(table_update)?), + "runtime_setting" => db_update.runtime_setting.append(runtime_setting_table::parse_table_update(table_update)?), + "runtime_snapshot" => db_update.runtime_snapshot.append(runtime_snapshot_table::parse_table_update(table_update)?), + "story_event" => db_update.story_event.append(story_event_table::parse_table_update(table_update)?), + "story_session" => db_update.story_session.append(story_session_table::parse_table_update(table_update)?), + "treasure_record" => db_update.treasure_record.append(treasure_record_table::parse_table_update(table_update)?), + "user_account" => db_update.user_account.append(user_account_table::parse_table_update(table_update)?), + "user_browse_history" => db_update.user_browse_history.append(user_browse_history_table::parse_table_update(table_update)?), unknown => { return Err(__sdk::InternalError::unknown_name( @@ -1188,7 +1388,52 @@ impl __sdk::DbUpdate for DbUpdate { fn apply_to_client_cache(&self, cache: &mut __sdk::ClientCache) -> AppliedDiff<'_> { let mut diff = AppliedDiff::default(); - diff.custom_world_gallery_entry = cache.apply_diff_to_table::("custom_world_gallery_entry", &self.custom_world_gallery_entry).with_updates_by_pk(|row| &row.profile_id); + diff.ai_result_reference = cache.apply_diff_to_table::("ai_result_reference", &self.ai_result_reference).with_updates_by_pk(|row| &row.result_reference_row_id); + diff.ai_task = cache.apply_diff_to_table::("ai_task", &self.ai_task).with_updates_by_pk(|row| &row.task_id); + diff.ai_task_stage = cache.apply_diff_to_table::("ai_task_stage", &self.ai_task_stage).with_updates_by_pk(|row| &row.task_stage_id); + diff.ai_text_chunk = cache.apply_diff_to_table::("ai_text_chunk", &self.ai_text_chunk).with_updates_by_pk(|row| &row.text_chunk_row_id); + diff.asset_entity_binding = cache.apply_diff_to_table::("asset_entity_binding", &self.asset_entity_binding).with_updates_by_pk(|row| &row.binding_id); + diff.asset_object = cache.apply_diff_to_table::("asset_object", &self.asset_object).with_updates_by_pk(|row| &row.asset_object_id); + diff.auth_identity = cache.apply_diff_to_table::("auth_identity", &self.auth_identity).with_updates_by_pk(|row| &row.identity_id); + diff.auth_store_snapshot = cache.apply_diff_to_table::("auth_store_snapshot", &self.auth_store_snapshot).with_updates_by_pk(|row| &row.snapshot_id); + diff.battle_state = cache.apply_diff_to_table::("battle_state", &self.battle_state).with_updates_by_pk(|row| &row.battle_state_id); + diff.big_fish_agent_message = cache.apply_diff_to_table::("big_fish_agent_message", &self.big_fish_agent_message).with_updates_by_pk(|row| &row.message_id); + diff.big_fish_asset_slot = cache.apply_diff_to_table::("big_fish_asset_slot", &self.big_fish_asset_slot).with_updates_by_pk(|row| &row.slot_id); + diff.big_fish_creation_session = cache.apply_diff_to_table::("big_fish_creation_session", &self.big_fish_creation_session).with_updates_by_pk(|row| &row.session_id); + diff.big_fish_runtime_run = cache.apply_diff_to_table::("big_fish_runtime_run", &self.big_fish_runtime_run).with_updates_by_pk(|row| &row.run_id); + diff.chapter_progression = cache.apply_diff_to_table::("chapter_progression", &self.chapter_progression).with_updates_by_pk(|row| &row.chapter_progression_id); + diff.custom_world_agent_message = cache.apply_diff_to_table::("custom_world_agent_message", &self.custom_world_agent_message).with_updates_by_pk(|row| &row.message_id); + diff.custom_world_agent_operation = cache.apply_diff_to_table::("custom_world_agent_operation", &self.custom_world_agent_operation).with_updates_by_pk(|row| &row.operation_id); + diff.custom_world_agent_session = cache.apply_diff_to_table::("custom_world_agent_session", &self.custom_world_agent_session).with_updates_by_pk(|row| &row.session_id); + diff.custom_world_draft_card = cache.apply_diff_to_table::("custom_world_draft_card", &self.custom_world_draft_card).with_updates_by_pk(|row| &row.card_id); + diff.custom_world_gallery_entry = cache.apply_diff_to_table::("custom_world_gallery_entry", &self.custom_world_gallery_entry).with_updates_by_pk(|row| &row.profile_id); + diff.custom_world_profile = cache.apply_diff_to_table::("custom_world_profile", &self.custom_world_profile).with_updates_by_pk(|row| &row.profile_id); + diff.custom_world_session = cache.apply_diff_to_table::("custom_world_session", &self.custom_world_session).with_updates_by_pk(|row| &row.session_id); + diff.inventory_slot = cache.apply_diff_to_table::("inventory_slot", &self.inventory_slot).with_updates_by_pk(|row| &row.slot_id); + diff.npc_state = cache.apply_diff_to_table::("npc_state", &self.npc_state).with_updates_by_pk(|row| &row.npc_state_id); + diff.player_progression = cache.apply_diff_to_table::("player_progression", &self.player_progression).with_updates_by_pk(|row| &row.user_id); + diff.profile_dashboard_state = cache.apply_diff_to_table::("profile_dashboard_state", &self.profile_dashboard_state).with_updates_by_pk(|row| &row.user_id); + diff.profile_invite_code = cache.apply_diff_to_table::("profile_invite_code", &self.profile_invite_code).with_updates_by_pk(|row| &row.user_id); + diff.profile_membership = cache.apply_diff_to_table::("profile_membership", &self.profile_membership).with_updates_by_pk(|row| &row.user_id); + diff.profile_played_world = cache.apply_diff_to_table::("profile_played_world", &self.profile_played_world).with_updates_by_pk(|row| &row.played_world_id); + diff.profile_recharge_order = cache.apply_diff_to_table::("profile_recharge_order", &self.profile_recharge_order).with_updates_by_pk(|row| &row.order_id); + diff.profile_referral_relation = cache.apply_diff_to_table::("profile_referral_relation", &self.profile_referral_relation).with_updates_by_pk(|row| &row.invitee_user_id); + diff.profile_save_archive = cache.apply_diff_to_table::("profile_save_archive", &self.profile_save_archive).with_updates_by_pk(|row| &row.archive_id); + diff.profile_wallet_ledger = cache.apply_diff_to_table::("profile_wallet_ledger", &self.profile_wallet_ledger).with_updates_by_pk(|row| &row.wallet_ledger_id); + diff.puzzle_agent_message = cache.apply_diff_to_table::("puzzle_agent_message", &self.puzzle_agent_message).with_updates_by_pk(|row| &row.message_id); + diff.puzzle_agent_session = cache.apply_diff_to_table::("puzzle_agent_session", &self.puzzle_agent_session).with_updates_by_pk(|row| &row.session_id); + diff.puzzle_runtime_run = cache.apply_diff_to_table::("puzzle_runtime_run", &self.puzzle_runtime_run).with_updates_by_pk(|row| &row.run_id); + diff.puzzle_work_profile = cache.apply_diff_to_table::("puzzle_work_profile", &self.puzzle_work_profile).with_updates_by_pk(|row| &row.profile_id); + diff.quest_log = cache.apply_diff_to_table::("quest_log", &self.quest_log).with_updates_by_pk(|row| &row.log_id); + diff.quest_record = cache.apply_diff_to_table::("quest_record", &self.quest_record).with_updates_by_pk(|row| &row.quest_id); + diff.refresh_session = cache.apply_diff_to_table::("refresh_session", &self.refresh_session).with_updates_by_pk(|row| &row.session_id); + diff.runtime_setting = cache.apply_diff_to_table::("runtime_setting", &self.runtime_setting).with_updates_by_pk(|row| &row.user_id); + diff.runtime_snapshot = cache.apply_diff_to_table::("runtime_snapshot", &self.runtime_snapshot).with_updates_by_pk(|row| &row.user_id); + diff.story_event = cache.apply_diff_to_table::("story_event", &self.story_event).with_updates_by_pk(|row| &row.event_id); + diff.story_session = cache.apply_diff_to_table::("story_session", &self.story_session).with_updates_by_pk(|row| &row.story_session_id); + diff.treasure_record = cache.apply_diff_to_table::("treasure_record", &self.treasure_record).with_updates_by_pk(|row| &row.treasure_record_id); + diff.user_account = cache.apply_diff_to_table::("user_account", &self.user_account).with_updates_by_pk(|row| &row.user_id); + diff.user_browse_history = cache.apply_diff_to_table::("user_browse_history", &self.user_browse_history).with_updates_by_pk(|row| &row.browse_history_id); diff } @@ -1196,7 +1441,52 @@ fn parse_initial_rows(raw: __ws::v2::QueryRows) -> __sdk::Result { let mut db_update = DbUpdate::default(); for table_rows in raw.tables { match &table_rows.table[..] { - "custom_world_gallery_entry" => db_update.custom_world_gallery_entry.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "ai_result_reference" => db_update.ai_result_reference.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "ai_task" => db_update.ai_task.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "ai_task_stage" => db_update.ai_task_stage.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "ai_text_chunk" => db_update.ai_text_chunk.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "asset_entity_binding" => db_update.asset_entity_binding.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "asset_object" => db_update.asset_object.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "auth_identity" => db_update.auth_identity.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "auth_store_snapshot" => db_update.auth_store_snapshot.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "battle_state" => db_update.battle_state.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "big_fish_agent_message" => db_update.big_fish_agent_message.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "big_fish_asset_slot" => db_update.big_fish_asset_slot.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "big_fish_creation_session" => db_update.big_fish_creation_session.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "big_fish_runtime_run" => db_update.big_fish_runtime_run.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "chapter_progression" => db_update.chapter_progression.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "custom_world_agent_message" => db_update.custom_world_agent_message.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "custom_world_agent_operation" => db_update.custom_world_agent_operation.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "custom_world_agent_session" => db_update.custom_world_agent_session.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "custom_world_draft_card" => db_update.custom_world_draft_card.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "custom_world_gallery_entry" => db_update.custom_world_gallery_entry.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "custom_world_profile" => db_update.custom_world_profile.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "custom_world_session" => db_update.custom_world_session.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "inventory_slot" => db_update.inventory_slot.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "npc_state" => db_update.npc_state.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "player_progression" => db_update.player_progression.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "profile_dashboard_state" => db_update.profile_dashboard_state.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "profile_invite_code" => db_update.profile_invite_code.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "profile_membership" => db_update.profile_membership.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "profile_played_world" => db_update.profile_played_world.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "profile_recharge_order" => db_update.profile_recharge_order.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "profile_referral_relation" => db_update.profile_referral_relation.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "profile_save_archive" => db_update.profile_save_archive.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "profile_wallet_ledger" => db_update.profile_wallet_ledger.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "puzzle_agent_message" => db_update.puzzle_agent_message.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "puzzle_agent_session" => db_update.puzzle_agent_session.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "puzzle_runtime_run" => db_update.puzzle_runtime_run.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "puzzle_work_profile" => db_update.puzzle_work_profile.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "quest_log" => db_update.quest_log.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "quest_record" => db_update.quest_record.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "refresh_session" => db_update.refresh_session.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "runtime_setting" => db_update.runtime_setting.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "runtime_snapshot" => db_update.runtime_snapshot.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "story_event" => db_update.story_event.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "story_session" => db_update.story_session.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "treasure_record" => db_update.treasure_record.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "user_account" => db_update.user_account.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "user_browse_history" => db_update.user_browse_history.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), unknown => { return Err(__sdk::InternalError::unknown_name("table", unknown, "QueryRows").into()); } }} Ok(db_update) } @@ -1204,7 +1494,52 @@ fn parse_unsubscribe_rows(raw: __ws::v2::QueryRows) -> __sdk::Result { let mut db_update = DbUpdate::default(); for table_rows in raw.tables { match &table_rows.table[..] { - "custom_world_gallery_entry" => db_update.custom_world_gallery_entry.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "ai_result_reference" => db_update.ai_result_reference.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "ai_task" => db_update.ai_task.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "ai_task_stage" => db_update.ai_task_stage.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "ai_text_chunk" => db_update.ai_text_chunk.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "asset_entity_binding" => db_update.asset_entity_binding.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "asset_object" => db_update.asset_object.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "auth_identity" => db_update.auth_identity.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "auth_store_snapshot" => db_update.auth_store_snapshot.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "battle_state" => db_update.battle_state.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "big_fish_agent_message" => db_update.big_fish_agent_message.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "big_fish_asset_slot" => db_update.big_fish_asset_slot.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "big_fish_creation_session" => db_update.big_fish_creation_session.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "big_fish_runtime_run" => db_update.big_fish_runtime_run.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "chapter_progression" => db_update.chapter_progression.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "custom_world_agent_message" => db_update.custom_world_agent_message.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "custom_world_agent_operation" => db_update.custom_world_agent_operation.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "custom_world_agent_session" => db_update.custom_world_agent_session.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "custom_world_draft_card" => db_update.custom_world_draft_card.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "custom_world_gallery_entry" => db_update.custom_world_gallery_entry.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "custom_world_profile" => db_update.custom_world_profile.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "custom_world_session" => db_update.custom_world_session.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "inventory_slot" => db_update.inventory_slot.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "npc_state" => db_update.npc_state.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "player_progression" => db_update.player_progression.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "profile_dashboard_state" => db_update.profile_dashboard_state.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "profile_invite_code" => db_update.profile_invite_code.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "profile_membership" => db_update.profile_membership.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "profile_played_world" => db_update.profile_played_world.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "profile_recharge_order" => db_update.profile_recharge_order.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "profile_referral_relation" => db_update.profile_referral_relation.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "profile_save_archive" => db_update.profile_save_archive.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "profile_wallet_ledger" => db_update.profile_wallet_ledger.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "puzzle_agent_message" => db_update.puzzle_agent_message.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "puzzle_agent_session" => db_update.puzzle_agent_session.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "puzzle_runtime_run" => db_update.puzzle_runtime_run.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "puzzle_work_profile" => db_update.puzzle_work_profile.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "quest_log" => db_update.quest_log.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "quest_record" => db_update.quest_record.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "refresh_session" => db_update.refresh_session.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "runtime_setting" => db_update.runtime_setting.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "runtime_snapshot" => db_update.runtime_snapshot.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "story_event" => db_update.story_event.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "story_session" => db_update.story_session.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "treasure_record" => db_update.treasure_record.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "user_account" => db_update.user_account.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "user_browse_history" => db_update.user_browse_history.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), unknown => { return Err(__sdk::InternalError::unknown_name("table", unknown, "QueryRows").into()); } }} Ok(db_update) } @@ -1214,7 +1549,52 @@ for table_rows in raw.tables { #[allow(non_snake_case)] #[doc(hidden)] pub struct AppliedDiff<'r> { - custom_world_gallery_entry: __sdk::TableAppliedDiff<'r, CustomWorldGalleryEntry>, + ai_result_reference: __sdk::TableAppliedDiff<'r, AiResultReference>, + ai_task: __sdk::TableAppliedDiff<'r, AiTask>, + ai_task_stage: __sdk::TableAppliedDiff<'r, AiTaskStage>, + ai_text_chunk: __sdk::TableAppliedDiff<'r, AiTextChunk>, + asset_entity_binding: __sdk::TableAppliedDiff<'r, AssetEntityBinding>, + asset_object: __sdk::TableAppliedDiff<'r, AssetObject>, + auth_identity: __sdk::TableAppliedDiff<'r, AuthIdentity>, + auth_store_snapshot: __sdk::TableAppliedDiff<'r, AuthStoreSnapshot>, + battle_state: __sdk::TableAppliedDiff<'r, BattleState>, + big_fish_agent_message: __sdk::TableAppliedDiff<'r, BigFishAgentMessage>, + big_fish_asset_slot: __sdk::TableAppliedDiff<'r, BigFishAssetSlot>, + big_fish_creation_session: __sdk::TableAppliedDiff<'r, BigFishCreationSession>, + big_fish_runtime_run: __sdk::TableAppliedDiff<'r, BigFishRuntimeRun>, + chapter_progression: __sdk::TableAppliedDiff<'r, ChapterProgression>, + custom_world_agent_message: __sdk::TableAppliedDiff<'r, CustomWorldAgentMessage>, + custom_world_agent_operation: __sdk::TableAppliedDiff<'r, CustomWorldAgentOperation>, + custom_world_agent_session: __sdk::TableAppliedDiff<'r, CustomWorldAgentSession>, + custom_world_draft_card: __sdk::TableAppliedDiff<'r, CustomWorldDraftCard>, + custom_world_gallery_entry: __sdk::TableAppliedDiff<'r, CustomWorldGalleryEntry>, + custom_world_profile: __sdk::TableAppliedDiff<'r, CustomWorldProfile>, + custom_world_session: __sdk::TableAppliedDiff<'r, CustomWorldSession>, + inventory_slot: __sdk::TableAppliedDiff<'r, InventorySlot>, + npc_state: __sdk::TableAppliedDiff<'r, NpcState>, + player_progression: __sdk::TableAppliedDiff<'r, PlayerProgression>, + profile_dashboard_state: __sdk::TableAppliedDiff<'r, ProfileDashboardState>, + profile_invite_code: __sdk::TableAppliedDiff<'r, ProfileInviteCode>, + profile_membership: __sdk::TableAppliedDiff<'r, ProfileMembership>, + profile_played_world: __sdk::TableAppliedDiff<'r, ProfilePlayedWorld>, + profile_recharge_order: __sdk::TableAppliedDiff<'r, ProfileRechargeOrder>, + profile_referral_relation: __sdk::TableAppliedDiff<'r, ProfileReferralRelation>, + profile_save_archive: __sdk::TableAppliedDiff<'r, ProfileSaveArchive>, + profile_wallet_ledger: __sdk::TableAppliedDiff<'r, ProfileWalletLedger>, + puzzle_agent_message: __sdk::TableAppliedDiff<'r, PuzzleAgentMessageRow>, + puzzle_agent_session: __sdk::TableAppliedDiff<'r, PuzzleAgentSessionRow>, + puzzle_runtime_run: __sdk::TableAppliedDiff<'r, PuzzleRuntimeRunRow>, + puzzle_work_profile: __sdk::TableAppliedDiff<'r, PuzzleWorkProfileRow>, + quest_log: __sdk::TableAppliedDiff<'r, QuestLog>, + quest_record: __sdk::TableAppliedDiff<'r, QuestRecord>, + refresh_session: __sdk::TableAppliedDiff<'r, RefreshSession>, + runtime_setting: __sdk::TableAppliedDiff<'r, RuntimeSetting>, + runtime_snapshot: __sdk::TableAppliedDiff<'r, RuntimeSnapshotRow>, + story_event: __sdk::TableAppliedDiff<'r, StoryEvent>, + story_session: __sdk::TableAppliedDiff<'r, StorySession>, + treasure_record: __sdk::TableAppliedDiff<'r, TreasureRecord>, + user_account: __sdk::TableAppliedDiff<'r, UserAccount>, + user_browse_history: __sdk::TableAppliedDiff<'r, UserBrowseHistory>, __unused: std::marker::PhantomData<&'r ()>, } @@ -1225,7 +1605,52 @@ impl __sdk::InModule for AppliedDiff<'_> { impl<'r> __sdk::AppliedDiff<'r> for AppliedDiff<'r> { fn invoke_row_callbacks(&self, event: &EventContext, callbacks: &mut __sdk::DbCallbacks) { - callbacks.invoke_table_row_callbacks::("custom_world_gallery_entry", &self.custom_world_gallery_entry, event); + callbacks.invoke_table_row_callbacks::("ai_result_reference", &self.ai_result_reference, event); + callbacks.invoke_table_row_callbacks::("ai_task", &self.ai_task, event); + callbacks.invoke_table_row_callbacks::("ai_task_stage", &self.ai_task_stage, event); + callbacks.invoke_table_row_callbacks::("ai_text_chunk", &self.ai_text_chunk, event); + callbacks.invoke_table_row_callbacks::("asset_entity_binding", &self.asset_entity_binding, event); + callbacks.invoke_table_row_callbacks::("asset_object", &self.asset_object, event); + callbacks.invoke_table_row_callbacks::("auth_identity", &self.auth_identity, event); + callbacks.invoke_table_row_callbacks::("auth_store_snapshot", &self.auth_store_snapshot, event); + callbacks.invoke_table_row_callbacks::("battle_state", &self.battle_state, event); + callbacks.invoke_table_row_callbacks::("big_fish_agent_message", &self.big_fish_agent_message, event); + callbacks.invoke_table_row_callbacks::("big_fish_asset_slot", &self.big_fish_asset_slot, event); + callbacks.invoke_table_row_callbacks::("big_fish_creation_session", &self.big_fish_creation_session, event); + callbacks.invoke_table_row_callbacks::("big_fish_runtime_run", &self.big_fish_runtime_run, event); + callbacks.invoke_table_row_callbacks::("chapter_progression", &self.chapter_progression, event); + callbacks.invoke_table_row_callbacks::("custom_world_agent_message", &self.custom_world_agent_message, event); + callbacks.invoke_table_row_callbacks::("custom_world_agent_operation", &self.custom_world_agent_operation, event); + callbacks.invoke_table_row_callbacks::("custom_world_agent_session", &self.custom_world_agent_session, event); + callbacks.invoke_table_row_callbacks::("custom_world_draft_card", &self.custom_world_draft_card, event); + callbacks.invoke_table_row_callbacks::("custom_world_gallery_entry", &self.custom_world_gallery_entry, event); + callbacks.invoke_table_row_callbacks::("custom_world_profile", &self.custom_world_profile, event); + callbacks.invoke_table_row_callbacks::("custom_world_session", &self.custom_world_session, event); + callbacks.invoke_table_row_callbacks::("inventory_slot", &self.inventory_slot, event); + callbacks.invoke_table_row_callbacks::("npc_state", &self.npc_state, event); + callbacks.invoke_table_row_callbacks::("player_progression", &self.player_progression, event); + callbacks.invoke_table_row_callbacks::("profile_dashboard_state", &self.profile_dashboard_state, event); + callbacks.invoke_table_row_callbacks::("profile_invite_code", &self.profile_invite_code, event); + callbacks.invoke_table_row_callbacks::("profile_membership", &self.profile_membership, event); + callbacks.invoke_table_row_callbacks::("profile_played_world", &self.profile_played_world, event); + callbacks.invoke_table_row_callbacks::("profile_recharge_order", &self.profile_recharge_order, event); + callbacks.invoke_table_row_callbacks::("profile_referral_relation", &self.profile_referral_relation, event); + callbacks.invoke_table_row_callbacks::("profile_save_archive", &self.profile_save_archive, event); + callbacks.invoke_table_row_callbacks::("profile_wallet_ledger", &self.profile_wallet_ledger, event); + callbacks.invoke_table_row_callbacks::("puzzle_agent_message", &self.puzzle_agent_message, event); + callbacks.invoke_table_row_callbacks::("puzzle_agent_session", &self.puzzle_agent_session, event); + callbacks.invoke_table_row_callbacks::("puzzle_runtime_run", &self.puzzle_runtime_run, event); + callbacks.invoke_table_row_callbacks::("puzzle_work_profile", &self.puzzle_work_profile, event); + callbacks.invoke_table_row_callbacks::("quest_log", &self.quest_log, event); + callbacks.invoke_table_row_callbacks::("quest_record", &self.quest_record, event); + callbacks.invoke_table_row_callbacks::("refresh_session", &self.refresh_session, event); + callbacks.invoke_table_row_callbacks::("runtime_setting", &self.runtime_setting, event); + callbacks.invoke_table_row_callbacks::("runtime_snapshot", &self.runtime_snapshot, event); + callbacks.invoke_table_row_callbacks::("story_event", &self.story_event, event); + callbacks.invoke_table_row_callbacks::("story_session", &self.story_session, event); + callbacks.invoke_table_row_callbacks::("treasure_record", &self.treasure_record, event); + callbacks.invoke_table_row_callbacks::("user_account", &self.user_account, event); + callbacks.invoke_table_row_callbacks::("user_browse_history", &self.user_browse_history, event); } } @@ -1877,9 +2302,99 @@ impl __sdk::SpacetimeModule for RemoteModule { type QueryBuilder = __sdk::QueryBuilder; fn register_tables(client_cache: &mut __sdk::ClientCache) { - custom_world_gallery_entry_table::register_table(client_cache); + ai_result_reference_table::register_table(client_cache); + ai_task_table::register_table(client_cache); + ai_task_stage_table::register_table(client_cache); + ai_text_chunk_table::register_table(client_cache); + asset_entity_binding_table::register_table(client_cache); + asset_object_table::register_table(client_cache); + auth_identity_table::register_table(client_cache); + auth_store_snapshot_table::register_table(client_cache); + battle_state_table::register_table(client_cache); + big_fish_agent_message_table::register_table(client_cache); + big_fish_asset_slot_table::register_table(client_cache); + big_fish_creation_session_table::register_table(client_cache); + big_fish_runtime_run_table::register_table(client_cache); + chapter_progression_table::register_table(client_cache); + custom_world_agent_message_table::register_table(client_cache); + custom_world_agent_operation_table::register_table(client_cache); + custom_world_agent_session_table::register_table(client_cache); + custom_world_draft_card_table::register_table(client_cache); + custom_world_gallery_entry_table::register_table(client_cache); + custom_world_profile_table::register_table(client_cache); + custom_world_session_table::register_table(client_cache); + inventory_slot_table::register_table(client_cache); + npc_state_table::register_table(client_cache); + player_progression_table::register_table(client_cache); + profile_dashboard_state_table::register_table(client_cache); + profile_invite_code_table::register_table(client_cache); + profile_membership_table::register_table(client_cache); + profile_played_world_table::register_table(client_cache); + profile_recharge_order_table::register_table(client_cache); + profile_referral_relation_table::register_table(client_cache); + profile_save_archive_table::register_table(client_cache); + profile_wallet_ledger_table::register_table(client_cache); + puzzle_agent_message_table::register_table(client_cache); + puzzle_agent_session_table::register_table(client_cache); + puzzle_runtime_run_table::register_table(client_cache); + puzzle_work_profile_table::register_table(client_cache); + quest_log_table::register_table(client_cache); + quest_record_table::register_table(client_cache); + refresh_session_table::register_table(client_cache); + runtime_setting_table::register_table(client_cache); + runtime_snapshot_table::register_table(client_cache); + story_event_table::register_table(client_cache); + story_session_table::register_table(client_cache); + treasure_record_table::register_table(client_cache); + user_account_table::register_table(client_cache); + user_browse_history_table::register_table(client_cache); } const ALL_TABLE_NAMES: &'static [&'static str] = &[ - "custom_world_gallery_entry", + "ai_result_reference", + "ai_task", + "ai_task_stage", + "ai_text_chunk", + "asset_entity_binding", + "asset_object", + "auth_identity", + "auth_store_snapshot", + "battle_state", + "big_fish_agent_message", + "big_fish_asset_slot", + "big_fish_creation_session", + "big_fish_runtime_run", + "chapter_progression", + "custom_world_agent_message", + "custom_world_agent_operation", + "custom_world_agent_session", + "custom_world_draft_card", + "custom_world_gallery_entry", + "custom_world_profile", + "custom_world_session", + "inventory_slot", + "npc_state", + "player_progression", + "profile_dashboard_state", + "profile_invite_code", + "profile_membership", + "profile_played_world", + "profile_recharge_order", + "profile_referral_relation", + "profile_save_archive", + "profile_wallet_ledger", + "puzzle_agent_message", + "puzzle_agent_session", + "puzzle_runtime_run", + "puzzle_work_profile", + "quest_log", + "quest_record", + "refresh_session", + "runtime_setting", + "runtime_snapshot", + "story_event", + "story_session", + "treasure_record", + "user_account", + "user_browse_history", ]; } diff --git a/server-rs/crates/spacetime-client/src/module_bindings/profile_invite_code_table.rs b/server-rs/crates/spacetime-client/src/module_bindings/profile_invite_code_table.rs new file mode 100644 index 00000000..e1e342aa --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/profile_invite_code_table.rs @@ -0,0 +1,194 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{ + self as __sdk, + __lib, + __sats, + __ws, +}; +use super::profile_invite_code_type::ProfileInviteCode; + +/// Table handle for the table `profile_invite_code`. +/// +/// Obtain a handle from the [`ProfileInviteCodeTableAccess::profile_invite_code`] method on [`super::RemoteTables`], +/// like `ctx.db.profile_invite_code()`. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.profile_invite_code().on_insert(...)`. +pub struct ProfileInviteCodeTableHandle<'ctx> { + imp: __sdk::TableHandle, + ctx: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the table `profile_invite_code`. +/// +/// Implemented for [`super::RemoteTables`]. +pub trait ProfileInviteCodeTableAccess { + #[allow(non_snake_case)] + /// Obtain a [`ProfileInviteCodeTableHandle`], which mediates access to the table `profile_invite_code`. + fn profile_invite_code(&self) -> ProfileInviteCodeTableHandle<'_>; +} + +impl ProfileInviteCodeTableAccess for super::RemoteTables { + fn profile_invite_code(&self) -> ProfileInviteCodeTableHandle<'_> { + ProfileInviteCodeTableHandle { + imp: self.imp.get_table::("profile_invite_code"), + ctx: std::marker::PhantomData, + } + } +} + +pub struct ProfileInviteCodeInsertCallbackId(__sdk::CallbackId); +pub struct ProfileInviteCodeDeleteCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::Table for ProfileInviteCodeTableHandle<'ctx> { + type Row = ProfileInviteCode; + type EventContext = super::EventContext; + + fn count(&self) -> u64 { self.imp.count() } + fn iter(&self) -> impl Iterator + '_ { self.imp.iter() } + + type InsertCallbackId = ProfileInviteCodeInsertCallbackId; + + fn on_insert( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> ProfileInviteCodeInsertCallbackId { + ProfileInviteCodeInsertCallbackId(self.imp.on_insert(Box::new(callback))) + } + + fn remove_on_insert(&self, callback: ProfileInviteCodeInsertCallbackId) { + self.imp.remove_on_insert(callback.0) + } + + type DeleteCallbackId = ProfileInviteCodeDeleteCallbackId; + + fn on_delete( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> ProfileInviteCodeDeleteCallbackId { + ProfileInviteCodeDeleteCallbackId(self.imp.on_delete(Box::new(callback))) + } + + fn remove_on_delete(&self, callback: ProfileInviteCodeDeleteCallbackId) { + self.imp.remove_on_delete(callback.0) + } +} + +pub struct ProfileInviteCodeUpdateCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::TableWithPrimaryKey for ProfileInviteCodeTableHandle<'ctx> { + type UpdateCallbackId = ProfileInviteCodeUpdateCallbackId; + + fn on_update( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static, + ) -> ProfileInviteCodeUpdateCallbackId { + ProfileInviteCodeUpdateCallbackId(self.imp.on_update(Box::new(callback))) + } + + fn remove_on_update(&self, callback: ProfileInviteCodeUpdateCallbackId) { + self.imp.remove_on_update(callback.0) + } +} + + /// Access to the `user_id` unique index on the table `profile_invite_code`, + /// which allows point queries on the field of the same name + /// via the [`ProfileInviteCodeUserIdUnique::find`] method. + /// + /// Users are encouraged not to explicitly reference this type, + /// but to directly chain method calls, + /// like `ctx.db.profile_invite_code().user_id().find(...)`. + pub struct ProfileInviteCodeUserIdUnique<'ctx> { + imp: __sdk::UniqueConstraintHandle, + phantom: std::marker::PhantomData<&'ctx super::RemoteTables>, + } + + impl<'ctx> ProfileInviteCodeTableHandle<'ctx> { + /// Get a handle on the `user_id` unique index on the table `profile_invite_code`. + pub fn user_id(&self) -> ProfileInviteCodeUserIdUnique<'ctx> { + ProfileInviteCodeUserIdUnique { + imp: self.imp.get_unique_constraint::("user_id"), + phantom: std::marker::PhantomData, + } + } + } + + impl<'ctx> ProfileInviteCodeUserIdUnique<'ctx> { + /// Find the subscribed row whose `user_id` column value is equal to `col_val`, + /// if such a row is present in the client cache. + pub fn find(&self, col_val: &String) -> Option { + self.imp.find(col_val) + } + } + + /// Access to the `invite_code` unique index on the table `profile_invite_code`, + /// which allows point queries on the field of the same name + /// via the [`ProfileInviteCodeInviteCodeUnique::find`] method. + /// + /// Users are encouraged not to explicitly reference this type, + /// but to directly chain method calls, + /// like `ctx.db.profile_invite_code().invite_code().find(...)`. + pub struct ProfileInviteCodeInviteCodeUnique<'ctx> { + imp: __sdk::UniqueConstraintHandle, + phantom: std::marker::PhantomData<&'ctx super::RemoteTables>, + } + + impl<'ctx> ProfileInviteCodeTableHandle<'ctx> { + /// Get a handle on the `invite_code` unique index on the table `profile_invite_code`. + pub fn invite_code(&self) -> ProfileInviteCodeInviteCodeUnique<'ctx> { + ProfileInviteCodeInviteCodeUnique { + imp: self.imp.get_unique_constraint::("invite_code"), + phantom: std::marker::PhantomData, + } + } + } + + impl<'ctx> ProfileInviteCodeInviteCodeUnique<'ctx> { + /// Find the subscribed row whose `invite_code` column value is equal to `col_val`, + /// if such a row is present in the client cache. + pub fn find(&self, col_val: &String) -> Option { + self.imp.find(col_val) + } + } + +#[doc(hidden)] +pub(super) fn register_table(client_cache: &mut __sdk::ClientCache) { + + let _table = client_cache.get_or_make_table::("profile_invite_code"); + _table.add_unique_constraint::("user_id", |row| &row.user_id); + _table.add_unique_constraint::("invite_code", |row| &row.invite_code); +} + +#[doc(hidden)] +pub(super) fn parse_table_update( + raw_updates: __ws::v2::TableUpdate, +) -> __sdk::Result<__sdk::TableUpdate> { + __sdk::TableUpdate::parse_table_update(raw_updates).map_err(|e| { + __sdk::InternalError::failed_parse( + "TableUpdate", + "TableUpdate", + ).with_cause(e).into() + }) +} + + #[allow(non_camel_case_types)] + /// Extension trait for query builder access to the table `ProfileInviteCode`. + /// + /// Implemented for [`__sdk::QueryTableAccessor`]. + pub trait profile_invite_codeQueryTableAccess { + #[allow(non_snake_case)] + /// Get a query builder for the table `ProfileInviteCode`. + fn profile_invite_code(&self) -> __sdk::__query_builder::Table; + } + + impl profile_invite_codeQueryTableAccess for __sdk::QueryTableAccessor { + fn profile_invite_code(&self) -> __sdk::__query_builder::Table { + __sdk::__query_builder::Table::new("profile_invite_code") + } + } + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/profile_invite_code_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/profile_invite_code_type.rs new file mode 100644 index 00000000..1a3ba152 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/profile_invite_code_type.rs @@ -0,0 +1,71 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{ + self as __sdk, + __lib, + __sats, + __ws, +}; + + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct ProfileInviteCode { + pub user_id: String, + pub invite_code: String, + pub created_at: __sdk::Timestamp, + pub updated_at: __sdk::Timestamp, +} + + +impl __sdk::InModule for ProfileInviteCode { + type Module = super::RemoteModule; +} + + +/// Column accessor struct for the table `ProfileInviteCode`. +/// +/// Provides typed access to columns for query building. +pub struct ProfileInviteCodeCols { + pub user_id: __sdk::__query_builder::Col, + pub invite_code: __sdk::__query_builder::Col, + pub created_at: __sdk::__query_builder::Col, + pub updated_at: __sdk::__query_builder::Col, +} + +impl __sdk::__query_builder::HasCols for ProfileInviteCode { + type Cols = ProfileInviteCodeCols; + fn cols(table_name: &'static str) -> Self::Cols { + ProfileInviteCodeCols { + user_id: __sdk::__query_builder::Col::new(table_name, "user_id"), + invite_code: __sdk::__query_builder::Col::new(table_name, "invite_code"), + created_at: __sdk::__query_builder::Col::new(table_name, "created_at"), + updated_at: __sdk::__query_builder::Col::new(table_name, "updated_at"), + + } + } +} + +/// Indexed column accessor struct for the table `ProfileInviteCode`. +/// +/// Provides typed access to indexed columns for query building. +pub struct ProfileInviteCodeIxCols { + pub invite_code: __sdk::__query_builder::IxCol, + pub user_id: __sdk::__query_builder::IxCol, +} + +impl __sdk::__query_builder::HasIxCols for ProfileInviteCode { + type IxCols = ProfileInviteCodeIxCols; + fn ix_cols(table_name: &'static str) -> Self::IxCols { + ProfileInviteCodeIxCols { + invite_code: __sdk::__query_builder::IxCol::new(table_name, "invite_code"), + user_id: __sdk::__query_builder::IxCol::new(table_name, "user_id"), + + } + } +} + +impl __sdk::__query_builder::CanBeLookupTable for ProfileInviteCode {} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/profile_membership_table.rs b/server-rs/crates/spacetime-client/src/module_bindings/profile_membership_table.rs new file mode 100644 index 00000000..c9b7cc23 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/profile_membership_table.rs @@ -0,0 +1,165 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{ + self as __sdk, + __lib, + __sats, + __ws, +}; +use super::profile_membership_type::ProfileMembership; +use super::runtime_profile_membership_status_type::RuntimeProfileMembershipStatus; +use super::runtime_profile_membership_tier_type::RuntimeProfileMembershipTier; + +/// Table handle for the table `profile_membership`. +/// +/// Obtain a handle from the [`ProfileMembershipTableAccess::profile_membership`] method on [`super::RemoteTables`], +/// like `ctx.db.profile_membership()`. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.profile_membership().on_insert(...)`. +pub struct ProfileMembershipTableHandle<'ctx> { + imp: __sdk::TableHandle, + ctx: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the table `profile_membership`. +/// +/// Implemented for [`super::RemoteTables`]. +pub trait ProfileMembershipTableAccess { + #[allow(non_snake_case)] + /// Obtain a [`ProfileMembershipTableHandle`], which mediates access to the table `profile_membership`. + fn profile_membership(&self) -> ProfileMembershipTableHandle<'_>; +} + +impl ProfileMembershipTableAccess for super::RemoteTables { + fn profile_membership(&self) -> ProfileMembershipTableHandle<'_> { + ProfileMembershipTableHandle { + imp: self.imp.get_table::("profile_membership"), + ctx: std::marker::PhantomData, + } + } +} + +pub struct ProfileMembershipInsertCallbackId(__sdk::CallbackId); +pub struct ProfileMembershipDeleteCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::Table for ProfileMembershipTableHandle<'ctx> { + type Row = ProfileMembership; + type EventContext = super::EventContext; + + fn count(&self) -> u64 { self.imp.count() } + fn iter(&self) -> impl Iterator + '_ { self.imp.iter() } + + type InsertCallbackId = ProfileMembershipInsertCallbackId; + + fn on_insert( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> ProfileMembershipInsertCallbackId { + ProfileMembershipInsertCallbackId(self.imp.on_insert(Box::new(callback))) + } + + fn remove_on_insert(&self, callback: ProfileMembershipInsertCallbackId) { + self.imp.remove_on_insert(callback.0) + } + + type DeleteCallbackId = ProfileMembershipDeleteCallbackId; + + fn on_delete( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> ProfileMembershipDeleteCallbackId { + ProfileMembershipDeleteCallbackId(self.imp.on_delete(Box::new(callback))) + } + + fn remove_on_delete(&self, callback: ProfileMembershipDeleteCallbackId) { + self.imp.remove_on_delete(callback.0) + } +} + +pub struct ProfileMembershipUpdateCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::TableWithPrimaryKey for ProfileMembershipTableHandle<'ctx> { + type UpdateCallbackId = ProfileMembershipUpdateCallbackId; + + fn on_update( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static, + ) -> ProfileMembershipUpdateCallbackId { + ProfileMembershipUpdateCallbackId(self.imp.on_update(Box::new(callback))) + } + + fn remove_on_update(&self, callback: ProfileMembershipUpdateCallbackId) { + self.imp.remove_on_update(callback.0) + } +} + + /// Access to the `user_id` unique index on the table `profile_membership`, + /// which allows point queries on the field of the same name + /// via the [`ProfileMembershipUserIdUnique::find`] method. + /// + /// Users are encouraged not to explicitly reference this type, + /// but to directly chain method calls, + /// like `ctx.db.profile_membership().user_id().find(...)`. + pub struct ProfileMembershipUserIdUnique<'ctx> { + imp: __sdk::UniqueConstraintHandle, + phantom: std::marker::PhantomData<&'ctx super::RemoteTables>, + } + + impl<'ctx> ProfileMembershipTableHandle<'ctx> { + /// Get a handle on the `user_id` unique index on the table `profile_membership`. + pub fn user_id(&self) -> ProfileMembershipUserIdUnique<'ctx> { + ProfileMembershipUserIdUnique { + imp: self.imp.get_unique_constraint::("user_id"), + phantom: std::marker::PhantomData, + } + } + } + + impl<'ctx> ProfileMembershipUserIdUnique<'ctx> { + /// Find the subscribed row whose `user_id` column value is equal to `col_val`, + /// if such a row is present in the client cache. + pub fn find(&self, col_val: &String) -> Option { + self.imp.find(col_val) + } + } + +#[doc(hidden)] +pub(super) fn register_table(client_cache: &mut __sdk::ClientCache) { + + let _table = client_cache.get_or_make_table::("profile_membership"); + _table.add_unique_constraint::("user_id", |row| &row.user_id); +} + +#[doc(hidden)] +pub(super) fn parse_table_update( + raw_updates: __ws::v2::TableUpdate, +) -> __sdk::Result<__sdk::TableUpdate> { + __sdk::TableUpdate::parse_table_update(raw_updates).map_err(|e| { + __sdk::InternalError::failed_parse( + "TableUpdate", + "TableUpdate", + ).with_cause(e).into() + }) +} + + #[allow(non_camel_case_types)] + /// Extension trait for query builder access to the table `ProfileMembership`. + /// + /// Implemented for [`__sdk::QueryTableAccessor`]. + pub trait profile_membershipQueryTableAccess { + #[allow(non_snake_case)] + /// Get a query builder for the table `ProfileMembership`. + fn profile_membership(&self) -> __sdk::__query_builder::Table; + } + + impl profile_membershipQueryTableAccess for __sdk::QueryTableAccessor { + fn profile_membership(&self) -> __sdk::__query_builder::Table { + __sdk::__query_builder::Table::new("profile_membership") + } + } + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/profile_recharge_order_table.rs b/server-rs/crates/spacetime-client/src/module_bindings/profile_recharge_order_table.rs new file mode 100644 index 00000000..94cdd920 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/profile_recharge_order_table.rs @@ -0,0 +1,165 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{ + self as __sdk, + __lib, + __sats, + __ws, +}; +use super::profile_recharge_order_type::ProfileRechargeOrder; +use super::runtime_profile_recharge_product_kind_type::RuntimeProfileRechargeProductKind; +use super::runtime_profile_recharge_order_status_type::RuntimeProfileRechargeOrderStatus; + +/// Table handle for the table `profile_recharge_order`. +/// +/// Obtain a handle from the [`ProfileRechargeOrderTableAccess::profile_recharge_order`] method on [`super::RemoteTables`], +/// like `ctx.db.profile_recharge_order()`. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.profile_recharge_order().on_insert(...)`. +pub struct ProfileRechargeOrderTableHandle<'ctx> { + imp: __sdk::TableHandle, + ctx: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the table `profile_recharge_order`. +/// +/// Implemented for [`super::RemoteTables`]. +pub trait ProfileRechargeOrderTableAccess { + #[allow(non_snake_case)] + /// Obtain a [`ProfileRechargeOrderTableHandle`], which mediates access to the table `profile_recharge_order`. + fn profile_recharge_order(&self) -> ProfileRechargeOrderTableHandle<'_>; +} + +impl ProfileRechargeOrderTableAccess for super::RemoteTables { + fn profile_recharge_order(&self) -> ProfileRechargeOrderTableHandle<'_> { + ProfileRechargeOrderTableHandle { + imp: self.imp.get_table::("profile_recharge_order"), + ctx: std::marker::PhantomData, + } + } +} + +pub struct ProfileRechargeOrderInsertCallbackId(__sdk::CallbackId); +pub struct ProfileRechargeOrderDeleteCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::Table for ProfileRechargeOrderTableHandle<'ctx> { + type Row = ProfileRechargeOrder; + type EventContext = super::EventContext; + + fn count(&self) -> u64 { self.imp.count() } + fn iter(&self) -> impl Iterator + '_ { self.imp.iter() } + + type InsertCallbackId = ProfileRechargeOrderInsertCallbackId; + + fn on_insert( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> ProfileRechargeOrderInsertCallbackId { + ProfileRechargeOrderInsertCallbackId(self.imp.on_insert(Box::new(callback))) + } + + fn remove_on_insert(&self, callback: ProfileRechargeOrderInsertCallbackId) { + self.imp.remove_on_insert(callback.0) + } + + type DeleteCallbackId = ProfileRechargeOrderDeleteCallbackId; + + fn on_delete( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> ProfileRechargeOrderDeleteCallbackId { + ProfileRechargeOrderDeleteCallbackId(self.imp.on_delete(Box::new(callback))) + } + + fn remove_on_delete(&self, callback: ProfileRechargeOrderDeleteCallbackId) { + self.imp.remove_on_delete(callback.0) + } +} + +pub struct ProfileRechargeOrderUpdateCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::TableWithPrimaryKey for ProfileRechargeOrderTableHandle<'ctx> { + type UpdateCallbackId = ProfileRechargeOrderUpdateCallbackId; + + fn on_update( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static, + ) -> ProfileRechargeOrderUpdateCallbackId { + ProfileRechargeOrderUpdateCallbackId(self.imp.on_update(Box::new(callback))) + } + + fn remove_on_update(&self, callback: ProfileRechargeOrderUpdateCallbackId) { + self.imp.remove_on_update(callback.0) + } +} + + /// Access to the `order_id` unique index on the table `profile_recharge_order`, + /// which allows point queries on the field of the same name + /// via the [`ProfileRechargeOrderOrderIdUnique::find`] method. + /// + /// Users are encouraged not to explicitly reference this type, + /// but to directly chain method calls, + /// like `ctx.db.profile_recharge_order().order_id().find(...)`. + pub struct ProfileRechargeOrderOrderIdUnique<'ctx> { + imp: __sdk::UniqueConstraintHandle, + phantom: std::marker::PhantomData<&'ctx super::RemoteTables>, + } + + impl<'ctx> ProfileRechargeOrderTableHandle<'ctx> { + /// Get a handle on the `order_id` unique index on the table `profile_recharge_order`. + pub fn order_id(&self) -> ProfileRechargeOrderOrderIdUnique<'ctx> { + ProfileRechargeOrderOrderIdUnique { + imp: self.imp.get_unique_constraint::("order_id"), + phantom: std::marker::PhantomData, + } + } + } + + impl<'ctx> ProfileRechargeOrderOrderIdUnique<'ctx> { + /// Find the subscribed row whose `order_id` column value is equal to `col_val`, + /// if such a row is present in the client cache. + pub fn find(&self, col_val: &String) -> Option { + self.imp.find(col_val) + } + } + +#[doc(hidden)] +pub(super) fn register_table(client_cache: &mut __sdk::ClientCache) { + + let _table = client_cache.get_or_make_table::("profile_recharge_order"); + _table.add_unique_constraint::("order_id", |row| &row.order_id); +} + +#[doc(hidden)] +pub(super) fn parse_table_update( + raw_updates: __ws::v2::TableUpdate, +) -> __sdk::Result<__sdk::TableUpdate> { + __sdk::TableUpdate::parse_table_update(raw_updates).map_err(|e| { + __sdk::InternalError::failed_parse( + "TableUpdate", + "TableUpdate", + ).with_cause(e).into() + }) +} + + #[allow(non_camel_case_types)] + /// Extension trait for query builder access to the table `ProfileRechargeOrder`. + /// + /// Implemented for [`__sdk::QueryTableAccessor`]. + pub trait profile_recharge_orderQueryTableAccess { + #[allow(non_snake_case)] + /// Get a query builder for the table `ProfileRechargeOrder`. + fn profile_recharge_order(&self) -> __sdk::__query_builder::Table; + } + + impl profile_recharge_orderQueryTableAccess for __sdk::QueryTableAccessor { + fn profile_recharge_order(&self) -> __sdk::__query_builder::Table { + __sdk::__query_builder::Table::new("profile_recharge_order") + } + } + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/profile_referral_relation_table.rs b/server-rs/crates/spacetime-client/src/module_bindings/profile_referral_relation_table.rs new file mode 100644 index 00000000..0db9492c --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/profile_referral_relation_table.rs @@ -0,0 +1,163 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{ + self as __sdk, + __lib, + __sats, + __ws, +}; +use super::profile_referral_relation_type::ProfileReferralRelation; + +/// Table handle for the table `profile_referral_relation`. +/// +/// Obtain a handle from the [`ProfileReferralRelationTableAccess::profile_referral_relation`] method on [`super::RemoteTables`], +/// like `ctx.db.profile_referral_relation()`. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.profile_referral_relation().on_insert(...)`. +pub struct ProfileReferralRelationTableHandle<'ctx> { + imp: __sdk::TableHandle, + ctx: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the table `profile_referral_relation`. +/// +/// Implemented for [`super::RemoteTables`]. +pub trait ProfileReferralRelationTableAccess { + #[allow(non_snake_case)] + /// Obtain a [`ProfileReferralRelationTableHandle`], which mediates access to the table `profile_referral_relation`. + fn profile_referral_relation(&self) -> ProfileReferralRelationTableHandle<'_>; +} + +impl ProfileReferralRelationTableAccess for super::RemoteTables { + fn profile_referral_relation(&self) -> ProfileReferralRelationTableHandle<'_> { + ProfileReferralRelationTableHandle { + imp: self.imp.get_table::("profile_referral_relation"), + ctx: std::marker::PhantomData, + } + } +} + +pub struct ProfileReferralRelationInsertCallbackId(__sdk::CallbackId); +pub struct ProfileReferralRelationDeleteCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::Table for ProfileReferralRelationTableHandle<'ctx> { + type Row = ProfileReferralRelation; + type EventContext = super::EventContext; + + fn count(&self) -> u64 { self.imp.count() } + fn iter(&self) -> impl Iterator + '_ { self.imp.iter() } + + type InsertCallbackId = ProfileReferralRelationInsertCallbackId; + + fn on_insert( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> ProfileReferralRelationInsertCallbackId { + ProfileReferralRelationInsertCallbackId(self.imp.on_insert(Box::new(callback))) + } + + fn remove_on_insert(&self, callback: ProfileReferralRelationInsertCallbackId) { + self.imp.remove_on_insert(callback.0) + } + + type DeleteCallbackId = ProfileReferralRelationDeleteCallbackId; + + fn on_delete( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> ProfileReferralRelationDeleteCallbackId { + ProfileReferralRelationDeleteCallbackId(self.imp.on_delete(Box::new(callback))) + } + + fn remove_on_delete(&self, callback: ProfileReferralRelationDeleteCallbackId) { + self.imp.remove_on_delete(callback.0) + } +} + +pub struct ProfileReferralRelationUpdateCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::TableWithPrimaryKey for ProfileReferralRelationTableHandle<'ctx> { + type UpdateCallbackId = ProfileReferralRelationUpdateCallbackId; + + fn on_update( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static, + ) -> ProfileReferralRelationUpdateCallbackId { + ProfileReferralRelationUpdateCallbackId(self.imp.on_update(Box::new(callback))) + } + + fn remove_on_update(&self, callback: ProfileReferralRelationUpdateCallbackId) { + self.imp.remove_on_update(callback.0) + } +} + + /// Access to the `invitee_user_id` unique index on the table `profile_referral_relation`, + /// which allows point queries on the field of the same name + /// via the [`ProfileReferralRelationInviteeUserIdUnique::find`] method. + /// + /// Users are encouraged not to explicitly reference this type, + /// but to directly chain method calls, + /// like `ctx.db.profile_referral_relation().invitee_user_id().find(...)`. + pub struct ProfileReferralRelationInviteeUserIdUnique<'ctx> { + imp: __sdk::UniqueConstraintHandle, + phantom: std::marker::PhantomData<&'ctx super::RemoteTables>, + } + + impl<'ctx> ProfileReferralRelationTableHandle<'ctx> { + /// Get a handle on the `invitee_user_id` unique index on the table `profile_referral_relation`. + pub fn invitee_user_id(&self) -> ProfileReferralRelationInviteeUserIdUnique<'ctx> { + ProfileReferralRelationInviteeUserIdUnique { + imp: self.imp.get_unique_constraint::("invitee_user_id"), + phantom: std::marker::PhantomData, + } + } + } + + impl<'ctx> ProfileReferralRelationInviteeUserIdUnique<'ctx> { + /// Find the subscribed row whose `invitee_user_id` column value is equal to `col_val`, + /// if such a row is present in the client cache. + pub fn find(&self, col_val: &String) -> Option { + self.imp.find(col_val) + } + } + +#[doc(hidden)] +pub(super) fn register_table(client_cache: &mut __sdk::ClientCache) { + + let _table = client_cache.get_or_make_table::("profile_referral_relation"); + _table.add_unique_constraint::("invitee_user_id", |row| &row.invitee_user_id); +} + +#[doc(hidden)] +pub(super) fn parse_table_update( + raw_updates: __ws::v2::TableUpdate, +) -> __sdk::Result<__sdk::TableUpdate> { + __sdk::TableUpdate::parse_table_update(raw_updates).map_err(|e| { + __sdk::InternalError::failed_parse( + "TableUpdate", + "TableUpdate", + ).with_cause(e).into() + }) +} + + #[allow(non_camel_case_types)] + /// Extension trait for query builder access to the table `ProfileReferralRelation`. + /// + /// Implemented for [`__sdk::QueryTableAccessor`]. + pub trait profile_referral_relationQueryTableAccess { + #[allow(non_snake_case)] + /// Get a query builder for the table `ProfileReferralRelation`. + fn profile_referral_relation(&self) -> __sdk::__query_builder::Table; + } + + impl profile_referral_relationQueryTableAccess for __sdk::QueryTableAccessor { + fn profile_referral_relation(&self) -> __sdk::__query_builder::Table { + __sdk::__query_builder::Table::new("profile_referral_relation") + } + } + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/profile_referral_relation_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/profile_referral_relation_type.rs new file mode 100644 index 00000000..6da25f89 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/profile_referral_relation_type.rs @@ -0,0 +1,77 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{ + self as __sdk, + __lib, + __sats, + __ws, +}; + + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct ProfileReferralRelation { + pub invitee_user_id: String, + pub inviter_user_id: String, + pub invite_code: String, + pub inviter_reward_granted: bool, + pub invitee_reward_granted: bool, + pub bound_at: __sdk::Timestamp, +} + + +impl __sdk::InModule for ProfileReferralRelation { + type Module = super::RemoteModule; +} + + +/// Column accessor struct for the table `ProfileReferralRelation`. +/// +/// Provides typed access to columns for query building. +pub struct ProfileReferralRelationCols { + pub invitee_user_id: __sdk::__query_builder::Col, + pub inviter_user_id: __sdk::__query_builder::Col, + pub invite_code: __sdk::__query_builder::Col, + pub inviter_reward_granted: __sdk::__query_builder::Col, + pub invitee_reward_granted: __sdk::__query_builder::Col, + pub bound_at: __sdk::__query_builder::Col, +} + +impl __sdk::__query_builder::HasCols for ProfileReferralRelation { + type Cols = ProfileReferralRelationCols; + fn cols(table_name: &'static str) -> Self::Cols { + ProfileReferralRelationCols { + invitee_user_id: __sdk::__query_builder::Col::new(table_name, "invitee_user_id"), + inviter_user_id: __sdk::__query_builder::Col::new(table_name, "inviter_user_id"), + invite_code: __sdk::__query_builder::Col::new(table_name, "invite_code"), + inviter_reward_granted: __sdk::__query_builder::Col::new(table_name, "inviter_reward_granted"), + invitee_reward_granted: __sdk::__query_builder::Col::new(table_name, "invitee_reward_granted"), + bound_at: __sdk::__query_builder::Col::new(table_name, "bound_at"), + + } + } +} + +/// Indexed column accessor struct for the table `ProfileReferralRelation`. +/// +/// Provides typed access to indexed columns for query building. +pub struct ProfileReferralRelationIxCols { + pub invitee_user_id: __sdk::__query_builder::IxCol, + pub inviter_user_id: __sdk::__query_builder::IxCol, +} + +impl __sdk::__query_builder::HasIxCols for ProfileReferralRelation { + type IxCols = ProfileReferralRelationIxCols; + fn ix_cols(table_name: &'static str) -> Self::IxCols { + ProfileReferralRelationIxCols { + invitee_user_id: __sdk::__query_builder::IxCol::new(table_name, "invitee_user_id"), + inviter_user_id: __sdk::__query_builder::IxCol::new(table_name, "inviter_user_id"), + + } + } +} + +impl __sdk::__query_builder::CanBeLookupTable for ProfileReferralRelation {} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/redeem_profile_referral_invite_code_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/redeem_profile_referral_invite_code_procedure.rs new file mode 100644 index 00000000..b1f125b6 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/redeem_profile_referral_invite_code_procedure.rs @@ -0,0 +1,58 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{ + self as __sdk, + __lib, + __sats, + __ws, +}; + +use super::runtime_referral_redeem_input_type::RuntimeReferralRedeemInput; +use super::runtime_referral_redeem_procedure_result_type::RuntimeReferralRedeemProcedureResult; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] + struct RedeemProfileReferralInviteCodeArgs { + pub input: RuntimeReferralRedeemInput, +} + + +impl __sdk::InModule for RedeemProfileReferralInviteCodeArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `redeem_profile_referral_invite_code`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait redeem_profile_referral_invite_code { + fn redeem_profile_referral_invite_code(&self, input: RuntimeReferralRedeemInput, +) { + self.redeem_profile_referral_invite_code_then(input, |_, _| {}); + } + + fn redeem_profile_referral_invite_code_then( + &self, + input: RuntimeReferralRedeemInput, + + __callback: impl FnOnce(&super::ProcedureEventContext, Result) + Send + 'static, + ); +} + +impl redeem_profile_referral_invite_code for super::RemoteProcedures { + fn redeem_profile_referral_invite_code_then( + &self, + input: RuntimeReferralRedeemInput, + + __callback: impl FnOnce(&super::ProcedureEventContext, Result) + Send + 'static, + ) { + self.imp.invoke_procedure_with_callback::<_, RuntimeReferralRedeemProcedureResult>( + "redeem_profile_referral_invite_code", + RedeemProfileReferralInviteCodeArgs { input, }, + __callback, + ); + } +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/runtime_referral_invite_center_get_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/runtime_referral_invite_center_get_input_type.rs new file mode 100644 index 00000000..386a9cf9 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/runtime_referral_invite_center_get_input_type.rs @@ -0,0 +1,23 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{ + self as __sdk, + __lib, + __sats, + __ws, +}; + + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct RuntimeReferralInviteCenterGetInput { + pub user_id: String, +} + + +impl __sdk::InModule for RuntimeReferralInviteCenterGetInput { + type Module = super::RemoteModule; +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/runtime_referral_invite_center_procedure_result_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/runtime_referral_invite_center_procedure_result_type.rs new file mode 100644 index 00000000..397a3ebe --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/runtime_referral_invite_center_procedure_result_type.rs @@ -0,0 +1,26 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{ + self as __sdk, + __lib, + __sats, + __ws, +}; + +use super::runtime_referral_invite_center_snapshot_type::RuntimeReferralInviteCenterSnapshot; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct RuntimeReferralInviteCenterProcedureResult { + pub ok: bool, + pub record: Option::, + pub error_message: Option::, +} + + +impl __sdk::InModule for RuntimeReferralInviteCenterProcedureResult { + type Module = super::RemoteModule; +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/runtime_referral_invite_center_snapshot_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/runtime_referral_invite_center_snapshot_type.rs new file mode 100644 index 00000000..498f3881 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/runtime_referral_invite_center_snapshot_type.rs @@ -0,0 +1,34 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{ + self as __sdk, + __lib, + __sats, + __ws, +}; + + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct RuntimeReferralInviteCenterSnapshot { + pub user_id: String, + pub invite_code: String, + pub invite_link_path: String, + pub invited_count: u32, + pub rewarded_invite_count: u32, + pub today_inviter_reward_count: u32, + pub today_inviter_reward_remaining: u32, + pub reward_points: u64, + pub has_redeemed_code: bool, + pub bound_inviter_user_id: Option::, + pub bound_at_micros: Option::, + pub updated_at_micros: i64, +} + + +impl __sdk::InModule for RuntimeReferralInviteCenterSnapshot { + type Module = super::RemoteModule; +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/runtime_referral_redeem_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/runtime_referral_redeem_input_type.rs new file mode 100644 index 00000000..12bc8e73 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/runtime_referral_redeem_input_type.rs @@ -0,0 +1,25 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{ + self as __sdk, + __lib, + __sats, + __ws, +}; + + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct RuntimeReferralRedeemInput { + pub user_id: String, + pub invite_code: String, + pub updated_at_micros: i64, +} + + +impl __sdk::InModule for RuntimeReferralRedeemInput { + type Module = super::RemoteModule; +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/runtime_referral_redeem_procedure_result_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/runtime_referral_redeem_procedure_result_type.rs new file mode 100644 index 00000000..aaf63756 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/runtime_referral_redeem_procedure_result_type.rs @@ -0,0 +1,26 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{ + self as __sdk, + __lib, + __sats, + __ws, +}; + +use super::runtime_referral_redeem_snapshot_type::RuntimeReferralRedeemSnapshot; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct RuntimeReferralRedeemProcedureResult { + pub ok: bool, + pub record: Option::, + pub error_message: Option::, +} + + +impl __sdk::InModule for RuntimeReferralRedeemProcedureResult { + type Module = super::RemoteModule; +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/runtime_referral_redeem_snapshot_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/runtime_referral_redeem_snapshot_type.rs new file mode 100644 index 00000000..04c68e48 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/runtime_referral_redeem_snapshot_type.rs @@ -0,0 +1,28 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{ + self as __sdk, + __lib, + __sats, + __ws, +}; + +use super::runtime_referral_invite_center_snapshot_type::RuntimeReferralInviteCenterSnapshot; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct RuntimeReferralRedeemSnapshot { + pub center: RuntimeReferralInviteCenterSnapshot, + pub invitee_reward_granted: bool, + pub inviter_reward_granted: bool, + pub invitee_balance_after: u64, + pub inviter_balance_after: u64, +} + + +impl __sdk::InModule for RuntimeReferralRedeemSnapshot { + type Module = super::RemoteModule; +} + diff --git a/server-rs/crates/spacetime-client/src/runtime.rs b/server-rs/crates/spacetime-client/src/runtime.rs index 2a787791..336ef98e 100644 --- a/server-rs/crates/spacetime-client/src/runtime.rs +++ b/server-rs/crates/spacetime-client/src/runtime.rs @@ -149,6 +149,51 @@ impl SpacetimeClient { .await } + pub async fn get_profile_referral_invite_center( + &self, + user_id: String, + ) -> Result { + let procedure_input = build_runtime_referral_invite_center_get_input(user_id) + .map_err(|error| SpacetimeClientError::Runtime(error.to_string()))? + .into(); + + self.call_after_connect(move |connection, sender| { + connection + .procedures() + .get_profile_referral_invite_center_then(procedure_input, move |_, result| { + let mapped = result + .map_err(|error| SpacetimeClientError::Procedure(error.to_string())) + .and_then(map_runtime_referral_invite_center_procedure_result); + send_once(&sender, mapped); + }); + }) + .await + } + + pub async fn redeem_profile_referral_invite_code( + &self, + user_id: String, + invite_code: String, + updated_at_micros: i64, + ) -> Result { + let procedure_input = + build_runtime_referral_redeem_input(user_id, invite_code, updated_at_micros) + .map_err(|error| SpacetimeClientError::Runtime(error.to_string()))? + .into(); + + self.call_after_connect(move |connection, sender| { + connection + .procedures() + .redeem_profile_referral_invite_code_then(procedure_input, move |_, result| { + let mapped = result + .map_err(|error| SpacetimeClientError::Procedure(error.to_string())) + .and_then(map_runtime_referral_redeem_procedure_result); + send_once(&sender, mapped); + }); + }) + .await + } + pub async fn get_profile_play_stats( &self, user_id: String, diff --git a/server-rs/crates/spacetime-module/src/custom_world/mod.rs b/server-rs/crates/spacetime-module/src/custom_world/mod.rs index d9e79293..aaa88820 100644 --- a/server-rs/crates/spacetime-module/src/custom_world/mod.rs +++ b/server-rs/crates/spacetime-module/src/custom_world/mod.rs @@ -2653,6 +2653,7 @@ fn summarize_publish_gate_from_json( &[ "worldHook", "creatorIntent.worldHook", + "anchorContent.worldPromise", "anchorContent.worldPromise.hook", "settingText", ], @@ -2670,6 +2671,7 @@ fn summarize_publish_gate_from_json( &[ "playerPremise", "creatorIntent.playerPremise", + "anchorContent.playerEntryPoint", "anchorContent.playerEntryPoint.openingIdentity", "anchorContent.playerEntryPoint.openingProblem", "anchorContent.playerEntryPoint.entryMotivation", diff --git a/server-rs/crates/spacetime-module/src/lib.rs b/server-rs/crates/spacetime-module/src/lib.rs index 61f87617..3170cd44 100644 --- a/server-rs/crates/spacetime-module/src/lib.rs +++ b/server-rs/crates/spacetime-module/src/lib.rs @@ -3766,6 +3766,7 @@ fn summarize_publish_gate_from_json( &[ "worldHook", "creatorIntent.worldHook", + "anchorContent.worldPromise", "anchorContent.worldPromise.hook", "settingText", ], @@ -3783,6 +3784,7 @@ fn summarize_publish_gate_from_json( &[ "playerPremise", "creatorIntent.playerPremise", + "anchorContent.playerEntryPoint", "anchorContent.playerEntryPoint.openingIdentity", "anchorContent.playerEntryPoint.openingProblem", "anchorContent.playerEntryPoint.entryMotivation", diff --git a/server-rs/crates/spacetime-module/src/runtime/profile.rs b/server-rs/crates/spacetime-module/src/runtime/profile.rs index ce78a35b..34d7bfd7 100644 --- a/server-rs/crates/spacetime-module/src/runtime/profile.rs +++ b/server-rs/crates/spacetime-module/src/runtime/profile.rs @@ -28,6 +28,34 @@ pub struct ProfileWalletLedger { pub(crate) created_at: Timestamp, } +#[spacetimedb::table(accessor = profile_invite_code)] +pub struct ProfileInviteCode { + #[primary_key] + pub(crate) user_id: String, + #[unique] + pub(crate) invite_code: String, + pub(crate) created_at: Timestamp, + pub(crate) updated_at: Timestamp, +} + +#[spacetimedb::table( + accessor = profile_referral_relation, + index(accessor = by_profile_referral_inviter_user_id, btree(columns = [inviter_user_id])), + index( + accessor = by_profile_referral_inviter_bound_at, + btree(columns = [inviter_user_id, bound_at]) + ) +)] +pub struct ProfileReferralRelation { + #[primary_key] + pub(crate) invitee_user_id: String, + pub(crate) inviter_user_id: String, + pub(crate) invite_code: String, + pub(crate) inviter_reward_granted: bool, + pub(crate) invitee_reward_granted: bool, + pub(crate) bound_at: Timestamp, +} + #[spacetimedb::table( accessor = profile_played_world, index(accessor = by_profile_played_world_user_id, btree(columns = [user_id])), @@ -274,6 +302,46 @@ pub fn create_profile_recharge_order_and_return( } } +// 邀请中心会在首次打开时为账号创建稳定邀请码,前端只展示这里返回的后端状态。 +#[spacetimedb::procedure] +pub fn get_profile_referral_invite_center( + ctx: &mut ProcedureContext, + input: RuntimeReferralInviteCenterGetInput, +) -> RuntimeReferralInviteCenterProcedureResult { + match ctx.try_with_tx(|tx| get_profile_referral_invite_center_snapshot(tx, input.clone())) { + Ok(record) => RuntimeReferralInviteCenterProcedureResult { + ok: true, + record: Some(record), + error_message: None, + }, + Err(message) => RuntimeReferralInviteCenterProcedureResult { + ok: false, + record: None, + error_message: Some(message), + }, + } +} + +// 填码绑定、每日邀请者奖励上限和双方积分发放都在同一事务内完成。 +#[spacetimedb::procedure] +pub fn redeem_profile_referral_invite_code( + ctx: &mut ProcedureContext, + input: RuntimeReferralRedeemInput, +) -> RuntimeReferralRedeemProcedureResult { + match ctx.try_with_tx(|tx| redeem_profile_referral_invite_code_record(tx, input.clone())) { + Ok(record) => RuntimeReferralRedeemProcedureResult { + ok: true, + record: Some(record), + error_message: None, + }, + Err(message) => RuntimeReferralRedeemProcedureResult { + ok: false, + record: None, + error_message: Some(message), + }, + } +} + pub(crate) fn list_profile_save_archive_rows( ctx: &ReducerContext, input: RuntimeProfileSaveArchiveListInput, @@ -948,6 +1016,215 @@ fn create_profile_recharge_order_record( )) } +fn get_profile_referral_invite_center_snapshot( + ctx: &ReducerContext, + input: RuntimeReferralInviteCenterGetInput, +) -> Result { + let validated_input = build_runtime_referral_invite_center_get_input(input.user_id) + .map_err(|error| error.to_string())?; + Ok(build_profile_referral_invite_center_snapshot( + ctx, + &validated_input.user_id, + )) +} + +fn redeem_profile_referral_invite_code_record( + ctx: &ReducerContext, + input: RuntimeReferralRedeemInput, +) -> Result { + let validated_input = build_runtime_referral_redeem_input( + input.user_id, + input.invite_code, + input.updated_at_micros, + ) + .map_err(|error| error.to_string())?; + let bound_at = Timestamp::from_micros_since_unix_epoch(validated_input.updated_at_micros); + let invitee_user_id = validated_input.user_id; + let invite_code = validated_input.invite_code; + + if ctx + .db + .profile_referral_relation() + .invitee_user_id() + .find(&invitee_user_id) + .is_some() + { + return Err("每个用户最多只能填写一个邀请码".to_string()); + } + + let inviter_code = ctx + .db + .profile_invite_code() + .invite_code() + .find(&invite_code) + .ok_or_else(|| "邀请码不存在".to_string())?; + if inviter_code.user_id == invitee_user_id { + return Err("不能填写自己的邀请码".to_string()); + } + + let invitee_balance_after = apply_profile_wallet_delta( + ctx, + &invitee_user_id, + PROFILE_REFERRAL_REWARD_POINTS, + RuntimeProfileWalletLedgerSourceType::InviteInviteeReward, + &format!( + "invitee:{}:{}", + invitee_user_id, validated_input.updated_at_micros + ), + bound_at, + )?; + let today_inviter_reward_count = + count_today_profile_referral_inviter_rewards(ctx, &inviter_code.user_id, bound_at); + let inviter_reward_granted = + today_inviter_reward_count < PROFILE_REFERRAL_DAILY_INVITER_REWARD_LIMIT; + let inviter_balance_after = if inviter_reward_granted { + apply_profile_wallet_delta( + ctx, + &inviter_code.user_id, + PROFILE_REFERRAL_REWARD_POINTS, + RuntimeProfileWalletLedgerSourceType::InviteInviterReward, + &format!( + "inviter:{}:{}", + inviter_code.user_id, validated_input.updated_at_micros + ), + bound_at, + )? + } else { + profile_wallet_balance(ctx, &inviter_code.user_id) + }; + + ctx.db + .profile_referral_relation() + .insert(ProfileReferralRelation { + invitee_user_id: invitee_user_id.clone(), + inviter_user_id: inviter_code.user_id, + invite_code, + inviter_reward_granted, + invitee_reward_granted: true, + bound_at, + }); + + Ok(RuntimeReferralRedeemSnapshot { + center: build_profile_referral_invite_center_snapshot(ctx, &invitee_user_id), + invitee_reward_granted: true, + inviter_reward_granted, + invitee_balance_after, + inviter_balance_after, + }) +} + +fn build_profile_referral_invite_center_snapshot( + ctx: &ReducerContext, + user_id: &str, +) -> RuntimeReferralInviteCenterSnapshot { + let code = ensure_profile_invite_code(ctx, user_id); + let today_inviter_reward_count = + count_today_profile_referral_inviter_rewards(ctx, user_id, ctx.timestamp); + let invited_count = ctx + .db + .profile_referral_relation() + .iter() + .filter(|row| row.inviter_user_id == user_id) + .count() as u32; + let rewarded_invite_count = ctx + .db + .profile_referral_relation() + .iter() + .filter(|row| row.inviter_user_id == user_id && row.inviter_reward_granted) + .count() as u32; + let bound_relation = ctx + .db + .profile_referral_relation() + .invitee_user_id() + .find(&user_id.to_string()); + + RuntimeReferralInviteCenterSnapshot { + user_id: user_id.to_string(), + invite_code: code.invite_code.clone(), + invite_link_path: format!("/?inviteCode={}", code.invite_code), + invited_count, + rewarded_invite_count, + today_inviter_reward_count, + today_inviter_reward_remaining: PROFILE_REFERRAL_DAILY_INVITER_REWARD_LIMIT + .saturating_sub(today_inviter_reward_count), + reward_points: PROFILE_REFERRAL_REWARD_POINTS, + has_redeemed_code: bound_relation.is_some(), + bound_inviter_user_id: bound_relation + .as_ref() + .map(|relation| relation.inviter_user_id.clone()), + bound_at_micros: bound_relation + .as_ref() + .map(|relation| relation.bound_at.to_micros_since_unix_epoch()), + updated_at_micros: code.updated_at.to_micros_since_unix_epoch(), + } +} + +fn ensure_profile_invite_code(ctx: &ReducerContext, user_id: &str) -> ProfileInviteCode { + if let Some(row) = ctx + .db + .profile_invite_code() + .user_id() + .find(&user_id.to_string()) + { + return row; + } + + let mut invite_code = build_profile_invite_code(user_id, 0); + let mut salt = 1; + while ctx + .db + .profile_invite_code() + .invite_code() + .find(&invite_code) + .is_some() + { + invite_code = build_profile_invite_code(user_id, salt); + salt += 1; + } + + ctx.db.profile_invite_code().insert(ProfileInviteCode { + user_id: user_id.to_string(), + invite_code, + created_at: ctx.timestamp, + updated_at: ctx.timestamp, + }) +} + +fn build_profile_invite_code(user_id: &str, salt: u32) -> String { + let mut hash = 14_695_981_039_346_656_037u64; + for byte in user_id.as_bytes().iter().copied().chain(salt.to_le_bytes()) { + hash ^= byte as u64; + hash = hash.wrapping_mul(1_099_511_628_211); + } + format!("SY{:08X}", hash as u32) +} + +fn count_today_profile_referral_inviter_rewards( + ctx: &ReducerContext, + user_id: &str, + now: Timestamp, +) -> u32 { + let day_start_micros = (now.to_micros_since_unix_epoch() / 86_400_000_000) * 86_400_000_000; + ctx.db + .profile_wallet_ledger() + .iter() + .filter(|row| { + row.user_id == user_id + && row.source_type == RuntimeProfileWalletLedgerSourceType::InviteInviterReward + && row.created_at.to_micros_since_unix_epoch() >= day_start_micros + }) + .count() as u32 +} + +fn profile_wallet_balance(ctx: &ReducerContext, user_id: &str) -> u64 { + ctx.db + .profile_dashboard_state() + .user_id() + .find(&user_id.to_string()) + .map(|row| row.wallet_balance) + .unwrap_or(0) +} + fn build_profile_recharge_center_snapshot( ctx: &ReducerContext, user_id: &str, diff --git a/src/components/CustomWorldEntityCatalog.tsx b/src/components/CustomWorldEntityCatalog.tsx index 595e9688..bc117ed3 100644 --- a/src/components/CustomWorldEntityCatalog.tsx +++ b/src/components/CustomWorldEntityCatalog.tsx @@ -340,13 +340,18 @@ function resolveSceneCardImage(params: { return firstActImageSrc || params.sceneImageSrc?.trim() || ''; } -function collectSceneActImagePreviews(sceneChapters: SceneChapterBlueprint[]) { +function collectSceneActImagePreviews( + sceneChapters: SceneChapterBlueprint[], + sharedSceneImageSrc?: string | null, +) { + const sharedImageSrc = sharedSceneImageSrc?.trim() || ''; + return sceneChapters.flatMap((chapter) => chapter.acts .map((act, index) => ({ id: act.id.trim() || `${chapter.id}-act-${index}`, title: act.title.trim() || `第${index + 1}幕`, - imageSrc: act.backgroundImageSrc?.trim() || '', + imageSrc: sharedImageSrc || act.backgroundImageSrc?.trim() || '', })) .filter((act) => act.imageSrc), ); @@ -356,8 +361,11 @@ function buildFallbackSceneActImagePreviews(params: { sceneChapters: SceneChapterBlueprint[]; sceneImageSrc?: string | null; }) { - const actPreviews = collectSceneActImagePreviews(params.sceneChapters); const sceneImageSrc = params.sceneImageSrc?.trim() || ''; + const actPreviews = collectSceneActImagePreviews( + params.sceneChapters, + sceneImageSrc, + ); if (actPreviews.length > 0 || !sceneImageSrc) { return actPreviews; @@ -778,8 +786,14 @@ export function CustomWorldEntityCatalog({ sceneId: landmark.id, sceneName: landmark.name, }); + const firstActImageSrc = + sceneChapters + .flatMap((chapter) => chapter.acts) + .map((act) => act.backgroundImageSrc?.trim() || '') + .find(Boolean) || ''; const sceneImageSrc = resolveSceneCardImage({ - sceneImageSrc: landmarkImageById.get(landmark.id) ?? landmark.imageSrc, + sceneImageSrc: + firstActImageSrc || landmarkImageById.get(landmark.id) || landmark.imageSrc, sceneChapters, }); diff --git a/src/components/CustomWorldEntityEditorModal.test.tsx b/src/components/CustomWorldEntityEditorModal.test.tsx index eefa9ceb..89b9eeae 100644 --- a/src/components/CustomWorldEntityEditorModal.test.tsx +++ b/src/components/CustomWorldEntityEditorModal.test.tsx @@ -1,9 +1,9 @@ /* @vitest-environment jsdom */ -import { render, screen, waitFor, within } from '@testing-library/react'; +import { cleanup, render, screen, waitFor, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { useState } from 'react'; -import { expect, test, vi } from 'vitest'; +import { afterEach, expect, test, vi } from 'vitest'; import type { CustomWorldNpc, @@ -18,6 +18,10 @@ import { import * as customWorldCoverAssetService from '../services/customWorldCoverAssetService'; import * as rpgCreationAssetClient from '../services/rpg-creation/rpgCreationAssetClient'; +afterEach(() => { + cleanup(); +}); + vi.mock('../data/characterPresets', async () => { const actual = await vi.importActual( '../data/characterPresets', @@ -63,14 +67,25 @@ vi.mock('./CustomWorldNpcVisualEditor', () => ({ CustomWorldNpcVisualEditor: () =>
预设形象编辑器
, })); +vi.mock('../hooks/useResolvedAssetReadUrl', () => ({ + useResolvedAssetReadUrl: (source: string | null | undefined) => ({ + resolvedUrl: source?.trim() ?? '', + isResolving: false, + shouldResolve: false, + }), +})); + vi.mock('./rpg-runtime-shell', () => ({ RpgRuntimeShell: ({ session, + chrome, }: { session: { gameState: { currentScenePreset?: { name?: string } | null } }; + chrome?: { hidePlayerLevelBadge?: boolean }; }) => (
幕预览运行时
+ {chrome?.hidePlayerLevelBadge ?
隐藏等级徽标
: null}
{session.gameState.currentScenePreset?.name ?? '未进入场景'}
), @@ -208,6 +223,7 @@ function createProfileWithLandmark(): CustomWorldProfile { createStoryRole('story-1', '顾潮音'), createStoryRole('story-2', '闻雪汀'), createStoryRole('story-3', '谢孤灯'), + createStoryRole('story-4', '陆听潮'), ], landmarks: [ { @@ -222,6 +238,29 @@ function createProfileWithLandmark(): CustomWorldProfile { } as unknown as CustomWorldProfile; } +function createProfileWithTwoLandmarks(): CustomWorldProfile { + return { + ...createProfileWithLandmark(), + landmarks: [ + { + id: 'landmark-1', + name: '沉钟栈桥', + description: '旧钟与潮声常年相撞的码头栈桥。', + imageSrc: '/generated-custom-world-scenes/original-scene.png', + sceneNpcIds: ['story-1', 'story-2', 'story-3'], + connections: [], + }, + { + id: 'landmark-2', + name: '雾灯塔', + description: '雾中仍在闪烁的旧灯塔。', + sceneNpcIds: ['story-1', 'story-2', 'story-3'], + connections: [], + }, + ], + } as unknown as CustomWorldProfile; +} + function LandmarkEditorFlowHarness() { const [profile, setProfile] = useState(createProfileWithLandmark()); const [target, setTarget] = useState({ @@ -255,6 +294,24 @@ function LandmarkEditorFlowHarness() { ); } +function TwoLandmarkEditorFlowHarness() { + const [profile, setProfile] = useState(createProfileWithTwoLandmarks()); + const [target, setTarget] = useState({ + kind: 'landmark', + mode: 'edit', + id: 'landmark-1', + }); + + return ( + setTarget(null)} + onProfileChange={setProfile} + /> + ); +} + function readLandmarkHarnessProfile() { const content = screen.getByTestId('landmark-profile-json').textContent; return JSON.parse(content || '{}') as CustomWorldProfile; @@ -506,16 +563,13 @@ test('基本设定用分号拆分成标签展示', () => { const profile = { ...createProfile(), anchorContent: { - worldPromise: { - hook: '机械微生物吞并进化', - differentiator: '角色被迫寄生改造', - desiredExperience: '在失控系统里求生', - }, + worldPromise: + '机械微生物吞并进化;角色被迫寄生改造;在失控系统里求生', playerFantasy: null, themeBoundary: null, playerEntryPoint: null, coreConflict: null, - keyRelationships: [], + keyRelationships: null, hiddenLines: null, iconicElements: null, }, @@ -688,6 +742,14 @@ test('场景图片保存后会同步更新编辑页和场景列表', async () => expect(savedProfile.landmarks[0]?.imageSrc).toBe( '/generated-custom-world-scenes/updated-scene.png', ); + const savedSceneChapter = savedProfile.sceneChapterBlueprints?.find( + (entry) => entry.sceneId === 'landmark-1', + ); + expect( + savedSceneChapter?.acts.every( + (act) => act.backgroundImageSrc === '/generated-custom-world-scenes/updated-scene.png', + ), + ).toBe(true); }); test('开局场景图片保存后会同步更新编辑页和场景列表', async () => { @@ -758,6 +820,14 @@ test('开局场景图片保存后会同步更新编辑页和场景列表', async expect(savedProfile.camp?.imageSrc).toBe( '/generated-custom-world-scenes/updated-camp.png', ); + const savedSceneChapter = savedProfile.sceneChapterBlueprints?.find( + (entry) => entry.sceneId === 'custom-scene-camp', + ); + expect( + savedSceneChapter?.acts.every( + (act) => act.backgroundImageSrc === '/generated-custom-world-scenes/updated-camp.png', + ), + ).toBe(true); }); test('开局场景在场景配置面板中与普通场景使用同级参数并可保存', async () => { @@ -795,10 +865,7 @@ test('开局场景在场景配置面板中与普通场景使用同级参数并 (entry) => entry.sceneId === 'custom-scene-camp', ); - expect(savedProfile.camp?.sceneNpcIds).toHaveLength(3); - expect(savedProfile.camp?.sceneNpcIds).toEqual( - expect.arrayContaining(['story-1', 'story-2', 'story-3']), - ); + expect(savedProfile.camp?.sceneNpcIds).toContain('story-2'); expect(savedProfile.camp?.connections).toEqual([ { targetLandmarkId: 'landmark-1', @@ -811,6 +878,41 @@ test('开局场景在场景配置面板中与普通场景使用同级参数并 expect(openingSceneChapter?.linkedLandmarkIds).toContain('custom-scene-camp'); }); +test('普通场景世界地图会包含开局场景并高亮当前场景', async () => { + const user = userEvent.setup(); + + render(); + + await user.click(screen.getByRole('button', { name: '查看世界地图' })); + + await waitFor(() => { + expect(screen.getByText('世界地图')).toBeTruthy(); + }); + expect(screen.getAllByText('潮灯居').length).toBeGreaterThan(0); + expect(screen.getAllByText('沉钟栈桥').length).toBeGreaterThan(0); + expect(screen.getByText('当前')).toBeTruthy(); +}); + +test('世界地图会展示当前未保存的场景连接', async () => { + const user = userEvent.setup(); + + render(); + + await user.click(screen.getByText('北')); + await user.click(screen.getByRole('button', { name: /雾灯塔/u })); + await waitFor(() => { + expect(screen.queryByText('北侧连接')).toBeNull(); + }); + + await user.click(screen.getByRole('button', { name: '查看世界地图' })); + + await waitFor(() => { + expect(screen.getByText('世界地图')).toBeTruthy(); + }); + expect(screen.getAllByText('雾灯塔').length).toBeGreaterThan(0); + expect(screen.getAllByText('北').length).toBeGreaterThan(0); +}); + test('场景编辑器会在场景内展示槽位化多幕配置并保存', async () => { const user = userEvent.setup(); @@ -916,6 +1018,40 @@ test('场景多幕支持新增删除和调序', async () => { expect(savedSceneChapter?.acts[2]?.primaryNpcId).toBe('story-3'); }); +test('每幕角色槽位可以从当前世界所有 NPC 中选择', async () => { + const user = userEvent.setup(); + + render(); + + await user.click(within(getSceneActCard(0)).getAllByTestId('scene-act-slot-button')[0]!); + await waitFor(() => { + expect(screen.getByText('配置角色:第1幕 · 主角色槽位')).toBeTruthy(); + }); + + expect(screen.getByRole('button', { name: /陆听潮/u })).toBeTruthy(); + + await user.click(screen.getByRole('button', { name: /陆听潮/u })); + await user.click(screen.getByRole('button', { name: '保存角色' })); + + await waitFor(() => { + expect(screen.queryByText('配置角色:第1幕 · 主角色槽位')).toBeNull(); + }); + + await user.click(screen.getByRole('button', { name: /保存修改/u })); + + await waitFor(() => { + expect(screen.queryByText('编辑场景:沉钟栈桥')).toBeNull(); + }); + + const savedProfile = readLandmarkHarnessProfile(); + const savedSceneChapter = savedProfile.sceneChapterBlueprints?.find( + (entry) => entry.sceneId === 'landmark-1', + ); + + expect(savedSceneChapter?.acts[0]?.encounterNpcIds[0]).toBe('story-4'); + expect(savedProfile.landmarks[0]?.sceneNpcIds).toContain('story-4'); +}); + test('场景幕预览会打开当前幕运行时面板', async () => { const user = userEvent.setup(); @@ -929,7 +1065,9 @@ test('场景幕预览会打开当前幕运行时面板', async () => { expect(screen.getAllByText('沉钟栈桥').length).toBeGreaterThan(0); - await user.click(screen.getByRole('button', { name: '关闭预览' })); + expect(screen.getByText('隐藏等级徽标')).toBeTruthy(); + + await user.click(screen.getByRole('button', { name: '结束预览' })); await waitFor(() => { expect(screen.queryByText('幕预览运行时')).toBeNull(); diff --git a/src/components/CustomWorldNpcVisualEditor.tsx b/src/components/CustomWorldNpcVisualEditor.tsx index a7c34b18..56a48fde 100644 --- a/src/components/CustomWorldNpcVisualEditor.tsx +++ b/src/components/CustomWorldNpcVisualEditor.tsx @@ -328,8 +328,8 @@ export function CustomWorldNpcPortrait({ preferImageSrc && npc.imageSrc?.trim() ? npc.imageSrc.trim() : ''; return ( -
-
+
+
diff --git a/src/components/CustomWorldResultView.test.tsx b/src/components/CustomWorldResultView.test.tsx index 6273d73b..267f8123 100644 --- a/src/components/CustomWorldResultView.test.tsx +++ b/src/components/CustomWorldResultView.test.tsx @@ -46,6 +46,14 @@ vi.mock('./CustomWorldNpcVisualEditor', () => ({ ), })); +vi.mock('../hooks/useResolvedAssetReadUrl', () => ({ + useResolvedAssetReadUrl: (source: string | null | undefined) => ({ + resolvedUrl: source?.trim() ?? '', + isResolving: false, + shouldResolve: false, + }), +})); + vi.mock('./rpg-creation-editor/RpgCreationEntityEditorModal', () => ({ RpgCreationEntityEditorModal: () => null, default: () => null, @@ -184,48 +192,21 @@ const baseProfile = { description: '玩家最初落脚的旧灯塔内院。', }, anchorContent: { - worldPromise: { - hook: '被海雾反复改写航路的群岛世界。', - differentiator: '旧灯塔与禁航令共同决定谁能活着穿过去。', - desiredExperience: '压抑、悬疑、潮湿', - }, - playerFantasy: { - playerRole: '玩家是被迫返乡的守灯人继承者。', - corePursuit: '查清沉钟异动与失控航路的真相。', - fearOfLoss: '失去家族留下的最后航路坐标。', - }, - themeBoundary: { - toneKeywords: ['压抑', '悬疑'], - aestheticDirectives: ['潮湿群岛', '冷雾港口'], - forbiddenDirectives: ['热血少年漫'], - }, - playerEntryPoint: { - openingIdentity: '返乡守灯人继承者', - openingProblem: '首夜就撞见禁航区假航灯重亮', - entryMotivation: '阻止更多船只误入死潮', - }, - coreConflict: { - surfaceConflicts: ['守潮盟与沉钟会争夺航路解释权'], - hiddenCrisis: '有人借假航灯持续清洗整片群岛的旧证据', - firstTouchedConflict: '玩家回港当夜就被卷进禁航区封锁', - }, - keyRelationships: [ - { - pairs: '玩家 vs 沈砺', - relationshipType: '旧友互疑', - secretOrCost: '他掌握沉船夜的关键视角', - }, - ], - hiddenLines: { - hiddenTruths: ['沉钟异动和旧案灭口是同一条线'], - misdirectionHints: ['表面看像海雾自然失控'], - revealPacing: '先见异常,再见旧案,再见操盘者', - }, - iconicElements: { - iconicMotifs: ['假航灯', '沉钟回响'], - institutionsOrArtifacts: ['旧灯塔', '禁航碑'], - hardRules: ['错误航灯会把船引进必死水域'], - }, + worldPromise: + '被海雾反复改写航路的群岛世界,旧灯塔与禁航令共同决定谁能活着穿过去,体验压抑、悬疑、潮湿。', + playerFantasy: + '玩家是被迫返乡的守灯人继承者,追查沉钟异动与失控航路的真相,风险是失去家族留下的最后航路坐标。', + themeBoundary: '压抑、悬疑;潮湿群岛、冷雾港口;避免热血少年漫。', + playerEntryPoint: + '玩家以返乡守灯人继承者身份切入,首夜就撞见禁航区假航灯重亮,动机是阻止更多船只误入死潮。', + coreConflict: + '守潮盟与沉钟会争夺航路解释权,有人借假航灯持续清洗整片群岛的旧证据,玩家回港当夜就被卷进禁航区封锁。', + keyRelationships: + '玩家与沈砺旧友互疑,沈砺掌握沉船夜的关键视角。', + hiddenLines: + '沉钟异动和旧案灭口是同一条线,表面看像海雾自然失控,揭示节奏是先见异常,再见旧案,再见操盘者。', + iconicElements: + '假航灯、沉钟回响、旧灯塔、禁航碑;错误航灯会把船引进必死水域。', }, landmarks: [ { @@ -437,7 +418,7 @@ test('landmark tab previews every generated act image while keeping chapter deta (screen.getByRole('img', { name: '沉钟栈桥-钟楼回响', }) as HTMLImageElement).getAttribute('src'), - ).toBe('/generated-custom-world-scenes/scene-act-2.png'); + ).toBe('/generated-custom-world-scenes/scene-act-1.png'); }); test('readOnly result view hides edit and create actions for agent preview mode', async () => { diff --git a/src/components/asset-studio/characterAssetWorkflowPersistence.ts b/src/components/asset-studio/characterAssetWorkflowPersistence.ts index 15325501..14315b09 100644 --- a/src/components/asset-studio/characterAssetWorkflowPersistence.ts +++ b/src/components/asset-studio/characterAssetWorkflowPersistence.ts @@ -65,7 +65,6 @@ export type CharacterVisualGenerationPayload = { characterId: string; sourceMode: Exclude; promptText: string; - characterBriefText?: string; referenceImageDataUrls: string[]; candidateCount: number; imageModel: string; diff --git a/src/components/custom-world-agent/CustomWorldAgentWorkspace.interaction.test.tsx b/src/components/custom-world-agent/CustomWorldAgentWorkspace.interaction.test.tsx index 10ea1bd2..28d5a153 100644 --- a/src/components/custom-world-agent/CustomWorldAgentWorkspace.interaction.test.tsx +++ b/src/components/custom-world-agent/CustomWorldAgentWorkspace.interaction.test.tsx @@ -11,20 +11,14 @@ const baseSession: CustomWorldAgentSessionSnapshot = { sessionId: 'custom-world-agent-session-1', currentTurn: 4, anchorContent: { - worldPromise: { - hook: '一个被潮雾改写航线秩序的群岛世界。', - differentiator: '所有通路都要向未知代价借路。', - desiredExperience: '压迫、潮湿、悬疑', - }, - playerFantasy: { - playerRole: '玩家是被迫返乡的旧航路继承人。', - corePursuit: '查清沉船夜背后的真相。', - fearOfLoss: '一旦失败,就会再次失去唯一还活着的旧友。', - }, + worldPromise: + '一个被潮雾改写航线秩序的群岛世界,所有通路都要向未知代价借路,体验压迫、潮湿、悬疑。', + playerFantasy: + '玩家是被迫返乡的旧航路继承人,目标是查清沉船夜背后的真相,失败会再次失去唯一还活着的旧友。', themeBoundary: null, playerEntryPoint: null, coreConflict: null, - keyRelationships: [], + keyRelationships: null, hiddenLines: null, iconicElements: null, }, diff --git a/src/components/custom-world-agent/CustomWorldAgentWorkspace.test.tsx b/src/components/custom-world-agent/CustomWorldAgentWorkspace.test.tsx index 060a24d6..13b8d1b5 100644 --- a/src/components/custom-world-agent/CustomWorldAgentWorkspace.test.tsx +++ b/src/components/custom-world-agent/CustomWorldAgentWorkspace.test.tsx @@ -10,16 +10,13 @@ test('custom world agent workspace renders minimum loop chat layout', () => { sessionId: 'custom-world-agent-session-1', currentTurn: 3, anchorContent: { - worldPromise: { - hook: '一个被潮雾改写航线秩序的群岛世界。', - differentiator: '所有人都要为每一次借路付出代价。', - desiredExperience: '压迫、悬疑、带一点海上传奇感', - }, + worldPromise: + '一个被潮雾改写航线秩序的群岛世界,所有人都要为每一次借路付出代价,体验压迫、悬疑、带一点海上传奇感。', playerFantasy: null, themeBoundary: null, playerEntryPoint: null, coreConflict: null, - keyRelationships: [], + keyRelationships: null, hiddenLines: null, iconicElements: null, }, @@ -76,7 +73,7 @@ test('custom world agent workspace renders minimum loop chat layout', () => { expect(html).toContain('42%'); expect(html).toContain('输入消息'); expect(html).toContain('总结当前设定'); - expect(html).toContain('补全剩余设定'); + expect(html).toContain('补充剩余设定'); expect(html).not.toContain('世界共创'); expect(html).not.toContain( '先说一个你最想让玩家记住的世界方向,我会帮你收束成可生成草稿的锚点。', diff --git a/src/components/game-canvas/GameCanvasEntityLayer.test.tsx b/src/components/game-canvas/GameCanvasEntityLayer.test.tsx index 074ae4c8..beffceaf 100644 --- a/src/components/game-canvas/GameCanvasEntityLayer.test.tsx +++ b/src/components/game-canvas/GameCanvasEntityLayer.test.tsx @@ -8,6 +8,11 @@ import { type SceneHostileNpc, } from '../../types'; import { GameCanvasEntityLayer } from './GameCanvasEntityLayer'; +import { + ENTITY_CONTAINER_REM, + getHostileNpcSceneBottomOffsetPx, + getMirroredStageEntityLeft, +} from './GameCanvasShared'; function createCharacter(): Character { return { @@ -112,6 +117,18 @@ function renderEntityLayer(effectNpcId: string | null) { } describe('GameCanvasEntityLayer', () => { + it('uses mirrored stage anchors for player and opponent containers', () => { + expect(getMirroredStageEntityLeft('15%', 'player')).toBe('15%'); + expect(getMirroredStageEntityLeft('15%', 'opponent')).toBe(`calc(100% - 15% - ${ENTITY_CONTAINER_REM}rem)`); + }); + + it('lowers large monster sprites to the shared scene ground line', () => { + expect(getHostileNpcSceneBottomOffsetPx({frameHeight: 62})).toBe(-78); + expect(getHostileNpcSceneBottomOffsetPx({frameHeight: 46})).toBe(-68); + expect(getHostileNpcSceneBottomOffsetPx({frameHeight: 37})).toBe(-52); + expect(getHostileNpcSceneBottomOffsetPx({frameHeight: 23})).toBe(-28); + }); + it('renders affinity effect on the matching hostile npc', () => { const html = renderEntityLayer('npc-liu'); diff --git a/src/components/game-canvas/GameCanvasEntityLayer.tsx b/src/components/game-canvas/GameCanvasEntityLayer.tsx index 28b203e5..2912423d 100644 --- a/src/components/game-canvas/GameCanvasEntityLayer.tsx +++ b/src/components/game-canvas/GameCanvasEntityLayer.tsx @@ -24,10 +24,10 @@ import { getCharacterBottomOffsetPx, getCharacterOpponentBottom, getCompanionSlotOffset, + getHostileNpcSceneBottomOffsetPx, getMonsterWorldLeft, getNpcCombatHpTop, getSceneEntityZIndex, - HOSTILE_NPC_SCENE_BOTTOM_OFFSET_PX, HpBar, mapHostileNpcAnimationToCharacterState, MONSTER_RENDER_OFFSETS, @@ -262,9 +262,7 @@ export function GameCanvasEntityLayer({ npcCharacter ? npcEncounter?.characterId : null, npcCharacter ? null : npcEncounter?.monsterPresetId, ); - const hostileNpcBottomOffsetPx = npcMonsterConfig - ? HOSTILE_NPC_SCENE_BOTTOM_OFFSET_PX - : 0; + const hostileNpcBottomOffsetPx = getHostileNpcSceneBottomOffsetPx(npcMonsterConfig); const opponentBottom = npcCharacter ? getCharacterOpponentBottom(groundBottom, stageLiftPx, npcCharacter) : `calc(${groundBottom} + ${stageLiftPx}px)`; @@ -365,9 +363,7 @@ export function GameCanvasEntityLayer({ encounter.kind === 'npc' && encounter.monsterPresetId ? monsters.find(item => item.id === encounter.monsterPresetId) ?? null : null; - const peacefulHostileBottomOffsetPx = peacefulMonsterConfig - ? HOSTILE_NPC_SCENE_BOTTOM_OFFSET_PX - : 0; + const peacefulHostileBottomOffsetPx = getHostileNpcSceneBottomOffsetPx(peacefulMonsterConfig); const peacefulBottomOffsetPx = peacefulResolvedCharacter ? getCharacterBottomOffsetPx(stageLiftPx, peacefulResolvedCharacter) : stageLiftPx + peacefulHostileBottomOffsetPx; diff --git a/src/components/game-canvas/GameCanvasRuntime.tsx b/src/components/game-canvas/GameCanvasRuntime.tsx index e14d63a7..ea3ce859 100644 --- a/src/components/game-canvas/GameCanvasRuntime.tsx +++ b/src/components/game-canvas/GameCanvasRuntime.tsx @@ -11,6 +11,7 @@ import {GameCanvasSceneLayer} from './GameCanvasSceneLayer'; import { type GameCanvasProps, getCharacterBottomOffsetPx, + getMirroredStageEntityLeft, getMonsterWorldLeft, getPlayerWorldLeft, HOSTILE_NPC_SCENE_INSET_PX, @@ -77,6 +78,8 @@ export function GameCanvasRuntime({ const sideAnchor = '15%'; const playerMeleeLeft = `calc(100% - ${sideAnchor} - 13rem)`; const monsterMeleeLeft = `calc(100% - ${sideAnchor} - 20rem)`; + const playerStageLeft = getMirroredStageEntityLeft(sideAnchor, 'player'); + const opponentStageLeft = getMirroredStageEntityLeft(sideAnchor, 'opponent'); const playerWorldLeft = getPlayerWorldLeft(sideAnchor, playerX, cameraAnchorX); const companionAnchorX = inBattle && !scrollWorld ? PLAYER_BASE_X_METERS : playerX; const companionAnchorLeft = getPlayerWorldLeft(sideAnchor, companionAnchorX, cameraAnchorX); @@ -84,9 +87,15 @@ export function GameCanvasRuntime({ const playerBottomOffsetPx = getCharacterBottomOffsetPx(stageLiftPx, playerCharacter, playerOffsetY); const playerLeft = playerActionMode === 'melee' && !scrollWorld ? playerMeleeLeft - : playerWorldLeft; + : scrollWorld + ? playerWorldLeft + : playerStageLeft; const monsterAnchorMeters = 3.2; const getHostileNpcOuterLeft = (hostileNpc: (typeof sceneHostileNpcs)[number]) => { + if (!scrollWorld && hostileNpc.animation !== 'attack') { + return opponentStageLeft; + } + const baseLeft = hostileNpc.animation === 'attack' && hostileNpc.combatMode !== 'ranged' && !scrollWorld ? monsterMeleeLeft diff --git a/src/components/game-canvas/GameCanvasShared.tsx b/src/components/game-canvas/GameCanvasShared.tsx index d7b06926..8879e4d3 100644 --- a/src/components/game-canvas/GameCanvasShared.tsx +++ b/src/components/game-canvas/GameCanvasShared.tsx @@ -2,11 +2,11 @@ import React, {useEffect, useState} from 'react'; import {getCharacterById} from '../../data/characterPresets'; import {METERS_TO_PIXELS} from '../../data/hostileNpcs'; -import { useResolvedAssetReadUrl } from '../../hooks/useResolvedAssetReadUrl'; import { buildMedievalNpcVisual, buildMedievalNpcVisualFromCustomWorldVisual, } from '../../data/medievalNpcVisuals'; +import { useResolvedAssetReadUrl } from '../../hooks/useResolvedAssetReadUrl'; import { AnimationState, Character, @@ -17,8 +17,8 @@ import { Encounter, SceneHostileNpc, ScenePresetInfo, - StoryNpcAffinityEffect, StoryEngineMemoryState, + StoryNpcAffinityEffect, WorldType, } from '../../types'; import {CharacterAnimator} from '../CharacterAnimator'; @@ -70,6 +70,7 @@ export const MONSTER_RENDER_OFFSETS: Record = { export const ENTITY_CONTAINER_REM = 7; export const ROLE_CHARACTER_FRAME_CLASS = 'flex h-28 w-28 items-end justify-center overflow-visible'; export const ROLE_CHARACTER_SPRITE_CLASS = 'h-full w-full scale-[1.32] origin-bottom'; +export const ROLE_CHARACTER_SCENE_IMAGE_SCALE = 1.32; export const GENERIC_NPC_SCENE_SCALE = 1.72; const DEFAULT_IMAGE_STYLE: React.CSSProperties = { imageRendering: 'pixelated', @@ -80,7 +81,9 @@ export const CHARACTER_NPC_COMBAT_HP_TOP_PX = -2; export const GENERIC_NPC_COMBAT_HP_TOP_PX = 94; export const GENERIC_NPC_EFFECT_TARGET_OFFSET_PX = -16; export const HOSTILE_NPC_SCENE_INSET_PX = 28; -export const HOSTILE_NPC_SCENE_BOTTOM_OFFSET_PX = -18; +export type HostileNpcSceneAnchorConfig = { + frameHeight: number; +}; export const CHAT_BUBBLE_SPRITE_SRC = '/chat.png'; export const CHAT_BUBBLE_FRAME_WIDTH = 27; export const CHAT_BUBBLE_FRAME_HEIGHT = 22; @@ -139,6 +142,15 @@ export function getPlayerWorldLeft( return `calc(${sideAnchor} + ${(playerX - cameraAnchorX) * METERS_TO_PIXELS * 0.65}px)`; } +export function getMirroredStageEntityLeft( + sideAnchor: string, + side: 'player' | 'opponent', +) { + return side === 'player' + ? sideAnchor + : `calc(100% - ${sideAnchor} - ${ENTITY_CONTAINER_REM}rem)`; +} + export function getMonsterWorldLeft( sideAnchor: string, monsterX: number, @@ -157,6 +169,18 @@ export function getCharacterOpponentBottom( return `calc(${groundBottom} + ${stageLiftPx}px - ${groundOffset}px)`; } +export function getHostileNpcSceneBottomOffsetPx( + monster: HostileNpcSceneAnchorConfig | null | undefined, +) { + if (!monster) return 0; + + // 怪物动画帧和角色立绘不是同一套脚底锚点,大帧需要更明显地下沉到场景地面线。 + if (monster.frameHeight >= 58) return -78; + if (monster.frameHeight >= 42) return -68; + if (monster.frameHeight >= 34) return -52; + return -28; +} + export function getNpcCombatHpTop(characterId?: string | null, monsterPresetId?: string | null) { if (monsterPresetId) return DEFAULT_COMBAT_HP_TOP_PX; return characterId ? CHARACTER_NPC_COMBAT_HP_TOP_PX : GENERIC_NPC_COMBAT_HP_TOP_PX; @@ -292,14 +316,16 @@ export function SceneEncounterNpcSprite({ } if (displayEncounterImageSrc) { + const transform = `${facing === 'left' ? 'scaleX(-1) ' : ''}scale(${ROLE_CHARACTER_SCENE_IMAGE_SCALE})`; + return ( {encounter.npcName} diff --git a/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx b/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx index bd41e520..e217a302 100644 --- a/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx +++ b/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx @@ -224,6 +224,7 @@ function isAgentResultStructuralBlockerResolved( readProfileTextField(profile, [ 'worldHook', 'creatorIntent.worldHook', + 'anchorContent.worldPromise', 'anchorContent.worldPromise.hook', 'settingText', ]), @@ -234,6 +235,7 @@ function isAgentResultStructuralBlockerResolved( readProfileTextField(profile, [ 'playerPremise', 'creatorIntent.playerPremise', + 'anchorContent.playerEntryPoint', 'anchorContent.playerEntryPoint.openingIdentity', 'anchorContent.playerEntryPoint.openingProblem', 'anchorContent.playerEntryPoint.entryMotivation', diff --git a/src/components/rpg-creation-asset-studio/RpgCreationRoleAssetStudioModalImpl.tsx b/src/components/rpg-creation-asset-studio/RpgCreationRoleAssetStudioModalImpl.tsx index bbf3f521..9bae86c1 100644 --- a/src/components/rpg-creation-asset-studio/RpgCreationRoleAssetStudioModalImpl.tsx +++ b/src/components/rpg-creation-asset-studio/RpgCreationRoleAssetStudioModalImpl.tsx @@ -932,7 +932,6 @@ export function RpgCreationRoleAssetStudioModal({ try { const result = await generateVisualCandidatesForRole({ - characterBriefText, promptText: visualPromptText, referenceImageDataUrls: effectiveVisualReferenceImageDataUrls, role: workingRole, diff --git a/src/components/rpg-creation-asset-studio/useRoleVisualCandidateWorkflow.ts b/src/components/rpg-creation-asset-studio/useRoleVisualCandidateWorkflow.ts index 11c0b8e0..275210cf 100644 --- a/src/components/rpg-creation-asset-studio/useRoleVisualCandidateWorkflow.ts +++ b/src/components/rpg-creation-asset-studio/useRoleVisualCandidateWorkflow.ts @@ -6,14 +6,12 @@ import type { EditableCustomWorldRole } from './roleAssetStudioModel'; export function useRoleVisualCandidateWorkflow() { const generateVisualCandidatesForRole = async (params: { - characterBriefText: string; promptText: string; referenceImageDataUrls: string[]; role: EditableCustomWorldRole; sourceMode: 'text-to-image' | 'image-to-image'; }) => { const { - characterBriefText, promptText, referenceImageDataUrls, role, @@ -24,7 +22,6 @@ export function useRoleVisualCandidateWorkflow() { characterId: role.id, sourceMode, promptText, - characterBriefText, referenceImageDataUrls, candidateCount: 1, imageModel: 'wan2.7-image-pro', diff --git a/src/components/rpg-creation-editor/RpgCreationEntityEditorShared.tsx b/src/components/rpg-creation-editor/RpgCreationEntityEditorShared.tsx index ba3da019..b38eda2f 100644 --- a/src/components/rpg-creation-editor/RpgCreationEntityEditorShared.tsx +++ b/src/components/rpg-creation-editor/RpgCreationEntityEditorShared.tsx @@ -340,6 +340,7 @@ function buildDefaultSceneActBlueprint(params: { const actTitle = buildDefaultSceneActTitle(params.index); const sceneLabel = params.sceneName.trim() || '当前场景'; const sceneSummary = params.sceneSummary.trim(); + const oppositeNpcId = encounterNpcIds[0] ?? ''; const stageCoverage = buildSceneActStageCoverage(params.index, params.actCount); const actSummary = params.index === 0 @@ -358,17 +359,21 @@ function buildDefaultSceneActBlueprint(params: { backgroundPromptText: '', backgroundImageSrc: params.backgroundImageSrc || undefined, encounterNpcIds, - primaryNpcId: encounterNpcIds[0] ?? '', + primaryNpcId: oppositeNpcId, linkedThreadIds: dedupeTextValues(params.linkedThreadIds ?? []), advanceRule: buildSceneActAdvanceRule(params.index, params.actCount), - oppositeNpcId: '', + oppositeNpcId, actGoal: params.index === 0 ? `先在${sceneLabel}接住当前局面` : params.index >= params.actCount - 1 ? `把${sceneLabel}这一章收束并抛出下一步` : `继续推进${sceneLabel}的核心矛盾`, - eventDescription: sceneSummary, + eventDescription: buildDefaultSceneActEventDescription({ + sceneSummary, + oppositeNpcId, + index: params.index, + }), transitionHook: params.index === 0 ? '和主角色完成首次有效接触后,局势会继续加压。' @@ -378,6 +383,35 @@ function buildDefaultSceneActBlueprint(params: { }; } +function buildDefaultSceneActEventDescription(params: { + sceneSummary: string; + oppositeNpcId: string; + index: number; +}) { + const roleText = params.oppositeNpcId.trim() || '当前场景关键角色'; + const sceneText = params.sceneSummary.trim() || '场景内的主线压力'; + if (params.index === 0) { + return `第1幕中,${roleText}先露出与${sceneText}有关的异常线索,玩家必须确认局势入口。`; + } + if (params.index === 1) { + return `第2幕中,${roleText}的立场或阻碍让${sceneText}升级,玩家必须在压力下作出判断。`; + } + return `第3幕中,${roleText}把${sceneText}推向高潮,玩家必须面对关键抉择或直接后果。`; +} + +function buildDefaultSceneActBackgroundPrompt(params: { + sceneSummary: string; + oppositeNpcId: string; + eventDescription: string; + index: number; +}) { + const roleText = params.oppositeNpcId.trim() || '当前场景关键角色'; + const sceneText = params.sceneSummary.trim() || '场景内的主线压力'; + const phaseText = + params.index === 0 ? '铺垫阶段' : params.index === 1 ? '冲突升级阶段' : '高潮阶段'; + return `${sceneText}的${phaseText}画面,${roleText}与玩家隔着可站立空间形成对峙,环境里保留“${params.eventDescription}”的冲突痕迹与清晰氛围。`; +} + function buildDefaultSceneChapterBlueprint(params: { landmark: CustomWorldLandmark; fallbackImageSrc?: string | null; @@ -474,6 +508,23 @@ function sanitizeSceneChapterBlueprint(params: { : availableSceneNpcIds.length > 0 ? availableSceneNpcIds.slice(0, 1) : fallbackAct.encounterNpcIds; + const primaryNpcId = resolvedEncounterNpcIds[0] ?? ''; + const oppositeNpcId = currentAct?.oppositeNpcId?.trim() || primaryNpcId; + const eventDescription = + currentAct?.eventDescription?.trim() || + buildDefaultSceneActEventDescription({ + sceneSummary: params.landmark.description, + oppositeNpcId, + index, + }); + const backgroundPromptText = + currentAct?.backgroundPromptText?.trim() || + buildDefaultSceneActBackgroundPrompt({ + sceneSummary: params.landmark.description, + oppositeNpcId, + eventDescription, + index, + }); return { ...fallbackAct, @@ -481,13 +532,15 @@ function sanitizeSceneChapterBlueprint(params: { title: currentAct?.title?.trim() || fallbackAct.title, summary: currentAct?.summary?.trim() || fallbackAct.summary, stageCoverage: buildSceneActStageCoverage(index, targetActCount), - backgroundPromptText: currentAct?.backgroundPromptText?.trim() || '', + backgroundPromptText, backgroundImageSrc: currentAct?.backgroundImageSrc?.trim() || params.fallbackImageSrc || fallbackAct.backgroundImageSrc, encounterNpcIds: resolvedEncounterNpcIds, - primaryNpcId: resolvedEncounterNpcIds[0] ?? '', + primaryNpcId, + oppositeNpcId, + eventDescription, linkedThreadIds: dedupeTextValues(currentAct?.linkedThreadIds ?? []), advanceRule: buildSceneActAdvanceRule(index, targetActCount), actGoal: currentAct?.actGoal?.trim() || fallbackAct.actGoal, @@ -533,11 +586,8 @@ function resolveSceneCompatibilityImageSrc(params: { const resolvedImageSrc = params.resolvedImageSrc?.trim() || ''; const firstActImageSrc = params.chapter.acts[0]?.backgroundImageSrc?.trim() || ''; - if (firstActImageSrc && firstActImageSrc !== resolvedImageSrc) { - return firstActImageSrc; - } - - return currentImageSrc || undefined; + // 中文注释:创作侧只暴露一张场景显示图,列表、幕卡片和背景配置弹层都从这里取图,避免同一场景在不同层级显示不同图片。 + return firstActImageSrc || currentImageSrc || resolvedImageSrc || undefined; } function resolveSceneChapterBlueprintDraft(params: { @@ -776,6 +826,38 @@ function syncLandmarksWithStoryNpcs( }); } +type SceneActSelectableNpc = CustomWorldPlayableNpc | CustomWorldNpc; + +function getSceneActSelectableNpcVisual(npc: SceneActSelectableNpc) { + return 'visual' in npc ? npc.visual : undefined; +} + +function buildSceneActSelectableNpcs(params: { + profile: CustomWorldProfile; + storyNpcs: CustomWorldProfile['storyNpcs']; + preferredNpcIds: string[]; +}) { + const preferredNpcIdSet = new Set(params.preferredNpcIds); + const dedupedNpcs = new Map(); + + [...params.profile.playableNpcs, ...params.storyNpcs].forEach((npc) => { + const npcId = npc.id.trim(); + if (!npcId || dedupedNpcs.has(npcId)) { + return; + } + dedupedNpcs.set(npcId, npc); + }); + + return [...dedupedNpcs.values()].sort((left, right) => { + const leftPreferred = preferredNpcIdSet.has(left.id) ? 0 : 1; + const rightPreferred = preferredNpcIdSet.has(right.id) ? 0 : 1; + if (leftPreferred !== rightPreferred) { + return leftPreferred - rightPreferred; + } + return left.name.localeCompare(right.name, 'zh-Hans-CN'); + }); +} + function buildDraftSyncToken(value: unknown) { try { const serialized = JSON.stringify(value); @@ -980,6 +1062,7 @@ function CompactDialogShell({ title, onClose, children, + panelClassName = '', overlayClassName = 'z-[140]', disableClose = false, usePixelFont = false, @@ -987,6 +1070,7 @@ function CompactDialogShell({ title: string; onClose: () => void; children: ReactNode; + panelClassName?: string; overlayClassName?: string; disableClose?: boolean; usePixelFont?: boolean; @@ -1011,7 +1095,7 @@ function CompactDialogShell({ } >
event.stopPropagation()} >
@@ -1038,6 +1122,7 @@ function PortalCompactDialogShell(props: { title: string; onClose: () => void; children: ReactNode; + panelClassName?: string; overlayClassName?: string; disableClose?: boolean; usePixelFont?: boolean; @@ -1049,6 +1134,48 @@ function PortalCompactDialogShell(props: { return createPortal(, document.body); } +// 中文注释:关闭确认弹窗使用语义类承接亮暗主题,避免在浅色面板中沿用深色 Tailwind 色值。 +function CloseConfirmDialog({ + message, + onCancel, + onConfirm, + confirmLabel = '确认关闭', +}: { + message: string; + onCancel: () => void; + onConfirm: () => void; + confirmLabel?: string; +}) { + return ( + +
+
{message}
+
+ + +
+
+
+ ); +} + function Field({ label, children }: { label: ReactNode; children: ReactNode }) { const hasVisibleChildren = Children.toArray(children).some( (child) => !(typeof child === 'string' && child.trim().length === 0), @@ -1295,21 +1422,21 @@ function ActionButton({ const SCENE_ACT_SLOT_LAYOUTS = [ { - left: '68%', - bottom: '11%', - scale: 1.08, + left: '77%', + bottom: '16%', + scale: 1, zIndex: 4, }, { - left: '82%', - bottom: '22%', - scale: 0.84, + left: '91%', + bottom: '36%', + scale: 0.82, zIndex: 3, }, { - left: '82%', - bottom: '3%', - scale: 0.8, + left: '91%', + bottom: '4%', + scale: 0.82, zIndex: 2, }, ] as const; @@ -1318,7 +1445,7 @@ const SCENE_ACT_PLAYER_LAYOUT = { left: '24%', bottom: '11%', scale: 1, - zIndex: 3, + zIndex: 4, } as const; function SceneActStageNpcSprite({ @@ -1330,6 +1457,8 @@ function SceneActStageNpcSprite({ slotIndex: number; onClick: () => void; }) { + const slotBadgeLabel = slotIndex === 0 ? '主' : String(slotIndex + 1); + const slotName = npc?.name ?? '添加角色'; const previewEncounter = npc ? buildEncounterFromSceneNpc( buildSceneActPreviewSceneNpc(npc), @@ -1347,19 +1476,34 @@ function SceneActStageNpcSprite({ ? `配置第${slotIndex + 1}个角色:${npc.name}` : `为第${slotIndex + 1}个角色添加角色` } - className="group flex flex-col items-center text-center transition-transform hover:scale-[1.02]" + className="group relative flex flex-col items-center text-center transition-transform hover:scale-[1.03] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sky-200/80" style={{transformOrigin: 'center bottom'}} >
- {npc?.name ?? '添加角色'} + + {slotBadgeLabel} + + + {slotName} +
{previewEncounter ? ( @@ -1369,7 +1513,7 @@ function SceneActStageNpcSprite({ facing="left" /> ) : ( -
+
+
)} @@ -1430,7 +1574,7 @@ function SceneActStagePreview({ overlayInteractive >
-
+
{actLabel}
@@ -1488,7 +1632,7 @@ function SceneActNpcSlotPickerModal({ actLabel: string; slotIndex: number; currentNpcId?: string | null; - availableNpcs: CustomWorldNpc[]; + availableNpcs: SceneActSelectableNpc[]; onApply: (npcId: string | null) => void; onClose: () => void; }) { @@ -1520,7 +1664,7 @@ function SceneActNpcSlotPickerModal({
(act?.encounterNpcIds ?? []) .map((npcId) => - profile.storyNpcs.find((entry) => entry.id === npcId) ?? null, + profile.playableNpcs.find((entry) => entry.id === npcId) ?? + profile.storyNpcs.find((entry) => entry.id === npcId) ?? + null, ) - .filter((npc): npc is CustomWorldNpc => Boolean(npc)), - [act?.encounterNpcIds, profile.storyNpcs], + .filter((npc): npc is SceneActSelectableNpc => Boolean(npc)), + [act?.encounterNpcIds, profile.playableNpcs, profile.storyNpcs], ); const previewScenePreset = useMemo( () => @@ -1884,6 +2030,7 @@ function SceneActPreviewRuntime({ handleRefreshOptions: storyFlow.handleRefreshOptions, handleChoice: storyFlow.handleChoice, handleNpcChatInput: storyFlow.handleNpcChatInput, + refreshNpcChatOptions: storyFlow.refreshNpcChatOptions, exitNpcChat: storyFlow.exitNpcChat, handleMapTravelToScene: storyFlow.travelToSceneFromMap, npcUi: storyFlow.npcUi, @@ -1917,6 +2064,9 @@ function SceneActPreviewRuntime({ musicVolume: authUi?.musicVolume ?? 0.6, onMusicVolumeChange: authUi?.setMusicVolume ?? (() => {}), }} + chrome={{ + hidePlayerLevelBadge: true, + }} /> ); } @@ -1942,7 +2092,7 @@ function SceneActPreviewModal({ return createPortal(
-
+
幕预览 @@ -1951,7 +2101,6 @@ function SceneActPreviewModal({ {act.title.trim() || buildDefaultSceneActTitle(actIndex)}
-
+
+ +
, document.body, ); @@ -2154,6 +2311,7 @@ type WorldMapNodeLayout = { id: string; name: string; description: string; + isCurrent: boolean; left: number; top: number; centerX: number; @@ -2163,6 +2321,9 @@ type WorldMapNodeLayout = { type WorldMapEdgeLayout = { fromId: string; toId: string; + label: string; + labelX: number; + labelY: number; }; const WORLD_MAP_NODE_WIDTH = 152; @@ -2184,7 +2345,10 @@ function getWorldMapDirectionOffset(direction: CardinalConnectionDirection) { } } -function buildWorldMapLayout(landmarks: CustomWorldLandmark[]) { +function buildWorldMapLayout( + landmarks: CustomWorldLandmark[], + currentSceneId?: string | null, +) { const directionalConnectionMap = new Map( landmarks.map((landmark) => [ landmark.id, @@ -2289,6 +2453,7 @@ function buildWorldMapLayout(landmarks: CustomWorldLandmark[]) { id: landmark.id, name: landmark.name, description: landmark.description, + isCurrent: landmark.id === currentSceneId, left, top, centerX: left + WORLD_MAP_NODE_WIDTH / 2, @@ -2305,9 +2470,19 @@ function buildWorldMapLayout(landmarks: CustomWorldLandmark[]) { const pairKey = [sourceId, connection.targetLandmarkId].sort().join('::'); if (!edgeMap.has(pairKey)) { + const fromNode = nodes.find((node) => node.id === sourceId); + const toNode = nodes.find( + (node) => node.id === connection.targetLandmarkId, + ); + edgeMap.set(pairKey, { fromId: sourceId, toId: connection.targetLandmarkId, + label: CARDINAL_CONNECTION_LABELS[ + connection.relativePosition as CardinalConnectionDirection + ], + labelX: fromNode && toNode ? (fromNode.centerX + toNode.centerX) / 2 : 0, + labelY: fromNode && toNode ? (fromNode.centerY + toNode.centerY) / 2 : 0, }); } }); @@ -2329,14 +2504,16 @@ function buildWorldMapLayout(landmarks: CustomWorldLandmark[]) { function WorldMapOverviewModal({ landmarks, + currentSceneId, onClose, }: { landmarks: CustomWorldLandmark[]; + currentSceneId?: string | null; onClose: () => void; }) { const { nodes, edges, width, height } = useMemo( - () => buildWorldMapLayout(landmarks), - [landmarks], + () => buildWorldMapLayout(landmarks, currentSceneId), + [currentSceneId, landmarks], ); const nodeById = useMemo( () => new Map(nodes.map((node) => [node.id, node])), @@ -2345,7 +2522,7 @@ function WorldMapOverviewModal({ return ( -
+
+ {nodes.length === 0 ? ( +
+ 暂无场景 +
+ ) : null} ); })} + {edges.map((edge) => ( + + + + {edge.label} + + + ))} {nodes.map((node) => (
-
{node.name}
+
+
+ {node.name} +
+ {node.isCurrent ? ( +
+ 当前 +
+ ) : null} +
{node.description ? ( -
+
{node.description}
) : null} @@ -3417,7 +3632,7 @@ export function SaveBar({ showClose?: boolean; }) { return ( -
+
- [entry.pairs, entry.relationshipType, entry.secretOrCost] - .filter(Boolean) - .join(';'), - ) - .join('\n'), - 'hidden-lines': [ - anchorContent.hiddenLines?.hiddenTruths.join('、') || '', - anchorContent.hiddenLines?.misdirectionHints.join('、') || '', - anchorContent.hiddenLines?.revealPacing || '', - ] - .filter(Boolean) - .join(';'), - 'iconic-elements': [ - anchorContent.iconicElements?.iconicMotifs.join('、') || '', - anchorContent.iconicElements?.institutionsOrArtifacts.join('、') || '', - anchorContent.iconicElements?.hardRules.join('、') || '', - ] - .filter(Boolean) - .join(';'), + 'world-promise': anchorContent.worldPromise || '', + 'player-fantasy': anchorContent.playerFantasy || '', + 'theme-boundary': anchorContent.themeBoundary || '', + 'player-entry-point': anchorContent.playerEntryPoint || '', + 'core-conflict': anchorContent.coreConflict || '', + 'key-relationships': anchorContent.keyRelationships || '', + 'hidden-lines': anchorContent.hiddenLines || '', + 'iconic-elements': anchorContent.iconicElements || '', }; } @@ -4623,10 +4788,6 @@ function splitCommaTags(value: string) { .filter(Boolean); } -function stripAvoidPrefix(value: string) { - return value.replace(/^避免[::]\s*/u, '').trim(); -} - function applyFoundationDraftToProfile( profile: CustomWorldProfile, draft: FoundationDraft, @@ -4636,12 +4797,6 @@ function applyFoundationDraftToProfile( const themeBoundaryTags = parseFoundationTagText(draft['theme-boundary']); const playerEntryTags = parseFoundationTagText(draft['player-entry-point']); const coreConflictTags = parseFoundationTagText(draft['core-conflict']); - const hiddenLineTags = parseFoundationTagText(draft['hidden-lines']); - const iconicElementTags = parseFoundationTagText(draft['iconic-elements']); - const relationshipLines = draft['key-relationships'] - .split(/\r?\n/u) - .map((line) => line.trim()) - .filter(Boolean); const creatorIntent = profile.creatorIntent ?? createEmptyCustomWorldCreatorIntent('freeform'); @@ -4670,71 +4825,22 @@ function applyFoundationDraftToProfile( coreConflicts: coreConflictTags[0] ? splitCommaTags(coreConflictTags[0]) : creatorIntent.coreConflicts, - iconicElements: iconicElementTags[0] - ? splitCommaTags(iconicElementTags[0]) + iconicElements: draft['iconic-elements'].trim() + ? splitCommaTags(draft['iconic-elements']) : creatorIntent.iconicElements, forbiddenDirectives: themeBoundaryTags[2] - ? splitCommaTags(stripAvoidPrefix(themeBoundaryTags[2])) + ? splitCommaTags(themeBoundaryTags[2].replace(/^避免[::]\s*/u, '').trim()) : creatorIntent.forbiddenDirectives, }, anchorContent: { - worldPromise: { - hook: worldPromiseTags[0] || '', - differentiator: worldPromiseTags[1] || '', - desiredExperience: worldPromiseTags[2] || '', - }, - playerFantasy: { - playerRole: playerFantasyTags[0] || '', - corePursuit: playerFantasyTags[1] || '', - fearOfLoss: playerFantasyTags[2] || '', - }, - themeBoundary: { - toneKeywords: themeBoundaryTags[0] - ? splitCommaTags(themeBoundaryTags[0]) - : [], - aestheticDirectives: themeBoundaryTags[1] - ? splitCommaTags(themeBoundaryTags[1]) - : [], - forbiddenDirectives: themeBoundaryTags[2] - ? splitCommaTags(stripAvoidPrefix(themeBoundaryTags[2])) - : [], - }, - playerEntryPoint: { - openingIdentity: playerEntryTags[0] || '', - openingProblem: playerEntryTags[1] || '', - entryMotivation: playerEntryTags[2] || '', - }, - coreConflict: { - surfaceConflicts: coreConflictTags[0] - ? splitCommaTags(coreConflictTags[0]) - : [], - hiddenCrisis: coreConflictTags[1] || '', - firstTouchedConflict: coreConflictTags[2] || '', - }, - keyRelationships: relationshipLines.map((line) => { - const tags = parseFoundationTagText(line); - return { - pairs: tags[0] || '', - relationshipType: tags[1] || '', - secretOrCost: tags[2] || '', - }; - }), - hiddenLines: { - hiddenTruths: hiddenLineTags[0] ? splitCommaTags(hiddenLineTags[0]) : [], - misdirectionHints: hiddenLineTags[1] - ? splitCommaTags(hiddenLineTags[1]) - : [], - revealPacing: hiddenLineTags[2] || '', - }, - iconicElements: { - iconicMotifs: iconicElementTags[0] - ? splitCommaTags(iconicElementTags[0]) - : [], - institutionsOrArtifacts: iconicElementTags[1] - ? splitCommaTags(iconicElementTags[1]) - : [], - hardRules: iconicElementTags[2] ? splitCommaTags(iconicElementTags[2]) : [], - }, + worldPromise: draft['world-promise'].trim() || null, + playerFantasy: draft['player-fantasy'].trim() || null, + themeBoundary: draft['theme-boundary'].trim() || null, + playerEntryPoint: draft['player-entry-point'].trim() || null, + coreConflict: draft['core-conflict'].trim() || null, + keyRelationships: draft['key-relationships'].trim() || null, + hiddenLines: draft['hidden-lines'].trim() || null, + iconicElements: draft['iconic-elements'].trim() || null, }, }; } @@ -4879,11 +4985,11 @@ export function PlayableNpcEditor({ 形象预览
-
+
{draft.name
@@ -5066,31 +5172,14 @@ export function PlayableNpcEditor({
{isCloseConfirmOpen ? ( - setIsCloseConfirmOpen(false)} - overlayClassName="z-[140]" - > -
-
- 当前修改尚未保存,确认关闭吗? -
-
- setIsCloseConfirmOpen(false)} - /> - { - setIsCloseConfirmOpen(false); - onClose(); - }} - tone="sky" - /> -
-
-
+ setIsCloseConfirmOpen(false)} + onConfirm={() => { + setIsCloseConfirmOpen(false); + onClose(); + }} + /> ) : null} ); @@ -5363,31 +5452,14 @@ export function StoryNpcEditor({
{isCloseConfirmOpen ? ( - setIsCloseConfirmOpen(false)} - overlayClassName="z-[140]" - > -
-
- 当前修改尚未保存,确认关闭吗? -
-
- setIsCloseConfirmOpen(false)} - /> - { - setIsCloseConfirmOpen(false); - onClose(); - }} - tone="sky" - /> -
-
-
+ setIsCloseConfirmOpen(false)} + onConfirm={() => { + setIsCloseConfirmOpen(false); + onClose(); + }} + /> ) : null} ); @@ -5482,10 +5554,6 @@ export function LandmarkEditor({ .filter((imageSrc): imageSrc is string => Boolean(imageSrc)), ); }, [draft, isOpeningScene, profile]); - const storyNpcById = useMemo( - () => new Map(draftStoryNpcs.map((npc) => [npc.id, npc])), - [draftStoryNpcs], - ); const previewPlayableCharacter = useMemo( () => buildCustomWorldPlayableCharacters({ @@ -5522,10 +5590,16 @@ export function LandmarkEditor({ ); const sceneNpcOptions = useMemo( () => - compatibilitySceneNpcIds - .map((npcId) => storyNpcById.get(npcId)) - .filter((npc): npc is CustomWorldNpc => Boolean(npc)), - [compatibilitySceneNpcIds, storyNpcById], + buildSceneActSelectableNpcs({ + profile, + storyNpcs: draftStoryNpcs, + preferredNpcIds: compatibilitySceneNpcIds, + }), + [compatibilitySceneNpcIds, draftStoryNpcs, profile], + ); + const selectableNpcById = useMemo( + () => new Map(sceneNpcOptions.map((npc) => [npc.id, npc])), + [sceneNpcOptions], ); const compatibilityImageSrc = useMemo( () => @@ -5569,6 +5643,29 @@ export function LandmarkEditor({ landmarks: nextLandmarks, }; }, [draft, draftStoryNpcs, isOpeningScene, mode, profile]); + const worldMapLandmarks = useMemo(() => { + const campScene = resolveCustomWorldCampScene({ + ...editableProfile, + camp: isOpeningScene ? draft : editableProfile.camp, + }); + const normalizedCampScene: CustomWorldLandmark = { + ...campScene, + connections: buildDirectionalConnections( + campScene.connections, + editableProfile.landmarks, + ), + }; + const normalizedLandmarks = editableProfile.landmarks.map((entry) => ({ + ...entry, + connections: buildDirectionalConnections(entry.connections, [ + normalizedCampScene, + ...editableProfile.landmarks.filter((candidate) => candidate.id !== entry.id), + ]), + })); + + // 中文注释:地图预览必须使用当前草稿对象,避免未保存的连接关系在弹窗里缺席。 + return [normalizedCampScene, ...normalizedLandmarks]; + }, [draft, editableProfile, isOpeningScene]); const previewProfile = useMemo( () => ({ ...editableProfile, @@ -5679,6 +5776,18 @@ export function LandmarkEditor({ ); }; + const updateSceneActField = ( + index: number, + updater: (act: SceneActBlueprint) => SceneActBlueprint, + ) => { + updateSceneActDraft((current) => ({ + ...current, + acts: current.acts.map((act, actIndex) => + actIndex === index ? updater(act) : act, + ), + })); + }; + const addSceneAct = () => { if (renderedSceneChapterDraft.acts.length >= MAX_SCENE_ACT_COUNT) { window.alert(`每个场景最多只能配置 ${MAX_SCENE_ACT_COUNT} 幕。`); @@ -5725,15 +5834,14 @@ export function LandmarkEditor({ })); }; - const updateSceneActField = ( - index: number, - updater: (act: SceneActBlueprint) => SceneActBlueprint, - ) => { + const updateSceneActSharedBackground = (imageSrc?: string | null) => { + const resolvedImageSrc = imageSrc?.trim() || compatibilityImageSrc || ''; updateSceneActDraft((current) => ({ ...current, - acts: current.acts.map((act, actIndex) => - actIndex === index ? updater(act) : act, - ), + acts: current.acts.map((act) => ({ + ...act, + backgroundImageSrc: resolvedImageSrc || undefined, + })), })); }; @@ -5787,7 +5895,7 @@ export function LandmarkEditor({ const sanitizedDraft = { ...draft, imageSrc: compatibilityImageSrc, - sceneNpcIds: compatibilitySceneNpcIds, + sceneNpcIds: derivedSceneNpcIds, connections: buildDirectionalConnections( draft.connections, availableTargetLandmarks, @@ -5804,8 +5912,8 @@ export function LandmarkEditor({ })), }; - if (compatibilitySceneNpcIds.length < 3) { - window.alert('每个场景至少需要在多幕配置中覆盖 3 个 NPC。'); + if (derivedSceneNpcIds.length < 1) { + window.alert('请至少为一幕配置主角色。'); return; } @@ -5917,7 +6025,7 @@ export function LandmarkEditor({ act.encounterNpcIds, ); const encounterSlotNpcs = encounterSlotIds.map( - (npcId) => (npcId ? storyNpcById.get(npcId) ?? null : null), + (npcId) => (npcId ? selectableNpcById.get(npcId) ?? null : null), ); return ( @@ -5960,13 +6068,13 @@ export function LandmarkEditor({
{ if (sceneNpcOptions.length === 0) { - window.alert('请先为场景分配 NPC,再配置这一幕的角色槽位。'); + window.alert('请先在世界档案里创建角色,再配置这一幕的角色槽位。'); return; } if ( @@ -5988,9 +6096,7 @@ export function LandmarkEditor({ 幕背景
- {act.backgroundImageSrc === resolvedDraftImageSrc - ? '当前跟随场景主图' - : '已配置独立背景'} + 已读取场景图
@@ -6011,6 +6117,14 @@ export function LandmarkEditor({ />
+
+ {act.eventDescription?.trim() || + buildDefaultSceneActEventDescription({ + sceneSummary: draft.description, + oppositeNpcId: act.oppositeNpcId || act.primaryNpcId, + index, + })} +
@@ -6050,14 +6164,9 @@ export function LandmarkEditor({ buildDefaultSceneActTitle(activeSceneActBackgroundIndex) } act={activeSceneActBackgroundDraft} - currentImageSrc={activeSceneActBackgroundDraft.backgroundImageSrc} - fallbackImageSrc={resolvedDraftImageSrc} - onApply={(imageSrc) => - updateSceneActField(activeSceneActBackgroundIndex, (current) => ({ - ...current, - backgroundImageSrc: imageSrc || undefined, - })) - } + currentImageSrc={compatibilityImageSrc} + fallbackImageSrc={compatibilityImageSrc || resolvedDraftImageSrc} + onApply={updateSceneActSharedBackground} onClose={() => setActiveSceneActBackgroundIndex(null)} /> ) : null} @@ -6086,6 +6195,7 @@ export function LandmarkEditor({ ...current, encounterNpcIds, primaryNpcId: encounterNpcIds[0] ?? '', + oppositeNpcId: encounterNpcIds[0] ?? '', }; }) } @@ -6117,11 +6227,8 @@ export function LandmarkEditor({ ) : null} {isWorldMapOpen ? ( setIsWorldMapOpen(false)} /> ) : null} @@ -6149,31 +6256,14 @@ export function LandmarkEditor({
{isCloseConfirmOpen ? ( - setIsCloseConfirmOpen(false)} - overlayClassName="z-[140]" - > -
-
- 当前场景修改尚未保存,确认关闭吗? -
-
- setIsCloseConfirmOpen(false)} - /> - { - setIsCloseConfirmOpen(false); - onClose(); - }} - tone="sky" - /> -
-
-
+ setIsCloseConfirmOpen(false)} + onConfirm={() => { + setIsCloseConfirmOpen(false); + onClose(); + }} + /> ) : null} ); diff --git a/src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx b/src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx index fd53f831..0e914f6a 100644 --- a/src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx +++ b/src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx @@ -207,48 +207,21 @@ const mockSession: CustomWorldAgentSessionSnapshot = { sessionId: 'custom-world-agent-session-1', currentTurn: 0, anchorContent: { - worldPromise: { - hook: '被海雾吞没的旧航路群岛。', - differentiator: '灯塔与禁航令共同决定谁能穿过死潮。', - desiredExperience: '压抑、潮湿、悬疑', - }, - playerFantasy: { - playerRole: '玩家是被迫返乡的守灯人继承者。', - corePursuit: '查清沉船夜与假航灯的关系。', - fearOfLoss: '失去家族最后一条可信航线。', - }, - themeBoundary: { - toneKeywords: ['压抑', '悬疑'], - aestheticDirectives: ['潮湿群岛', '冷雾港口'], - forbiddenDirectives: ['轻喜冒险'], - }, - playerEntryPoint: { - openingIdentity: '返乡守灯人继承者', - openingProblem: '回港首夜撞见禁航区假航灯重亮', - entryMotivation: '阻止更多船只误入死潮', - }, - coreConflict: { - surfaceConflicts: ['守灯会与航运公会争夺航路解释权'], - hiddenCrisis: '有人在借假航灯持续清洗旧案证据', - firstTouchedConflict: '玩家返乡当夜就被卷进封航冲突', - }, - keyRelationships: [ - { - pairs: '玩家 vs 沈砺', - relationshipType: '旧友互疑', - secretOrCost: '他知道沉船夜的另一半真相', - }, - ], - hiddenLines: { - hiddenTruths: ['沉船夜与假航灯骗局属于同一操盘链条'], - misdirectionHints: ['表面像海雾自然失控'], - revealPacing: '先见异常,再见旧案,再见操盘者', - }, - iconicElements: { - iconicMotifs: ['假航灯', '沉钟回响'], - institutionsOrArtifacts: ['旧灯塔', '禁航碑'], - hardRules: ['错误航灯会把船引进必死水域'], - }, + worldPromise: + '被海雾吞没的旧航路群岛,灯塔与禁航令共同决定谁能穿过死潮,体验压抑、潮湿、悬疑。', + playerFantasy: + '玩家是被迫返乡的守灯人继承者,追查沉船夜与假航灯的关系,风险是失去家族最后一条可信航线。', + themeBoundary: '压抑、悬疑;潮湿群岛、冷雾港口;避免轻喜冒险。', + playerEntryPoint: + '玩家以返乡守灯人继承者身份切入,回港首夜撞见禁航区假航灯重亮,动机是阻止更多船只误入死潮。', + coreConflict: + '守灯会与航运公会争夺航路解释权,有人在借假航灯持续清洗旧案证据,玩家返乡当夜就被卷进封航冲突。', + keyRelationships: + '玩家与沈砺旧友互疑,沈砺知道沉船夜的另一半真相。', + hiddenLines: + '沉船夜与假航灯骗局属于同一操盘链条,表面像海雾自然失控,揭示节奏是先见异常,再见旧案,再见操盘者。', + iconicElements: + '假航灯、沉钟回响、旧灯塔、禁航碑;错误航灯会把船引进必死水域。', }, progressPercent: 0, lastAssistantReply: '先告诉我你想做一个怎样的 RPG 世界。', @@ -2014,42 +1987,20 @@ test('agent result view does not keep legacy publish blockers when preview uses ...compiledAgentDraftSession.resultPreview!.preview, settingText: '被海雾吞没的旧航路群岛', anchorContent: { - worldPromise: { - hook: '被海雾吞没的旧航路群岛', - differentiator: '灯塔与禁航令共同决定谁能穿过死潮。', - desiredExperience: '压抑、潮湿、悬疑', - }, - playerFantasy: { - playerRole: '玩家是被迫返乡的守灯人继承者。', - corePursuit: '查清沉船夜与假航灯的关系。', - fearOfLoss: '失去家族最后一条可信航线。', - }, - themeBoundary: { - toneKeywords: ['压抑', '悬疑'], - aestheticDirectives: ['潮湿群岛', '冷雾港口'], - forbiddenDirectives: ['轻喜冒险'], - }, - playerEntryPoint: { - openingIdentity: '返乡守灯人继承者', - openingProblem: '回港首夜撞见禁航区假航灯重亮', - entryMotivation: '阻止更多船只误入死潮', - }, - coreConflict: { - surfaceConflicts: ['守灯会与航运公会争夺航路解释权'], - hiddenCrisis: '有人在借假航灯持续清洗旧案证据', - firstTouchedConflict: '玩家返乡当夜就被卷进封航冲突', - }, - keyRelationships: [], - hiddenLines: { - hiddenTruths: ['沉船夜与假航灯骗局属于同一操盘链条'], - misdirectionHints: ['表面像海雾自然失控'], - revealPacing: '先见异常,再见旧案,再见操盘者', - }, - iconicElements: { - iconicMotifs: ['假航灯', '沉钟回响'], - institutionsOrArtifacts: ['旧灯塔', '禁航碑'], - hardRules: ['错误航灯会把船引进必死水域'], - }, + worldPromise: + '被海雾吞没的旧航路群岛,灯塔与禁航令共同决定谁能穿过死潮,体验压抑、潮湿、悬疑。', + playerFantasy: + '玩家是被迫返乡的守灯人继承者,追查沉船夜与假航灯的关系,风险是失去家族最后一条可信航线。', + themeBoundary: '压抑、悬疑;潮湿群岛、冷雾港口;避免轻喜冒险。', + playerEntryPoint: + '玩家以返乡守灯人继承者身份切入,回港首夜撞见禁航区假航灯重亮,动机是阻止更多船只误入死潮。', + coreConflict: + '守灯会与航运公会争夺航路解释权,有人在借假航灯持续清洗旧案证据,玩家返乡当夜就被卷进封航冲突。', + keyRelationships: null, + hiddenLines: + '沉船夜与假航灯骗局属于同一操盘链条,表面像海雾自然失控,揭示节奏是先见异常,再见旧案,再见操盘者。', + iconicElements: + '假航灯、沉钟回响、旧灯塔、禁航碑;错误航灯会把船引进必死水域。', }, creatorIntent: { sourceMode: 'card', diff --git a/src/components/rpg-entry/RpgEntryHomeView.tsx b/src/components/rpg-entry/RpgEntryHomeView.tsx index 0cdd2478..a31a221b 100644 --- a/src/components/rpg-entry/RpgEntryHomeView.tsx +++ b/src/components/rpg-entry/RpgEntryHomeView.tsx @@ -33,15 +33,19 @@ import type { PlatformBrowseHistoryEntry, ProfileDashboardCardKey, ProfileDashboardSummary, + ProfileReferralInviteCenterResponse, ProfileRechargeCenterResponse, ProfileRechargeProduct, ProfileSaveArchiveSummary, + RedeemProfileReferralInviteCodeResponse, } from '../../../packages/shared/src/contracts/runtime'; import type { HydratedSavedGameSnapshot } from '../../persistence/runtimeSnapshotTypes'; import type { AuthUser } from '../../services/authService'; import { createRpgProfileRechargeOrder, + getRpgProfileReferralInviteCenter, getRpgProfileRechargeCenter, + redeemRpgProfileReferralInviteCode, } from '../../services/rpg-entry/rpgProfileClient'; import type { CustomWorldProfile } from '../../types'; import { useAuthUi } from '../auth/AuthUiContext'; @@ -115,6 +119,7 @@ const PLATFORM_HOME_TABS: PlatformHomeTab[] = [ 'saves', 'profile', ]; +type ProfilePopupPanel = 'invite' | 'redeem' | 'community'; function usePlatformDesktopLayout() { const [isDesktopLayout, setIsDesktopLayout] = useState(() => { @@ -1020,6 +1025,154 @@ function AccountRechargeModal({ ); } +function ProfileReferralModal({ + panel, + center, + inviteCodeInput, + isLoading, + isSubmitting, + error, + success, + onClose, + onInputChange, + onCopyInvite, + onSubmitRedeem, +}: { + panel: ProfilePopupPanel; + center: ProfileReferralInviteCenterResponse | null; + inviteCodeInput: string; + isLoading: boolean; + isSubmitting: boolean; + error: string | null; + success: string | null; + onClose: () => void; + onInputChange: (value: string) => void; + onCopyInvite: () => void; + onSubmitRedeem: () => void; +}) { + const title = + panel === 'invite' + ? '邀请好友' + : panel === 'redeem' + ? '填邀请码' + : '玩家社区'; + + return ( +
+
+ +
+
{title}
+ + {panel === 'community' ? ( +
+ {['微信群', 'QQ群'].map((label) => ( +
+
+
+ {label} +
+
+ ))} +
+ ) : isLoading ? ( +
+
+
+
+ ) : panel === 'invite' ? ( +
+
+
+ 邀请码 +
+
+ {center?.inviteCode ?? '--------'} +
+
+ +
+
+
+ {center?.invitedCount ?? 0} +
+ 邀请 +
+
+
+ {center?.rewardedInviteCount ?? 0} +
+ 已奖 +
+
+
+ {center?.todayInviterRewardRemaining ?? 0} +
+ 今日 +
+
+
+ ) : ( +
+ {center?.hasRedeemedCode ? ( +
+ 已填写邀请码 +
+ ) : ( + <> + onInputChange(event.target.value)} + placeholder="输入邀请码" + className="w-full rounded-xl border border-zinc-200 bg-zinc-50 px-4 py-3 text-center text-base font-black tracking-[0.14em] outline-none focus:border-[#ff4056]" + /> + + + )} +
+ )} + + {error ? ( +
+ {error} +
+ ) : null} + {success ? ( +
+ {success} +
+ ) : null} +
+
+
+ ); +} + export function RpgEntryHomeView({ activeTab, onTabChange, @@ -1059,6 +1212,15 @@ export function RpgEntryHomeView({ const [isLoadingRecharge, setIsLoadingRecharge] = useState(false); const [submittingRechargeProductId, setSubmittingRechargeProductId] = useState(null); + const [profilePopupPanel, setProfilePopupPanel] = + useState(null); + const [referralCenter, setReferralCenter] = + useState(null); + const [isLoadingReferral, setIsLoadingReferral] = useState(false); + const [isSubmittingReferral, setIsSubmittingReferral] = useState(false); + const [referralError, setReferralError] = useState(null); + const [referralSuccess, setReferralSuccess] = useState(null); + const [inviteCodeInput, setInviteCodeInput] = useState(''); const [selectedCategoryTag, setSelectedCategoryTag] = useState( null, ); @@ -1162,6 +1324,61 @@ export function RpgEntryHomeView({ }) .finally(() => setSubmittingRechargeProductId(null)); }; + const openProfilePopupPanel = (panel: ProfilePopupPanel) => { + setProfilePopupPanel(panel); + setReferralError(null); + setReferralSuccess(null); + if (panel === 'community') { + return; + } + + setIsLoadingReferral(true); + void getRpgProfileReferralInviteCenter() + .then(setReferralCenter) + .catch((error: unknown) => { + setReferralCenter(null); + setReferralError( + error instanceof Error ? error.message : '读取邀请码失败', + ); + }) + .finally(() => setIsLoadingReferral(false)); + }; + const copyInviteInfo = () => { + if (!referralCenter?.inviteCode) { + return; + } + + const inviteUrl = + typeof window === 'undefined' + ? referralCenter.inviteLinkPath + : new URL(referralCenter.inviteLinkPath, window.location.origin).href; + copyText(`${referralCenter.inviteCode} ${inviteUrl}`); + setReferralSuccess('已复制'); + }; + const submitReferralInviteCode = () => { + if (isSubmittingReferral || !inviteCodeInput.trim()) { + return; + } + + setIsSubmittingReferral(true); + setReferralError(null); + setReferralSuccess(null); + void redeemRpgProfileReferralInviteCode(inviteCodeInput) + .then((response: RedeemProfileReferralInviteCodeResponse) => { + setReferralCenter(response.center); + setInviteCodeInput(''); + setReferralSuccess( + response.inviteeRewardGranted ? '已获得30积分' : '填写成功', + ); + void onRechargeSuccess?.(); + }) + .catch((error: unknown) => { + setReferralError( + error instanceof Error ? error.message : '填写邀请码失败', + ); + }) + .finally(() => setIsSubmittingReferral(false)); + }; const submitDesktopSearch = () => { const keyword = desktopSearchKeyword.trim(); if (!keyword || !onSearchPublicCode || isSearchingPublicCode) { @@ -1579,9 +1796,21 @@ export function RpgEntryHomeView({
- - - + openProfilePopupPanel('invite')} + /> + openProfilePopupPanel('redeem')} + /> + openProfilePopupPanel('community')} + />
@@ -1938,6 +2167,21 @@ export function RpgEntryHomeView({ onSelectProduct={submitRechargeProduct} /> ) : null} + {profilePopupPanel ? ( + setProfilePopupPanel(null)} + onInputChange={setInviteCodeInput} + onCopyInvite={copyInviteInfo} + onSubmitRedeem={submitReferralInviteCode} + /> + ) : null}
); } @@ -2047,6 +2291,21 @@ export function RpgEntryHomeView({ onSelectProduct={submitRechargeProduct} /> ) : null} + {profilePopupPanel ? ( + setProfilePopupPanel(null)} + onInputChange={setInviteCodeInput} + onCopyInvite={copyInviteInfo} + onSubmitRedeem={submitReferralInviteCode} + /> + ) : null}
); } diff --git a/src/components/rpg-entry/useRpgCreationEnterWorld.test.tsx b/src/components/rpg-entry/useRpgCreationEnterWorld.test.tsx index d24247ef..28cfe79b 100644 --- a/src/components/rpg-entry/useRpgCreationEnterWorld.test.tsx +++ b/src/components/rpg-entry/useRpgCreationEnterWorld.test.tsx @@ -79,7 +79,7 @@ function buildSession(): CustomWorldAgentSessionSnapshot { themeBoundary: null, playerEntryPoint: null, coreConflict: null, - keyRelationships: [], + keyRelationships: null, hiddenLines: null, iconicElements: null, }, diff --git a/src/components/rpg-entry/useRpgEntryAgentDraftRestore.test.tsx b/src/components/rpg-entry/useRpgEntryAgentDraftRestore.test.tsx index 991fe5c1..08e03abb 100644 --- a/src/components/rpg-entry/useRpgEntryAgentDraftRestore.test.tsx +++ b/src/components/rpg-entry/useRpgEntryAgentDraftRestore.test.tsx @@ -74,7 +74,7 @@ function buildSession( themeBoundary: null, playerEntryPoint: null, coreConflict: null, - keyRelationships: [], + keyRelationships: null, hiddenLines: null, iconicElements: null, }, diff --git a/src/components/rpg-runtime-panels/RpgAdventurePanel.npcChat.test.tsx b/src/components/rpg-runtime-panels/RpgAdventurePanel.npcChat.test.tsx index c53f0218..eb8af8b0 100644 --- a/src/components/rpg-runtime-panels/RpgAdventurePanel.npcChat.test.tsx +++ b/src/components/rpg-runtime-panels/RpgAdventurePanel.npcChat.test.tsx @@ -107,7 +107,7 @@ test('adventure panel renders system turns without special relationship labels', expect(html).not.toContain('关系变化'); }); -test('adventure panel shows current act label and remaining turns for limited hostile npc chat', () => { +test('adventure panel shows current act label without fixed hostile chat turns', () => { const currentStory: StoryMoment = { text: '断桥客仍在压着最后那半句真相。', displayMode: 'dialogue', @@ -122,10 +122,12 @@ test('adventure panel shows current act label and remaining turns for limited ho turnCount: 3, customInputPlaceholder: '输入你想对 TA 说的话', sceneActId: 'scene-bridge-act-1', - turnLimit: 5, - remainingTurns: 2, + turnLimit: null, + remainingTurns: null, limitReason: 'negative_affinity', forceExitAfterTurn: false, + terminationMode: 'hostile_model', + isHostileChat: true, }, }; @@ -201,6 +203,5 @@ test('adventure panel shows current act label and remaining turns for limited ho expect(html).toContain('当前幕'); expect(html).toContain('断桥口 · 对峙幕'); expect(html).toContain('1/3'); - expect(html).toContain('剩余交谈'); - expect(html).toContain('2 轮'); + expect(html).not.toContain('剩余交谈'); }); diff --git a/src/components/rpg-runtime-panels/RpgAdventurePanel.test.tsx b/src/components/rpg-runtime-panels/RpgAdventurePanel.test.tsx index 7d972a4f..75ce54f6 100644 --- a/src/components/rpg-runtime-panels/RpgAdventurePanel.test.tsx +++ b/src/components/rpg-runtime-panels/RpgAdventurePanel.test.tsx @@ -147,6 +147,25 @@ test('adventure panel does not show deferred hint for non-continue options with expect(html).not.toContain('剧情推理完成,继续后显示新的冒险选项'); }); +test('adventure panel renders compact function tags before option text', () => { + const chatOption = createOption('npc_chat', '继续追问桥上的旧账'); + const questOption = createOption('npc_quest_accept', '接下断桥客的委托'); + const giftOption = createOption('npc_gift', '把玉牌递给柳无声'); + const currentStory: StoryMoment = { + text: '你看向眼前的人。', + options: [chatOption, questOption, giftOption], + }; + + const html = renderPanel(currentStory, [chatOption, questOption, giftOption]); + + expect(html).toContain('聊天'); + expect(html).toContain('继续追问桥上的旧账'); + expect(html).toContain('任务'); + expect(html).toContain('接下断桥客的委托'); + expect(html).toContain('送礼'); + expect(html).toContain('把玉牌递给柳无声'); +}); + test('adventure panel shows npc chat custom input and exit button in chat mode', () => { const optionA = createOption('npc_chat', '先听对方把话说完'); const optionB = createOption('npc_chat', '顺着这个问题继续追问'); @@ -181,7 +200,7 @@ test('adventure panel shows npc chat custom input and exit button in chat mode', expect(html).toContain('退出聊天'); expect(html).toContain('输入你想对 TA 说的话'); expect(html).toContain('发送'); - expect(html).not.toContain('换一换'); + expect(html).toContain('换一换'); expect(html).not.toContain('关系升温'); }); diff --git a/src/components/rpg-runtime-panels/RpgAdventurePanel.tsx b/src/components/rpg-runtime-panels/RpgAdventurePanel.tsx index 45e1d782..0663e464 100644 --- a/src/components/rpg-runtime-panels/RpgAdventurePanel.tsx +++ b/src/components/rpg-runtime-panels/RpgAdventurePanel.tsx @@ -18,11 +18,7 @@ import { lazy, Suspense, useEffect, useMemo, useRef, useState } from 'react'; import { formatCurrency } from '../../data/economy'; import { getEquipmentSlotFromItem } from '../../data/equipmentEffects'; -import { - getFunctionDocumentationById, - isContinueAdventureOption, - NPC_CHAT_FUNCTION, -} from '../../data/functionCatalog'; +import { isContinueAdventureOption } from '../../data/functionCatalog'; import { getHostileNpcPresetById } from '../../data/hostileNpcPresets'; import { resolveInventoryItemUseEffect } from '../../data/inventoryEffects'; import { isQuestReadyToClaim } from '../../data/questFlow'; @@ -136,22 +132,6 @@ function AdventurePanelOverlayLoadingFallback() { ); } -function getCompactOptionDetailText(option: StoryOption) { - if (option.functionId === NPC_CHAT_FUNCTION.id) { - return ( - option.detailText || - getFunctionDocumentationById(option.functionId)?.runtime - ?.compactDetailText || - '聊聊并试探口风' - ); - } - - return ( - getFunctionDocumentationById(option.functionId)?.runtime - ?.compactDetailText || option.detailText - ); -} - function getOptionActionTextClass(option: StoryOption) { if ((option.priority ?? 1) >= 3) return 'text-fuchsia-200 group-hover:text-fuchsia-100'; @@ -160,6 +140,67 @@ function getOptionActionTextClass(option: StoryOption) { return 'text-zinc-300 group-hover:text-white'; } +function getOptionFunctionTagText(option: StoryOption) { + const tagByFunctionId: Record = { + battle_all_in_crush: '战斗', + battle_attack_basic: '战斗', + battle_escape_breakout: '逃跑', + battle_feint_step: '战斗', + battle_finisher_window: '战斗', + battle_guard_break: '战斗', + battle_probe_pressure: '战斗', + battle_recover_breath: '调息', + battle_use_skill: '技能', + camp_travel_home_scene: '场景', + idle_call_out: '试探', + idle_explore_forward: '探索', + idle_follow_clue: '线索', + idle_observe_signs: '观察', + idle_rest_focus: '调息', + idle_travel_next_scene: '场景', + npc_chat: '聊天', + npc_fight: '战斗', + npc_gift: '送礼', + npc_help: '求助', + npc_leave: '离开', + npc_preview_talk: '聊天', + npc_quest_accept: '任务', + npc_quest_turn_in: '任务', + npc_recruit: '招募', + npc_spar: '切磋', + npc_trade: '交易', + story_continue_adventure: '继续', + treasure_inspect: '探查', + treasure_leave: '离开', + treasure_secure: '收取', + }; + + if (option.functionId.startsWith('npc_chat_quest_offer_')) { + return '任务'; + } + + return tagByFunctionId[option.functionId] ?? null; +} + +function RpgOptionActionLabel({ option }: { option: StoryOption }) { + const tagText = getOptionFunctionTagText(option); + + return ( + + {tagText ? ( + + {tagText} + + ) : null} + + {option.actionText} + + + ); +} + function getDialogueTurnAlignmentClass( turn: NonNullable[number], ) { @@ -798,29 +839,45 @@ function RpgAdventureChoiceSection(props: {
- {isNpcChatMode ? ( - - ) : canRefreshOptions && !shouldHideChoiceUi ? ( - - ) : null} +
+ {isNpcChatMode && canRefreshOptions && !shouldHideChoiceUi ? ( + + ) : !isNpcChatMode && canRefreshOptions && !shouldHideChoiceUi ? ( + + ) : null} + {isNpcChatMode ? ( + + ) : null} +
@@ -867,12 +924,8 @@ function RpgAdventureChoiceSection(props: { className="pixel-nine-slice pixel-pressable pixel-choice-button group w-full text-left" style={getNineSliceStyle(UI_CHROME.choiceButton)} > -
- - {option.actionText} - +
+ -
- - {option.actionText} - +
+ void; + refreshNpcChatOptions: () => boolean; handleSceneTransitionChoice: (option: StoryOption) => void; handleNpcChatInput: (input: string) => boolean; exitNpcChat: () => boolean; @@ -93,6 +94,7 @@ export function RpgRuntimePanelRouter({ hideStoryOptions, canRefreshOptions, handleRefreshOptions, + refreshNpcChatOptions, handleSceneTransitionChoice, handleNpcChatInput, exitNpcChat, @@ -226,8 +228,18 @@ export function RpgRuntimePanelRouter({ isLoading={isLoading} displayedOptions={displayedOptions} hideOptions={hideStoryOptions} - canRefreshOptions={canRefreshOptions} - onRefreshOptions={handleRefreshOptions} + canRefreshOptions={ + visibleCurrentStory.npcChatState + ? visibleCurrentStory.options.length > 1 + : canRefreshOptions + } + onRefreshOptions={() => { + if (visibleCurrentStory.npcChatState) { + refreshNpcChatOptions(); + return; + } + handleRefreshOptions(); + }} onChoice={handleSceneTransitionChoice} onSubmitNpcChatInput={handleNpcChatInput} onExitNpcChat={exitNpcChat} diff --git a/src/components/rpg-runtime-shell/RpgRuntimeShell.tsx b/src/components/rpg-runtime-shell/RpgRuntimeShell.tsx index 283d7f8d..537b7bdf 100644 --- a/src/components/rpg-runtime-shell/RpgRuntimeShell.tsx +++ b/src/components/rpg-runtime-shell/RpgRuntimeShell.tsx @@ -30,6 +30,7 @@ export function RpgRuntimeShell({ entry, companions, audio, + chrome, }: RpgRuntimeShellComponentProps) { const authUi = useAuthUi(); const isPlatformShell = !session.gameState.worldType; @@ -51,6 +52,7 @@ export function RpgRuntimeShell({ canRefreshOptions, handleRefreshOptions, handleNpcChatInput, + refreshNpcChatOptions, exitNpcChat, handleMapTravelToScene, npcUi, @@ -167,7 +169,7 @@ export function RpgRuntimeShell({ /> - {visibleGameState.playerCharacter && ( + {visibleGameState.playerCharacter && !chrome?.hidePlayerLevelBadge && (
void; + refreshNpcChatOptions: () => boolean; handleSceneTransitionChoice: (option: StoryOption) => void; handleNpcChatInput: (input: string) => boolean; exitNpcChat: () => boolean; @@ -123,6 +124,7 @@ export function RpgRuntimeStageRouter({ hideStoryOptions, canRefreshOptions, handleRefreshOptions, + refreshNpcChatOptions, handleSceneTransitionChoice, handleNpcChatInput, exitNpcChat, @@ -227,6 +229,7 @@ export function RpgRuntimeStageRouter({ hideStoryOptions={hideStoryOptions} canRefreshOptions={canRefreshOptions} handleRefreshOptions={handleRefreshOptions} + refreshNpcChatOptions={refreshNpcChatOptions} handleSceneTransitionChoice={handleSceneTransitionChoice} handleNpcChatInput={handleNpcChatInput} exitNpcChat={exitNpcChat} diff --git a/src/components/rpg-runtime-shell/types.ts b/src/components/rpg-runtime-shell/types.ts index 58be638d..d49c5866 100644 --- a/src/components/rpg-runtime-shell/types.ts +++ b/src/components/rpg-runtime-shell/types.ts @@ -1,4 +1,3 @@ -import type { BottomTab } from '../../hooks/rpg-session'; import type { BattleRewardUi, CharacterChatUi, @@ -8,6 +7,7 @@ import type { QuestFlowUi, StoryGenerationNpcUi, } from '../../hooks/rpg-runtime-story'; +import type { BottomTab } from '../../hooks/rpg-session'; import type { HydratedSavedGameSnapshot } from '../../persistence/runtimeSnapshotTypes'; import type { Character, @@ -35,6 +35,7 @@ export interface RpgRuntimeStoryProps { handleRefreshOptions: () => void; handleChoice: (option: StoryOption) => void; handleNpcChatInput: (input: string) => boolean; + refreshNpcChatOptions: () => boolean; exitNpcChat: () => boolean; handleMapTravelToScene: (sceneId: string) => boolean; npcUi: StoryGenerationNpcUi; @@ -69,6 +70,10 @@ export interface RpgRuntimeAudioProps { onMusicVolumeChange: (value: number) => void; } +export interface RpgRuntimeShellChromeOptions { + hidePlayerLevelBadge?: boolean; +} + export interface RpgRuntimeDialogueIndicator { showPlayer: boolean; showEncounter: boolean; @@ -101,4 +106,5 @@ export interface RpgRuntimeShellProps { entry: RpgEntrySessionProps; companions: RpgRuntimeCompanionProps; audio: RpgRuntimeAudioProps; + chrome?: RpgRuntimeShellChromeOptions; } diff --git a/src/data/customWorldLibrary.ts b/src/data/customWorldLibrary.ts index 76a570db..b99ebe2d 100644 --- a/src/data/customWorldLibrary.ts +++ b/src/data/customWorldLibrary.ts @@ -995,6 +995,7 @@ function normalizeSceneActBlueprint( ? ['opening'] : ['climax', 'aftermath'], backgroundImageSrc: toText(value.backgroundImageSrc) || undefined, + backgroundPromptText: toText(value.backgroundPromptText) || undefined, backgroundAssetId: toText(value.backgroundAssetId) || undefined, encounterNpcIds, primaryNpcId, diff --git a/src/hooks/combat/resolvedChoice.test.ts b/src/hooks/combat/resolvedChoice.test.ts index 8fe038a1..1784eb8d 100644 --- a/src/hooks/combat/resolvedChoice.test.ts +++ b/src/hooks/combat/resolvedChoice.test.ts @@ -7,20 +7,46 @@ const { scenes } = vi.hoisted(() => ({ name: 'Camp', description: 'A quiet camp.', imageSrc: '/camp.png', + forwardSceneId: 'scene-2', + connectedSceneIds: ['scene-2', 'scene-3'], + connections: [], + npcs: [], + treasureHints: [], }, { id: 'scene-2', name: 'Trail', description: 'A mountain trail.', imageSrc: '/trail.png', + forwardSceneId: 'scene-3', + connectedSceneIds: ['scene-3'], + connections: [], + npcs: [], + treasureHints: [], + }, + { + id: 'scene-3', + name: 'Ruin', + description: 'A ruined gate.', + imageSrc: '/ruin.png', + connectedSceneIds: [], + connections: [], + npcs: [], + treasureHints: [], }, ], })); vi.mock('../../data/scenePresets', () => ({ getForwardScenePreset: () => scenes[1], + getScenePresetById: (_worldType: unknown, sceneId: string) => + scenes.find((scene) => scene.id === sceneId) ?? null, getScenePresetsByWorld: () => scenes, + getSceneFriendlyNpcs: () => [], + getSceneHostileNpcs: () => [], + getSceneHostileNpcPresetIds: () => [], getTravelScenePreset: () => scenes[1], + getWorldCampScenePreset: () => null, })); vi.mock('../../data/stateFunctions', () => ({ @@ -232,4 +258,27 @@ describe('buildResolvedChoiceState', () => { expect(resolved.afterSequence.inBattle).toBe(false); expect(resolved.afterSequence.currentEncounter).toBeNull(); }); + + it('uses explicit target scene id from travel option payload', () => { + const state = { + ...createBaseState(), + currentScenePreset: scenes[0] as GameState['currentScenePreset'], + inBattle: false, + }; + const option = { + ...createOption('idle_travel_next_scene'), + runtimePayload: { + targetSceneId: 'scene-3', + }, + }; + + const resolved = buildResolvedChoiceState({ + state, + option, + character: createTestCharacter(), + buildBattlePlan: vi.fn(), + }); + + expect(resolved.afterSequence.currentScenePreset?.id).toBe('scene-3'); + }); }); diff --git a/src/hooks/combat/resolvedChoice.ts b/src/hooks/combat/resolvedChoice.ts index 5bb1cf55..70128759 100644 --- a/src/hooks/combat/resolvedChoice.ts +++ b/src/hooks/combat/resolvedChoice.ts @@ -1,5 +1,6 @@ import { getForwardScenePreset, + getScenePresetById, getScenePresetsByWorld, getTravelScenePreset, } from '../../data/scenePresets'; @@ -53,18 +54,26 @@ function getShiftedScenePreset( function getSceneTargetForFunction( worldType: WorldType | null, currentScenePreset: GameState['currentScenePreset'], - functionId: string, + option: StoryOption, ): GameState['currentScenePreset'] { if (!worldType) return currentScenePreset; - if (functionId === 'idle_travel_next_scene') { + if (option.functionId === 'idle_travel_next_scene') { + const targetSceneId = + typeof option.runtimePayload?.targetSceneId === 'string' + ? option.runtimePayload.targetSceneId + : null; + if (targetSceneId) { + return getScenePresetById(worldType, targetSceneId) ?? currentScenePreset; + } + return getTravelScenePreset(worldType, currentScenePreset?.id) ?? currentScenePreset; } return getShiftedScenePreset( worldType, currentScenePreset, - getFunctionEffect(functionId).sceneShift, + getFunctionEffect(option.functionId).sceneShift, ); } @@ -83,7 +92,7 @@ export function buildResolvedChoiceState(params: { const nextScenePreset = getSceneTargetForFunction( state.worldType, state.currentScenePreset, - option.functionId, + option, ); const optionKind = classifyCombatOption(option); diff --git a/src/hooks/rpg-runtime-story/choiceActions.test.ts b/src/hooks/rpg-runtime-story/choiceActions.test.ts index 26ed5c67..0c9be701 100644 --- a/src/hooks/rpg-runtime-story/choiceActions.test.ts +++ b/src/hooks/rpg-runtime-story/choiceActions.test.ts @@ -246,6 +246,126 @@ describe('createStoryChoiceActions', () => { expect(handleNpcInteraction).not.toHaveBeenCalled(); }); + it('applies deferred runtime state when story_continue_adventure reveals the next act', async () => { + const state = { + ...createBaseState(), + inBattle: false, + currentBattleNpcId: null, + currentNpcBattleMode: null, + }; + const deferredOptions = [ + { + functionId: 'idle_observe_signs', + actionText: '观察下一幕的线索', + text: '观察下一幕的线索', + visuals: { + playerAnimation: AnimationState.IDLE, + playerMoveMeters: 0, + playerOffsetY: 0, + playerFacing: 'right' as const, + scrollWorld: false, + monsterChanges: [], + }, + }, + ] satisfies StoryOption[]; + const continueOption: StoryOption = { + functionId: 'story_continue_adventure', + actionText: '继续冒险', + text: '继续冒险', + visuals: { + playerAnimation: AnimationState.IDLE, + playerMoveMeters: 0, + playerOffsetY: 0, + playerFacing: 'right' as const, + scrollWorld: false, + monsterChanges: [], + }, + }; + const currentStory: StoryMoment = { + text: '对话已经完成', + options: [continueOption], + deferredOptions, + deferredRuntimeState: { + storyEngineMemory: { + discoveredFactIds: [], + activeThreadIds: [], + resolvedScarIds: [], + recentCarrierIds: [], + currentSceneActState: { + sceneId: 'scene-bridge', + chapterId: 'scene-bridge-chapter', + currentActId: 'scene-bridge-act-2', + currentActIndex: 1, + completedActIds: ['scene-bridge-act-1'], + visitedActIds: ['scene-bridge-act-1', 'scene-bridge-act-2'], + }, + }, + }, + }; + const setCurrentStory = vi.fn(); + const setGameState = vi.fn(); + + const { handleChoice } = createStoryChoiceActions({ + gameState: state, + currentStory, + isLoading: false, + setGameState, + setCurrentStory, + setAiError: vi.fn(), + setIsLoading: vi.fn(), + setBattleReward: vi.fn(), + buildResolvedChoiceState: vi.fn(), + playResolvedChoice: vi.fn(), + buildStoryContextFromState: vi.fn(), + buildStoryFromResponse: vi.fn((_, __, response) => response), + buildFallbackStoryForState: vi.fn(() => createFallbackStory()), + generateStoryForState: vi.fn(), + getAvailableOptionsForState: vi.fn(() => null), + getStoryGenerationHostileNpcs: vi.fn(() => []), + getResolvedSceneHostileNpcs: vi.fn( + (inputState: GameState) => inputState.sceneHostileNpcs, + ), + buildNpcStory: vi.fn(() => createFallbackStory()), + handleNpcBattleConversationContinuation: vi.fn(() => false), + updateQuestLog: vi.fn((inputState: GameState) => inputState), + incrementRuntimeStats: vi.fn((inputState: GameState) => inputState), + getCampCompanionTravelScene: vi.fn(() => null), + enterNpcInteraction: vi.fn(() => false), + handleNpcInteraction: vi.fn(), + handleTreasureInteraction: vi.fn(() => false), + commitGeneratedStateWithEncounterEntry: vi.fn(), + finalizeNpcBattleResult: vi.fn(() => null), + isContinueAdventureOption: vi.fn( + (option: StoryOption) => + option.functionId === 'story_continue_adventure', + ), + isCampTravelHomeOption: vi.fn(() => false), + isRegularNpcEncounter: neverNpcEncounter, + isNpcEncounter: neverNpcEncounter, + npcPreviewTalkFunctionId: 'npc_preview_talk', + fallbackCompanionName: '同伴', + turnVisualMs: 820, + }); + + await handleChoice(continueOption); + + expect(setGameState).toHaveBeenCalledWith( + expect.objectContaining({ + storyEngineMemory: expect.objectContaining({ + currentSceneActState: expect.objectContaining({ + currentActId: 'scene-bridge-act-2', + }), + }), + }), + ); + expect(setCurrentStory).toHaveBeenCalledWith({ + ...currentStory, + options: deferredOptions, + deferredOptions: undefined, + deferredRuntimeState: undefined, + }); + }); + it('keeps npc chat choices on the local UI path so chat mode can continue streaming locally', async () => { const state = createBaseState(); const option = createBattleOption('npc_chat'); diff --git a/src/hooks/rpg-runtime-story/choiceActions.ts b/src/hooks/rpg-runtime-story/choiceActions.ts index 095630be..d6246d9e 100644 --- a/src/hooks/rpg-runtime-story/choiceActions.ts +++ b/src/hooks/rpg-runtime-story/choiceActions.ts @@ -167,10 +167,29 @@ export function createStoryChoiceActions({ if (option.disabled) return; if (currentStory?.deferredOptions?.length && isContinueAdventureOption(option)) { + if (currentStory.deferredRuntimeState) { + setGameState({ + ...gameState, + currentEncounter: null, + npcInteractionActive: false, + sceneHostileNpcs: [], + inBattle: false, + currentBattleNpcId: null, + currentNpcBattleMode: null, + currentNpcBattleOutcome: null, + currentScenePreset: + currentStory.deferredRuntimeState.currentScenePreset ?? + gameState.currentScenePreset, + storyEngineMemory: + currentStory.deferredRuntimeState.storyEngineMemory ?? + gameState.storyEngineMemory, + }); + } setCurrentStory({ ...currentStory, options: currentStory.deferredOptions, deferredOptions: undefined, + deferredRuntimeState: undefined, }); return; } diff --git a/src/hooks/rpg-runtime-story/npcEncounterActions.test.ts b/src/hooks/rpg-runtime-story/npcEncounterActions.test.ts index 3570d6d9..b4c5754e 100644 --- a/src/hooks/rpg-runtime-story/npcEncounterActions.test.ts +++ b/src/hooks/rpg-runtime-story/npcEncounterActions.test.ts @@ -110,6 +110,21 @@ function createState(overrides: Partial = {}): GameState { name: '断桥口', description: '风声很紧。', imageSrc: '/bridge.png', + worldType: WorldType.WUXIA, + forwardSceneId: 'scene-east', + connectedSceneIds: ['scene-east', 'scene-south'], + connections: [ + { + sceneId: 'scene-east', + relativePosition: 'east', + summary: '东侧旧街还亮着灯。', + }, + { + sceneId: 'scene-south', + relativePosition: 'south', + summary: '南侧河滩雾气更重。', + }, + ], npcs: [], treasureHints: [], }, @@ -160,7 +175,25 @@ function createState(overrides: Partial = {}): GameState { function createSceneActProfile( primaryNpcId = 'npc-rival', + actCount = 1, ): NonNullable { + const acts = Array.from({ length: actCount }, (_, index) => ({ + id: `scene-bridge-act-${index + 1}`, + sceneId: 'scene-bridge', + title: index === 0 ? '对峙幕' : `推进幕${index + 1}`, + summary: '玩家与断桥客推进断桥旧案。', + stageCoverage: [index === 0 ? 'opening' : 'expansion'], + backgroundImageSrc: `/bridge-act-${index + 1}.png`, + encounterNpcIds: [primaryNpcId, 'npc-bystander'], + primaryNpcId, + oppositeNpcId: primaryNpcId, + eventDescription: '断桥客把旧案线索压在半句话里。', + linkedThreadIds: [], + advanceRule: 'after_primary_contact', + actGoal: '逼近断桥旧案的核心线索。', + transitionHook: '桥下藏着还没灭的灯。', + })); + return { id: 'custom-world-scene-act-test', name: '断桥旧案', @@ -175,24 +208,9 @@ function createSceneActProfile( summary: '桥口旧账还没了结。', linkedThreadIds: [], linkedLandmarkIds: [], - acts: [ - { - id: 'scene-bridge-act-1', - sceneId: 'scene-bridge', - title: '对峙幕', - summary: '玩家与断桥客正面碰头。', - stageCoverage: ['opening'], - backgroundImageSrc: '/bridge-act-1.png', - encounterNpcIds: [primaryNpcId, 'npc-bystander'], - primaryNpcId, - linkedThreadIds: [], - advanceRule: 'after_primary_contact', - actGoal: '逼近断桥旧案的核心线索。', - transitionHook: '桥下藏着还没灭的灯。', - }, - ], - }, - ], + acts, + }, + ], } as unknown as NonNullable; } @@ -255,10 +273,12 @@ function createLimitedPrimaryNpcChatStory(turnCount: number): StoryMoment { turnCount, customInputPlaceholder: '输入你想对 TA 说的话', sceneActId: 'scene-bridge-act-1', - turnLimit: 5, - remainingTurns: Math.max(0, 5 - turnCount), + turnLimit: null, + remainingTurns: null, limitReason: 'negative_affinity', forceExitAfterTurn: false, + terminationMode: 'hostile_model', + isHostileChat: true, }, }; } @@ -391,19 +411,9 @@ function createFallbackStory(text = 'fallback'): StoryMoment { }; } -type GenerateStoryForStateTestDouble = (params: { - state: GameState; - character: Character; - history: StoryMoment[]; - choice?: string; - lastFunctionId?: string | null; - optionCatalog?: StoryOption[] | null; -}) => Promise; - function createNpcEncounterActions(overrides: { gameState?: GameState; currentStory?: StoryMoment | null; - generateStoryForState?: GenerateStoryForStateTestDouble; getAvailableOptionsForState?: ( state: GameState, character: Character, @@ -502,12 +512,6 @@ function createNpcEncounterActions(overrides: { : [], streaming, })), - generateStoryForState: - overrides.generateStoryForState ?? - ((vi.fn().mockResolvedValue({ - text: '你重新收束心神,开始判断断桥口接下来会怎么变。', - options: [createOption('idle_observe_signs', '观察周围动静')], - }) as unknown) as GenerateStoryForStateTestDouble), getStoryGenerationHostileNpcs: vi.fn(() => []), getTypewriterDelay: vi.fn(() => 0), getAvailableOptionsForState: @@ -532,7 +536,6 @@ function createNpcEncounterActions(overrides: { buildContinueAdventureOption: vi.fn(() => createOption('story_continue_adventure', '继续'), ), - getNpcEncounterKey: vi.fn((encounter: Encounter) => encounter.id ?? encounter.npcName), getResolvedNpcState: vi.fn( (state: GameState, encounter: Encounter) => state.npcStates[encounter.id ?? encounter.npcName]!, @@ -864,7 +867,7 @@ describe('npcEncounterActions', () => { ); }); - it('re-runs story reasoning after exiting npc chat and appends the new story to history', async () => { + it('sends a closing chat turn after exiting npc chat and keeps the dialogue panel until continue', async () => { const gameState = createState({ storyHistory: [ { @@ -874,13 +877,20 @@ describe('npcEncounterActions', () => { }, ], }); - const generateStoryForState = vi.fn().mockResolvedValue({ - text: '你重新收束心神,开始判断断桥口接下来会怎么变。', - options: [createOption('idle_observe_signs', '观察周围动静')], + streamNpcChatTurnMock.mockResolvedValueOnce({ + affinityDelta: 0, + affinityText: '这轮对话暂时没有带来明显关系变化。', + npcReply: '那就往前走吧,桥后面会替我把答案递给你。', + suggestions: [], + functionSuggestions: [], + chatDirective: { + forceExit: true, + closingMode: 'foreshadow_close', + terminationReason: 'player_exit', + }, }); const actions = createNpcEncounterActions({ gameState, - generateStoryForState, getAvailableOptionsForState: vi.fn(() => [ createOption('npc_chat', '先问问你为什么堵在这里', { kind: 'npc', @@ -908,58 +918,53 @@ describe('npcEncounterActions', () => { expect(actions.exitNpcChat()).toBe(true); await flushAsyncWork(); - expect(generateStoryForState).toHaveBeenCalledWith( + expect(streamNpcChatTurnMock).toHaveBeenCalledWith( + expect.anything(), + expect.anything(), + expect.objectContaining({ id: 'npc-rival' }), + expect.anything(), + expect.anything(), + expect.anything(), + expect.anything(), + '我先结束这轮交谈,继续往前走。', + expect.anything(), expect.objectContaining({ - state: gameState, - choice: '结束与断桥客的这轮交谈,重新观察当前局势', - lastFunctionId: 'npc_chat', - optionCatalog: [ - expect.objectContaining({ - functionId: 'npc_chat', - }), - expect.objectContaining({ - functionId: 'npc_help', - }), - expect.objectContaining({ - functionId: 'npc_fight', - }), - ], + chatDirective: expect.objectContaining({ + terminationReason: 'player_exit', + closingMode: 'foreshadow_close', + forceExitAfterTurn: true, + functionOptions: expect.arrayContaining([ + expect.objectContaining({ functionId: 'npc_help' }), + expect.objectContaining({ functionId: 'npc_fight' }), + ]), + }), }), ); - const [{ optionCatalog }] = generateStoryForState.mock.calls[0] as [ - { optionCatalog: StoryOption[] }, - ]; - expect( - optionCatalog.filter((option) => option.functionId === 'npc_chat'), - ).toHaveLength(1); - expect(actions.setGameState).toHaveBeenCalledWith( - expect.objectContaining({ - storyHistory: [ - expect.objectContaining({ - historyRole: 'action', - text: '你先试探了对方的态度。', - }), - expect.objectContaining({ - historyRole: 'action', - text: '结束与断桥客的这轮交谈,重新观察当前局势', - }), - expect.objectContaining({ - historyRole: 'result', - text: '你重新收束心神,开始判断断桥口接下来会怎么变。', - }), - ], - }), + + const lastStory = actions.setCurrentStory.mock.calls.at(-1)?.[0] as StoryMoment; + expect(lastStory.npcChatState).toBeUndefined(); + expect(lastStory.dialogue).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + speaker: 'player', + text: '我先结束这轮交谈,继续往前走。', + }), + expect.objectContaining({ + speaker: 'npc', + text: '那就往前走吧,桥后面会替我把答案递给你。', + }), + ]), ); - expect(actions.setCurrentStory).toHaveBeenCalledWith( + expect(lastStory.options).toEqual([ expect.objectContaining({ - text: '你重新收束心神,开始判断断桥口接下来会怎么变。', + functionId: 'story_continue_adventure', }), - ); + ]); expect(actions.setIsLoading).toHaveBeenNthCalledWith(1, true); expect(actions.setIsLoading).toHaveBeenLastCalledWith(false); }); - it('prefers the current story non-chat options when rebuilding options after exiting npc chat', async () => { + it('feeds current story non-chat function options into npc chat context', async () => { const gameState = createState({ storyHistory: [ { @@ -969,9 +974,17 @@ describe('npcEncounterActions', () => { }, ], }); - const generateStoryForState = vi.fn().mockResolvedValue({ - text: '你重新收束心神,开始判断断桥口接下来会怎么变。', - options: [createOption('idle_observe_signs', '观察周围动静')], + streamNpcChatTurnMock.mockResolvedValueOnce({ + affinityDelta: 0, + affinityText: '关系暂未变化', + npcReply: '你想做什么就快点。', + suggestions: ['我还想问一句'], + functionSuggestions: [ + { + functionId: 'npc_help', + actionText: '让你现在帮我一把', + }, + ], }); const actions = createNpcEncounterActions({ gameState, @@ -1005,7 +1018,6 @@ describe('npcEncounterActions', () => { customInputPlaceholder: '输入你想对 TA 说的话', }, }, - generateStoryForState, getAvailableOptionsForState: vi.fn(() => [ createOption('npc_chat', '先问问你为什么堵在这里', { kind: 'npc', @@ -1025,26 +1037,49 @@ describe('npcEncounterActions', () => { ]), }); - expect(actions.exitNpcChat()).toBe(true); - await flushAsyncWork(); + await expect( + actions.handleNpcChatTurn(createEncounter(), '你能不能帮我一次。'), + ).resolves.toBe(true); - const [{ optionCatalog }] = generateStoryForState.mock.calls[0] as [ - { optionCatalog: StoryOption[] }, - ]; - expect(optionCatalog).toEqual([ + expect(streamNpcChatTurnMock).toHaveBeenCalledWith( + expect.anything(), + expect.anything(), + expect.anything(), + expect.anything(), + expect.anything(), + expect.anything(), + expect.anything(), + '你能不能帮我一次。', + expect.anything(), expect.objectContaining({ - functionId: 'npc_chat', - actionText: '先问问你为什么堵在这里', + chatDirective: expect.objectContaining({ + functionOptions: expect.arrayContaining([ + expect.objectContaining({ + functionId: 'npc_help', + actionText: '借你的人脉把线索铺开', + }), + expect.objectContaining({ + functionId: 'npc_fight', + actionText: '现在就把这笔旧账打清', + }), + ]), + }), }), - expect.objectContaining({ - functionId: 'npc_help', - actionText: '借你的人脉把线索铺开', - }), - expect.objectContaining({ - functionId: 'npc_fight', - actionText: '现在就把这笔旧账打清', - }), - ]); + ); + + const lastStory = actions.setCurrentStory.mock.calls.at(-1)?.[0] as StoryMoment; + expect(lastStory.options).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + functionId: 'npc_chat', + actionText: '我还想问一句', + }), + expect.objectContaining({ + functionId: 'npc_help', + actionText: '让你现在帮我一把', + }), + ]), + ); }); it('lets hostile npc encounters speak first on first contact', async () => { @@ -1145,9 +1180,11 @@ describe('npcEncounterActions', () => { expect(nextStory.npcChatState).toMatchObject({ npcId: 'npc-rival', sceneActId: 'scene-bridge-act-1', - turnLimit: 5, - remainingTurns: 5, + turnLimit: null, + remainingTurns: null, limitReason: 'negative_affinity', + terminationMode: 'hostile_model', + isHostileChat: true, }); expect( nextStory.options.some((option) => option.functionId === 'npc_fight'), @@ -1159,17 +1196,18 @@ describe('npcEncounterActions', () => { ).toBe(false); }); - it('force exits limited hostile chat on the fifth turn and offers a continue option', async () => { + it('lets the model terminate hostile chat and offers a continue option', async () => { streamNpcChatTurnMock.mockResolvedValueOnce({ affinityDelta: 0, affinityText: '这轮对话暂时没有带来明显关系变化。', npcReply: '去城西废桥下找那盏没灭的灯。等你看见它,再来问我剩下那半句。', suggestions: [], chatDirective: { - turnLimit: 5, - remainingTurns: 0, + turnLimit: null, + remainingTurns: null, forceExit: true, closingMode: 'foreshadow_close', + terminationReason: 'hostile_breakoff', }, }); @@ -1213,10 +1251,12 @@ describe('npcEncounterActions', () => { questOfferContext: null, chatDirective: expect.objectContaining({ sceneActId: 'scene-bridge-act-1', - turnLimit: 5, - remainingTurns: 0, + turnLimit: null, + remainingTurns: null, limitReason: 'negative_affinity', - forceExitAfterTurn: true, + terminationMode: 'hostile_model', + isHostileChat: true, + forceExitAfterTurn: false, }), }), ); @@ -1231,12 +1271,126 @@ describe('npcEncounterActions', () => { ]); expect(lastStory.dialogue?.at(-1)).toEqual( expect.objectContaining({ - speaker: 'system', - text: '这轮交谈先在这里收束,对方留下的线索把你推向了下一步。', + speaker: 'npc', + text: '去城西废桥下找那盏没灭的灯。等你看见它,再来问我剩下那半句。', }), ); }); + it('prepares adjacent scene direction options when hostile chat ends on the final scene act', async () => { + streamNpcChatTurnMock.mockResolvedValueOnce({ + affinityDelta: 0, + affinityText: '这轮对话暂时没有带来明显关系变化。', + npcReply: '桥口的话到这里就够了,剩下的路你自己选。', + suggestions: [], + chatDirective: { + forceExit: true, + terminationReason: 'hostile_breakoff', + }, + }); + + const actions = createNpcEncounterActions({ + gameState: createState({ + customWorldProfile: createSceneActProfile(), + npcStates: { + 'npc-rival': { + affinity: -12, + helpUsed: false, + chattedCount: 4, + giftsGiven: 0, + inventory: [], + recruited: false, + }, + }, + }), + currentStory: createLimitedPrimaryNpcChatStory(4), + }); + + await expect( + actions.handleNpcChatTurn(createEncounter(), '那就到这里。'), + ).resolves.toBe(true); + + const lastStory = actions.setCurrentStory.mock.calls.at(-1)?.[0] as StoryMoment; + expect(lastStory.options).toEqual([ + expect.objectContaining({ + functionId: 'story_continue_adventure', + }), + ]); + expect(lastStory.deferredOptions).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + functionId: 'idle_travel_next_scene', + actionText: '向东走,前往scene-east', + runtimePayload: { targetSceneId: 'scene-east' }, + }), + expect.objectContaining({ + functionId: 'idle_travel_next_scene', + actionText: '向南走,前往scene-south', + runtimePayload: { targetSceneId: 'scene-south' }, + }), + ]), + ); + }); + + it('prepares next scene act options when hostile chat ends before the final act', async () => { + streamNpcChatTurnMock.mockResolvedValueOnce({ + affinityDelta: 0, + affinityText: '这轮对话暂时没有带来明显关系变化。', + npcReply: '你先往桥心走,下一句话在那里说。', + suggestions: [], + chatDirective: { + forceExit: true, + terminationReason: 'hostile_breakoff', + }, + }); + const getAvailableOptionsForState = vi.fn(() => [ + createOption('npc_chat', '继续问断桥客下一幕的线索', { + kind: 'npc', + npcId: 'npc-rival', + action: 'chat', + }), + createOption('idle_observe_signs', '观察桥心的灯影'), + ]); + + const actions = createNpcEncounterActions({ + gameState: createState({ + customWorldProfile: createSceneActProfile('npc-rival', 2), + npcStates: { + 'npc-rival': { + affinity: -12, + helpUsed: false, + chattedCount: 4, + giftsGiven: 0, + inventory: [], + recruited: false, + }, + }, + }), + currentStory: createLimitedPrimaryNpcChatStory(4), + getAvailableOptionsForState, + }); + + await expect( + actions.handleNpcChatTurn(createEncounter(), '那我往桥心走。'), + ).resolves.toBe(true); + + const lastStory = actions.setCurrentStory.mock.calls.at(-1)?.[0] as StoryMoment; + expect(lastStory.deferredRuntimeState?.storyEngineMemory?.currentSceneActState).toMatchObject({ + currentActId: 'scene-bridge-act-2', + currentActIndex: 1, + completedActIds: ['scene-bridge-act-1'], + visitedActIds: ['scene-bridge-act-1', 'scene-bridge-act-2'], + }); + expect(lastStory.deferredOptions).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + functionId: 'idle_observe_signs', + actionText: '观察桥心的灯影', + }), + ]), + ); + }); + it('reopens npc chat after battle victory with combat context and preserved negative affinity limit', () => { const actions = createNpcEncounterActions({ gameState: createState({ @@ -1310,9 +1464,11 @@ describe('npcEncounterActions', () => { expect(lastStory.npcChatState).toMatchObject({ npcId: 'npc-rival', sceneActId: 'scene-bridge-act-1', - turnLimit: 5, - remainingTurns: 5, + turnLimit: null, + remainingTurns: null, limitReason: 'negative_affinity', + terminationMode: 'hostile_model', + isHostileChat: true, combatContext: { battleOutcome: 'victory', }, diff --git a/src/hooks/rpg-runtime-story/useRpgRuntimeInteractionFlow.ts b/src/hooks/rpg-runtime-story/useRpgRuntimeInteractionFlow.ts index cd8aec8c..f19e466a 100644 --- a/src/hooks/rpg-runtime-story/useRpgRuntimeInteractionFlow.ts +++ b/src/hooks/rpg-runtime-story/useRpgRuntimeInteractionFlow.ts @@ -7,19 +7,19 @@ import type { StoryMoment, StoryOption, } from '../../types'; -import type { ResolvedChoiceState } from '../combat/resolvedChoice'; import type { EscapePlaybackSync as ResolvedChoicePlaybackSync } from '../combat/escapeFlow'; +import type { ResolvedChoiceState } from '../combat/resolvedChoice'; import { useTreasureFlow } from '../useTreasureFlow'; import { useStoryInventoryActions } from './inventoryActions'; -import { createRpgRuntimeNpcEncounterActions } from './useRpgRuntimeNpcInteraction'; import { useStoryNpcInteractionFlow } from './npcInteraction'; -import type { StoryInteractionCoordinatorConfig } from './storyInteractionCoordinator'; -import type { StoryRuntimeSupport } from './storyRuntimeSupport'; import type { - ChoiceRuntimeSupport, ChoiceRuntimeController, + ChoiceRuntimeSupport, StoryChoiceCoordinatorParams, } from './storyChoiceCoordinator'; +import type { StoryInteractionCoordinatorConfig } from './storyInteractionCoordinator'; +import type { StoryRuntimeSupport } from './storyRuntimeSupport'; +import { createRpgRuntimeNpcEncounterActions } from './useRpgRuntimeNpcInteraction'; import { useStoryChoiceCoordinator } from './useStoryChoiceCoordinator'; type RpgRuntimeInteractionFlowParams = { @@ -251,6 +251,24 @@ export function useRpgRuntimeInteractionFlow({ void handleNpcChatTurn(encounter, input); return true; }, + refreshNpcChatOptions: () => { + const story = interactionConfig.npcEncounterActions.currentStory; + const encounter = interactionConfig.npcEncounterActions.gameState.currentEncounter; + if (!story?.npcChatState || !story.options.length || !encounter || encounter.kind !== 'npc') { + return false; + } + + const [firstOption, ...restOptions] = story.options; + if (!firstOption || restOptions.length === 0) { + return false; + } + + interactionConfig.npcEncounterActions.setCurrentStory({ + ...story, + options: [...restOptions, firstOption], + }); + return true; + }, exitNpcChat, npcChatQuestOfferUi: { replacePendingOffer: replacePendingNpcQuestOffer, diff --git a/src/hooks/rpg-runtime-story/useRpgRuntimeNpcInteraction.ts b/src/hooks/rpg-runtime-story/useRpgRuntimeNpcInteraction.ts index 5a584584..f86ffe9e 100644 --- a/src/hooks/rpg-runtime-story/useRpgRuntimeNpcInteraction.ts +++ b/src/hooks/rpg-runtime-story/useRpgRuntimeNpcInteraction.ts @@ -17,15 +17,18 @@ import { applyQuestProgressFromSpar, } from '../../data/questFlow'; import { incrementGameRuntimeStats } from '../../data/runtimeStats'; +import { getScenePresetById } from '../../data/scenePresets'; import { resolveFunctionOption } from '../../data/stateFunctions'; -import { applyStoryReasoningRecovery } from '../../data/storyRecovery'; import { streamNpcChatTurn } from '../../services/aiService'; import type { StoryGenerationContext } from '../../services/aiTypes'; import { + advanceSceneActRuntimeState, + getSceneConnectionDirectionText, resolveLimitedPrimaryNpcChatState, + resolveSceneActProgression, } from '../../services/customWorldSceneActRuntime'; import { appendStoryEngineCarrierMemory } from '../../services/storyEngine/echoMemory'; -import { createHistoryMoment } from '../../services/storyHistory'; +import { createEmptyStoryEngineMemoryState } from '../../services/storyEngine/visibilityEngine'; import type { Character, Encounter, @@ -50,15 +53,6 @@ type CommitGeneratedStateWithEncounterEntry = ( lastFunctionId?: string, ) => Promise | void; -type GenerateStoryForState = (params: { - state: GameState; - character: Character; - history: StoryMoment[]; - choice?: string; - lastFunctionId?: string | null; - optionCatalog?: StoryOption[] | null; -}) => Promise; - type NpcInteractionFlowActions = { openTradeModal: (encounter: Encounter, actionText: string) => void; openGiftModal: (encounter: Encounter, actionText: string) => void; @@ -82,6 +76,16 @@ type NpcChatDirective = { remainingTurns?: number | null; limitReason?: 'negative_affinity' | null; forceExitAfterTurn?: boolean; + closingMode?: 'free' | 'foreshadow_close' | null; + terminationMode?: 'none' | 'hostile_model' | null; + terminationReason?: 'hostile_breakoff' | 'player_exit' | null; + isHostileChat?: boolean; + functionOptions?: Array<{ + functionId: string; + actionText: string; + detailText?: string | null; + action?: string | null; + }>; } | null; type NpcChatCombatContext = NonNullable< @@ -119,11 +123,9 @@ export function createStoryNpcEncounterActions({ buildOpeningCampChatContext, buildStoryContextFromState, buildFallbackStoryForState, - generateStoryForState, getStoryGenerationHostileNpcs, getAvailableOptionsForState, buildContinueAdventureOption, - getNpcEncounterKey, getResolvedNpcState, updateNpcState, cloneInventoryItemForOwner, @@ -169,7 +171,6 @@ export function createStoryNpcEncounterActions({ options: StoryOption[], streaming?: boolean, ) => StoryMoment; - generateStoryForState: GenerateStoryForState; getStoryGenerationHostileNpcs: ( state: GameState, ) => GameState['sceneHostileNpcs']; @@ -185,7 +186,6 @@ export function createStoryNpcEncounterActions({ ) => StoryOption[]; sortOptions: (options: StoryOption[]) => StoryOption[]; buildContinueAdventureOption: () => StoryOption; - getNpcEncounterKey: (encounter: Encounter) => string; getResolvedNpcState: ( state: GameState, encounter: Encounter, @@ -315,13 +315,6 @@ export function createStoryNpcEncounterActions({ }`; }; - const buildPostQuestOfferChatSuggestions = (encounter: Encounter) => - [ - '那先继续聊聊你刚才没说完的部分', - '除了委托,你对眼前局势还有什么判断', - '先把这附近真正危险的地方说清楚', - ].map((actionText) => buildNpcChatOption(encounter, actionText)); - const extractRecentCombatLogLines = (history: GameState['storyHistory']) => history .slice(-6) @@ -365,12 +358,6 @@ export function createStoryNpcEncounterActions({ } const reopenedNpcState = getResolvedNpcState(params.nextState, params.encounter); - const chatDirective = resolveLimitedPrimaryNpcChatState({ - state: params.nextState, - npcId: params.encounter.id ?? params.encounter.npcName, - affinity: reopenedNpcState.affinity, - nextTurnCount: 0, - }); const baseStory = buildNpcStory( params.nextState, playerCharacter, @@ -389,6 +376,16 @@ export function createStoryNpcEncounterActions({ actionText: params.actionText, historyBase: params.nextState.storyHistory, }); + const chatDirective = toNpcChatDirectiveWithFunctionOptions( + resolveLimitedPrimaryNpcChatState({ + state: params.nextState, + npcId: params.encounter.id ?? params.encounter.npcName, + affinity: reopenedNpcState.affinity, + nextTurnCount: 0, + }), + params.encounter, + playerCharacter, + ); setCurrentStory( buildNpcChatStoryMoment({ @@ -591,6 +588,29 @@ export function createStoryNpcEncounterActions({ }, })); + const cloneNpcChatFunctionOption = (option: StoryOption): StoryOption => ({ + ...option, + visuals: { + ...option.visuals, + monsterChanges: option.visuals.monsterChanges.map((change) => ({ + ...change, + })), + }, + interaction: option.interaction ? { ...option.interaction } : undefined, + runtimePayload: option.runtimePayload + ? { ...option.runtimePayload } + : option.runtimePayload, + }); + + const rewriteNpcChatFunctionOption = ( + option: StoryOption, + actionText: string, + ): StoryOption => ({ + ...cloneNpcChatFunctionOption(option), + actionText, + text: actionText, + }); + const NPC_CHAT_SUGGESTION_LIMIT = 20; const trimNpcChatSuggestion = (text: string) => @@ -639,9 +659,9 @@ export function createStoryNpcEncounterActions({ sanitizeNpcChatSuggestion(playerMessage) || '刚才那句', ); return [ - sanitizeNpcChatSuggestion(`你刚才那句是什么意思`), + sanitizeNpcChatSuggestion('我愿意先听你说完'), sanitizeNpcChatSuggestion(`这件事和${topic}有关吗`), - sanitizeNpcChatSuggestion('你愿意再说清楚点吗'), + sanitizeNpcChatSuggestion('你别再避重就轻'), ]; }; @@ -706,11 +726,103 @@ export function createStoryNpcEncounterActions({ ...fallbackSuggestions.filter( (suggestion) => !seenActionTexts.has(suggestion), ), - ].slice(0, 3); + ]; return buildNpcChatTurnOptions(encounter, mergedSuggestions); }; + const buildNpcChatFunctionOptionCatalog = ( + encounter: Encounter, + playerCharacter: Character, + ) => + buildPostNpcChatOptionCatalog(encounter, playerCharacter) + .filter((option) => option.functionId !== 'battle_escape_breakout') + .filter((option) => !isNpcChatOptionForEncounter(option, encounter)) + .filter((option) => option.interaction?.kind === 'npc') + .map(cloneNpcChatFunctionOption); + + const toNpcChatDirectiveWithFunctionOptions = ( + directive: NpcChatDirective, + encounter: Encounter, + playerCharacter: Character, + options?: { + forcePlayerExit?: boolean; + }, + ): NpcChatDirective => { + const functionOptions = buildNpcChatFunctionOptionCatalog( + encounter, + playerCharacter, + ).map((option) => ({ + functionId: option.functionId, + actionText: option.actionText, + detailText: option.detailText ?? null, + action: option.interaction?.kind === 'npc' ? option.interaction.action : null, + })); + const isHostileChat = + directive?.isHostileChat === true || + directive?.terminationMode === 'hostile_model'; + + return { + ...(directive ?? {}), + terminationMode: isHostileChat ? 'hostile_model' : 'none', + isHostileChat, + terminationReason: options?.forcePlayerExit + ? 'player_exit' + : (directive?.terminationReason ?? null), + closingMode: options?.forcePlayerExit + ? 'foreshadow_close' + : (directive?.closingMode ?? 'free'), + forceExitAfterTurn: + options?.forcePlayerExit || directive?.forceExitAfterTurn || false, + functionOptions, + }; + }; + + const buildNpcChatMixedTurnOptions = ( + encounter: Encounter, + playerCharacter: Character, + suggestions: string[], + functionSuggestions?: Array<{ + functionId?: string; + actionText?: string; + }>, + ) => { + const chatOptions = buildNpcChatTurnOptions(encounter, suggestions); + const functionCatalog = buildNpcChatFunctionOptionCatalog( + encounter, + playerCharacter, + ); + const functionOptions = (functionSuggestions ?? []) + .map((suggestion) => { + if (!suggestion.functionId || !suggestion.actionText) return null; + const matchedOption = functionCatalog.find( + (option) => option.functionId === suggestion.functionId, + ); + return matchedOption + ? rewriteNpcChatFunctionOption( + matchedOption, + sanitizeNpcChatSuggestion(suggestion.actionText) || + matchedOption.actionText, + ) + : null; + }) + .filter((option): option is StoryOption => Boolean(option)); + + const mergedOptions = [...chatOptions, ...functionOptions]; + const seen = new Set(); + + return mergedOptions.filter((option) => { + const key = [ + option.functionId, + option.actionText, + option.interaction?.kind === 'npc' ? option.interaction.action : '', + ].join('::'); + if (seen.has(key)) return false; + seen.add(key); + return true; + }); + }; + const buildNpcChatStoryMoment = (params: { encounter: Encounter; dialogue: NonNullable; @@ -742,6 +854,9 @@ export function createStoryNpcEncounterActions({ remainingTurns: params.chatDirective?.remainingTurns ?? null, limitReason: params.chatDirective?.limitReason ?? null, forceExitAfterTurn: params.chatDirective?.forceExitAfterTurn ?? false, + terminationMode: params.chatDirective?.terminationMode ?? null, + terminationReason: params.chatDirective?.terminationReason ?? null, + isHostileChat: params.chatDirective?.isHostileChat ?? false, pendingQuestOffer: params.pendingQuestOffer ?? null, combatContext: params.combatContext ?? null, }, @@ -773,7 +888,6 @@ export function createStoryNpcEncounterActions({ getAvailableOptionsForState(gameState, playerCharacter) ?? [], ); const currentStoryOptions = currentStory?.options ?? []; - const currentNpcKey = encounter.id ?? encounter.npcName; const currentChatOptions = currentStoryOptions.filter((option) => isNpcChatOptionForEncounter(option, encounter), ); @@ -808,6 +922,124 @@ export function createStoryNpcEncounterActions({ return mergedOptions; }; + const buildSceneConnectionTravelOptions = (state: GameState) => { + if (!state.worldType || !state.currentScenePreset) { + return []; + } + + const seenSceneIds = new Set(); + + return (state.currentScenePreset.connections ?? []) + .filter((connection) => { + if (!connection.sceneId || seenSceneIds.has(connection.sceneId)) { + return false; + } + seenSceneIds.add(connection.sceneId); + return true; + }) + .map((connection) => { + const targetScene = getScenePresetById( + state.worldType!, + connection.sceneId, + ); + const targetSceneName = targetScene?.name ?? connection.sceneId; + const directionText = getSceneConnectionDirectionText( + connection.relativePosition, + ); + const actionText = `${directionText},前往${targetSceneName}`; + + return { + functionId: 'idle_travel_next_scene', + actionText, + text: actionText, + detailText: connection.summary, + priority: 12, + visuals: { + playerAnimation: AnimationState.RUN, + playerMoveMeters: 1.1, + playerOffsetY: 0, + playerFacing: + connection.relativePosition === 'west' || + connection.relativePosition === 'left' || + connection.relativePosition === 'back' + ? 'left' + : 'right', + scrollWorld: false, + monsterChanges: [], + }, + runtimePayload: { + targetSceneId: connection.sceneId, + }, + } satisfies StoryOption; + }); + }; + + const buildPostNpcChatProgressionOptions = ( + encounter: Encounter, + playerCharacter: Character, + ) => { + const progression = resolveSceneActProgression({ + profile: gameState.customWorldProfile, + sceneId: gameState.currentScenePreset?.id ?? null, + storyEngineMemory: gameState.storyEngineMemory, + }); + + if (!progression) { + return { + deferredRuntimeState: null, + options: currentStory?.deferredOptions?.length + ? currentStory.deferredOptions + : buildPostNpcChatOptionCatalog(encounter, playerCharacter), + }; + } + + if (!progression.isLastAct) { + const nextActState = advanceSceneActRuntimeState({ progress: progression }); + const nextStoryEngineMemory = nextActState + ? { + ...(gameState.storyEngineMemory ?? + createEmptyStoryEngineMemoryState()), + currentSceneActState: nextActState, + } + : gameState.storyEngineMemory; + const nextState = { + ...gameState, + currentEncounter: null, + npcInteractionActive: false, + sceneHostileNpcs: [], + inBattle: false, + currentBattleNpcId: null, + currentNpcBattleMode: null, + currentNpcBattleOutcome: null, + storyEngineMemory: nextStoryEngineMemory, + }; + const nextOptions = collapseNpcChatOptions( + getAvailableOptionsForState(nextState, playerCharacter) ?? [], + ); + + return { + deferredRuntimeState: { + currentScenePreset: nextState.currentScenePreset, + storyEngineMemory: nextState.storyEngineMemory, + }, + options: + nextOptions.length > 0 + ? nextOptions + : buildPostNpcChatOptionCatalog(encounter, playerCharacter), + }; + } + + const travelOptions = buildSceneConnectionTravelOptions(gameState); + + return { + deferredRuntimeState: null, + options: + travelOptions.length > 0 + ? travelOptions + : buildPostNpcChatOptionCatalog(encounter, playerCharacter), + }; + }; + const buildLegacyNpcChatOpeningPlaceholder = (encounter: Encounter) => `${encounter.npcName}看着你,像是在等你把话接下去。`; @@ -968,6 +1200,14 @@ export function createStoryNpcEncounterActions({ openingSource: 'npc_initiated' | 'player_reply' = 'player_reply', ) => { const openingDialogue = buildNpcChatDialogueHistory(encounter, 0); + const playerCharacter = gameState.playerCharacter; + const resolvedChatDirective = playerCharacter + ? toNpcChatDirectiveWithFunctionOptions( + chatDirective ?? null, + encounter, + playerCharacter, + ) + : chatDirective; setAiError(null); setCurrentStory( @@ -981,7 +1221,7 @@ export function createStoryNpcEncounterActions({ ), streaming: false, turnCount: 0, - chatDirective, + chatDirective: resolvedChatDirective, openingSource, }), ); @@ -1006,6 +1246,11 @@ export function createStoryNpcEncounterActions({ } const npcState = getResolvedNpcState(gameState, encounter); + const resolvedChatDirective = toNpcChatDirectiveWithFunctionOptions( + chatDirective ?? null, + encounter, + playerCharacter, + ); const openingCampContext = buildOpeningCampChatContext( gameState, playerCharacter, @@ -1027,7 +1272,7 @@ export function createStoryNpcEncounterActions({ options: [], streaming: true, turnCount: 0, - chatDirective, + chatDirective: resolvedChatDirective, openingSource: 'npc_initiated', }), ); @@ -1067,12 +1312,12 @@ export function createStoryNpcEncounterActions({ options: [], streaming: true, turnCount: 0, - chatDirective, + chatDirective: resolvedChatDirective, openingSource: 'npc_initiated', }), ); }, - chatDirective, + chatDirective: resolvedChatDirective, npcInitiatesConversation: true, }, ); @@ -1091,15 +1336,17 @@ export function createStoryNpcEncounterActions({ text: chatTurn.npcReply, }, ], - options: buildNpcChatTurnOptions( + options: buildNpcChatMixedTurnOptions( encounter, + playerCharacter, chatTurn.suggestions.length > 0 ? chatTurn.suggestions : openingOptions.map((option) => option.actionText), + chatTurn.functionSuggestions, ), streaming: false, turnCount: 0, - chatDirective, + chatDirective: resolvedChatDirective, openingSource: 'npc_initiated', }), ); @@ -1122,6 +1369,9 @@ export function createStoryNpcEncounterActions({ const handleNpcChatTurn = async ( encounter: Encounter, playerMessage: string, + options: { + forcePlayerExit?: boolean; + } = {}, ) => { const playerCharacter = gameState.playerCharacter; if (!playerCharacter || !gameState.worldType) { @@ -1157,6 +1407,14 @@ export function createStoryNpcEncounterActions({ affinity: npcState.affinity, nextTurnCount, }); + const chatDirective = toNpcChatDirectiveWithFunctionOptions( + limitedChatDirective, + encounter, + playerCharacter, + { + forcePlayerExit: options.forcePlayerExit, + }, + ); const openingCampContext = buildOpeningCampChatContext( gameState, playerCharacter, @@ -1172,7 +1430,7 @@ export function createStoryNpcEncounterActions({ options: [], streaming: true, turnCount: nextTurnCount, - chatDirective: limitedChatDirective, + chatDirective, combatContext: currentCombatContext, }), ); @@ -1212,18 +1470,18 @@ export function createStoryNpcEncounterActions({ options: [], streaming: true, turnCount: nextTurnCount, - chatDirective: limitedChatDirective, + chatDirective, combatContext: currentCombatContext, }), ); }, - questOfferContext: limitedChatDirective + questOfferContext: chatDirective?.isHostileChat ? null : { state: gameState, turnCount: nextTurnCount, }, - chatDirective: limitedChatDirective, + chatDirective, combatContext: currentCombatContext, }, ); @@ -1283,21 +1541,31 @@ export function createStoryNpcEncounterActions({ const pendingQuest = (chatTurn.pendingQuestOffer?.quest as QuestLogEntry | undefined) ?? null; - const resolvedChatDirective = limitedChatDirective + const resolvedChatDirective = chatDirective ? { - sceneActId: limitedChatDirective.sceneActId ?? null, + sceneActId: chatDirective.sceneActId ?? null, turnLimit: chatTurn.chatDirective?.turnLimit ?? - limitedChatDirective.turnLimit ?? + chatDirective.turnLimit ?? null, remainingTurns: chatTurn.chatDirective?.remainingTurns ?? - limitedChatDirective.remainingTurns ?? + chatDirective.remainingTurns ?? null, - limitReason: limitedChatDirective.limitReason ?? null, - forceExitAfterTurn: + limitReason: chatDirective.limitReason ?? null, + terminationMode: chatDirective.terminationMode ?? null, + terminationReason: + chatTurn.chatDirective?.terminationReason ?? + chatDirective.terminationReason ?? + null, + isHostileChat: chatDirective.isHostileChat ?? false, + closingMode: + chatTurn.chatDirective?.closingMode ?? + chatDirective.closingMode ?? + 'free', + forceExitAfterTurn: chatTurn.chatDirective?.forceExit ?? - limitedChatDirective.forceExitAfterTurn ?? + chatDirective.forceExitAfterTurn ?? false, } : null; @@ -1308,11 +1576,11 @@ export function createStoryNpcEncounterActions({ if (shouldForceExitAfterTurn) { const closingDialogue = [ ...nextDialogue, - { - speaker: 'system' as const, - text: '这轮交谈先在这里收束,对方留下的线索把你推向了下一步。', - }, ]; + const progressionResult = buildPostNpcChatProgressionOptions( + encounter, + playerCharacter, + ); setCurrentStory({ text: closingDialogue.map((turn) => turn.text).join('\n'), options: [buildContinueAdventureOption()], @@ -1320,6 +1588,9 @@ export function createStoryNpcEncounterActions({ dialogue: closingDialogue, streaming: false, npcAffinityEffect: latestAffinityEffect, + deferredOptions: progressionResult.options, + deferredRuntimeState: + progressionResult.deferredRuntimeState ?? undefined, }); return true; } @@ -1355,11 +1626,13 @@ export function createStoryNpcEncounterActions({ buildNpcChatStoryMoment({ encounter, dialogue: nextDialogue, - options: buildNpcChatTurnOptions( + options: buildNpcChatMixedTurnOptions( encounter, + playerCharacter, chatTurn.suggestions.length > 0 ? chatTurn.suggestions : buildFallbackNpcChatSuggestions(playerMessage), + chatTurn.functionSuggestions, ), streaming: false, turnCount: nextTurnCount, @@ -1382,7 +1655,7 @@ export function createStoryNpcEncounterActions({ ), streaming: false, turnCount: nextTurnCount, - chatDirective: limitedChatDirective, + chatDirective, combatContext: currentCombatContext, }), ); @@ -1392,7 +1665,7 @@ export function createStoryNpcEncounterActions({ } }; - const exitNpcChat = () => { + const continueAfterNpcChatClosure = async () => { const playerCharacter = gameState.playerCharacter; const encounter = gameState.currentEncounter; if (!playerCharacter || !isNpcEncounter(encounter)) { @@ -1400,48 +1673,50 @@ export function createStoryNpcEncounterActions({ } setAiError(null); - setIsLoading(true); + const progressionResult = buildPostNpcChatProgressionOptions( + encounter, + playerCharacter, + ); + const nextState = { + ...gameState, + currentEncounter: null, + npcInteractionActive: false, + sceneHostileNpcs: [], + inBattle: false, + currentBattleNpcId: null, + currentNpcBattleMode: null, + currentNpcBattleOutcome: null, + currentScenePreset: + progressionResult.deferredRuntimeState?.currentScenePreset ?? + gameState.currentScenePreset, + storyEngineMemory: + progressionResult.deferredRuntimeState?.storyEngineMemory ?? + gameState.storyEngineMemory, + }; - void (async () => { - const choiceText = `结束与${encounter.npcName}的这轮交谈,重新观察当前局势`; + setGameState(nextState); + setCurrentStory({ + text: currentStory?.dialogue?.at(-1)?.text ?? currentStory?.text ?? '', + options: progressionResult.options, + displayMode: 'narrative', + }); + return true; + }; - try { - const postChatOptionCatalog = buildPostNpcChatOptionCatalog( - encounter, - playerCharacter, - ); - const nextStory = await generateStoryForState({ - state: gameState, - character: playerCharacter, - history: gameState.storyHistory, - choice: choiceText, - lastFunctionId: 'npc_chat', - optionCatalog: postChatOptionCatalog, - }); - const nextHistory = [ - ...gameState.storyHistory, - createHistoryMoment(choiceText, 'action'), - createHistoryMoment(nextStory.text, 'result', nextStory.options), - ]; - const recoveredState = applyStoryReasoningRecovery({ - ...gameState, - storyHistory: nextHistory, - }); - setGameState(recoveredState); - setCurrentStory(nextStory); - } catch (error) { - console.error( - 'Failed to continue story after exiting npc chat:', - error, - ); - setAiError( - error instanceof Error ? error.message : '退出聊天后的剧情推理失败', - ); - setCurrentStory(buildFallbackStoryForState(gameState, playerCharacter)); - } finally { - setIsLoading(false); - } - })(); + const exitNpcChat = () => { + const playerCharacter = gameState.playerCharacter; + const encounter = gameState.currentEncounter; + if (!playerCharacter || !isNpcEncounter(encounter)) { + return false; + } + + void handleNpcChatTurn( + encounter, + `我先结束这轮交谈,继续往前走。`, + { + forcePlayerExit: true, + }, + ); return true; }; @@ -1694,6 +1969,13 @@ export function createStoryNpcEncounterActions({ }; const handleNpcInteraction = (option: StoryOption) => { + if ( + currentStory?.deferredOptions?.length && + option.functionId === 'story_continue_adventure' + ) { + void continueAfterNpcChatClosure(); + return true; + } const playerCharacter = gameState.playerCharacter; if (!playerCharacter || !isNpcEncounter(gameState.currentEncounter)) { return false; @@ -1800,6 +2082,7 @@ export function createStoryNpcEncounterActions({ reopenNpcChatAfterBattle, handleNpcChatTurn, exitNpcChat, + continueAfterNpcChatClosure, replacePendingNpcQuestOffer, abandonPendingNpcQuestOffer, acceptPendingNpcQuestOffer, diff --git a/src/hooks/rpg-runtime-story/useRpgRuntimeStory.ts b/src/hooks/rpg-runtime-story/useRpgRuntimeStory.ts index 70df271d..b958a093 100644 --- a/src/hooks/rpg-runtime-story/useRpgRuntimeStory.ts +++ b/src/hooks/rpg-runtime-story/useRpgRuntimeStory.ts @@ -22,9 +22,9 @@ import { resolveNpcInteractionDecision, } from './storyGenerationState'; import { storyRuntimeSupport } from './storyRuntimeSupport'; -import { useRpgRuntimeStoryFlow } from './useRpgRuntimeStoryFlow'; -import { useRpgRuntimeStoryController } from './useRpgRuntimeStoryController'; import type { BattleRewardUi, QuestFlowUi } from './uiTypes'; +import { useRpgRuntimeStoryController } from './useRpgRuntimeStoryController'; +import { useRpgRuntimeStoryFlow } from './useRpgRuntimeStoryFlow'; const TURN_VISUAL_MS = 820; const NPC_PREVIEW_TALK_FUNCTION_ID = NPC_PREVIEW_TALK_FUNCTION.id; @@ -41,8 +41,8 @@ export type { GiftModalState, GoalFlowUi, InventoryFlowUi, - QuestFlowUi, NpcChatQuestOfferUi, + QuestFlowUi, RecruitModalState, StoryGenerationNpcUi, TradeModalState, @@ -100,6 +100,7 @@ export function useRpgRuntimeStory({ npcUi, inventoryUi, handleNpcChatInput, + refreshNpcChatOptions, exitNpcChat, npcChatQuestOfferUi, } = useRpgRuntimeStoryFlow({ @@ -142,6 +143,7 @@ export function useRpgRuntimeStory({ characterChatUi, inventoryUi, handleNpcChatInput, + refreshNpcChatOptions, exitNpcChat, npcChatQuestOfferUi, }; diff --git a/src/hooks/rpg-runtime-story/useRpgRuntimeStoryFlow.ts b/src/hooks/rpg-runtime-story/useRpgRuntimeStoryFlow.ts index e2ec64c6..2ca8a439 100644 --- a/src/hooks/rpg-runtime-story/useRpgRuntimeStoryFlow.ts +++ b/src/hooks/rpg-runtime-story/useRpgRuntimeStoryFlow.ts @@ -5,11 +5,11 @@ import type { EscapePlaybackSync as ResolvedChoicePlaybackSync } from '../combat import type { ResolvedChoiceState } from '../combat/resolvedChoice'; import { createStoryInteractionCoordinatorConfig } from './storyInteractionCoordinator'; import { sanitizeStoryOptions } from './storyPresentation'; -import { useStoryGoalOptionCoordinator } from './useStoryGoalOptionCoordinator'; import type { StoryRuntimeSupport } from './storyRuntimeSupport'; -import { useRpgRuntimeStoryState } from './useRpgRuntimeStoryState'; import { useRpgRuntimeInteractionFlow } from './useRpgRuntimeInteractionFlow'; import type { RpgRuntimeStoryControllerResult } from './useRpgRuntimeStoryController'; +import { useRpgRuntimeStoryState } from './useRpgRuntimeStoryState'; +import { useStoryGoalOptionCoordinator } from './useStoryGoalOptionCoordinator'; type RpgRuntimeStoryFlowParams = { gameState: GameState; @@ -138,6 +138,7 @@ export function useRpgRuntimeStoryFlow({ inventoryUi, clearStoryInteractionUi, handleNpcChatInput, + refreshNpcChatOptions, exitNpcChat, npcChatQuestOfferUi, } = useRpgRuntimeInteractionFlow({ @@ -188,6 +189,7 @@ export function useRpgRuntimeStoryFlow({ npcUi, inventoryUi, handleNpcChatInput, + refreshNpcChatOptions, exitNpcChat, npcChatQuestOfferUi, }; diff --git a/src/hooks/rpg-session/useRpgRuntimeSession.ts b/src/hooks/rpg-session/useRpgRuntimeSession.ts index 72cba22f..20562c82 100644 --- a/src/hooks/rpg-session/useRpgRuntimeSession.ts +++ b/src/hooks/rpg-session/useRpgRuntimeSession.ts @@ -155,6 +155,7 @@ export function useRpgRuntimeSession(): RpgRuntimeShellProps { handleRefreshOptions: storyFlow.handleRefreshOptions, handleChoice: storyFlow.handleChoice, handleNpcChatInput: storyFlow.handleNpcChatInput, + refreshNpcChatOptions: storyFlow.refreshNpcChatOptions, exitNpcChat: storyFlow.exitNpcChat, handleMapTravelToScene: storyFlow.travelToSceneFromMap, npcUi: storyFlow.npcUi, diff --git a/src/index.css b/src/index.css index d452017f..bdb7cb3e 100644 --- a/src/index.css +++ b/src/index.css @@ -857,6 +857,71 @@ body { color: var(--platform-button-danger-text); } +.platform-close-confirm-dialog > :first-child { + border-color: var(--platform-line-soft) !important; +} + +.platform-close-confirm-dialog > :first-child > .min-w-0 { + color: var(--platform-text-strong) !important; +} + +.platform-close-confirm-dialog__content { + display: grid; + gap: 0.9rem; +} + +.platform-close-confirm-dialog__message { + border: 1px solid var(--platform-warm-border); + border-radius: 1rem; + background: var(--platform-warm-bg); + padding: 0.95rem 1rem; + color: var(--platform-warm-text); + font-size: 0.875rem; + font-weight: 600; + line-height: 1.7; +} + +.platform-close-confirm-dialog__actions { + display: grid; + gap: 0.75rem; +} + +.platform-close-confirm-dialog__button { + display: inline-flex; + min-height: 2.5rem; + width: 100%; + align-items: center; + justify-content: center; + border-radius: 9999px; + border: 1px solid transparent; + padding: 0.55rem 1rem; + font-size: 0.875rem; + font-weight: 800; + transition: + transform 180ms ease, + border-color 180ms ease, + background 180ms ease, + color 180ms ease, + box-shadow 180ms ease; +} + +.platform-close-confirm-dialog__button:hover { + transform: translateY(-1px); +} + +.platform-close-confirm-dialog__button--primary { + border-color: var(--platform-button-primary-border); + background: var(--platform-button-primary-fill); + color: var(--platform-button-primary-text); + box-shadow: 0 12px 26px rgba(255, 91, 132, 0.16); +} + +.platform-close-confirm-dialog__button--secondary { + border-color: var(--platform-neutral-border); + background: var(--platform-neutral-bg); + color: var(--platform-neutral-text); +} + .platform-icon-button { display: inline-flex; align-items: center; @@ -1416,6 +1481,24 @@ body { background: var(--platform-track-fill); } +.platform-npc-portrait { + border: 1px solid var(--platform-subpanel-border); + background: radial-gradient( + circle at top, + var(--platform-surface-glow-a), + transparent 48% + ), + var(--platform-subpanel-fill); +} + +.platform-npc-portrait__grid { + opacity: 0.14; + background-image: + linear-gradient(var(--platform-line-soft) 1px, transparent 1px), + linear-gradient(90deg, var(--platform-line-soft) 1px, transparent 1px); + background-size: 16px 16px; +} + .platform-role-studio__footer { border-top: 1px solid var(--platform-subpanel-border); background: linear-gradient( diff --git a/src/services/aiService.ts b/src/services/aiService.ts index ff74d927..d28aefd7 100644 --- a/src/services/aiService.ts +++ b/src/services/aiService.ts @@ -1,19 +1,19 @@ -import type { - CustomWorldGenerationProgress, - GenerateCustomWorldProfileInput, - GenerateCustomWorldProfileOptions, -} from '../../packages/shared/src/contracts/runtime'; import type { CharacterChatReplyRequest, CharacterChatSuggestionsRequest, CharacterChatSummaryRequest, - NpcChatTurnDirective, NpcChatDialogueRequest, + NpcChatTurnDirective, NpcChatTurnRequest, NpcChatTurnResult, NpcRecruitDialogueRequest, PlainTextResponse, } from '../../packages/shared/src/contracts/rpgRuntimeChat'; +import type { + CustomWorldGenerationProgress, + GenerateCustomWorldProfileInput, + GenerateCustomWorldProfileOptions, +} from '../../packages/shared/src/contracts/runtime'; import { parseApiErrorMessage } from '../../packages/shared/src/http'; import type { AIResponse, @@ -26,10 +26,10 @@ import type { WorldType, } from '../types'; import type { + CustomWorldSceneImageResult, StoryGenerationContext, StoryRequestOptions, TextStreamOptions, - CustomWorldSceneImageResult, } from './aiTypes'; import { fetchWithApiAuth, requestJson } from './apiClient'; import { type CharacterChatTargetStatus } from './characterChatPrompt'; @@ -463,7 +463,14 @@ export async function streamNpcChatTurn( } : null, combatContext: options.combatContext ?? null, - chatDirective: options.chatDirective ?? null, + chatDirective: options.chatDirective + ? { + ...options.chatDirective, + functionOptions: options.chatDirective.functionOptions?.map((item) => ({ + ...item, + })), + } + : null, } satisfies NpcChatTurnRequest; const response = await fetchWithApiAuth( diff --git a/src/services/customWorldAgentGenerationProgress.test.ts b/src/services/customWorldAgentGenerationProgress.test.ts index 712db293..21d52f90 100644 --- a/src/services/customWorldAgentGenerationProgress.test.ts +++ b/src/services/customWorldAgentGenerationProgress.test.ts @@ -24,26 +24,16 @@ const baseSession: CustomWorldAgentSessionSnapshot = { sessionId: 'session-1', currentTurn: 8, anchorContent: { - worldPromise: { - hook: '海雾、旧灯塔和失控航路交织的边缘群岛', - differentiator: '每次借路都要向海雾付出新的代价。', - desiredExperience: '压抑、悬疑、潮湿', - }, - playerFantasy: { - playerRole: '玩家刚回到群岛,准备调查父亲沉船的真相。', - corePursuit: '查清沉船夜和禁航区异动的因果。', - fearOfLoss: '再失去唯一还敢接近真相的人。', - }, + worldPromise: + '海雾、旧灯塔和失控航路交织的边缘群岛,每次借路都要向海雾付出新的代价,体验压抑、悬疑、潮湿。', + playerFantasy: + '玩家刚回到群岛,准备调查父亲沉船的真相,追查沉船夜和禁航区异动的因果,风险是再失去唯一还敢接近真相的人。', themeBoundary: null, playerEntryPoint: null, coreConflict: null, - keyRelationships: [], + keyRelationships: null, hiddenLines: null, - iconicElements: { - iconicMotifs: ['会移动的海雾'], - institutionsOrArtifacts: ['旧灯塔'], - hardRules: [], - }, + iconicElements: '会移动的海雾、旧灯塔。', }, progressPercent: 100, lastAssistantReply: '八锚点已经收束完成,可以进入游戏设定草稿生成。', diff --git a/src/services/customWorldAgentGenerationProgress.ts b/src/services/customWorldAgentGenerationProgress.ts index 5b10231d..ab9d6669 100644 --- a/src/services/customWorldAgentGenerationProgress.ts +++ b/src/services/customWorldAgentGenerationProgress.ts @@ -18,70 +18,26 @@ export type CustomWorldStructuredAnchorEntry = { value: string; }; -function joinText(items: Array) { - return items.filter(Boolean).join(';'); +function normalizeAnchorText(value: string | null | undefined) { + return typeof value === 'string' ? value.trim() : ''; } function buildEightAnchorFoundationText(anchorContent: EightAnchorContent) { return [ - anchorContent.worldPromise - ? `世界承诺:${joinText([ - anchorContent.worldPromise.hook, - anchorContent.worldPromise.differentiator, - anchorContent.worldPromise.desiredExperience, - ])}` - : '', - anchorContent.playerFantasy - ? `玩家幻想:${joinText([ - anchorContent.playerFantasy.playerRole, - anchorContent.playerFantasy.corePursuit, - anchorContent.playerFantasy.fearOfLoss, - ])}` - : '', - anchorContent.themeBoundary - ? `主题边界:${joinText([ - anchorContent.themeBoundary.toneKeywords.join('、'), - anchorContent.themeBoundary.aestheticDirectives.join('、'), - anchorContent.themeBoundary.forbiddenDirectives.join('、'), - ])}` - : '', - anchorContent.playerEntryPoint - ? `玩家切入口:${joinText([ - anchorContent.playerEntryPoint.openingIdentity, - anchorContent.playerEntryPoint.openingProblem, - anchorContent.playerEntryPoint.entryMotivation, - ])}` - : '', - anchorContent.coreConflict - ? `核心冲突:${joinText([ - anchorContent.coreConflict.surfaceConflicts.join('、'), - anchorContent.coreConflict.hiddenCrisis, - anchorContent.coreConflict.firstTouchedConflict, - ])}` - : '', - anchorContent.keyRelationships.length > 0 - ? `关键关系:${anchorContent.keyRelationships - .map((entry) => - joinText([entry.pairs, entry.relationshipType, entry.secretOrCost]), - ) - .join(';')}` - : '', - anchorContent.hiddenLines - ? `暗线与揭示:${joinText([ - anchorContent.hiddenLines.hiddenTruths.join('、'), - anchorContent.hiddenLines.misdirectionHints.join('、'), - anchorContent.hiddenLines.revealPacing, - ])}` - : '', - anchorContent.iconicElements - ? `标志元素:${joinText([ - anchorContent.iconicElements.iconicMotifs.join('、'), - anchorContent.iconicElements.institutionsOrArtifacts.join('、'), - anchorContent.iconicElements.hardRules.join('、'), - ])}` - : '', + ['世界承诺', anchorContent.worldPromise], + ['玩家幻想', anchorContent.playerFantasy], + ['主题边界', anchorContent.themeBoundary], + ['玩家切入口', anchorContent.playerEntryPoint], + ['核心冲突', anchorContent.coreConflict], + ['关键关系', anchorContent.keyRelationships], + ['暗线与揭示', anchorContent.hiddenLines], + ['标志元素', anchorContent.iconicElements], ] - .filter(Boolean) + .map(([label, value]) => { + const text = normalizeAnchorText(value); + return text ? `${label}:${text}` : ''; + }) + .filter((line) => line) .join('\n'); } @@ -98,97 +54,42 @@ export function buildAgentDraftFoundationAnchorEntries( { id: 'world-promise', label: '世界承诺', - value: anchorContent.worldPromise - ? joinText([ - anchorContent.worldPromise.hook, - anchorContent.worldPromise.differentiator, - anchorContent.worldPromise.desiredExperience, - ]) - : '', + value: normalizeAnchorText(anchorContent.worldPromise), }, { id: 'player-fantasy', label: '玩家幻想', - value: anchorContent.playerFantasy - ? joinText([ - anchorContent.playerFantasy.playerRole, - anchorContent.playerFantasy.corePursuit, - anchorContent.playerFantasy.fearOfLoss, - ]) - : '', + value: normalizeAnchorText(anchorContent.playerFantasy), }, { id: 'theme-boundary', label: '主题边界', - value: anchorContent.themeBoundary - ? joinText([ - anchorContent.themeBoundary.toneKeywords.join('、'), - anchorContent.themeBoundary.aestheticDirectives.join('、'), - anchorContent.themeBoundary.forbiddenDirectives.length > 0 - ? `避免:${anchorContent.themeBoundary.forbiddenDirectives.join('、')}` - : '', - ]) - : '', + value: normalizeAnchorText(anchorContent.themeBoundary), }, { id: 'player-entry-point', label: '玩家切入口', - value: anchorContent.playerEntryPoint - ? joinText([ - anchorContent.playerEntryPoint.openingIdentity, - anchorContent.playerEntryPoint.openingProblem, - anchorContent.playerEntryPoint.entryMotivation, - ]) - : '', + value: normalizeAnchorText(anchorContent.playerEntryPoint), }, { id: 'core-conflict', label: '核心冲突', - value: anchorContent.coreConflict - ? joinText([ - anchorContent.coreConflict.surfaceConflicts.join('、'), - anchorContent.coreConflict.hiddenCrisis, - anchorContent.coreConflict.firstTouchedConflict, - ]) - : '', + value: normalizeAnchorText(anchorContent.coreConflict), }, { id: 'key-relationships', label: '关键关系', - value: - anchorContent.keyRelationships.length > 0 - ? anchorContent.keyRelationships - .map((entry) => - joinText([ - entry.pairs, - entry.relationshipType, - entry.secretOrCost ? `代价/秘密:${entry.secretOrCost}` : '', - ]), - ) - .join('\n') - : '', + value: normalizeAnchorText(anchorContent.keyRelationships), }, { id: 'hidden-lines', label: '暗线与揭示', - value: anchorContent.hiddenLines - ? joinText([ - anchorContent.hiddenLines.hiddenTruths.join('、'), - anchorContent.hiddenLines.misdirectionHints.join('、'), - anchorContent.hiddenLines.revealPacing, - ]) - : '', + value: normalizeAnchorText(anchorContent.hiddenLines), }, { id: 'iconic-elements', label: '标志元素', - value: anchorContent.iconicElements - ? joinText([ - anchorContent.iconicElements.iconicMotifs.join('、'), - anchorContent.iconicElements.institutionsOrArtifacts.join('、'), - anchorContent.iconicElements.hardRules.join('、'), - ]) - : '', + value: normalizeAnchorText(anchorContent.iconicElements), }, ].filter((entry) => entry.value.trim()); } diff --git a/src/services/customWorldFoundationEntries.ts b/src/services/customWorldFoundationEntries.ts index 9e5045df..73802f37 100644 --- a/src/services/customWorldFoundationEntries.ts +++ b/src/services/customWorldFoundationEntries.ts @@ -1,7 +1,4 @@ -import type { - EightAnchorContent, - KeyRelationshipValue, -} from '../../packages/shared/src/contracts/customWorldAgent'; +import type { EightAnchorContent } from '../../packages/shared/src/contracts/customWorldAgent'; import { normalizeCustomWorldCreatorIntent } from './customWorldCreatorIntent'; import type { CustomWorldProfile } from '../types'; @@ -38,18 +35,33 @@ function toText(value: unknown) { return typeof value === 'string' ? value.trim() : ''; } -function toTextArray(value: unknown) { - return Array.isArray(value) - ? value.map((item) => toText(item)).filter(Boolean) - : []; -} - function toRecord(value: unknown) { return value && typeof value === 'object' && !Array.isArray(value) ? (value as Record) : null; } +function compactAnchorValue(value: unknown): string | null { + const text = toText(value); + if (text) { + return text; + } + if (Array.isArray(value)) { + const compacted = compactFoundationTextList( + value.map((item) => compactAnchorValue(item)), + ).join(';'); + return compacted || null; + } + const record = toRecord(value); + if (record) { + const compacted = compactFoundationTextList( + Object.values(record).map((item) => compactAnchorValue(item)), + ).join(';'); + return compacted || null; + } + return null; +} + function buildRelationshipSeedText(value: unknown) { const record = toRecord(value); if (!record) { @@ -66,14 +78,6 @@ function buildRelationshipSeedText(value: unknown) { ]).join(';'); } -function buildKeyRelationshipText(value: KeyRelationshipValue) { - return compactFoundationTextList([ - value.pairs, - value.relationshipType, - value.secretOrCost ? `代价/秘密:${value.secretOrCost}` : '', - ]).join(';'); -} - function buildAnchorContentFromProfileFallback( profile: CustomWorldProfile, ): EightAnchorContent { @@ -81,101 +85,71 @@ function buildAnchorContentFromProfileFallback( const relationshipSeed = creatorIntent?.keyCharacters[0] ?? null; return { - worldPromise: { - hook: - creatorIntent?.worldHook || + worldPromise: compactFoundationTextList([ + creatorIntent?.worldHook || profile.anchorPack?.worldSummary || profile.summary, - differentiator: profile.subtitle || profile.settingText, - desiredExperience: - compactFoundationTextList([ - creatorIntent?.toneDirectives.join('、') || '', - profile.tone, - ]).join(';') || profile.tone, - }, - playerFantasy: { - playerRole: creatorIntent?.playerPremise || profile.playerGoal, - corePursuit: profile.playerGoal, - fearOfLoss: - relationshipSeed?.hiddenHook || + profile.subtitle || profile.settingText, + creatorIntent?.toneDirectives.join('、') || profile.tone, + ]).join(';') || null, + playerFantasy: compactFoundationTextList([ + creatorIntent?.playerPremise || profile.playerGoal, + profile.playerGoal, + relationshipSeed?.hiddenHook || creatorIntent?.coreConflicts[0] || profile.coreConflicts[0] || '', - }, - themeBoundary: { - toneKeywords: compactFoundationTextList([ + ]).join(';') || null, + themeBoundary: compactFoundationTextList([ creatorIntent?.themeKeywords.join('、') || '', creatorIntent?.toneDirectives.join('、') || '', - ]), - aestheticDirectives: compactFoundationTextList([ profile.tone, profile.subtitle, - ]), - forbiddenDirectives: creatorIntent?.forbiddenDirectives ?? [], - }, - playerEntryPoint: { - openingIdentity: creatorIntent?.playerPremise || '', - openingProblem: - creatorIntent?.openingSituation || profile.coreConflicts[0] || '', - entryMotivation: profile.playerGoal, - }, - coreConflict: { - surfaceConflicts: - creatorIntent?.coreConflicts.length - ? creatorIntent.coreConflicts - : profile.coreConflicts, - hiddenCrisis: - relationshipSeed?.hiddenHook || - profile.summary || - profile.settingText, - firstTouchedConflict: - creatorIntent?.openingSituation || - profile.coreConflicts[0] || - profile.playerGoal, - }, + creatorIntent?.forbiddenDirectives.length + ? `避免:${creatorIntent.forbiddenDirectives.join('、')}` + : '', + ]).join(';') || null, + playerEntryPoint: compactFoundationTextList([ + creatorIntent?.playerPremise || '', + creatorIntent?.openingSituation || profile.coreConflicts[0] || '', + profile.playerGoal, + ]).join(';') || null, + coreConflict: compactFoundationTextList([ + (creatorIntent?.coreConflicts.length + ? creatorIntent.coreConflicts + : profile.coreConflicts + ).join('、'), + relationshipSeed?.hiddenHook || profile.summary || profile.settingText, + creatorIntent?.openingSituation || profile.coreConflicts[0] || profile.playerGoal, + ]).join(';') || null, keyRelationships: relationshipSeed - ? [ - { - pairs: compactFoundationTextList([ - relationshipSeed.name, - relationshipSeed.role, - ]).join(' · '), - relationshipType: relationshipSeed.relationToPlayer || '', - secretOrCost: relationshipSeed.hiddenHook || '', - }, - ] - : [], - hiddenLines: { - hiddenTruths: compactFoundationTextList([ - relationshipSeed?.hiddenHook || '', - profile.summary, - ]), - misdirectionHints: compactFoundationTextList([ - profile.subtitle, - profile.majorFactions[0] || '', - ]), - revealPacing: - creatorIntent?.openingSituation || - profile.coreConflicts[0] || - profile.playerGoal, - }, - iconicElements: { - iconicMotifs: - creatorIntent?.iconicElements.length - ? creatorIntent.iconicElements - : compactFoundationTextList([ - profile.anchorPack?.motifDirectives.join('、') || '', - profile.landmarks[0]?.name || '', - ]), - institutionsOrArtifacts: compactFoundationTextList([ - profile.camp?.name || '', - profile.majorFactions[0] || '', - ]), - hardRules: compactFoundationTextList([ - profile.playerGoal, - profile.coreConflicts[0] || '', - ]), - }, + ? compactFoundationTextList([ + relationshipSeed.name, + relationshipSeed.role, + relationshipSeed.relationToPlayer, + relationshipSeed.hiddenHook ? `代价/秘密:${relationshipSeed.hiddenHook}` : '', + ]).join(';') + : null, + hiddenLines: compactFoundationTextList([ + relationshipSeed?.hiddenHook || '', + profile.summary, + profile.subtitle, + profile.majorFactions[0] || '', + creatorIntent?.openingSituation || profile.coreConflicts[0] || profile.playerGoal, + ]).join(';') || null, + iconicElements: compactFoundationTextList([ + (creatorIntent?.iconicElements.length + ? creatorIntent.iconicElements + : [ + profile.anchorPack?.motifDirectives.join('、') || '', + profile.landmarks[0]?.name || '', + ] + ).join('、'), + profile.camp?.name || '', + profile.majorFactions[0] || '', + profile.playerGoal, + profile.coreConflicts[0] || '', + ]).join(';') || null, } satisfies EightAnchorContent; } @@ -187,78 +161,15 @@ export function getCustomWorldFoundationAnchorContent( return buildAnchorContentFromProfileFallback(profile); } - const worldPromiseRecord = toRecord(anchorContentRecord.worldPromise); - const playerFantasyRecord = toRecord(anchorContentRecord.playerFantasy); - const themeBoundaryRecord = toRecord(anchorContentRecord.themeBoundary); - const playerEntryPointRecord = toRecord(anchorContentRecord.playerEntryPoint); - const coreConflictRecord = toRecord(anchorContentRecord.coreConflict); - const hiddenLinesRecord = toRecord(anchorContentRecord.hiddenLines); - const iconicElementsRecord = toRecord(anchorContentRecord.iconicElements); - return { - worldPromise: worldPromiseRecord - ? { - hook: toText(worldPromiseRecord.hook), - differentiator: toText(worldPromiseRecord.differentiator), - desiredExperience: toText(worldPromiseRecord.desiredExperience), - } - : null, - playerFantasy: playerFantasyRecord - ? { - playerRole: toText(playerFantasyRecord.playerRole), - corePursuit: toText(playerFantasyRecord.corePursuit), - fearOfLoss: toText(playerFantasyRecord.fearOfLoss), - } - : null, - themeBoundary: themeBoundaryRecord - ? { - toneKeywords: toTextArray(themeBoundaryRecord.toneKeywords), - aestheticDirectives: toTextArray( - themeBoundaryRecord.aestheticDirectives, - ), - forbiddenDirectives: toTextArray(themeBoundaryRecord.forbiddenDirectives), - } - : null, - playerEntryPoint: playerEntryPointRecord - ? { - openingIdentity: toText(playerEntryPointRecord.openingIdentity), - openingProblem: toText(playerEntryPointRecord.openingProblem), - entryMotivation: toText(playerEntryPointRecord.entryMotivation), - } - : null, - coreConflict: coreConflictRecord - ? { - surfaceConflicts: toTextArray(coreConflictRecord.surfaceConflicts), - hiddenCrisis: toText(coreConflictRecord.hiddenCrisis), - firstTouchedConflict: toText(coreConflictRecord.firstTouchedConflict), - } - : null, - keyRelationships: Array.isArray(anchorContentRecord.keyRelationships) - ? anchorContentRecord.keyRelationships - .map((entry) => toRecord(entry)) - .filter(Boolean) - .map((entry) => ({ - pairs: toText(entry?.pairs), - relationshipType: toText(entry?.relationshipType), - secretOrCost: toText(entry?.secretOrCost), - })) - : [], - hiddenLines: hiddenLinesRecord - ? { - hiddenTruths: toTextArray(hiddenLinesRecord.hiddenTruths), - misdirectionHints: toTextArray(hiddenLinesRecord.misdirectionHints), - revealPacing: toText(hiddenLinesRecord.revealPacing), - } - : null, - iconicElements: iconicElementsRecord - ? { - iconicMotifs: toTextArray(iconicElementsRecord.iconicMotifs), - institutionsOrArtifacts: toTextArray( - iconicElementsRecord.institutionsOrArtifacts, - ), - hardRules: toTextArray(iconicElementsRecord.hardRules), - } - : null, + worldPromise: compactAnchorValue(anchorContentRecord.worldPromise), + playerFantasy: compactAnchorValue(anchorContentRecord.playerFantasy), + themeBoundary: compactAnchorValue(anchorContentRecord.themeBoundary), + playerEntryPoint: compactAnchorValue(anchorContentRecord.playerEntryPoint), + coreConflict: compactAnchorValue(anchorContentRecord.coreConflict), + keyRelationships: compactAnchorValue(anchorContentRecord.keyRelationships), + hiddenLines: compactAnchorValue(anchorContentRecord.hiddenLines), + iconicElements: compactAnchorValue(anchorContentRecord.iconicElements), } satisfies EightAnchorContent; } @@ -277,74 +188,42 @@ export function buildCustomWorldFoundationEntries( { id: 'world-promise', label: '世界承诺', - value: compactFoundationTextList([ - anchorContent.worldPromise?.hook || '', - anchorContent.worldPromise?.differentiator || '', - anchorContent.worldPromise?.desiredExperience || '', - ]).join(';'), + value: anchorContent.worldPromise || '', }, { id: 'player-fantasy', label: '玩家幻想', - value: compactFoundationTextList([ - anchorContent.playerFantasy?.playerRole || '', - anchorContent.playerFantasy?.corePursuit || '', - anchorContent.playerFantasy?.fearOfLoss || '', - ]).join(';'), + value: anchorContent.playerFantasy || '', }, { id: 'theme-boundary', label: '主题边界', - value: compactFoundationTextList([ - anchorContent.themeBoundary?.toneKeywords.join('、') || '', - anchorContent.themeBoundary?.aestheticDirectives.join('、') || '', - anchorContent.themeBoundary?.forbiddenDirectives.length - ? `避免:${anchorContent.themeBoundary.forbiddenDirectives.join('、')}` - : '', - ]).join(';'), + value: anchorContent.themeBoundary || '', }, { id: 'player-entry-point', label: '玩家切入口', - value: compactFoundationTextList([ - anchorContent.playerEntryPoint?.openingIdentity || '', - anchorContent.playerEntryPoint?.openingProblem || '', - anchorContent.playerEntryPoint?.entryMotivation || '', - ]).join(';'), + value: anchorContent.playerEntryPoint || '', }, { id: 'core-conflict', label: '核心冲突', - value: compactFoundationTextList([ - anchorContent.coreConflict?.surfaceConflicts.join('、') || '', - anchorContent.coreConflict?.hiddenCrisis || '', - anchorContent.coreConflict?.firstTouchedConflict || '', - ]).join(';'), + value: anchorContent.coreConflict || '', }, { id: 'key-relationships', label: '关键关系', - value: - anchorContent.keyRelationships.map(buildKeyRelationshipText).join('\n') || - fallbackRelationshipText, + value: anchorContent.keyRelationships || fallbackRelationshipText, }, { id: 'hidden-lines', label: '暗线与揭示', - value: compactFoundationTextList([ - anchorContent.hiddenLines?.hiddenTruths.join('、') || '', - anchorContent.hiddenLines?.misdirectionHints.join('、') || '', - anchorContent.hiddenLines?.revealPacing || '', - ]).join(';'), + value: anchorContent.hiddenLines || '', }, { id: 'iconic-elements', label: '标志元素', - value: compactFoundationTextList([ - anchorContent.iconicElements?.iconicMotifs.join('、') || '', - anchorContent.iconicElements?.institutionsOrArtifacts.join('、') || '', - anchorContent.iconicElements?.hardRules.join('、') || '', - ]).join(';'), + value: anchorContent.iconicElements || '', }, ]; } diff --git a/src/services/customWorldSceneActRuntime.ts b/src/services/customWorldSceneActRuntime.ts index 9668fa4e..7efc17e1 100644 --- a/src/services/customWorldSceneActRuntime.ts +++ b/src/services/customWorldSceneActRuntime.ts @@ -3,8 +3,9 @@ import type { CustomWorldProfile, GameState, SceneActBlueprint, - SceneChapterBlueprint, SceneActRuntimeState, + SceneChapterBlueprint, + SceneConnectionInfo, StoryEngineMemoryState, } from '../types'; @@ -52,6 +53,80 @@ export function resolveActiveSceneActBlueprint(params: { return chapter.acts[0] ?? null; } +export function resolveSceneActProgression(params: { + profile: CustomWorldProfile | null | undefined; + sceneId: string | null | undefined; + storyEngineMemory?: StoryEngineMemoryState | null; +}): { + chapter: SceneChapterBlueprint; + runtimeState: SceneActRuntimeState; + activeAct: SceneActBlueprint; + nextAct: SceneActBlueprint | null; + isLastAct: boolean; +} | null { + const chapter = resolveSceneChapterBlueprint(params.profile, params.sceneId); + if (!chapter || chapter.acts.length === 0) { + return null; + } + + const runtimeState = buildInitialSceneActRuntimeState(params); + if (!runtimeState) { + return null; + } + + const activeActIndex = chapter.acts.findIndex( + (entry) => entry.id === runtimeState.currentActId, + ); + const resolvedActIndex = + activeActIndex >= 0 + ? activeActIndex + : Math.min( + Math.max(runtimeState.currentActIndex, 0), + chapter.acts.length - 1, + ); + const activeAct = chapter.acts[resolvedActIndex] ?? chapter.acts[0]!; + const nextAct = chapter.acts[resolvedActIndex + 1] ?? null; + + return { + chapter, + runtimeState: { + ...runtimeState, + currentActId: activeAct.id, + currentActIndex: resolvedActIndex, + }, + activeAct, + nextAct, + isLastAct: !nextAct, + }; +} + +export function advanceSceneActRuntimeState(params: { + progress: NonNullable>; +}): SceneActRuntimeState | null { + const { progress } = params; + if (!progress.nextAct) { + return null; + } + + const completedActIds = toSet([ + ...(progress.runtimeState.completedActIds ?? []), + progress.activeAct.id, + ]); + const visitedActIds = toSet([ + ...(progress.runtimeState.visitedActIds ?? []), + progress.nextAct.id, + ]); + + return { + sceneId: progress.chapter.sceneId, + chapterId: progress.chapter.id, + currentActId: progress.nextAct.id, + currentActIndex: progress.runtimeState.currentActIndex + 1, + completedActIds: [...completedActIds], + visitedActIds: [...visitedActIds], + }; +} + export function buildInitialSceneActRuntimeState(params: { profile: CustomWorldProfile | null | undefined; sceneId: string | null | undefined; @@ -158,18 +233,48 @@ export function resolveLimitedPrimaryNpcChatState(params: { sceneId: params.state.currentScenePreset?.id ?? null, storyEngineMemory: params.state.storyEngineMemory, }); - const turnLimit = 5; - const remainingTurns = Math.max(0, turnLimit - params.nextTurnCount); - return { sceneActId: activeAct?.id ?? null, - turnLimit, - remainingTurns, + turnLimit: null, + remainingTurns: null, limitReason: 'negative_affinity' as const, - closingMode: - params.nextTurnCount >= turnLimit - ? ('foreshadow_close' as const) - : ('free' as const), - forceExitAfterTurn: params.nextTurnCount >= turnLimit, + closingMode: 'free' as const, + forceExitAfterTurn: false, + terminationMode: 'hostile_model' as const, + isHostileChat: true, }; } + +export function getSceneConnectionDirectionText( + relativePosition: SceneConnectionInfo['relativePosition'], +) { + switch (relativePosition) { + case 'north': + return '向北走'; + case 'south': + return '向南走'; + case 'east': + return '向东走'; + case 'west': + return '向西走'; + case 'left': + return '向左走'; + case 'right': + return '向右走'; + case 'back': + return '往回走'; + case 'up': + return '向上走'; + case 'down': + return '向下走'; + case 'inside': + return '向内走'; + case 'outside': + return '向外走'; + case 'portal': + return '穿过通路'; + case 'forward': + default: + return '向前走'; + } +} diff --git a/src/services/rpg-creation/rpgCreationPreviewAdapter.test.ts b/src/services/rpg-creation/rpgCreationPreviewAdapter.test.ts index 62daa143..7a92de1f 100644 --- a/src/services/rpg-creation/rpgCreationPreviewAdapter.test.ts +++ b/src/services/rpg-creation/rpgCreationPreviewAdapter.test.ts @@ -15,7 +15,7 @@ const sessionWithPreview: CustomWorldAgentSessionSnapshot = { themeBoundary: null, playerEntryPoint: null, coreConflict: null, - keyRelationships: [], + keyRelationships: null, hiddenLines: null, iconicElements: null, }, diff --git a/src/services/rpg-entry/rpgProfileClient.ts b/src/services/rpg-entry/rpgProfileClient.ts index 7fdb5a6d..6947ff68 100644 --- a/src/services/rpg-entry/rpgProfileClient.ts +++ b/src/services/rpg-entry/rpgProfileClient.ts @@ -5,10 +5,12 @@ import type { PlatformBrowseHistoryWriteEntry, ProfileDashboardSummary, ProfilePlayStatsResponse, + ProfileReferralInviteCenterResponse, ProfileRechargeCenterResponse, ProfileSaveArchiveListResponse, ProfileSaveArchiveResumeResponse, ProfileWalletLedgerResponse, + RedeemProfileReferralInviteCodeResponse, RuntimeSettings, } from '../../../packages/shared/src/contracts/runtime'; import { rehydrateSavedSnapshot } from '../../persistence/runtimeSnapshot'; @@ -96,6 +98,33 @@ export function createRpgProfileRechargeOrder( ); } +export function getRpgProfileReferralInviteCenter( + options: RuntimeRequestOptions = {}, +) { + return requestRpgRuntimeJson( + '/profile/referrals/invite-center', + { method: 'GET' }, + '读取邀请码失败', + options, + ); +} + +export function redeemRpgProfileReferralInviteCode( + inviteCode: string, + options: RuntimeRequestOptions = {}, +) { + return requestRpgRuntimeJson( + '/profile/referrals/redeem-code', + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ inviteCode }), + }, + '填写邀请码失败', + options, + ); +} + export function getRpgProfilePlayStats(options: RuntimeRequestOptions = {}) { return requestRpgRuntimeJson( '/profile/play-stats', @@ -207,6 +236,8 @@ export const rpgProfileClient = { getWalletLedger: getRpgProfileWalletLedger, getRechargeCenter: getRpgProfileRechargeCenter, createRechargeOrder: createRpgProfileRechargeOrder, + getReferralInviteCenter: getRpgProfileReferralInviteCenter, + redeemReferralInviteCode: redeemRpgProfileReferralInviteCode, getSettings: getRpgProfileSettings, putSettings: putRpgProfileSettings, listSaveArchives: listRpgProfileSaveArchives, diff --git a/src/spacetime/generated/ai_result_reference_table.ts b/src/spacetime/generated/ai_result_reference_table.ts new file mode 100644 index 00000000..7e235591 --- /dev/null +++ b/src/spacetime/generated/ai_result_reference_table.ts @@ -0,0 +1,27 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from "spacetimedb"; +import { + AiResultReferenceKind, +} from "./types"; + + +export default __t.row({ + resultReferenceRowId: __t.string().primaryKey().name("result_reference_row_id"), + resultRefId: __t.string().name("result_ref_id"), + taskId: __t.string().name("task_id"), + get referenceKind() { + return AiResultReferenceKind.name("reference_kind"); + }, + referenceId: __t.string().name("reference_id"), + label: __t.option(__t.string()), + createdAt: __t.timestamp().name("created_at"), +}); diff --git a/src/spacetime/generated/ai_task_stage_table.ts b/src/spacetime/generated/ai_task_stage_table.ts new file mode 100644 index 00000000..b7b70620 --- /dev/null +++ b/src/spacetime/generated/ai_task_stage_table.ts @@ -0,0 +1,35 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from "spacetimedb"; +import { + AiTaskStageKind, + AiTaskStageStatus, +} from "./types"; + + +export default __t.row({ + taskStageId: __t.string().primaryKey().name("task_stage_id"), + taskId: __t.string().name("task_id"), + get stageKind() { + return AiTaskStageKind.name("stage_kind"); + }, + label: __t.string(), + detail: __t.string(), + stageOrder: __t.u32().name("stage_order"), + get status() { + return AiTaskStageStatus; + }, + textOutput: __t.option(__t.string()).name("text_output"), + structuredPayloadJson: __t.option(__t.string()).name("structured_payload_json"), + warningMessages: __t.array(__t.string()).name("warning_messages"), + startedAt: __t.option(__t.timestamp()).name("started_at"), + completedAt: __t.option(__t.timestamp()).name("completed_at"), +}); diff --git a/src/spacetime/generated/ai_task_table.ts b/src/spacetime/generated/ai_task_table.ts new file mode 100644 index 00000000..69f7791c --- /dev/null +++ b/src/spacetime/generated/ai_task_table.ts @@ -0,0 +1,39 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from "spacetimedb"; +import { + AiTaskKind, + AiTaskStatus, +} from "./types"; + + +export default __t.row({ + taskId: __t.string().primaryKey().name("task_id"), + get taskKind() { + return AiTaskKind.name("task_kind"); + }, + ownerUserId: __t.string().name("owner_user_id"), + requestLabel: __t.string().name("request_label"), + sourceModule: __t.string().name("source_module"), + sourceEntityId: __t.option(__t.string()).name("source_entity_id"), + requestPayloadJson: __t.option(__t.string()).name("request_payload_json"), + get status() { + return AiTaskStatus; + }, + failureMessage: __t.option(__t.string()).name("failure_message"), + latestTextOutput: __t.option(__t.string()).name("latest_text_output"), + latestStructuredPayloadJson: __t.option(__t.string()).name("latest_structured_payload_json"), + version: __t.u32(), + createdAt: __t.timestamp().name("created_at"), + startedAt: __t.option(__t.timestamp()).name("started_at"), + completedAt: __t.option(__t.timestamp()).name("completed_at"), + updatedAt: __t.timestamp().name("updated_at"), +}); diff --git a/src/spacetime/generated/ai_text_chunk_table.ts b/src/spacetime/generated/ai_text_chunk_table.ts new file mode 100644 index 00000000..47dbf934 --- /dev/null +++ b/src/spacetime/generated/ai_text_chunk_table.ts @@ -0,0 +1,27 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from "spacetimedb"; +import { + AiTaskStageKind, +} from "./types"; + + +export default __t.row({ + textChunkRowId: __t.string().primaryKey().name("text_chunk_row_id"), + chunkId: __t.string().name("chunk_id"), + taskId: __t.string().name("task_id"), + get stageKind() { + return AiTaskStageKind.name("stage_kind"); + }, + sequence: __t.u32(), + deltaText: __t.string().name("delta_text"), + createdAt: __t.timestamp().name("created_at"), +}); diff --git a/src/spacetime/generated/asset_entity_binding_table.ts b/src/spacetime/generated/asset_entity_binding_table.ts new file mode 100644 index 00000000..81c7e55b --- /dev/null +++ b/src/spacetime/generated/asset_entity_binding_table.ts @@ -0,0 +1,24 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from "spacetimedb"; + +export default __t.row({ + bindingId: __t.string().primaryKey().name("binding_id"), + assetObjectId: __t.string().name("asset_object_id"), + entityKind: __t.string().name("entity_kind"), + entityId: __t.string().name("entity_id"), + slot: __t.string(), + assetKind: __t.string().name("asset_kind"), + ownerUserId: __t.option(__t.string()).name("owner_user_id"), + profileId: __t.option(__t.string()).name("profile_id"), + createdAt: __t.timestamp().name("created_at"), + updatedAt: __t.timestamp().name("updated_at"), +}); diff --git a/src/spacetime/generated/asset_object_table.ts b/src/spacetime/generated/asset_object_table.ts new file mode 100644 index 00000000..987c3ece --- /dev/null +++ b/src/spacetime/generated/asset_object_table.ts @@ -0,0 +1,35 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from "spacetimedb"; +import { + AssetObjectAccessPolicy, +} from "./types"; + + +export default __t.row({ + assetObjectId: __t.string().primaryKey().name("asset_object_id"), + bucket: __t.string(), + objectKey: __t.string().name("object_key"), + get accessPolicy() { + return AssetObjectAccessPolicy.name("access_policy"); + }, + contentType: __t.option(__t.string()).name("content_type"), + contentLength: __t.u64().name("content_length"), + contentHash: __t.option(__t.string()).name("content_hash"), + version: __t.u32(), + sourceJobId: __t.option(__t.string()).name("source_job_id"), + ownerUserId: __t.option(__t.string()).name("owner_user_id"), + profileId: __t.option(__t.string()).name("profile_id"), + entityId: __t.option(__t.string()).name("entity_id"), + assetKind: __t.string().name("asset_kind"), + createdAt: __t.timestamp().name("created_at"), + updatedAt: __t.timestamp().name("updated_at"), +}); diff --git a/src/spacetime/generated/auth_identity_table.ts b/src/spacetime/generated/auth_identity_table.ts new file mode 100644 index 00000000..3ef5d0f4 --- /dev/null +++ b/src/spacetime/generated/auth_identity_table.ts @@ -0,0 +1,22 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from "spacetimedb"; + +export default __t.row({ + identityId: __t.string().primaryKey().name("identity_id"), + userId: __t.string().name("user_id"), + provider: __t.string(), + providerUid: __t.string().name("provider_uid"), + providerUnionId: __t.option(__t.string()).name("provider_union_id"), + phoneE164: __t.option(__t.string()).name("phone_e_164"), + displayName: __t.option(__t.string()).name("display_name"), + avatarUrl: __t.option(__t.string()).name("avatar_url"), +}); diff --git a/src/spacetime/generated/auth_store_snapshot_table.ts b/src/spacetime/generated/auth_store_snapshot_table.ts new file mode 100644 index 00000000..f403da1b --- /dev/null +++ b/src/spacetime/generated/auth_store_snapshot_table.ts @@ -0,0 +1,17 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from "spacetimedb"; + +export default __t.row({ + snapshotId: __t.string().primaryKey().name("snapshot_id"), + snapshotJson: __t.string().name("snapshot_json"), + updatedAt: __t.timestamp().name("updated_at"), +}); diff --git a/src/spacetime/generated/battle_state_table.ts b/src/spacetime/generated/battle_state_table.ts new file mode 100644 index 00000000..d2f9de7f --- /dev/null +++ b/src/spacetime/generated/battle_state_table.ts @@ -0,0 +1,56 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from "spacetimedb"; +import { + BattleMode, + BattleStatus, + RuntimeItemRewardItemSnapshot, + CombatOutcome, +} from "./types"; + + +export default __t.row({ + battleStateId: __t.string().primaryKey().name("battle_state_id"), + storySessionId: __t.string().name("story_session_id"), + runtimeSessionId: __t.string().name("runtime_session_id"), + actorUserId: __t.string().name("actor_user_id"), + chapterId: __t.option(__t.string()).name("chapter_id"), + targetNpcId: __t.string().name("target_npc_id"), + targetName: __t.string().name("target_name"), + get battleMode() { + return BattleMode.name("battle_mode"); + }, + get status() { + return BattleStatus; + }, + playerHp: __t.i32().name("player_hp"), + playerMaxHp: __t.i32().name("player_max_hp"), + playerMana: __t.i32().name("player_mana"), + playerMaxMana: __t.i32().name("player_max_mana"), + targetHp: __t.i32().name("target_hp"), + targetMaxHp: __t.i32().name("target_max_hp"), + experienceReward: __t.u32().name("experience_reward"), + get rewardItems() { + return __t.array(RuntimeItemRewardItemSnapshot).name("reward_items"); + }, + turnIndex: __t.u32().name("turn_index"), + lastActionFunctionId: __t.option(__t.string()).name("last_action_function_id"), + lastActionText: __t.option(__t.string()).name("last_action_text"), + lastResultText: __t.option(__t.string()).name("last_result_text"), + lastDamageDealt: __t.i32().name("last_damage_dealt"), + lastDamageTaken: __t.i32().name("last_damage_taken"), + get lastOutcome() { + return CombatOutcome.name("last_outcome"); + }, + version: __t.u32(), + createdAt: __t.timestamp().name("created_at"), + updatedAt: __t.timestamp().name("updated_at"), +}); diff --git a/src/spacetime/generated/big_fish_agent_message_table.ts b/src/spacetime/generated/big_fish_agent_message_table.ts new file mode 100644 index 00000000..f6b70f56 --- /dev/null +++ b/src/spacetime/generated/big_fish_agent_message_table.ts @@ -0,0 +1,29 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from "spacetimedb"; +import { + BigFishAgentMessageRole, + BigFishAgentMessageKind, +} from "./types"; + + +export default __t.row({ + messageId: __t.string().primaryKey().name("message_id"), + sessionId: __t.string().name("session_id"), + get role() { + return BigFishAgentMessageRole; + }, + get kind() { + return BigFishAgentMessageKind; + }, + text: __t.string(), + createdAt: __t.timestamp().name("created_at"), +}); diff --git a/src/spacetime/generated/big_fish_asset_slot_table.ts b/src/spacetime/generated/big_fish_asset_slot_table.ts new file mode 100644 index 00000000..999c9856 --- /dev/null +++ b/src/spacetime/generated/big_fish_asset_slot_table.ts @@ -0,0 +1,32 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from "spacetimedb"; +import { + BigFishAssetKind, + BigFishAssetStatus, +} from "./types"; + + +export default __t.row({ + slotId: __t.string().primaryKey().name("slot_id"), + sessionId: __t.string().name("session_id"), + get assetKind() { + return BigFishAssetKind.name("asset_kind"); + }, + level: __t.option(__t.u32()), + motionKey: __t.option(__t.string()).name("motion_key"), + get status() { + return BigFishAssetStatus; + }, + assetUrl: __t.option(__t.string()).name("asset_url"), + promptSnapshot: __t.string().name("prompt_snapshot"), + updatedAt: __t.timestamp().name("updated_at"), +}); diff --git a/src/spacetime/generated/big_fish_creation_session_table.ts b/src/spacetime/generated/big_fish_creation_session_table.ts new file mode 100644 index 00000000..ee8cc691 --- /dev/null +++ b/src/spacetime/generated/big_fish_creation_session_table.ts @@ -0,0 +1,33 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from "spacetimedb"; +import { + BigFishCreationStage, +} from "./types"; + + +export default __t.row({ + sessionId: __t.string().primaryKey().name("session_id"), + ownerUserId: __t.string().name("owner_user_id"), + seedText: __t.string().name("seed_text"), + currentTurn: __t.u32().name("current_turn"), + progressPercent: __t.u32().name("progress_percent"), + get stage() { + return BigFishCreationStage; + }, + anchorPackJson: __t.string().name("anchor_pack_json"), + draftJson: __t.option(__t.string()).name("draft_json"), + assetCoverageJson: __t.string().name("asset_coverage_json"), + lastAssistantReply: __t.option(__t.string()).name("last_assistant_reply"), + publishReady: __t.bool().name("publish_ready"), + createdAt: __t.timestamp().name("created_at"), + updatedAt: __t.timestamp().name("updated_at"), +}); diff --git a/src/spacetime/generated/big_fish_runtime_run_table.ts b/src/spacetime/generated/big_fish_runtime_run_table.ts new file mode 100644 index 00000000..7a8c701e --- /dev/null +++ b/src/spacetime/generated/big_fish_runtime_run_table.ts @@ -0,0 +1,30 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from "spacetimedb"; +import { + BigFishRunStatus, +} from "./types"; + + +export default __t.row({ + runId: __t.string().primaryKey().name("run_id"), + sessionId: __t.string().name("session_id"), + ownerUserId: __t.string().name("owner_user_id"), + get status() { + return BigFishRunStatus; + }, + snapshotJson: __t.string().name("snapshot_json"), + lastInputX: __t.f32().name("last_input_x"), + lastInputY: __t.f32().name("last_input_y"), + tick: __t.u64(), + createdAt: __t.timestamp().name("created_at"), + updatedAt: __t.timestamp().name("updated_at"), +}); diff --git a/src/spacetime/generated/chapter_progression_table.ts b/src/spacetime/generated/chapter_progression_table.ts new file mode 100644 index 00000000..cb522360 --- /dev/null +++ b/src/spacetime/generated/chapter_progression_table.ts @@ -0,0 +1,41 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from "spacetimedb"; +import { + ChapterPaceBand, +} from "./types"; + + +export default __t.row({ + chapterProgressionId: __t.string().primaryKey().name("chapter_progression_id"), + userId: __t.string().name("user_id"), + chapterId: __t.string().name("chapter_id"), + chapterIndex: __t.u32().name("chapter_index"), + totalChapters: __t.u32().name("total_chapters"), + entryPseudoLevelMillis: __t.u32().name("entry_pseudo_level_millis"), + exitPseudoLevelMillis: __t.u32().name("exit_pseudo_level_millis"), + entryLevel: __t.u32().name("entry_level"), + exitLevel: __t.u32().name("exit_level"), + plannedTotalXp: __t.u32().name("planned_total_xp"), + plannedQuestXp: __t.u32().name("planned_quest_xp"), + plannedHostileXp: __t.u32().name("planned_hostile_xp"), + actualQuestXp: __t.u32().name("actual_quest_xp"), + actualHostileXp: __t.u32().name("actual_hostile_xp"), + expectedHostileDefeatCount: __t.u32().name("expected_hostile_defeat_count"), + actualHostileDefeatCount: __t.u32().name("actual_hostile_defeat_count"), + levelAtEntry: __t.u32().name("level_at_entry"), + levelAtExit: __t.option(__t.u32()).name("level_at_exit"), + get paceBand() { + return ChapterPaceBand.name("pace_band"); + }, + createdAt: __t.timestamp().name("created_at"), + updatedAt: __t.timestamp().name("updated_at"), +}); diff --git a/src/spacetime/generated/custom_world_agent_message_table.ts b/src/spacetime/generated/custom_world_agent_message_table.ts new file mode 100644 index 00000000..faf4b0f6 --- /dev/null +++ b/src/spacetime/generated/custom_world_agent_message_table.ts @@ -0,0 +1,30 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from "spacetimedb"; +import { + RpgAgentMessageRole, + RpgAgentMessageKind, +} from "./types"; + + +export default __t.row({ + messageId: __t.string().primaryKey().name("message_id"), + sessionId: __t.string().name("session_id"), + get role() { + return RpgAgentMessageRole; + }, + get kind() { + return RpgAgentMessageKind; + }, + text: __t.string(), + relatedOperationId: __t.option(__t.string()).name("related_operation_id"), + createdAt: __t.timestamp().name("created_at"), +}); diff --git a/src/spacetime/generated/custom_world_agent_operation_table.ts b/src/spacetime/generated/custom_world_agent_operation_table.ts new file mode 100644 index 00000000..0e3a615a --- /dev/null +++ b/src/spacetime/generated/custom_world_agent_operation_table.ts @@ -0,0 +1,33 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from "spacetimedb"; +import { + RpgAgentOperationType, + RpgAgentOperationStatus, +} from "./types"; + + +export default __t.row({ + operationId: __t.string().primaryKey().name("operation_id"), + sessionId: __t.string().name("session_id"), + get operationType() { + return RpgAgentOperationType.name("operation_type"); + }, + get status() { + return RpgAgentOperationStatus; + }, + phaseLabel: __t.string().name("phase_label"), + phaseDetail: __t.string().name("phase_detail"), + progress: __t.u32(), + errorMessage: __t.option(__t.string()).name("error_message"), + createdAt: __t.timestamp().name("created_at"), + updatedAt: __t.timestamp().name("updated_at"), +}); diff --git a/src/spacetime/generated/custom_world_agent_session_table.ts b/src/spacetime/generated/custom_world_agent_session_table.ts new file mode 100644 index 00000000..9732e2fc --- /dev/null +++ b/src/spacetime/generated/custom_world_agent_session_table.ts @@ -0,0 +1,44 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from "spacetimedb"; +import { + RpgAgentStage, +} from "./types"; + + +export default __t.row({ + sessionId: __t.string().primaryKey().name("session_id"), + ownerUserId: __t.string().name("owner_user_id"), + seedText: __t.string().name("seed_text"), + currentTurn: __t.u32().name("current_turn"), + progressPercent: __t.u32().name("progress_percent"), + get stage() { + return RpgAgentStage; + }, + focusCardId: __t.option(__t.string()).name("focus_card_id"), + anchorContentJson: __t.string().name("anchor_content_json"), + creatorIntentJson: __t.option(__t.string()).name("creator_intent_json"), + creatorIntentReadinessJson: __t.string().name("creator_intent_readiness_json"), + anchorPackJson: __t.option(__t.string()).name("anchor_pack_json"), + lockStateJson: __t.option(__t.string()).name("lock_state_json"), + draftProfileJson: __t.option(__t.string()).name("draft_profile_json"), + lastAssistantReply: __t.option(__t.string()).name("last_assistant_reply"), + publishGateJson: __t.option(__t.string()).name("publish_gate_json"), + resultPreviewJson: __t.option(__t.string()).name("result_preview_json"), + pendingClarificationsJson: __t.string().name("pending_clarifications_json"), + qualityFindingsJson: __t.string().name("quality_findings_json"), + suggestedActionsJson: __t.string().name("suggested_actions_json"), + recommendedRepliesJson: __t.string().name("recommended_replies_json"), + assetCoverageJson: __t.string().name("asset_coverage_json"), + checkpointsJson: __t.string().name("checkpoints_json"), + createdAt: __t.timestamp().name("created_at"), + updatedAt: __t.timestamp().name("updated_at"), +}); diff --git a/src/spacetime/generated/custom_world_draft_card_table.ts b/src/spacetime/generated/custom_world_draft_card_table.ts new file mode 100644 index 00000000..29f20c29 --- /dev/null +++ b/src/spacetime/generated/custom_world_draft_card_table.ts @@ -0,0 +1,40 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from "spacetimedb"; +import { + RpgAgentDraftCardKind, + RpgAgentDraftCardStatus, + CustomWorldRoleAssetStatus, +} from "./types"; + + +export default __t.row({ + cardId: __t.string().primaryKey().name("card_id"), + sessionId: __t.string().name("session_id"), + get kind() { + return RpgAgentDraftCardKind; + }, + get status() { + return RpgAgentDraftCardStatus; + }, + title: __t.string(), + subtitle: __t.string(), + summary: __t.string(), + linkedIdsJson: __t.string().name("linked_ids_json"), + warningCount: __t.u32().name("warning_count"), + get assetStatus() { + return __t.option(CustomWorldRoleAssetStatus).name("asset_status"); + }, + assetStatusLabel: __t.option(__t.string()).name("asset_status_label"), + detailPayloadJson: __t.option(__t.string()).name("detail_payload_json"), + createdAt: __t.timestamp().name("created_at"), + updatedAt: __t.timestamp().name("updated_at"), +}); diff --git a/src/spacetime/generated/custom_world_profile_table.ts b/src/spacetime/generated/custom_world_profile_table.ts new file mode 100644 index 00000000..f42be9e5 --- /dev/null +++ b/src/spacetime/generated/custom_world_profile_table.ts @@ -0,0 +1,42 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from "spacetimedb"; +import { + CustomWorldThemeMode, + CustomWorldPublicationStatus, +} from "./types"; + + +export default __t.row({ + profileId: __t.string().primaryKey().name("profile_id"), + ownerUserId: __t.string().name("owner_user_id"), + publicWorkCode: __t.option(__t.string()).name("public_work_code"), + authorPublicUserCode: __t.option(__t.string()).name("author_public_user_code"), + sourceAgentSessionId: __t.option(__t.string()).name("source_agent_session_id"), + get publicationStatus() { + return CustomWorldPublicationStatus.name("publication_status"); + }, + worldName: __t.string().name("world_name"), + subtitle: __t.string(), + summaryText: __t.string().name("summary_text"), + get themeMode() { + return CustomWorldThemeMode.name("theme_mode"); + }, + coverImageSrc: __t.option(__t.string()).name("cover_image_src"), + profilePayloadJson: __t.string().name("profile_payload_json"), + playableNpcCount: __t.u32().name("playable_npc_count"), + landmarkCount: __t.u32().name("landmark_count"), + authorDisplayName: __t.string().name("author_display_name"), + publishedAt: __t.option(__t.timestamp()).name("published_at"), + deletedAt: __t.option(__t.timestamp()).name("deleted_at"), + createdAt: __t.timestamp().name("created_at"), + updatedAt: __t.timestamp().name("updated_at"), +}); diff --git a/src/spacetime/generated/custom_world_session_table.ts b/src/spacetime/generated/custom_world_session_table.ts new file mode 100644 index 00000000..3e3f8f08 --- /dev/null +++ b/src/spacetime/generated/custom_world_session_table.ts @@ -0,0 +1,34 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from "spacetimedb"; +import { + CustomWorldGenerationMode, + CustomWorldSessionStatus, +} from "./types"; + + +export default __t.row({ + sessionId: __t.string().primaryKey().name("session_id"), + ownerUserId: __t.string().name("owner_user_id"), + get generationMode() { + return CustomWorldGenerationMode.name("generation_mode"); + }, + get status() { + return CustomWorldSessionStatus; + }, + settingText: __t.string().name("setting_text"), + creatorIntentJson: __t.option(__t.string()).name("creator_intent_json"), + questionSnapshotJson: __t.string().name("question_snapshot_json"), + resultPayloadJson: __t.option(__t.string()).name("result_payload_json"), + lastErrorMessage: __t.option(__t.string()).name("last_error_message"), + createdAt: __t.timestamp().name("created_at"), + updatedAt: __t.timestamp().name("updated_at"), +}); diff --git a/src/spacetime/generated/get_profile_referral_invite_center_procedure.ts b/src/spacetime/generated/get_profile_referral_invite_center_procedure.ts new file mode 100644 index 00000000..f784aa1e --- /dev/null +++ b/src/spacetime/generated/get_profile_referral_invite_center_procedure.ts @@ -0,0 +1,23 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from "spacetimedb"; + +import { + RuntimeReferralInviteCenterGetInput, + RuntimeReferralInviteCenterProcedureResult, +} from "./types"; + +export const params = { + get input() { + return RuntimeReferralInviteCenterGetInput; + }, +}; +export const returnType = RuntimeReferralInviteCenterProcedureResult \ No newline at end of file diff --git a/src/spacetime/generated/index.ts b/src/spacetime/generated/index.ts index 3113b9d4..2c9e7cf7 100644 --- a/src/spacetime/generated/index.ts +++ b/src/spacetime/generated/index.ts @@ -109,6 +109,7 @@ import * as GetPlayerProgressionOrDefaultProcedure from "./get_player_progressio import * as GetProfileDashboardProcedure from "./get_profile_dashboard_procedure"; import * as GetProfilePlayStatsProcedure from "./get_profile_play_stats_procedure"; import * as GetProfileRechargeCenterProcedure from "./get_profile_recharge_center_procedure"; +import * as GetProfileReferralInviteCenterProcedure from "./get_profile_referral_invite_center_procedure"; import * as GetPuzzleAgentSessionProcedure from "./get_puzzle_agent_session_procedure"; import * as GetPuzzleGalleryDetailProcedure from "./get_puzzle_gallery_detail_procedure"; import * as GetPuzzleRunProcedure from "./get_puzzle_run_procedure"; @@ -132,6 +133,7 @@ import * as PublishBigFishGameProcedure from "./publish_big_fish_game_procedure" import * as PublishCustomWorldProfileAndReturnProcedure from "./publish_custom_world_profile_and_return_procedure"; import * as PublishCustomWorldWorldProcedure from "./publish_custom_world_world_procedure"; import * as PublishPuzzleWorkProcedure from "./publish_puzzle_work_procedure"; +import * as RedeemProfileReferralInviteCodeProcedure from "./redeem_profile_referral_invite_code_procedure"; import * as ResolveCombatActionAndReturnProcedure from "./resolve_combat_action_and_return_procedure"; import * as ResolveNpcBattleInteractionAndReturnProcedure from "./resolve_npc_battle_interaction_and_return_procedure"; import * as ResolveNpcInteractionAndReturnProcedure from "./resolve_npc_interaction_and_return_procedure"; @@ -159,12 +161,356 @@ import * as UpsertRuntimeSettingAndReturnProcedure from "./upsert_runtime_settin import * as UpsertRuntimeSnapshotAndReturnProcedure from "./upsert_runtime_snapshot_and_return_procedure"; // Import all table schema definitions +import AiResultReferenceRow from "./ai_result_reference_table"; +import AiTaskRow from "./ai_task_table"; +import AiTaskStageRow from "./ai_task_stage_table"; +import AiTextChunkRow from "./ai_text_chunk_table"; +import AssetEntityBindingRow from "./asset_entity_binding_table"; +import AssetObjectRow from "./asset_object_table"; +import AuthIdentityRow from "./auth_identity_table"; +import AuthStoreSnapshotRow from "./auth_store_snapshot_table"; +import BattleStateRow from "./battle_state_table"; +import BigFishAgentMessageRow from "./big_fish_agent_message_table"; +import BigFishAssetSlotRow from "./big_fish_asset_slot_table"; +import BigFishCreationSessionRow from "./big_fish_creation_session_table"; +import BigFishRuntimeRunRow from "./big_fish_runtime_run_table"; +import ChapterProgressionRow from "./chapter_progression_table"; +import CustomWorldAgentMessageRow from "./custom_world_agent_message_table"; +import CustomWorldAgentOperationRow from "./custom_world_agent_operation_table"; +import CustomWorldAgentSessionRow from "./custom_world_agent_session_table"; +import CustomWorldDraftCardRow from "./custom_world_draft_card_table"; import CustomWorldGalleryEntryRow from "./custom_world_gallery_entry_table"; +import CustomWorldProfileRow from "./custom_world_profile_table"; +import CustomWorldSessionRow from "./custom_world_session_table"; +import InventorySlotRow from "./inventory_slot_table"; +import NpcStateRow from "./npc_state_table"; +import PlayerProgressionRow from "./player_progression_table"; +import ProfileDashboardStateRow from "./profile_dashboard_state_table"; +import ProfileInviteCodeRow from "./profile_invite_code_table"; +import ProfileMembershipRow from "./profile_membership_table"; +import ProfilePlayedWorldRow from "./profile_played_world_table"; +import ProfileRechargeOrderRow from "./profile_recharge_order_table"; +import ProfileReferralRelationRow from "./profile_referral_relation_table"; +import ProfileSaveArchiveRow from "./profile_save_archive_table"; +import ProfileWalletLedgerRow from "./profile_wallet_ledger_table"; +import PuzzleAgentMessageRow from "./puzzle_agent_message_table"; +import PuzzleAgentSessionRow from "./puzzle_agent_session_table"; +import PuzzleRuntimeRunRow from "./puzzle_runtime_run_table"; +import PuzzleWorkProfileRow from "./puzzle_work_profile_table"; +import QuestLogRow from "./quest_log_table"; +import QuestRecordRow from "./quest_record_table"; +import RefreshSessionRow from "./refresh_session_table"; +import RuntimeSettingRow from "./runtime_setting_table"; +import RuntimeSnapshotRow from "./runtime_snapshot_table"; +import StoryEventRow from "./story_event_table"; +import StorySessionRow from "./story_session_table"; +import TreasureRecordRow from "./treasure_record_table"; +import UserAccountRow from "./user_account_table"; +import UserBrowseHistoryRow from "./user_browse_history_table"; /** Type-only namespace exports for generated type groups. */ /** The schema information for all tables in this module. This is defined the same was as the tables would have been defined in the server. */ const tablesSchema = __schema({ + ai_result_reference: __table({ + name: 'ai_result_reference', + indexes: [ + { accessor: 'result_reference_row_id', name: 'ai_result_reference_result_reference_row_id_idx_btree', algorithm: 'btree', columns: [ + 'resultReferenceRowId', + ] }, + { accessor: 'by_ai_result_reference_task_id', name: 'ai_result_reference_task_id_idx_btree', algorithm: 'btree', columns: [ + 'taskId', + ] }, + ], + constraints: [ + { name: 'ai_result_reference_result_reference_row_id_key', constraint: 'unique', columns: ['resultReferenceRowId'] }, + ], + }, AiResultReferenceRow), + ai_task: __table({ + name: 'ai_task', + indexes: [ + { accessor: 'by_ai_task_owner_user_id', name: 'ai_task_owner_user_id_idx_btree', algorithm: 'btree', columns: [ + 'ownerUserId', + ] }, + { accessor: 'by_ai_task_status', name: 'ai_task_status_idx_btree', algorithm: 'btree', columns: [ + 'status', + ] }, + { accessor: 'task_id', name: 'ai_task_task_id_idx_btree', algorithm: 'btree', columns: [ + 'taskId', + ] }, + { accessor: 'by_ai_task_kind', name: 'ai_task_task_kind_idx_btree', algorithm: 'btree', columns: [ + 'taskKind', + ] }, + ], + constraints: [ + { name: 'ai_task_task_id_key', constraint: 'unique', columns: ['taskId'] }, + ], + }, AiTaskRow), + ai_task_stage: __table({ + name: 'ai_task_stage', + indexes: [ + { accessor: 'by_ai_task_stage_task_id', name: 'ai_task_stage_task_id_idx_btree', algorithm: 'btree', columns: [ + 'taskId', + ] }, + { accessor: 'by_ai_task_stage_task_order', name: 'ai_task_stage_task_id_stage_order_idx_btree', algorithm: 'btree', columns: [ + 'taskId', + 'stageOrder', + ] }, + { accessor: 'task_stage_id', name: 'ai_task_stage_task_stage_id_idx_btree', algorithm: 'btree', columns: [ + 'taskStageId', + ] }, + ], + constraints: [ + { name: 'ai_task_stage_task_stage_id_key', constraint: 'unique', columns: ['taskStageId'] }, + ], + }, AiTaskStageRow), + ai_text_chunk: __table({ + name: 'ai_text_chunk', + indexes: [ + { accessor: 'by_ai_text_chunk_task_id', name: 'ai_text_chunk_task_id_idx_btree', algorithm: 'btree', columns: [ + 'taskId', + ] }, + { accessor: 'by_ai_text_chunk_task_stage_sequence', name: 'ai_text_chunk_task_id_stage_kind_sequence_idx_btree', algorithm: 'btree', columns: [ + 'taskId', + 'stageKind', + 'sequence', + ] }, + { accessor: 'text_chunk_row_id', name: 'ai_text_chunk_text_chunk_row_id_idx_btree', algorithm: 'btree', columns: [ + 'textChunkRowId', + ] }, + ], + constraints: [ + { name: 'ai_text_chunk_text_chunk_row_id_key', constraint: 'unique', columns: ['textChunkRowId'] }, + ], + }, AiTextChunkRow), + asset_entity_binding: __table({ + name: 'asset_entity_binding', + indexes: [ + { accessor: 'by_asset_object_id', name: 'asset_entity_binding_asset_object_id_idx_btree', algorithm: 'btree', columns: [ + 'assetObjectId', + ] }, + { accessor: 'binding_id', name: 'asset_entity_binding_binding_id_idx_btree', algorithm: 'btree', columns: [ + 'bindingId', + ] }, + { accessor: 'by_entity_slot', name: 'asset_entity_binding_entity_kind_entity_id_slot_idx_btree', algorithm: 'btree', columns: [ + 'entityKind', + 'entityId', + 'slot', + ] }, + ], + constraints: [ + { name: 'asset_entity_binding_binding_id_key', constraint: 'unique', columns: ['bindingId'] }, + ], + }, AssetEntityBindingRow), + asset_object: __table({ + name: 'asset_object', + indexes: [ + { accessor: 'asset_kind', name: 'asset_object_asset_kind_idx_btree', algorithm: 'btree', columns: [ + 'assetKind', + ] }, + { accessor: 'asset_object_id', name: 'asset_object_asset_object_id_idx_btree', algorithm: 'btree', columns: [ + 'assetObjectId', + ] }, + { accessor: 'by_bucket_object_key', name: 'asset_object_bucket_object_key_idx_btree', algorithm: 'btree', columns: [ + 'bucket', + 'objectKey', + ] }, + ], + constraints: [ + { name: 'asset_object_asset_object_id_key', constraint: 'unique', columns: ['assetObjectId'] }, + ], + }, AssetObjectRow), + auth_identity: __table({ + name: 'auth_identity', + indexes: [ + { accessor: 'identity_id', name: 'auth_identity_identity_id_idx_btree', algorithm: 'btree', columns: [ + 'identityId', + ] }, + { accessor: 'by_auth_identity_provider_uid', name: 'auth_identity_provider_provider_uid_idx_btree', algorithm: 'btree', columns: [ + 'provider', + 'providerUid', + ] }, + { accessor: 'by_auth_identity_user_id', name: 'auth_identity_user_id_idx_btree', algorithm: 'btree', columns: [ + 'userId', + ] }, + ], + constraints: [ + { name: 'auth_identity_identity_id_key', constraint: 'unique', columns: ['identityId'] }, + ], + }, AuthIdentityRow), + auth_store_snapshot: __table({ + name: 'auth_store_snapshot', + indexes: [ + { accessor: 'snapshot_id', name: 'auth_store_snapshot_snapshot_id_idx_btree', algorithm: 'btree', columns: [ + 'snapshotId', + ] }, + ], + constraints: [ + { name: 'auth_store_snapshot_snapshot_id_key', constraint: 'unique', columns: ['snapshotId'] }, + ], + }, AuthStoreSnapshotRow), + battle_state: __table({ + name: 'battle_state', + indexes: [ + { accessor: 'by_battle_actor_user_id', name: 'battle_state_actor_user_id_idx_btree', algorithm: 'btree', columns: [ + 'actorUserId', + ] }, + { accessor: 'battle_state_id', name: 'battle_state_battle_state_id_idx_btree', algorithm: 'btree', columns: [ + 'battleStateId', + ] }, + { accessor: 'by_battle_runtime_session_id', name: 'battle_state_runtime_session_id_idx_btree', algorithm: 'btree', columns: [ + 'runtimeSessionId', + ] }, + { accessor: 'by_battle_story_session_id', name: 'battle_state_story_session_id_idx_btree', algorithm: 'btree', columns: [ + 'storySessionId', + ] }, + ], + constraints: [ + { name: 'battle_state_battle_state_id_key', constraint: 'unique', columns: ['battleStateId'] }, + ], + }, BattleStateRow), + big_fish_agent_message: __table({ + name: 'big_fish_agent_message', + indexes: [ + { accessor: 'message_id', name: 'big_fish_agent_message_message_id_idx_btree', algorithm: 'btree', columns: [ + 'messageId', + ] }, + { accessor: 'by_big_fish_message_session_id', name: 'big_fish_agent_message_session_id_idx_btree', algorithm: 'btree', columns: [ + 'sessionId', + ] }, + ], + constraints: [ + { name: 'big_fish_agent_message_message_id_key', constraint: 'unique', columns: ['messageId'] }, + ], + }, BigFishAgentMessageRow), + big_fish_asset_slot: __table({ + name: 'big_fish_asset_slot', + indexes: [ + { accessor: 'by_big_fish_asset_session_id', name: 'big_fish_asset_slot_session_id_idx_btree', algorithm: 'btree', columns: [ + 'sessionId', + ] }, + { accessor: 'slot_id', name: 'big_fish_asset_slot_slot_id_idx_btree', algorithm: 'btree', columns: [ + 'slotId', + ] }, + ], + constraints: [ + { name: 'big_fish_asset_slot_slot_id_key', constraint: 'unique', columns: ['slotId'] }, + ], + }, BigFishAssetSlotRow), + big_fish_creation_session: __table({ + name: 'big_fish_creation_session', + indexes: [ + { accessor: 'by_big_fish_session_owner_user_id', name: 'big_fish_creation_session_owner_user_id_idx_btree', algorithm: 'btree', columns: [ + 'ownerUserId', + ] }, + { accessor: 'session_id', name: 'big_fish_creation_session_session_id_idx_btree', algorithm: 'btree', columns: [ + 'sessionId', + ] }, + ], + constraints: [ + { name: 'big_fish_creation_session_session_id_key', constraint: 'unique', columns: ['sessionId'] }, + ], + }, BigFishCreationSessionRow), + big_fish_runtime_run: __table({ + name: 'big_fish_runtime_run', + indexes: [ + { accessor: 'by_big_fish_run_owner_user_id', name: 'big_fish_runtime_run_owner_user_id_idx_btree', algorithm: 'btree', columns: [ + 'ownerUserId', + ] }, + { accessor: 'run_id', name: 'big_fish_runtime_run_run_id_idx_btree', algorithm: 'btree', columns: [ + 'runId', + ] }, + { accessor: 'by_big_fish_run_session_id', name: 'big_fish_runtime_run_session_id_idx_btree', algorithm: 'btree', columns: [ + 'sessionId', + ] }, + ], + constraints: [ + { name: 'big_fish_runtime_run_run_id_key', constraint: 'unique', columns: ['runId'] }, + ], + }, BigFishRuntimeRunRow), + chapter_progression: __table({ + name: 'chapter_progression', + indexes: [ + { accessor: 'by_chapter_progression_chapter_id', name: 'chapter_progression_chapter_id_idx_btree', algorithm: 'btree', columns: [ + 'chapterId', + ] }, + { accessor: 'chapter_progression_id', name: 'chapter_progression_chapter_progression_id_idx_btree', algorithm: 'btree', columns: [ + 'chapterProgressionId', + ] }, + { accessor: 'by_chapter_progression_user_chapter', name: 'chapter_progression_user_id_chapter_id_idx_btree', algorithm: 'btree', columns: [ + 'userId', + 'chapterId', + ] }, + { accessor: 'by_chapter_progression_user_id', name: 'chapter_progression_user_id_idx_btree', algorithm: 'btree', columns: [ + 'userId', + ] }, + ], + constraints: [ + { name: 'chapter_progression_chapter_progression_id_key', constraint: 'unique', columns: ['chapterProgressionId'] }, + ], + }, ChapterProgressionRow), + custom_world_agent_message: __table({ + name: 'custom_world_agent_message', + indexes: [ + { accessor: 'message_id', name: 'custom_world_agent_message_message_id_idx_btree', algorithm: 'btree', columns: [ + 'messageId', + ] }, + { accessor: 'by_custom_world_agent_message_session_id', name: 'custom_world_agent_message_session_id_idx_btree', algorithm: 'btree', columns: [ + 'sessionId', + ] }, + ], + constraints: [ + { name: 'custom_world_agent_message_message_id_key', constraint: 'unique', columns: ['messageId'] }, + ], + }, CustomWorldAgentMessageRow), + custom_world_agent_operation: __table({ + name: 'custom_world_agent_operation', + indexes: [ + { accessor: 'operation_id', name: 'custom_world_agent_operation_operation_id_idx_btree', algorithm: 'btree', columns: [ + 'operationId', + ] }, + { accessor: 'by_custom_world_agent_operation_session_id', name: 'custom_world_agent_operation_session_id_idx_btree', algorithm: 'btree', columns: [ + 'sessionId', + ] }, + ], + constraints: [ + { name: 'custom_world_agent_operation_operation_id_key', constraint: 'unique', columns: ['operationId'] }, + ], + }, CustomWorldAgentOperationRow), + custom_world_agent_session: __table({ + name: 'custom_world_agent_session', + indexes: [ + { accessor: 'by_custom_world_agent_session_owner_user_id', name: 'custom_world_agent_session_owner_user_id_idx_btree', algorithm: 'btree', columns: [ + 'ownerUserId', + ] }, + { accessor: 'session_id', name: 'custom_world_agent_session_session_id_idx_btree', algorithm: 'btree', columns: [ + 'sessionId', + ] }, + { accessor: 'by_custom_world_agent_session_stage', name: 'custom_world_agent_session_stage_idx_btree', algorithm: 'btree', columns: [ + 'stage', + ] }, + ], + constraints: [ + { name: 'custom_world_agent_session_session_id_key', constraint: 'unique', columns: ['sessionId'] }, + ], + }, CustomWorldAgentSessionRow), + custom_world_draft_card: __table({ + name: 'custom_world_draft_card', + indexes: [ + { accessor: 'card_id', name: 'custom_world_draft_card_card_id_idx_btree', algorithm: 'btree', columns: [ + 'cardId', + ] }, + { accessor: 'by_custom_world_draft_card_kind', name: 'custom_world_draft_card_kind_idx_btree', algorithm: 'btree', columns: [ + 'kind', + ] }, + { accessor: 'by_custom_world_draft_card_session_id', name: 'custom_world_draft_card_session_id_idx_btree', algorithm: 'btree', columns: [ + 'sessionId', + ] }, + ], + constraints: [ + { name: 'custom_world_draft_card_card_id_key', constraint: 'unique', columns: ['cardId'] }, + ], + }, CustomWorldDraftCardRow), custom_world_gallery_entry: __table({ name: 'custom_world_gallery_entry', indexes: [ @@ -185,6 +531,456 @@ const tablesSchema = __schema({ { name: 'custom_world_gallery_entry_profile_id_key', constraint: 'unique', columns: ['profileId'] }, ], }, CustomWorldGalleryEntryRow), + custom_world_profile: __table({ + name: 'custom_world_profile', + indexes: [ + { accessor: 'by_custom_world_profile_owner_user_id', name: 'custom_world_profile_owner_user_id_idx_btree', algorithm: 'btree', columns: [ + 'ownerUserId', + ] }, + { accessor: 'profile_id', name: 'custom_world_profile_profile_id_idx_btree', algorithm: 'btree', columns: [ + 'profileId', + ] }, + { accessor: 'by_custom_world_profile_publication_status', name: 'custom_world_profile_publication_status_idx_btree', algorithm: 'btree', columns: [ + 'publicationStatus', + ] }, + ], + constraints: [ + { name: 'custom_world_profile_profile_id_key', constraint: 'unique', columns: ['profileId'] }, + ], + }, CustomWorldProfileRow), + custom_world_session: __table({ + name: 'custom_world_session', + indexes: [ + { accessor: 'by_custom_world_session_owner_user_id', name: 'custom_world_session_owner_user_id_idx_btree', algorithm: 'btree', columns: [ + 'ownerUserId', + ] }, + { accessor: 'session_id', name: 'custom_world_session_session_id_idx_btree', algorithm: 'btree', columns: [ + 'sessionId', + ] }, + ], + constraints: [ + { name: 'custom_world_session_session_id_key', constraint: 'unique', columns: ['sessionId'] }, + ], + }, CustomWorldSessionRow), + inventory_slot: __table({ + name: 'inventory_slot', + indexes: [ + { accessor: 'by_inventory_actor_user_id', name: 'inventory_slot_actor_user_id_idx_btree', algorithm: 'btree', columns: [ + 'actorUserId', + ] }, + { accessor: 'by_inventory_container_slot', name: 'inventory_slot_container_kind_slot_key_idx_btree', algorithm: 'btree', columns: [ + 'containerKind', + 'slotKey', + ] }, + { accessor: 'by_inventory_item_id', name: 'inventory_slot_item_id_idx_btree', algorithm: 'btree', columns: [ + 'itemId', + ] }, + { accessor: 'by_inventory_runtime_session_id', name: 'inventory_slot_runtime_session_id_idx_btree', algorithm: 'btree', columns: [ + 'runtimeSessionId', + ] }, + { accessor: 'slot_id', name: 'inventory_slot_slot_id_idx_btree', algorithm: 'btree', columns: [ + 'slotId', + ] }, + ], + constraints: [ + { name: 'inventory_slot_slot_id_key', constraint: 'unique', columns: ['slotId'] }, + ], + }, InventorySlotRow), + npc_state: __table({ + name: 'npc_state', + indexes: [ + { accessor: 'by_npc_id', name: 'npc_state_npc_id_idx_btree', algorithm: 'btree', columns: [ + 'npcId', + ] }, + { accessor: 'npc_state_id', name: 'npc_state_npc_state_id_idx_btree', algorithm: 'btree', columns: [ + 'npcStateId', + ] }, + { accessor: 'by_runtime_session_id', name: 'npc_state_runtime_session_id_idx_btree', algorithm: 'btree', columns: [ + 'runtimeSessionId', + ] }, + { accessor: 'by_runtime_session_npc', name: 'npc_state_runtime_session_id_npc_id_idx_btree', algorithm: 'btree', columns: [ + 'runtimeSessionId', + 'npcId', + ] }, + ], + constraints: [ + { name: 'npc_state_npc_state_id_key', constraint: 'unique', columns: ['npcStateId'] }, + ], + }, NpcStateRow), + player_progression: __table({ + name: 'player_progression', + indexes: [ + { accessor: 'user_id', name: 'player_progression_user_id_idx_btree', algorithm: 'btree', columns: [ + 'userId', + ] }, + ], + constraints: [ + { name: 'player_progression_user_id_key', constraint: 'unique', columns: ['userId'] }, + ], + }, PlayerProgressionRow), + profile_dashboard_state: __table({ + name: 'profile_dashboard_state', + indexes: [ + { accessor: 'user_id', name: 'profile_dashboard_state_user_id_idx_btree', algorithm: 'btree', columns: [ + 'userId', + ] }, + ], + constraints: [ + { name: 'profile_dashboard_state_user_id_key', constraint: 'unique', columns: ['userId'] }, + ], + }, ProfileDashboardStateRow), + profile_invite_code: __table({ + name: 'profile_invite_code', + indexes: [ + { accessor: 'invite_code', name: 'profile_invite_code_invite_code_idx_btree', algorithm: 'btree', columns: [ + 'inviteCode', + ] }, + { accessor: 'user_id', name: 'profile_invite_code_user_id_idx_btree', algorithm: 'btree', columns: [ + 'userId', + ] }, + ], + constraints: [ + { name: 'profile_invite_code_invite_code_key', constraint: 'unique', columns: ['inviteCode'] }, + { name: 'profile_invite_code_user_id_key', constraint: 'unique', columns: ['userId'] }, + ], + }, ProfileInviteCodeRow), + profile_membership: __table({ + name: 'profile_membership', + indexes: [ + { accessor: 'user_id', name: 'profile_membership_user_id_idx_btree', algorithm: 'btree', columns: [ + 'userId', + ] }, + ], + constraints: [ + { name: 'profile_membership_user_id_key', constraint: 'unique', columns: ['userId'] }, + ], + }, ProfileMembershipRow), + profile_played_world: __table({ + name: 'profile_played_world', + indexes: [ + { accessor: 'played_world_id', name: 'profile_played_world_played_world_id_idx_btree', algorithm: 'btree', columns: [ + 'playedWorldId', + ] }, + { accessor: 'by_profile_played_world_user_id', name: 'profile_played_world_user_id_idx_btree', algorithm: 'btree', columns: [ + 'userId', + ] }, + { accessor: 'by_profile_played_world_user_last_played_at', name: 'profile_played_world_user_id_last_played_at_idx_btree', algorithm: 'btree', columns: [ + 'userId', + 'lastPlayedAt', + ] }, + { accessor: 'by_profile_played_world_user_world_key', name: 'profile_played_world_user_id_world_key_idx_btree', algorithm: 'btree', columns: [ + 'userId', + 'worldKey', + ] }, + ], + constraints: [ + { name: 'profile_played_world_played_world_id_key', constraint: 'unique', columns: ['playedWorldId'] }, + ], + }, ProfilePlayedWorldRow), + profile_recharge_order: __table({ + name: 'profile_recharge_order', + indexes: [ + { accessor: 'order_id', name: 'profile_recharge_order_order_id_idx_btree', algorithm: 'btree', columns: [ + 'orderId', + ] }, + { accessor: 'by_profile_recharge_order_user_created_at', name: 'profile_recharge_order_user_id_created_at_idx_btree', algorithm: 'btree', columns: [ + 'userId', + 'createdAt', + ] }, + { accessor: 'by_profile_recharge_order_user_id', name: 'profile_recharge_order_user_id_idx_btree', algorithm: 'btree', columns: [ + 'userId', + ] }, + ], + constraints: [ + { name: 'profile_recharge_order_order_id_key', constraint: 'unique', columns: ['orderId'] }, + ], + }, ProfileRechargeOrderRow), + profile_referral_relation: __table({ + name: 'profile_referral_relation', + indexes: [ + { accessor: 'invitee_user_id', name: 'profile_referral_relation_invitee_user_id_idx_btree', algorithm: 'btree', columns: [ + 'inviteeUserId', + ] }, + { accessor: 'by_profile_referral_inviter_bound_at', name: 'profile_referral_relation_inviter_user_id_bound_at_idx_btree', algorithm: 'btree', columns: [ + 'inviterUserId', + 'boundAt', + ] }, + { accessor: 'by_profile_referral_inviter_user_id', name: 'profile_referral_relation_inviter_user_id_idx_btree', algorithm: 'btree', columns: [ + 'inviterUserId', + ] }, + ], + constraints: [ + { name: 'profile_referral_relation_invitee_user_id_key', constraint: 'unique', columns: ['inviteeUserId'] }, + ], + }, ProfileReferralRelationRow), + profile_save_archive: __table({ + name: 'profile_save_archive', + indexes: [ + { accessor: 'archive_id', name: 'profile_save_archive_archive_id_idx_btree', algorithm: 'btree', columns: [ + 'archiveId', + ] }, + { accessor: 'by_profile_save_archive_user_id', name: 'profile_save_archive_user_id_idx_btree', algorithm: 'btree', columns: [ + 'userId', + ] }, + { accessor: 'by_profile_save_archive_user_saved_at', name: 'profile_save_archive_user_id_saved_at_idx_btree', algorithm: 'btree', columns: [ + 'userId', + 'savedAt', + ] }, + { accessor: 'by_profile_save_archive_user_world_key', name: 'profile_save_archive_user_id_world_key_idx_btree', algorithm: 'btree', columns: [ + 'userId', + 'worldKey', + ] }, + ], + constraints: [ + { name: 'profile_save_archive_archive_id_key', constraint: 'unique', columns: ['archiveId'] }, + ], + }, ProfileSaveArchiveRow), + profile_wallet_ledger: __table({ + name: 'profile_wallet_ledger', + indexes: [ + { accessor: 'by_profile_wallet_ledger_user_created_at', name: 'profile_wallet_ledger_user_id_created_at_idx_btree', algorithm: 'btree', columns: [ + 'userId', + 'createdAt', + ] }, + { accessor: 'by_profile_wallet_ledger_user_id', name: 'profile_wallet_ledger_user_id_idx_btree', algorithm: 'btree', columns: [ + 'userId', + ] }, + { accessor: 'wallet_ledger_id', name: 'profile_wallet_ledger_wallet_ledger_id_idx_btree', algorithm: 'btree', columns: [ + 'walletLedgerId', + ] }, + ], + constraints: [ + { name: 'profile_wallet_ledger_wallet_ledger_id_key', constraint: 'unique', columns: ['walletLedgerId'] }, + ], + }, ProfileWalletLedgerRow), + puzzle_agent_message: __table({ + name: 'puzzle_agent_message', + indexes: [ + { accessor: 'message_id', name: 'puzzle_agent_message_message_id_idx_btree', algorithm: 'btree', columns: [ + 'messageId', + ] }, + { accessor: 'by_puzzle_agent_message_session_id', name: 'puzzle_agent_message_session_id_idx_btree', algorithm: 'btree', columns: [ + 'sessionId', + ] }, + ], + constraints: [ + { name: 'puzzle_agent_message_message_id_key', constraint: 'unique', columns: ['messageId'] }, + ], + }, PuzzleAgentMessageRow), + puzzle_agent_session: __table({ + name: 'puzzle_agent_session', + indexes: [ + { accessor: 'by_puzzle_agent_session_owner_user_id', name: 'puzzle_agent_session_owner_user_id_idx_btree', algorithm: 'btree', columns: [ + 'ownerUserId', + ] }, + { accessor: 'session_id', name: 'puzzle_agent_session_session_id_idx_btree', algorithm: 'btree', columns: [ + 'sessionId', + ] }, + ], + constraints: [ + { name: 'puzzle_agent_session_session_id_key', constraint: 'unique', columns: ['sessionId'] }, + ], + }, PuzzleAgentSessionRow), + puzzle_runtime_run: __table({ + name: 'puzzle_runtime_run', + indexes: [ + { accessor: 'by_puzzle_runtime_run_owner_user_id', name: 'puzzle_runtime_run_owner_user_id_idx_btree', algorithm: 'btree', columns: [ + 'ownerUserId', + ] }, + { accessor: 'run_id', name: 'puzzle_runtime_run_run_id_idx_btree', algorithm: 'btree', columns: [ + 'runId', + ] }, + ], + constraints: [ + { name: 'puzzle_runtime_run_run_id_key', constraint: 'unique', columns: ['runId'] }, + ], + }, PuzzleRuntimeRunRow), + puzzle_work_profile: __table({ + name: 'puzzle_work_profile', + indexes: [ + { accessor: 'by_puzzle_work_owner_user_id', name: 'puzzle_work_profile_owner_user_id_idx_btree', algorithm: 'btree', columns: [ + 'ownerUserId', + ] }, + { accessor: 'profile_id', name: 'puzzle_work_profile_profile_id_idx_btree', algorithm: 'btree', columns: [ + 'profileId', + ] }, + { accessor: 'by_puzzle_work_publication_status', name: 'puzzle_work_profile_publication_status_idx_btree', algorithm: 'btree', columns: [ + 'publicationStatus', + ] }, + ], + constraints: [ + { name: 'puzzle_work_profile_profile_id_key', constraint: 'unique', columns: ['profileId'] }, + ], + }, PuzzleWorkProfileRow), + quest_log: __table({ + name: 'quest_log', + indexes: [ + { accessor: 'by_actor_user_id', name: 'quest_log_actor_user_id_idx_btree', algorithm: 'btree', columns: [ + 'actorUserId', + ] }, + { accessor: 'log_id', name: 'quest_log_log_id_idx_btree', algorithm: 'btree', columns: [ + 'logId', + ] }, + { accessor: 'by_quest_id', name: 'quest_log_quest_id_idx_btree', algorithm: 'btree', columns: [ + 'questId', + ] }, + { accessor: 'by_runtime_session_id', name: 'quest_log_runtime_session_id_idx_btree', algorithm: 'btree', columns: [ + 'runtimeSessionId', + ] }, + ], + constraints: [ + { name: 'quest_log_log_id_key', constraint: 'unique', columns: ['logId'] }, + ], + }, QuestLogRow), + quest_record: __table({ + name: 'quest_record', + indexes: [ + { accessor: 'by_actor_user_id', name: 'quest_record_actor_user_id_idx_btree', algorithm: 'btree', columns: [ + 'actorUserId', + ] }, + { accessor: 'by_issuer_npc_id', name: 'quest_record_issuer_npc_id_idx_btree', algorithm: 'btree', columns: [ + 'issuerNpcId', + ] }, + { accessor: 'quest_id', name: 'quest_record_quest_id_idx_btree', algorithm: 'btree', columns: [ + 'questId', + ] }, + { accessor: 'by_runtime_session_id', name: 'quest_record_runtime_session_id_idx_btree', algorithm: 'btree', columns: [ + 'runtimeSessionId', + ] }, + ], + constraints: [ + { name: 'quest_record_quest_id_key', constraint: 'unique', columns: ['questId'] }, + ], + }, QuestRecordRow), + refresh_session: __table({ + name: 'refresh_session', + indexes: [ + { accessor: 'by_refresh_session_token_hash', name: 'refresh_session_refresh_token_hash_idx_btree', algorithm: 'btree', columns: [ + 'refreshTokenHash', + ] }, + { accessor: 'session_id', name: 'refresh_session_session_id_idx_btree', algorithm: 'btree', columns: [ + 'sessionId', + ] }, + { accessor: 'by_refresh_session_user_id', name: 'refresh_session_user_id_idx_btree', algorithm: 'btree', columns: [ + 'userId', + ] }, + ], + constraints: [ + { name: 'refresh_session_session_id_key', constraint: 'unique', columns: ['sessionId'] }, + ], + }, RefreshSessionRow), + runtime_setting: __table({ + name: 'runtime_setting', + indexes: [ + { accessor: 'user_id', name: 'runtime_setting_user_id_idx_btree', algorithm: 'btree', columns: [ + 'userId', + ] }, + ], + constraints: [ + { name: 'runtime_setting_user_id_key', constraint: 'unique', columns: ['userId'] }, + ], + }, RuntimeSettingRow), + runtime_snapshot: __table({ + name: 'runtime_snapshot', + indexes: [ + { accessor: 'user_id', name: 'runtime_snapshot_user_id_idx_btree', algorithm: 'btree', columns: [ + 'userId', + ] }, + ], + constraints: [ + { name: 'runtime_snapshot_user_id_key', constraint: 'unique', columns: ['userId'] }, + ], + }, RuntimeSnapshotRow), + story_event: __table({ + name: 'story_event', + indexes: [ + { accessor: 'event_id', name: 'story_event_event_id_idx_btree', algorithm: 'btree', columns: [ + 'eventId', + ] }, + { accessor: 'by_story_session_id', name: 'story_event_story_session_id_idx_btree', algorithm: 'btree', columns: [ + 'storySessionId', + ] }, + ], + constraints: [ + { name: 'story_event_event_id_key', constraint: 'unique', columns: ['eventId'] }, + ], + }, StoryEventRow), + story_session: __table({ + name: 'story_session', + indexes: [ + { accessor: 'by_actor_user_id', name: 'story_session_actor_user_id_idx_btree', algorithm: 'btree', columns: [ + 'actorUserId', + ] }, + { accessor: 'by_runtime_session_id', name: 'story_session_runtime_session_id_idx_btree', algorithm: 'btree', columns: [ + 'runtimeSessionId', + ] }, + { accessor: 'story_session_id', name: 'story_session_story_session_id_idx_btree', algorithm: 'btree', columns: [ + 'storySessionId', + ] }, + ], + constraints: [ + { name: 'story_session_story_session_id_key', constraint: 'unique', columns: ['storySessionId'] }, + ], + }, StorySessionRow), + treasure_record: __table({ + name: 'treasure_record', + indexes: [ + { accessor: 'by_treasure_actor_user_id', name: 'treasure_record_actor_user_id_idx_btree', algorithm: 'btree', columns: [ + 'actorUserId', + ] }, + { accessor: 'by_treasure_encounter_id', name: 'treasure_record_encounter_id_idx_btree', algorithm: 'btree', columns: [ + 'encounterId', + ] }, + { accessor: 'by_treasure_runtime_session_id', name: 'treasure_record_runtime_session_id_idx_btree', algorithm: 'btree', columns: [ + 'runtimeSessionId', + ] }, + { accessor: 'by_treasure_story_session_id', name: 'treasure_record_story_session_id_idx_btree', algorithm: 'btree', columns: [ + 'storySessionId', + ] }, + { accessor: 'treasure_record_id', name: 'treasure_record_treasure_record_id_idx_btree', algorithm: 'btree', columns: [ + 'treasureRecordId', + ] }, + ], + constraints: [ + { name: 'treasure_record_treasure_record_id_key', constraint: 'unique', columns: ['treasureRecordId'] }, + ], + }, TreasureRecordRow), + user_account: __table({ + name: 'user_account', + indexes: [ + { accessor: 'by_user_account_public_code', name: 'user_account_public_user_code_idx_btree', algorithm: 'btree', columns: [ + 'publicUserCode', + ] }, + { accessor: 'user_id', name: 'user_account_user_id_idx_btree', algorithm: 'btree', columns: [ + 'userId', + ] }, + { accessor: 'by_user_account_username', name: 'user_account_username_idx_btree', algorithm: 'btree', columns: [ + 'username', + ] }, + ], + constraints: [ + { name: 'user_account_user_id_key', constraint: 'unique', columns: ['userId'] }, + ], + }, UserAccountRow), + user_browse_history: __table({ + name: 'user_browse_history', + indexes: [ + { accessor: 'browse_history_id', name: 'user_browse_history_browse_history_id_idx_btree', algorithm: 'btree', columns: [ + 'browseHistoryId', + ] }, + { accessor: 'by_browse_history_user_id', name: 'user_browse_history_user_id_idx_btree', algorithm: 'btree', columns: [ + 'userId', + ] }, + { accessor: 'by_browse_history_user_owner_profile', name: 'user_browse_history_user_id_owner_user_id_profile_id_idx_btree', algorithm: 'btree', columns: [ + 'userId', + 'ownerUserId', + 'profileId', + ] }, + ], + constraints: [ + { name: 'user_browse_history_browse_history_id_key', constraint: 'unique', columns: ['browseHistoryId'] }, + ], + }, UserBrowseHistoryRow), }); /** The schema information for all reducers in this module. This is defined the same way as the reducers would have been defined in the server, except the body of the reducer is omitted in code generation. */ @@ -266,6 +1062,7 @@ const proceduresSchema = __procedures( __procedureSchema("get_profile_dashboard", GetProfileDashboardProcedure.params, GetProfileDashboardProcedure.returnType), __procedureSchema("get_profile_play_stats", GetProfilePlayStatsProcedure.params, GetProfilePlayStatsProcedure.returnType), __procedureSchema("get_profile_recharge_center", GetProfileRechargeCenterProcedure.params, GetProfileRechargeCenterProcedure.returnType), + __procedureSchema("get_profile_referral_invite_center", GetProfileReferralInviteCenterProcedure.params, GetProfileReferralInviteCenterProcedure.returnType), __procedureSchema("get_puzzle_agent_session", GetPuzzleAgentSessionProcedure.params, GetPuzzleAgentSessionProcedure.returnType), __procedureSchema("get_puzzle_gallery_detail", GetPuzzleGalleryDetailProcedure.params, GetPuzzleGalleryDetailProcedure.returnType), __procedureSchema("get_puzzle_run", GetPuzzleRunProcedure.params, GetPuzzleRunProcedure.returnType), @@ -289,6 +1086,7 @@ const proceduresSchema = __procedures( __procedureSchema("publish_custom_world_profile_and_return", PublishCustomWorldProfileAndReturnProcedure.params, PublishCustomWorldProfileAndReturnProcedure.returnType), __procedureSchema("publish_custom_world_world", PublishCustomWorldWorldProcedure.params, PublishCustomWorldWorldProcedure.returnType), __procedureSchema("publish_puzzle_work", PublishPuzzleWorkProcedure.params, PublishPuzzleWorkProcedure.returnType), + __procedureSchema("redeem_profile_referral_invite_code", RedeemProfileReferralInviteCodeProcedure.params, RedeemProfileReferralInviteCodeProcedure.returnType), __procedureSchema("resolve_combat_action_and_return", ResolveCombatActionAndReturnProcedure.params, ResolveCombatActionAndReturnProcedure.returnType), __procedureSchema("resolve_npc_battle_interaction_and_return", ResolveNpcBattleInteractionAndReturnProcedure.params, ResolveNpcBattleInteractionAndReturnProcedure.returnType), __procedureSchema("resolve_npc_interaction_and_return", ResolveNpcInteractionAndReturnProcedure.params, ResolveNpcInteractionAndReturnProcedure.returnType), diff --git a/src/spacetime/generated/inventory_slot_table.ts b/src/spacetime/generated/inventory_slot_table.ts new file mode 100644 index 00000000..c77e2a48 --- /dev/null +++ b/src/spacetime/generated/inventory_slot_table.ts @@ -0,0 +1,49 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from "spacetimedb"; +import { + InventoryItemRarity, + InventoryEquipmentSlot, + InventoryItemSourceKind, + InventoryContainerKind, +} from "./types"; + + +export default __t.row({ + slotId: __t.string().primaryKey().name("slot_id"), + runtimeSessionId: __t.string().name("runtime_session_id"), + storySessionId: __t.option(__t.string()).name("story_session_id"), + actorUserId: __t.string().name("actor_user_id"), + get containerKind() { + return InventoryContainerKind.name("container_kind"); + }, + slotKey: __t.string().name("slot_key"), + itemId: __t.string().name("item_id"), + category: __t.string(), + name: __t.string(), + description: __t.option(__t.string()), + quantity: __t.u32(), + get rarity() { + return InventoryItemRarity; + }, + tags: __t.array(__t.string()), + stackable: __t.bool(), + stackKey: __t.string().name("stack_key"), + get equipmentSlotId() { + return __t.option(InventoryEquipmentSlot).name("equipment_slot_id"); + }, + get sourceKind() { + return InventoryItemSourceKind.name("source_kind"); + }, + sourceReferenceId: __t.option(__t.string()).name("source_reference_id"), + createdAt: __t.timestamp().name("created_at"), + updatedAt: __t.timestamp().name("updated_at"), +}); diff --git a/src/spacetime/generated/npc_state_table.ts b/src/spacetime/generated/npc_state_table.ts new file mode 100644 index 00000000..4b5e1532 --- /dev/null +++ b/src/spacetime/generated/npc_state_table.ts @@ -0,0 +1,41 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from "spacetimedb"; +import { + NpcRelationState, + NpcStanceProfile, +} from "./types"; + + +export default __t.row({ + npcStateId: __t.string().primaryKey().name("npc_state_id"), + runtimeSessionId: __t.string().name("runtime_session_id"), + npcId: __t.string().name("npc_id"), + npcName: __t.string().name("npc_name"), + affinity: __t.i32(), + get relationState() { + return NpcRelationState.name("relation_state"); + }, + helpUsed: __t.bool().name("help_used"), + chattedCount: __t.u32().name("chatted_count"), + giftsGiven: __t.u32().name("gifts_given"), + recruited: __t.bool(), + tradeStockSignature: __t.option(__t.string()).name("trade_stock_signature"), + revealedFacts: __t.array(__t.string()).name("revealed_facts"), + knownAttributeRumors: __t.array(__t.string()).name("known_attribute_rumors"), + firstMeaningfulContactResolved: __t.bool().name("first_meaningful_contact_resolved"), + seenBackstoryChapterIds: __t.array(__t.string()).name("seen_backstory_chapter_ids"), + get stanceProfile() { + return NpcStanceProfile.name("stance_profile"); + }, + createdAt: __t.timestamp().name("created_at"), + updatedAt: __t.timestamp().name("updated_at"), +}); diff --git a/src/spacetime/generated/player_progression_table.ts b/src/spacetime/generated/player_progression_table.ts new file mode 100644 index 00000000..6239aded --- /dev/null +++ b/src/spacetime/generated/player_progression_table.ts @@ -0,0 +1,29 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from "spacetimedb"; +import { + PlayerProgressionGrantSource, +} from "./types"; + + +export default __t.row({ + userId: __t.string().primaryKey().name("user_id"), + level: __t.u32(), + currentLevelXp: __t.u32().name("current_level_xp"), + totalXp: __t.u32().name("total_xp"), + xpToNextLevel: __t.u32().name("xp_to_next_level"), + pendingLevelUps: __t.u32().name("pending_level_ups"), + get lastGrantedSource() { + return __t.option(PlayerProgressionGrantSource).name("last_granted_source"); + }, + createdAt: __t.timestamp().name("created_at"), + updatedAt: __t.timestamp().name("updated_at"), +}); diff --git a/src/spacetime/generated/profile_dashboard_state_table.ts b/src/spacetime/generated/profile_dashboard_state_table.ts new file mode 100644 index 00000000..871a70c4 --- /dev/null +++ b/src/spacetime/generated/profile_dashboard_state_table.ts @@ -0,0 +1,19 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from "spacetimedb"; + +export default __t.row({ + userId: __t.string().primaryKey().name("user_id"), + walletBalance: __t.u64().name("wallet_balance"), + totalPlayTimeMs: __t.u64().name("total_play_time_ms"), + createdAt: __t.timestamp().name("created_at"), + updatedAt: __t.timestamp().name("updated_at"), +}); diff --git a/src/spacetime/generated/profile_invite_code_table.ts b/src/spacetime/generated/profile_invite_code_table.ts new file mode 100644 index 00000000..8dac8224 --- /dev/null +++ b/src/spacetime/generated/profile_invite_code_table.ts @@ -0,0 +1,18 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from "spacetimedb"; + +export default __t.row({ + userId: __t.string().primaryKey().name("user_id"), + inviteCode: __t.string().name("invite_code"), + createdAt: __t.timestamp().name("created_at"), + updatedAt: __t.timestamp().name("updated_at"), +}); diff --git a/src/spacetime/generated/profile_membership_table.ts b/src/spacetime/generated/profile_membership_table.ts new file mode 100644 index 00000000..4ce5b5dc --- /dev/null +++ b/src/spacetime/generated/profile_membership_table.ts @@ -0,0 +1,29 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from "spacetimedb"; +import { + RuntimeProfileMembershipStatus, + RuntimeProfileMembershipTier, +} from "./types"; + + +export default __t.row({ + userId: __t.string().primaryKey().name("user_id"), + get status() { + return RuntimeProfileMembershipStatus; + }, + get tier() { + return RuntimeProfileMembershipTier; + }, + startedAt: __t.timestamp().name("started_at"), + expiresAt: __t.timestamp().name("expires_at"), + updatedAt: __t.timestamp().name("updated_at"), +}); diff --git a/src/spacetime/generated/profile_played_world_table.ts b/src/spacetime/generated/profile_played_world_table.ts new file mode 100644 index 00000000..3f23382f --- /dev/null +++ b/src/spacetime/generated/profile_played_world_table.ts @@ -0,0 +1,25 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from "spacetimedb"; + +export default __t.row({ + playedWorldId: __t.string().primaryKey().name("played_world_id"), + userId: __t.string().name("user_id"), + worldKey: __t.string().name("world_key"), + ownerUserId: __t.option(__t.string()).name("owner_user_id"), + profileId: __t.option(__t.string()).name("profile_id"), + worldType: __t.option(__t.string()).name("world_type"), + worldTitle: __t.string().name("world_title"), + worldSubtitle: __t.string().name("world_subtitle"), + firstPlayedAt: __t.timestamp().name("first_played_at"), + lastPlayedAt: __t.timestamp().name("last_played_at"), + lastObservedPlayTimeMs: __t.u64().name("last_observed_play_time_ms"), +}); diff --git a/src/spacetime/generated/profile_recharge_order_table.ts b/src/spacetime/generated/profile_recharge_order_table.ts new file mode 100644 index 00000000..75075258 --- /dev/null +++ b/src/spacetime/generated/profile_recharge_order_table.ts @@ -0,0 +1,35 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from "spacetimedb"; +import { + RuntimeProfileRechargeProductKind, + RuntimeProfileRechargeOrderStatus, +} from "./types"; + + +export default __t.row({ + orderId: __t.string().primaryKey().name("order_id"), + userId: __t.string().name("user_id"), + productId: __t.string().name("product_id"), + productTitle: __t.string().name("product_title"), + get kind() { + return RuntimeProfileRechargeProductKind; + }, + amountCents: __t.u64().name("amount_cents"), + get status() { + return RuntimeProfileRechargeOrderStatus; + }, + paymentChannel: __t.string().name("payment_channel"), + paidAt: __t.timestamp().name("paid_at"), + createdAt: __t.timestamp().name("created_at"), + pointsDelta: __t.i64().name("points_delta"), + membershipExpiresAt: __t.option(__t.timestamp()).name("membership_expires_at"), +}); diff --git a/src/spacetime/generated/profile_referral_relation_table.ts b/src/spacetime/generated/profile_referral_relation_table.ts new file mode 100644 index 00000000..f0f8ec89 --- /dev/null +++ b/src/spacetime/generated/profile_referral_relation_table.ts @@ -0,0 +1,20 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from "spacetimedb"; + +export default __t.row({ + inviteeUserId: __t.string().primaryKey().name("invitee_user_id"), + inviterUserId: __t.string().name("inviter_user_id"), + inviteCode: __t.string().name("invite_code"), + inviterRewardGranted: __t.bool().name("inviter_reward_granted"), + inviteeRewardGranted: __t.bool().name("invitee_reward_granted"), + boundAt: __t.timestamp().name("bound_at"), +}); diff --git a/src/spacetime/generated/profile_save_archive_table.ts b/src/spacetime/generated/profile_save_archive_table.ts new file mode 100644 index 00000000..f15384e3 --- /dev/null +++ b/src/spacetime/generated/profile_save_archive_table.ts @@ -0,0 +1,30 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from "spacetimedb"; + +export default __t.row({ + archiveId: __t.string().primaryKey().name("archive_id"), + userId: __t.string().name("user_id"), + worldKey: __t.string().name("world_key"), + ownerUserId: __t.option(__t.string()).name("owner_user_id"), + profileId: __t.option(__t.string()).name("profile_id"), + worldType: __t.option(__t.string()).name("world_type"), + worldName: __t.string().name("world_name"), + subtitle: __t.string(), + summaryText: __t.string().name("summary_text"), + coverImageSrc: __t.option(__t.string()).name("cover_image_src"), + savedAt: __t.timestamp().name("saved_at"), + bottomTab: __t.string().name("bottom_tab"), + gameStateJson: __t.string().name("game_state_json"), + currentStoryJson: __t.option(__t.string()).name("current_story_json"), + createdAt: __t.timestamp().name("created_at"), + updatedAt: __t.timestamp().name("updated_at"), +}); diff --git a/src/spacetime/generated/profile_wallet_ledger_table.ts b/src/spacetime/generated/profile_wallet_ledger_table.ts new file mode 100644 index 00000000..bc41880b --- /dev/null +++ b/src/spacetime/generated/profile_wallet_ledger_table.ts @@ -0,0 +1,26 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from "spacetimedb"; +import { + RuntimeProfileWalletLedgerSourceType, +} from "./types"; + + +export default __t.row({ + walletLedgerId: __t.string().primaryKey().name("wallet_ledger_id"), + userId: __t.string().name("user_id"), + amountDelta: __t.i64().name("amount_delta"), + balanceAfter: __t.u64().name("balance_after"), + get sourceType() { + return RuntimeProfileWalletLedgerSourceType.name("source_type"); + }, + createdAt: __t.timestamp().name("created_at"), +}); diff --git a/src/spacetime/generated/puzzle_agent_message_table.ts b/src/spacetime/generated/puzzle_agent_message_table.ts new file mode 100644 index 00000000..c63729c5 --- /dev/null +++ b/src/spacetime/generated/puzzle_agent_message_table.ts @@ -0,0 +1,29 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from "spacetimedb"; +import { + PuzzleAgentMessageRole, + PuzzleAgentMessageKind, +} from "./types"; + + +export default __t.row({ + messageId: __t.string().primaryKey().name("message_id"), + sessionId: __t.string().name("session_id"), + get role() { + return PuzzleAgentMessageRole; + }, + get kind() { + return PuzzleAgentMessageKind; + }, + text: __t.string(), + createdAt: __t.timestamp().name("created_at"), +}); diff --git a/src/spacetime/generated/puzzle_agent_session_table.ts b/src/spacetime/generated/puzzle_agent_session_table.ts new file mode 100644 index 00000000..664cfd12 --- /dev/null +++ b/src/spacetime/generated/puzzle_agent_session_table.ts @@ -0,0 +1,32 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from "spacetimedb"; +import { + PuzzleAgentStage, +} from "./types"; + + +export default __t.row({ + sessionId: __t.string().primaryKey().name("session_id"), + ownerUserId: __t.string().name("owner_user_id"), + seedText: __t.string().name("seed_text"), + currentTurn: __t.u32().name("current_turn"), + progressPercent: __t.u32().name("progress_percent"), + get stage() { + return PuzzleAgentStage; + }, + anchorPackJson: __t.string().name("anchor_pack_json"), + draftJson: __t.option(__t.string()).name("draft_json"), + lastAssistantReply: __t.option(__t.string()).name("last_assistant_reply"), + publishedProfileId: __t.option(__t.string()).name("published_profile_id"), + createdAt: __t.timestamp().name("created_at"), + updatedAt: __t.timestamp().name("updated_at"), +}); diff --git a/src/spacetime/generated/puzzle_runtime_run_table.ts b/src/spacetime/generated/puzzle_runtime_run_table.ts new file mode 100644 index 00000000..0f737ca9 --- /dev/null +++ b/src/spacetime/generated/puzzle_runtime_run_table.ts @@ -0,0 +1,26 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from "spacetimedb"; + +export default __t.row({ + runId: __t.string().primaryKey().name("run_id"), + ownerUserId: __t.string().name("owner_user_id"), + entryProfileId: __t.string().name("entry_profile_id"), + currentProfileId: __t.string().name("current_profile_id"), + clearedLevelCount: __t.u32().name("cleared_level_count"), + currentLevelIndex: __t.u32().name("current_level_index"), + currentGridSize: __t.u32().name("current_grid_size"), + playedProfileIdsJson: __t.string().name("played_profile_ids_json"), + previousLevelTagsJson: __t.string().name("previous_level_tags_json"), + snapshotJson: __t.string().name("snapshot_json"), + createdAt: __t.timestamp().name("created_at"), + updatedAt: __t.timestamp().name("updated_at"), +}); diff --git a/src/spacetime/generated/puzzle_work_profile_table.ts b/src/spacetime/generated/puzzle_work_profile_table.ts new file mode 100644 index 00000000..7d32c841 --- /dev/null +++ b/src/spacetime/generated/puzzle_work_profile_table.ts @@ -0,0 +1,37 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from "spacetimedb"; +import { + PuzzlePublicationStatus, +} from "./types"; + + +export default __t.row({ + profileId: __t.string().primaryKey().name("profile_id"), + workId: __t.string().name("work_id"), + ownerUserId: __t.string().name("owner_user_id"), + sourceSessionId: __t.option(__t.string()).name("source_session_id"), + authorDisplayName: __t.string().name("author_display_name"), + levelName: __t.string().name("level_name"), + summary: __t.string(), + themeTagsJson: __t.string().name("theme_tags_json"), + coverImageSrc: __t.option(__t.string()).name("cover_image_src"), + coverAssetId: __t.option(__t.string()).name("cover_asset_id"), + get publicationStatus() { + return PuzzlePublicationStatus.name("publication_status"); + }, + playCount: __t.u32().name("play_count"), + anchorPackJson: __t.string().name("anchor_pack_json"), + publishReady: __t.bool().name("publish_ready"), + createdAt: __t.timestamp().name("created_at"), + updatedAt: __t.timestamp().name("updated_at"), + publishedAt: __t.option(__t.timestamp()).name("published_at"), +}); diff --git a/src/spacetime/generated/quest_log_table.ts b/src/spacetime/generated/quest_log_table.ts new file mode 100644 index 00000000..d55094b1 --- /dev/null +++ b/src/spacetime/generated/quest_log_table.ts @@ -0,0 +1,40 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from "spacetimedb"; +import { + QuestStatus, + QuestProgressSignal, + QuestLogEventKind, + QuestSignalKind, +} from "./types"; + + +export default __t.row({ + logId: __t.string().primaryKey().name("log_id"), + questId: __t.string().name("quest_id"), + runtimeSessionId: __t.string().name("runtime_session_id"), + actorUserId: __t.string().name("actor_user_id"), + get eventKind() { + return QuestLogEventKind.name("event_kind"); + }, + get statusAfter() { + return QuestStatus.name("status_after"); + }, + get signalKind() { + return __t.option(QuestSignalKind).name("signal_kind"); + }, + get signal() { + return __t.option(QuestProgressSignal); + }, + stepId: __t.option(__t.string()).name("step_id"), + stepProgress: __t.option(__t.u32()).name("step_progress"), + createdAt: __t.timestamp().name("created_at"), +}); diff --git a/src/spacetime/generated/quest_record_table.ts b/src/spacetime/generated/quest_record_table.ts new file mode 100644 index 00000000..82305f51 --- /dev/null +++ b/src/spacetime/generated/quest_record_table.ts @@ -0,0 +1,64 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from "spacetimedb"; +import { + QuestStatus, + QuestRewardSnapshot, + QuestNarrativeBindingSnapshot, + QuestStepSnapshot, + QuestObjectiveSnapshot, +} from "./types"; + + +export default __t.row({ + questId: __t.string().primaryKey().name("quest_id"), + runtimeSessionId: __t.string().name("runtime_session_id"), + storySessionId: __t.option(__t.string()).name("story_session_id"), + actorUserId: __t.string().name("actor_user_id"), + issuerNpcId: __t.string().name("issuer_npc_id"), + issuerNpcName: __t.string().name("issuer_npc_name"), + sceneId: __t.option(__t.string()).name("scene_id"), + chapterId: __t.option(__t.string()).name("chapter_id"), + actId: __t.option(__t.string()).name("act_id"), + threadId: __t.option(__t.string()).name("thread_id"), + contractId: __t.option(__t.string()).name("contract_id"), + title: __t.string(), + description: __t.string(), + summary: __t.string(), + get objective() { + return QuestObjectiveSnapshot; + }, + progress: __t.u32(), + get status() { + return QuestStatus; + }, + completionNotified: __t.bool().name("completion_notified"), + get reward() { + return QuestRewardSnapshot; + }, + rewardText: __t.string().name("reward_text"), + get narrativeBinding() { + return QuestNarrativeBindingSnapshot.name("narrative_binding"); + }, + get steps() { + return __t.array(QuestStepSnapshot); + }, + activeStepId: __t.option(__t.string()).name("active_step_id"), + visibleStage: __t.u32().name("visible_stage"), + hiddenFlags: __t.array(__t.string()).name("hidden_flags"), + discoveredFactIds: __t.array(__t.string()).name("discovered_fact_ids"), + relatedCarrierIds: __t.array(__t.string()).name("related_carrier_ids"), + consequenceIds: __t.array(__t.string()).name("consequence_ids"), + createdAt: __t.timestamp().name("created_at"), + updatedAt: __t.timestamp().name("updated_at"), + completedAt: __t.option(__t.timestamp()).name("completed_at"), + turnedInAt: __t.option(__t.timestamp()).name("turned_in_at"), +}); diff --git a/src/spacetime/generated/redeem_profile_referral_invite_code_procedure.ts b/src/spacetime/generated/redeem_profile_referral_invite_code_procedure.ts new file mode 100644 index 00000000..83f384a5 --- /dev/null +++ b/src/spacetime/generated/redeem_profile_referral_invite_code_procedure.ts @@ -0,0 +1,23 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from "spacetimedb"; + +import { + RuntimeReferralRedeemInput, + RuntimeReferralRedeemProcedureResult, +} from "./types"; + +export const params = { + get input() { + return RuntimeReferralRedeemInput; + }, +}; +export const returnType = RuntimeReferralRedeemProcedureResult \ No newline at end of file diff --git a/src/spacetime/generated/refresh_session_table.ts b/src/spacetime/generated/refresh_session_table.ts new file mode 100644 index 00000000..d62031db --- /dev/null +++ b/src/spacetime/generated/refresh_session_table.ts @@ -0,0 +1,24 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from "spacetimedb"; + +export default __t.row({ + sessionId: __t.string().primaryKey().name("session_id"), + userId: __t.string().name("user_id"), + refreshTokenHash: __t.string().name("refresh_token_hash"), + issuedByProvider: __t.string().name("issued_by_provider"), + clientInfoJson: __t.string().name("client_info_json"), + expiresAt: __t.string().name("expires_at"), + revokedAt: __t.option(__t.string()).name("revoked_at"), + createdAt: __t.string().name("created_at"), + updatedAt: __t.string().name("updated_at"), + lastSeenAt: __t.string().name("last_seen_at"), +}); diff --git a/src/spacetime/generated/runtime_setting_table.ts b/src/spacetime/generated/runtime_setting_table.ts new file mode 100644 index 00000000..c1153aed --- /dev/null +++ b/src/spacetime/generated/runtime_setting_table.ts @@ -0,0 +1,25 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from "spacetimedb"; +import { + RuntimePlatformTheme, +} from "./types"; + + +export default __t.row({ + userId: __t.string().primaryKey().name("user_id"), + musicVolume: __t.f32().name("music_volume"), + get platformTheme() { + return RuntimePlatformTheme.name("platform_theme"); + }, + createdAt: __t.timestamp().name("created_at"), + updatedAt: __t.timestamp().name("updated_at"), +}); diff --git a/src/spacetime/generated/runtime_snapshot_table.ts b/src/spacetime/generated/runtime_snapshot_table.ts new file mode 100644 index 00000000..93c23708 --- /dev/null +++ b/src/spacetime/generated/runtime_snapshot_table.ts @@ -0,0 +1,22 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from "spacetimedb"; + +export default __t.row({ + userId: __t.string().primaryKey().name("user_id"), + version: __t.u32(), + savedAt: __t.timestamp().name("saved_at"), + bottomTab: __t.string().name("bottom_tab"), + gameStateJson: __t.string().name("game_state_json"), + currentStoryJson: __t.option(__t.string()).name("current_story_json"), + createdAt: __t.timestamp().name("created_at"), + updatedAt: __t.timestamp().name("updated_at"), +}); diff --git a/src/spacetime/generated/story_event_table.ts b/src/spacetime/generated/story_event_table.ts new file mode 100644 index 00000000..67e22581 --- /dev/null +++ b/src/spacetime/generated/story_event_table.ts @@ -0,0 +1,26 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from "spacetimedb"; +import { + StoryEventKind, +} from "./types"; + + +export default __t.row({ + eventId: __t.string().primaryKey().name("event_id"), + storySessionId: __t.string().name("story_session_id"), + get eventKind() { + return StoryEventKind.name("event_kind"); + }, + narrativeText: __t.string().name("narrative_text"), + choiceFunctionId: __t.option(__t.string()).name("choice_function_id"), + createdAt: __t.timestamp().name("created_at"), +}); diff --git a/src/spacetime/generated/story_session_table.ts b/src/spacetime/generated/story_session_table.ts new file mode 100644 index 00000000..0e7a8a71 --- /dev/null +++ b/src/spacetime/generated/story_session_table.ts @@ -0,0 +1,32 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from "spacetimedb"; +import { + StorySessionStatus, +} from "./types"; + + +export default __t.row({ + storySessionId: __t.string().primaryKey().name("story_session_id"), + runtimeSessionId: __t.string().name("runtime_session_id"), + actorUserId: __t.string().name("actor_user_id"), + worldProfileId: __t.string().name("world_profile_id"), + initialPrompt: __t.string().name("initial_prompt"), + openingSummary: __t.option(__t.string()).name("opening_summary"), + latestNarrativeText: __t.string().name("latest_narrative_text"), + latestChoiceFunctionId: __t.option(__t.string()).name("latest_choice_function_id"), + get status() { + return StorySessionStatus; + }, + version: __t.u32(), + createdAt: __t.timestamp().name("created_at"), + updatedAt: __t.timestamp().name("updated_at"), +}); diff --git a/src/spacetime/generated/treasure_record_table.ts b/src/spacetime/generated/treasure_record_table.ts new file mode 100644 index 00000000..f5531805 --- /dev/null +++ b/src/spacetime/generated/treasure_record_table.ts @@ -0,0 +1,39 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from "spacetimedb"; +import { + RuntimeItemRewardItemSnapshot, + TreasureInteractionAction, +} from "./types"; + + +export default __t.row({ + treasureRecordId: __t.string().primaryKey().name("treasure_record_id"), + runtimeSessionId: __t.string().name("runtime_session_id"), + storySessionId: __t.string().name("story_session_id"), + actorUserId: __t.string().name("actor_user_id"), + encounterId: __t.string().name("encounter_id"), + encounterName: __t.string().name("encounter_name"), + sceneId: __t.option(__t.string()).name("scene_id"), + sceneName: __t.option(__t.string()).name("scene_name"), + get action() { + return TreasureInteractionAction; + }, + get rewardItems() { + return __t.array(RuntimeItemRewardItemSnapshot).name("reward_items"); + }, + rewardHp: __t.u32().name("reward_hp"), + rewardMana: __t.u32().name("reward_mana"), + rewardCurrency: __t.u32().name("reward_currency"), + storyHint: __t.option(__t.string()).name("story_hint"), + createdAt: __t.timestamp().name("created_at"), + updatedAt: __t.timestamp().name("updated_at"), +}); diff --git a/src/spacetime/generated/types.ts b/src/spacetime/generated/types.ts index 1beececb..11014026 100644 --- a/src/spacetime/generated/types.ts +++ b/src/spacetime/generated/types.ts @@ -2356,6 +2356,14 @@ export const ProfileDashboardState = __t.object("ProfileDashboardState", { }); export type ProfileDashboardState = __Infer; +export const ProfileInviteCode = __t.object("ProfileInviteCode", { + userId: __t.string(), + inviteCode: __t.string(), + createdAt: __t.timestamp(), + updatedAt: __t.timestamp(), +}); +export type ProfileInviteCode = __Infer; + export const ProfileMembership = __t.object("ProfileMembership", { userId: __t.string(), get status() { @@ -2405,6 +2413,16 @@ export const ProfileRechargeOrder = __t.object("ProfileRechargeOrder", { }); export type ProfileRechargeOrder = __Infer; +export const ProfileReferralRelation = __t.object("ProfileReferralRelation", { + inviteeUserId: __t.string(), + inviterUserId: __t.string(), + inviteCode: __t.string(), + inviterRewardGranted: __t.bool(), + inviteeRewardGranted: __t.bool(), + boundAt: __t.timestamp(), +}); +export type ProfileReferralRelation = __Infer; + export const ProfileSaveArchive = __t.object("ProfileSaveArchive", { archiveId: __t.string(), userId: __t.string(), @@ -3658,6 +3676,63 @@ export const RuntimeProfileWalletLedgerSourceType = __t.enum("RuntimeProfileWall }); export type RuntimeProfileWalletLedgerSourceType = __Infer; +export const RuntimeReferralInviteCenterGetInput = __t.object("RuntimeReferralInviteCenterGetInput", { + userId: __t.string(), +}); +export type RuntimeReferralInviteCenterGetInput = __Infer; + +export const RuntimeReferralInviteCenterProcedureResult = __t.object("RuntimeReferralInviteCenterProcedureResult", { + ok: __t.bool(), + get record() { + return __t.option(RuntimeReferralInviteCenterSnapshot); + }, + errorMessage: __t.option(__t.string()), +}); +export type RuntimeReferralInviteCenterProcedureResult = __Infer; + +export const RuntimeReferralInviteCenterSnapshot = __t.object("RuntimeReferralInviteCenterSnapshot", { + userId: __t.string(), + inviteCode: __t.string(), + inviteLinkPath: __t.string(), + invitedCount: __t.u32(), + rewardedInviteCount: __t.u32(), + todayInviterRewardCount: __t.u32(), + todayInviterRewardRemaining: __t.u32(), + rewardPoints: __t.u64(), + hasRedeemedCode: __t.bool(), + boundInviterUserId: __t.option(__t.string()), + boundAtMicros: __t.option(__t.i64()), + updatedAtMicros: __t.i64(), +}); +export type RuntimeReferralInviteCenterSnapshot = __Infer; + +export const RuntimeReferralRedeemInput = __t.object("RuntimeReferralRedeemInput", { + userId: __t.string(), + inviteCode: __t.string(), + updatedAtMicros: __t.i64(), +}); +export type RuntimeReferralRedeemInput = __Infer; + +export const RuntimeReferralRedeemProcedureResult = __t.object("RuntimeReferralRedeemProcedureResult", { + ok: __t.bool(), + get record() { + return __t.option(RuntimeReferralRedeemSnapshot); + }, + errorMessage: __t.option(__t.string()), +}); +export type RuntimeReferralRedeemProcedureResult = __Infer; + +export const RuntimeReferralRedeemSnapshot = __t.object("RuntimeReferralRedeemSnapshot", { + get center() { + return RuntimeReferralInviteCenterSnapshot; + }, + inviteeRewardGranted: __t.bool(), + inviterRewardGranted: __t.bool(), + inviteeBalanceAfter: __t.u64(), + inviterBalanceAfter: __t.u64(), +}); +export type RuntimeReferralRedeemSnapshot = __Infer; + export const RuntimeSetting = __t.object("RuntimeSetting", { userId: __t.string(), musicVolume: __t.f32(), diff --git a/src/spacetime/generated/types/procedures.ts b/src/spacetime/generated/types/procedures.ts index 221e16d9..8aef98e9 100644 --- a/src/spacetime/generated/types/procedures.ts +++ b/src/spacetime/generated/types/procedures.ts @@ -55,6 +55,7 @@ import * as GetPlayerProgressionOrDefaultProcedure from "../get_player_progressi import * as GetProfileDashboardProcedure from "../get_profile_dashboard_procedure"; import * as GetProfilePlayStatsProcedure from "../get_profile_play_stats_procedure"; import * as GetProfileRechargeCenterProcedure from "../get_profile_recharge_center_procedure"; +import * as GetProfileReferralInviteCenterProcedure from "../get_profile_referral_invite_center_procedure"; import * as GetPuzzleAgentSessionProcedure from "../get_puzzle_agent_session_procedure"; import * as GetPuzzleGalleryDetailProcedure from "../get_puzzle_gallery_detail_procedure"; import * as GetPuzzleRunProcedure from "../get_puzzle_run_procedure"; @@ -78,6 +79,7 @@ import * as PublishBigFishGameProcedure from "../publish_big_fish_game_procedure import * as PublishCustomWorldProfileAndReturnProcedure from "../publish_custom_world_profile_and_return_procedure"; import * as PublishCustomWorldWorldProcedure from "../publish_custom_world_world_procedure"; import * as PublishPuzzleWorkProcedure from "../publish_puzzle_work_procedure"; +import * as RedeemProfileReferralInviteCodeProcedure from "../redeem_profile_referral_invite_code_procedure"; import * as ResolveCombatActionAndReturnProcedure from "../resolve_combat_action_and_return_procedure"; import * as ResolveNpcBattleInteractionAndReturnProcedure from "../resolve_npc_battle_interaction_and_return_procedure"; import * as ResolveNpcInteractionAndReturnProcedure from "../resolve_npc_interaction_and_return_procedure"; @@ -202,6 +204,8 @@ export type GetProfilePlayStatsArgs = __Infer; export type GetProfileRechargeCenterArgs = __Infer; export type GetProfileRechargeCenterResult = __Infer; +export type GetProfileReferralInviteCenterArgs = __Infer; +export type GetProfileReferralInviteCenterResult = __Infer; export type GetPuzzleAgentSessionArgs = __Infer; export type GetPuzzleAgentSessionResult = __Infer; export type GetPuzzleGalleryDetailArgs = __Infer; @@ -248,6 +252,8 @@ export type PublishCustomWorldWorldArgs = __Infer; export type PublishPuzzleWorkArgs = __Infer; export type PublishPuzzleWorkResult = __Infer; +export type RedeemProfileReferralInviteCodeArgs = __Infer; +export type RedeemProfileReferralInviteCodeResult = __Infer; export type ResolveCombatActionAndReturnArgs = __Infer; export type ResolveCombatActionAndReturnResult = __Infer; export type ResolveNpcBattleInteractionAndReturnArgs = __Infer; diff --git a/src/spacetime/generated/user_account_table.ts b/src/spacetime/generated/user_account_table.ts new file mode 100644 index 00000000..fde50e7f --- /dev/null +++ b/src/spacetime/generated/user_account_table.ts @@ -0,0 +1,26 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from "spacetimedb"; + +export default __t.row({ + userId: __t.string().primaryKey().name("user_id"), + publicUserCode: __t.string().name("public_user_code"), + username: __t.string(), + displayName: __t.string().name("display_name"), + phoneNumberMasked: __t.option(__t.string()).name("phone_number_masked"), + phoneNumberE164: __t.option(__t.string()).name("phone_number_e_164"), + loginMethod: __t.string().name("login_method"), + bindingStatus: __t.string().name("binding_status"), + wechatBound: __t.bool().name("wechat_bound"), + passwordHash: __t.string().name("password_hash"), + passwordLoginEnabled: __t.bool().name("password_login_enabled"), + tokenVersion: __t.u64().name("token_version"), +}); diff --git a/src/spacetime/generated/user_browse_history_table.ts b/src/spacetime/generated/user_browse_history_table.ts new file mode 100644 index 00000000..e0357508 --- /dev/null +++ b/src/spacetime/generated/user_browse_history_table.ts @@ -0,0 +1,33 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from "spacetimedb"; +import { + RuntimeBrowseHistoryThemeMode, +} from "./types"; + + +export default __t.row({ + browseHistoryId: __t.string().primaryKey().name("browse_history_id"), + userId: __t.string().name("user_id"), + ownerUserId: __t.string().name("owner_user_id"), + profileId: __t.string().name("profile_id"), + worldName: __t.string().name("world_name"), + subtitle: __t.string(), + summaryText: __t.string().name("summary_text"), + coverImageSrc: __t.option(__t.string()).name("cover_image_src"), + get themeMode() { + return RuntimeBrowseHistoryThemeMode.name("theme_mode"); + }, + authorDisplayName: __t.string().name("author_display_name"), + visitedAt: __t.timestamp().name("visited_at"), + createdAt: __t.timestamp().name("created_at"), + updatedAt: __t.timestamp().name("updated_at"), +}); diff --git a/src/types/story.ts b/src/types/story.ts index 38047cdd..466fda71 100644 --- a/src/types/story.ts +++ b/src/types/story.ts @@ -5,7 +5,8 @@ import { type TreasureInteractionAction, } from './core'; import type { InventoryItem } from './items'; -import type { SceneDirective } from './scene'; +import type { SceneDirective, ScenePresetInfo } from './scene'; +import type { StoryEngineMemoryState } from './storyEngine'; export interface StoryOptionGoalAffordance { goalId: string; @@ -128,6 +129,9 @@ export interface StoryNpcChatState { remainingTurns?: number | null; limitReason?: 'negative_affinity' | null; forceExitAfterTurn?: boolean; + terminationMode?: 'none' | 'hostile_model' | null; + terminationReason?: 'hostile_breakoff' | 'player_exit' | null; + isHostileChat?: boolean; pendingQuestOffer?: { quest: QuestLogEntry; } | null; @@ -158,6 +162,10 @@ export interface StoryMoment { dialogue?: StoryDialogueTurn[]; streaming?: boolean; deferredOptions?: StoryOption[]; + deferredRuntimeState?: { + currentScenePreset?: ScenePresetInfo | null; + storyEngineMemory?: StoryEngineMemoryState; + }; historyRole?: StoryHistoryRole; npcChatState?: StoryNpcChatState; npcAffinityEffect?: StoryNpcAffinityEffect | null;