use module_big_fish::BigFishAnchorPack; use serde_json::Value as JsonValue; use spacetime_client::{ BigFishAgentMessageRecord, BigFishAnchorPackRecord, BigFishGameDraftRecord, BigFishLevelBlueprintRecord, BigFishSessionRecord, }; use crate::creation_agent_anchor_templates::{ get_creation_agent_anchor_template, render_anchor_question_block, }; use crate::creation_agent_chat::render_quick_fill_extra_rules; pub(crate) const BIG_FISH_AGENT_SYSTEM_PROMPT: &str = r#"你是一个负责和陶泥儿主共创“大鱼吃小鱼”竖屏玩法的中文创意策划。 你必须把用户灵感收束成可以编译为可玩草稿的玩法、生态视觉、成长阶梯和风险节奏。 你必须同时输出: 1. 一段直接发给用户的中文回复 replyText 2. 当前进度 progressPercent 3. 下一轮完整可用的 nextAnchorPack 硬约束: 1. 只能输出 JSON,不能输出代码块或解释 2. nextAnchorPack 必须是完整对象,不能只输出 patch 3. replyText 必须是自然中文,不能提“字段”“锚点”“结构”“JSON”等内部词 4. replyText 一次最多推进一个最关键问题 5. 必须对齐 RPG 共创的体验:先理解玩家幻想,再收束成能进入运行时的可玩效果 6. progressPercent 范围只能是 0 到 100 7. status 只能使用 missing / inferred / confirmed / locked "#; const BIG_FISH_AGENT_OUTPUT_CONTRACT: &str = r#"请严格按以下 JSON 输出,不要输出其他文字: { "replyText": "", "progressPercent": 0, "nextAnchorPack": { "gameplayPromise": { "key": "gameplayPromise", "label": "玩法承诺", "value": "", "status": "missing" }, "ecologyVisualTheme": { "key": "ecologyVisualTheme", "label": "生态视觉主题", "value": "", "status": "missing" }, "growthLadder": { "key": "growthLadder", "label": "成长阶梯", "value": "", "status": "missing" }, "riskTempo": { "key": "riskTempo", "label": "风险节奏", "value": "", "status": "missing" } } }"#; /// 大鱼吃小鱼草稿生成对话提示词脚本。 /// /// 这里单独承载 Agent 共创阶段的 system prompt 与 user prompt 组装, /// 避免聊天契约、草稿编译路由和结果页资产生成混在同一个业务文件里。 pub(crate) fn build_big_fish_agent_prompt( session: &BigFishSessionRecord, quick_fill_requested: bool, ) -> String { let anchor_question_block = get_creation_agent_anchor_template("big_fish") .map(render_anchor_question_block) .unwrap_or_else(|| "模板目标:收束成可玩的竖屏大鱼吃小鱼玩法草稿。".to_string()); let quick_fill_rules = if quick_fill_requested { format!( "\n\n{}", render_quick_fill_extra_rules( "当前玩法方向里的成长、生态、风险节奏等缺失关键词", "不要要求用户再提供等级、鱼群、场景或节奏信息", "输出完整 nextAnchorPack,直接补齐 value 为空或 status 为 missing 的项", "生成结果页", ) ) } else { String::new() }; format!( "{anchor_question_block}{quick_fill_rules}\n\n当前是第 {turn} 轮,当前进度 {progress}% 。\n\n是否要求自动补充剩余关键字:{quick_fill_requested_text}\n\n当前 anchor pack:\n{anchor_pack}\n\n最近聊天记录:\n{chat_history}\n\n{contract}", anchor_question_block = anchor_question_block, quick_fill_rules = quick_fill_rules, turn = session.current_turn.saturating_add(1), progress = session.progress_percent, quick_fill_requested_text = if quick_fill_requested { "是" } else { "否" }, anchor_pack = serialize_record_anchor_pack(&session.anchor_pack), chat_history = serde_json::to_string_pretty(&build_chat_history(session.messages.as_slice())) .unwrap_or_else(|_| "[]".to_string()), contract = BIG_FISH_AGENT_OUTPUT_CONTRACT, ) } /// 大鱼吃小鱼主图生成提示词脚本。 pub(crate) fn build_big_fish_level_main_image_prompt( _draft: &BigFishGameDraftRecord, level: &BigFishLevelBlueprintRecord, ) -> String { vec![ "生成角色形象图片。".to_string(), format!( "等级:Lv.{},名称:{},幻想描述:{}。", level.level, level.name, level.one_line_fantasy ), format!("文字描述:{}。", level.text_description), format!("轮廓方向:{}。", level.silhouette_direction), format!("形象描述:{}。", level.visual_description), format!("主图提示词:{}。", level.visual_prompt_seed), "等级对形象的影响规则:等级越高越霸气、有气场、看起来强大、画面细节丰富,等级级别越低越弱小、普通。最低等级为1级,最高等级可能是6-12级".to_string(), "画面要求:1:1 正方形画布,画面中心构图,角色主体完整置于画面中央,不要裁切主体顶部和底部,不要镜头透视,不要特写。背景固定为纯绿色绿幕,只作为抠像底色,不出现建筑、室内布景、风景、地面道具、漂浮物、烟雾叙事元素、文字或其他角色以外的场景内容。".to_string(), "背景要求:透明背景 PNG 风格,不出现任何场景、水草、气泡、阴影地面、UI、文字、logo、水印、对话框或边框;不要出现多只主体。".to_string(), ] .join("") } /// 大鱼吃小鱼动作关键帧生成提示词脚本。 pub(crate) fn build_big_fish_level_motion_prompt( draft: &BigFishGameDraftRecord, level: &BigFishLevelBlueprintRecord, motion_key: &str, ) -> String { let motion_text = match motion_key { "move_swim" => format!( "{} 向右游动的关键帧预览,身体与尾鳍有清晰推进姿态,带轻微水流拖尾。", level.move_motion_description ), _ => format!( "{} 待机漂浮的关键帧预览,身体轻微摆动,姿态稳定,适合作为 idle 状态。", level.idle_motion_description ), }; vec![ format!( "为竖屏移动游戏《{}》生成一张等级生物动作关键帧静态预览图。", draft.title ), format!("生态主题:{}。", draft.ecology_theme), format!( "等级:Lv.{},名称:{},幻想描述:{}。", level.level, level.name, level.one_line_fantasy ), format!("文字描述:{}。", level.text_description), format!("动作提示词种子:{}。", level.motion_prompt_seed), format!("动作要求:{motion_text}"), "画面要求:按 RPG 角色动画资产口径生成,单体鱼形生物完整入镜,轮廓清晰,动作方向明确,2D 高完成度游戏插画,适合作为 Big Fish 动作槽位的静态 keyframe。".to_string(), "背景要求:透明背景 PNG 风格,不出现任何场景、水草、气泡、阴影地面、UI、文字、logo、水印、对话框或边框;不要生成序列帧拼图,不要出现多只主体。".to_string(), ] .join("") } /// 大鱼吃小鱼场地背景生成提示词脚本。 pub(crate) fn build_big_fish_stage_background_prompt(draft: &BigFishGameDraftRecord) -> String { let background = &draft.background; vec![ "生成一张 9:16 的游戏场景背景图。".to_string(), format!("生态主题:{}。", draft.ecology_theme), format!("背景主题:{}。色彩氛围:{}。", background.theme, background.color_mood), format!("前景提示:{}。", background.foreground_hints), format!("中景构图:{}。", background.midground_composition), format!("背景纵深:{}。", background.background_depth), format!("安全操作区:{}。", background.safe_play_area_hint), format!("出生边缘:{}。", background.spawn_edge_hint), format!("背景提示词种子:{}。", background.background_prompt_seed), "画面要求:竖屏9:16,大场地,全屏运行态背景,中央 80% 保持开阔清爽,边缘只保留少量出生区环境提示。".to_string(), "元素要求:画面中不出现任何形象主体、密集装饰、UI、文字、logo、水印、对话框或边框;不要把中央操作区画得过暗或过复杂。".to_string(), ] .join("") } /// 大鱼吃小鱼图片生成默认反向提示词脚本。 pub(crate) const BIG_FISH_DEFAULT_NEGATIVE_PROMPT: &str = "文字,水印,logo,UI界面,对话框,边框,多余肢体,畸形鱼体,低清晰度,模糊,压缩噪点,现代摄影棚,写实照片背景,复杂背景"; /// 大鱼吃小鱼透明主体类图片生成默认反向提示词脚本。 pub(crate) const BIG_FISH_TRANSPARENT_ASSET_NEGATIVE_PROMPT: &str = "文字,水印,logo,UI界面,对话框,边框,多余肢体,畸形鱼体,低清晰度,模糊,压缩噪点,现代摄影棚,写实照片背景,场景背景,水草背景,气泡背景,多只主体,阴影地面"; fn build_chat_history(messages: &[BigFishAgentMessageRecord]) -> Vec { messages .iter() .map(|message| { serde_json::json!({ "role": message.role, "kind": message.kind, "content": message.text, }) }) .collect() } pub(crate) fn serialize_record_anchor_pack(anchor_pack: &BigFishAnchorPackRecord) -> String { serde_json::to_string_pretty(&map_big_fish_record_anchor_pack(anchor_pack)) .unwrap_or_else(|_| "{}".to_string()) } fn map_big_fish_record_anchor_pack(record: &BigFishAnchorPackRecord) -> BigFishAnchorPack { BigFishAnchorPack { gameplay_promise: map_big_fish_record_anchor_item(&record.gameplay_promise), ecology_visual_theme: map_big_fish_record_anchor_item(&record.ecology_visual_theme), growth_ladder: map_big_fish_record_anchor_item(&record.growth_ladder), risk_tempo: map_big_fish_record_anchor_item(&record.risk_tempo), } } fn map_big_fish_record_anchor_item( record: &spacetime_client::BigFishAnchorItemRecord, ) -> module_big_fish::BigFishAnchorItem { module_big_fish::BigFishAnchorItem { key: record.key.clone(), label: record.label.clone(), value: record.value.clone(), status: match record.status.as_str() { "confirmed" => module_big_fish::BigFishAnchorStatus::Confirmed, "locked" => module_big_fish::BigFishAnchorStatus::Locked, "inferred" => module_big_fish::BigFishAnchorStatus::Inferred, _ => module_big_fish::BigFishAnchorStatus::Missing, }, } } #[cfg(test)] mod tests { use super::{ BIG_FISH_DEFAULT_NEGATIVE_PROMPT, BIG_FISH_TRANSPARENT_ASSET_NEGATIVE_PROMPT, build_big_fish_agent_prompt, build_big_fish_level_main_image_prompt, build_big_fish_level_motion_prompt, build_big_fish_stage_background_prompt, }; fn anchor_item( key: &str, label: &str, value: &str, status: &str, ) -> spacetime_client::BigFishAnchorItemRecord { spacetime_client::BigFishAnchorItemRecord { key: key.to_string(), label: label.to_string(), value: value.to_string(), status: status.to_string(), } } fn empty_session_record() -> spacetime_client::BigFishSessionRecord { spacetime_client::BigFishSessionRecord { session_id: "big-fish-session-test".to_string(), current_turn: 2, progress_percent: 60, stage: "collecting_anchors".to_string(), anchor_pack: spacetime_client::BigFishAnchorPackRecord { gameplay_promise: anchor_item( "gameplayPromise", "玩法承诺", "微光小鱼逆袭深海巨兽", "confirmed", ), ecology_visual_theme: anchor_item( "ecologyVisualTheme", "生态视觉主题", "幽蓝珊瑚海沟", "confirmed", ), growth_ladder: anchor_item("growthLadder", "成长阶梯", "", "missing"), risk_tempo: anchor_item("riskTempo", "风险节奏", "", "missing"), }, draft: None, asset_slots: Vec::new(), asset_coverage: spacetime_client::BigFishAssetCoverageRecord { level_main_image_ready_count: 0, level_motion_ready_count: 0, background_ready: false, required_level_count: 8, publish_ready: false, blockers: Vec::new(), }, messages: Vec::new(), last_assistant_reply: None, publish_ready: false, updated_at: "2026-04-24T10:00:00.000Z".to_string(), } } fn sample_draft() -> spacetime_client::BigFishGameDraftRecord { spacetime_client::BigFishGameDraftRecord { title: "深海逆袭".to_string(), subtitle: "从微光幼体吞噬到深渊王座".to_string(), core_fun: "吞噬成长与风险闪避".to_string(), ecology_theme: "幽蓝海沟珊瑚裂谷".to_string(), levels: vec![spacetime_client::BigFishLevelBlueprintRecord { level: 3, name: "裂潮猎游者".to_string(), one_line_fantasy: "在电光海沟中疾行收割的中阶猎鱼".to_string(), text_description: "裂潮猎游者是中阶进化体,已经具备更清晰的猎食轮廓和压迫感。" .to_string(), silhouette_direction: "长尾前探、背鳍后掠".to_string(), size_ratio: 1.8, visual_description: "深海霓虹风格的中阶猎鱼,长尾锐利,骨质鳍刃明显,轮廓成熟。" .to_string(), visual_prompt_seed: "深海霓虹、锐利长尾、骨质鳍刃".to_string(), idle_motion_description: "待机时身体轻微悬停,尾鳍保持低频摆动,像是在观察猎物距离。".to_string(), move_motion_description: "移动时长尾快速摆动,身体前探,形成明显突进巡游姿态。" .to_string(), motion_prompt_seed: "突进摆尾、鳍面拉伸、水流拖尾".to_string(), merge_source_level: Some(2), prey_window: vec![1, 2], threat_window: vec![4, 5], is_final_level: false, }], background: spacetime_client::BigFishBackgroundBlueprintRecord { theme: "裂谷荧光水域".to_string(), color_mood: "蓝绿冷光、边缘紫雾".to_string(), foreground_hints: "边角保留细碎水母草和岩脊".to_string(), midground_composition: "中央留大面积清爽水道".to_string(), background_depth: "远处海沟层叠透视".to_string(), safe_play_area_hint: "中央 80% 为操作安全区".to_string(), spawn_edge_hint: "左右边缘保留出生点环境提示".to_string(), background_prompt_seed: "荧光裂谷、冷色纵深、轻体积光".to_string(), }, runtime_params: spacetime_client::BigFishRuntimeParamsRecord { level_count: 8, merge_count_per_upgrade: 3, spawn_target_count: 12, leader_move_speed: 240.0, follower_catch_up_speed: 280.0, offscreen_cull_seconds: 4.5, prey_spawn_delta_levels: vec![0, 1], threat_spawn_delta_levels: vec![1, 2], win_level: 8, }, } } #[test] fn quick_fill_prompt_forbids_follow_up_questions() { let prompt = build_big_fish_agent_prompt(&empty_session_record(), true); assert!(prompt.contains("用户刚刚主动要求你自动补充剩余关键字")); assert!(prompt.contains("不要再继续提问")); assert!(prompt.contains("progressPercent 直接输出为 100")); } #[test] fn level_main_image_prompt_keeps_core_constraints() { let draft = sample_draft(); let prompt = build_big_fish_level_main_image_prompt(&draft, &draft.levels[0]); assert!(prompt.contains("裂潮猎游者")); assert!(prompt.contains("形象描述")); assert!(prompt.contains("透明背景 PNG 风格")); assert!(prompt.contains("主图提示词")); } #[test] fn level_motion_prompt_varies_with_motion_key() { let draft = sample_draft(); let move_prompt = build_big_fish_level_motion_prompt(&draft, &draft.levels[0], "move_swim"); let idle_prompt = build_big_fish_level_motion_prompt(&draft, &draft.levels[0], "idle_float"); assert!(move_prompt.contains("向右游动的关键帧预览")); assert!(idle_prompt.contains("待机漂浮的关键帧预览")); assert!(move_prompt.contains("透明背景 PNG 风格")); } #[test] fn stage_background_prompt_keeps_runtime_field_constraints() { let draft = sample_draft(); let prompt = build_big_fish_stage_background_prompt(&draft); assert!(prompt.contains("生成一张 9:16 的游戏场景背景图")); assert!(prompt.contains("中央 80% 保持开阔清爽")); assert!(prompt.contains("背景提示词种子")); } #[test] fn negative_prompts_keep_text_and_background_blockers() { assert!(BIG_FISH_DEFAULT_NEGATIVE_PROMPT.contains("文字")); assert!(BIG_FISH_DEFAULT_NEGATIVE_PROMPT.contains("复杂背景")); assert!(BIG_FISH_TRANSPARENT_ASSET_NEGATIVE_PROMPT.contains("场景背景")); assert!(BIG_FISH_TRANSPARENT_ASSET_NEGATIVE_PROMPT.contains("多只主体")); } }