Close DDD cleanup and tests-support closure
This commit is contained in:
@@ -1,20 +1,27 @@
|
||||
//! 大鱼吃小鱼应用编排过渡落位。
|
||||
//! 大鱼吃小鱼应用编排。
|
||||
//!
|
||||
//! 这里只组合领域规则并返回结果或事件,不直接调用外部图片、视频或存储服务。
|
||||
|
||||
use shared_kernel::normalize_required_string;
|
||||
|
||||
use crate::{
|
||||
BIG_FISH_DEFAULT_LEVEL_COUNT, BIG_FISH_TARGET_WILD_COUNT, BigFishAssetSlotSnapshot,
|
||||
build_asset_coverage,
|
||||
commands::{
|
||||
EvaluateBigFishPublishReadinessCommand, StartBigFishRunCommand, SubmitBigFishInputCommand,
|
||||
BigFishAssetGenerateInput, BigFishDraftCompileInput, BigFishInputSubmitInput,
|
||||
BigFishMessageFinalizeInput, BigFishMessageSubmitInput, BigFishPlayRecordInput,
|
||||
BigFishPublishInput, BigFishRunGetInput, BigFishRunStartInput, BigFishSessionCreateInput,
|
||||
BigFishSessionGetInput, BigFishWorksListInput, EvaluateBigFishPublishReadinessCommand,
|
||||
StartBigFishRunCommand, SubmitBigFishInputCommand,
|
||||
},
|
||||
domain::{
|
||||
BIG_FISH_ASSET_SLOT_ID_PREFIX, BIG_FISH_DEFAULT_LEVEL_COUNT,
|
||||
BIG_FISH_MERGE_COUNT_PER_UPGRADE, BIG_FISH_OFFSCREEN_CULL_SECONDS,
|
||||
BIG_FISH_TARGET_WILD_COUNT, BigFishAnchorItem, BigFishAnchorPack, BigFishAnchorStatus,
|
||||
BigFishAssetCoverage, BigFishAssetKind, BigFishAssetSlotSnapshot, BigFishAssetStatus,
|
||||
BigFishBackgroundBlueprint, BigFishGameDraft, BigFishLevelBlueprint,
|
||||
BigFishPublishReadiness, BigFishRunStatus, BigFishRuntimeEntitySnapshot,
|
||||
BigFishRuntimeSnapshot, BigFishVector2,
|
||||
BigFishRuntimeParams, BigFishRuntimeSnapshot, BigFishVector2,
|
||||
},
|
||||
errors::BigFishApplicationError,
|
||||
errors::{BigFishApplicationError, BigFishFieldError},
|
||||
events::BigFishDomainEvent,
|
||||
};
|
||||
|
||||
@@ -578,6 +585,515 @@ fn settlement_events(
|
||||
}]
|
||||
}
|
||||
|
||||
pub fn empty_anchor_pack() -> BigFishAnchorPack {
|
||||
BigFishAnchorPack {
|
||||
gameplay_promise: BigFishAnchorItem {
|
||||
key: "gameplayPromise".to_string(),
|
||||
label: "玩法承诺".to_string(),
|
||||
value: String::new(),
|
||||
status: BigFishAnchorStatus::Missing,
|
||||
},
|
||||
ecology_visual_theme: BigFishAnchorItem {
|
||||
key: "ecologyVisualTheme".to_string(),
|
||||
label: "生态与视觉母题".to_string(),
|
||||
value: String::new(),
|
||||
status: BigFishAnchorStatus::Missing,
|
||||
},
|
||||
growth_ladder: BigFishAnchorItem {
|
||||
key: "growthLadder".to_string(),
|
||||
label: "成长阶梯".to_string(),
|
||||
value: String::new(),
|
||||
status: BigFishAnchorStatus::Missing,
|
||||
},
|
||||
risk_tempo: BigFishAnchorItem {
|
||||
key: "riskTempo".to_string(),
|
||||
label: "风险节奏".to_string(),
|
||||
value: "平衡".to_string(),
|
||||
status: BigFishAnchorStatus::Inferred,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn infer_anchor_pack(seed_text: &str, latest_message: Option<&str>) -> BigFishAnchorPack {
|
||||
let source = normalize_required_string(latest_message.unwrap_or(seed_text))
|
||||
.or_else(|| normalize_required_string(seed_text))
|
||||
.unwrap_or_else(|| "深海弱小逆袭,逐级吞噬成长".to_string());
|
||||
let mut pack = empty_anchor_pack();
|
||||
pack.gameplay_promise.value = if source.contains("可爱") {
|
||||
"可爱生态成长".to_string()
|
||||
} else if source.contains("机械") {
|
||||
"机械微生物吞并进化".to_string()
|
||||
} else {
|
||||
"弱小逆袭和群体吞并".to_string()
|
||||
};
|
||||
pack.gameplay_promise.status = BigFishAnchorStatus::Inferred;
|
||||
pack.ecology_visual_theme.value = if source.contains("机械") {
|
||||
"机械微生物水域".to_string()
|
||||
} else if source.contains("梦") {
|
||||
"梦境纸鱼生态".to_string()
|
||||
} else {
|
||||
"深海生物生态".to_string()
|
||||
};
|
||||
pack.ecology_visual_theme.status = BigFishAnchorStatus::Inferred;
|
||||
pack.growth_ladder.value = "8 级连续进化,从幼小个体成长为终局巨兽".to_string();
|
||||
pack.growth_ladder.status = BigFishAnchorStatus::Inferred;
|
||||
pack.risk_tempo.value = if source.contains("爽") {
|
||||
"偏爽快".to_string()
|
||||
} else if source.contains("压迫") {
|
||||
"偏压迫".to_string()
|
||||
} else {
|
||||
"平衡".to_string()
|
||||
};
|
||||
pack
|
||||
}
|
||||
|
||||
pub fn compile_default_draft(anchor_pack: &BigFishAnchorPack) -> BigFishGameDraft {
|
||||
let level_count = BIG_FISH_DEFAULT_LEVEL_COUNT;
|
||||
let theme = fallback_anchor_value(&anchor_pack.ecology_visual_theme, "深海生物生态");
|
||||
let core_fun = fallback_anchor_value(&anchor_pack.gameplay_promise, "弱小逆袭和群体吞并");
|
||||
let risk_tempo = fallback_anchor_value(&anchor_pack.risk_tempo, "平衡");
|
||||
|
||||
let levels = (1..=level_count)
|
||||
.map(|level| build_level_blueprint(level, level_count, &theme))
|
||||
.collect();
|
||||
|
||||
BigFishGameDraft {
|
||||
title: format!("{theme} 大鱼吃小鱼"),
|
||||
subtitle: format!("{core_fun} · {risk_tempo}节奏"),
|
||||
core_fun,
|
||||
ecology_theme: theme.clone(),
|
||||
levels,
|
||||
background: BigFishBackgroundBlueprint {
|
||||
theme: theme.clone(),
|
||||
color_mood: "深蓝、青绿、带少量暖色生物光".to_string(),
|
||||
foreground_hints: "只保留少量漂浮颗粒和边缘水草,不遮挡中央操作区".to_string(),
|
||||
midground_composition: "中央留出大面积清晰活动区域,边缘只做出生缓冲层".to_string(),
|
||||
background_depth: "简洁纵深水域与极少量远处剪影".to_string(),
|
||||
safe_play_area_hint: "9:16 竖屏中央 80% 为主要活动区".to_string(),
|
||||
spawn_edge_hint: "四周边缘以少量暗礁或水草提示野生实体出生区".to_string(),
|
||||
background_prompt_seed: format!(
|
||||
"{theme},竖屏 9:16,全屏大场地游戏背景,元素少,中央开阔,无文字,无 UI 框"
|
||||
),
|
||||
},
|
||||
runtime_params: BigFishRuntimeParams {
|
||||
level_count,
|
||||
merge_count_per_upgrade: BIG_FISH_MERGE_COUNT_PER_UPGRADE,
|
||||
spawn_target_count: BIG_FISH_TARGET_WILD_COUNT as u32,
|
||||
leader_move_speed: 160.0,
|
||||
follower_catch_up_speed: 120.0,
|
||||
offscreen_cull_seconds: BIG_FISH_OFFSCREEN_CULL_SECONDS,
|
||||
prey_spawn_delta_levels: vec![1, 2],
|
||||
threat_spawn_delta_levels: vec![1, 2],
|
||||
win_level: level_count,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_asset_coverage(
|
||||
draft: Option<&BigFishGameDraft>,
|
||||
asset_slots: &[BigFishAssetSlotSnapshot],
|
||||
) -> BigFishAssetCoverage {
|
||||
let required_level_count = draft
|
||||
.map(|value| value.runtime_params.level_count)
|
||||
.unwrap_or(BIG_FISH_DEFAULT_LEVEL_COUNT);
|
||||
let main_ready = asset_slots
|
||||
.iter()
|
||||
.filter(|slot| {
|
||||
slot.asset_kind == BigFishAssetKind::LevelMainImage
|
||||
&& slot.status == BigFishAssetStatus::Ready
|
||||
})
|
||||
.count() as u32;
|
||||
let motion_ready = asset_slots
|
||||
.iter()
|
||||
.filter(|slot| {
|
||||
slot.asset_kind == BigFishAssetKind::LevelMotion
|
||||
&& slot.status == BigFishAssetStatus::Ready
|
||||
})
|
||||
.count() as u32;
|
||||
let background_ready = asset_slots.iter().any(|slot| {
|
||||
slot.asset_kind == BigFishAssetKind::StageBackground
|
||||
&& slot.status == BigFishAssetStatus::Ready
|
||||
});
|
||||
|
||||
let required_motion_count = required_level_count * 2;
|
||||
let mut blockers = Vec::new();
|
||||
if draft.is_none() {
|
||||
blockers.push("玩法草稿尚未编译".to_string());
|
||||
}
|
||||
if main_ready < required_level_count {
|
||||
blockers.push(format!(
|
||||
"还缺少 {} 个等级主图",
|
||||
required_level_count.saturating_sub(main_ready)
|
||||
));
|
||||
}
|
||||
if motion_ready < required_motion_count {
|
||||
blockers.push(format!(
|
||||
"还缺少 {} 个基础动作",
|
||||
required_motion_count.saturating_sub(motion_ready)
|
||||
));
|
||||
}
|
||||
if !background_ready {
|
||||
blockers.push("还缺少活动区域背景图".to_string());
|
||||
}
|
||||
|
||||
BigFishAssetCoverage {
|
||||
level_main_image_ready_count: main_ready,
|
||||
level_motion_ready_count: motion_ready,
|
||||
background_ready,
|
||||
required_level_count,
|
||||
publish_ready: blockers.is_empty(),
|
||||
blockers,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_generated_asset_slot(
|
||||
session_id: &str,
|
||||
draft: &BigFishGameDraft,
|
||||
asset_kind: BigFishAssetKind,
|
||||
level: Option<u32>,
|
||||
motion_key: Option<String>,
|
||||
asset_url: Option<String>,
|
||||
updated_at_micros: i64,
|
||||
) -> Result<BigFishAssetSlotSnapshot, BigFishFieldError> {
|
||||
let session_id =
|
||||
normalize_required_string(session_id).ok_or(BigFishFieldError::MissingSessionId)?;
|
||||
let prompt_snapshot =
|
||||
build_asset_prompt_snapshot(draft, asset_kind, level, motion_key.as_deref())?;
|
||||
let slot_id = build_asset_slot_id(&session_id, asset_kind, level, motion_key.as_deref());
|
||||
let resolved_asset_url = normalize_required_string(asset_url.as_deref().unwrap_or_default())
|
||||
.unwrap_or_else(|| build_placeholder_asset_url(asset_kind, level, updated_at_micros));
|
||||
|
||||
Ok(BigFishAssetSlotSnapshot {
|
||||
slot_id,
|
||||
session_id,
|
||||
asset_kind,
|
||||
level,
|
||||
motion_key,
|
||||
status: BigFishAssetStatus::Ready,
|
||||
asset_url: Some(resolved_asset_url),
|
||||
prompt_snapshot,
|
||||
updated_at_micros,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn validate_session_get_input(input: &BigFishSessionGetInput) -> Result<(), BigFishFieldError> {
|
||||
validate_session_owner(&input.session_id, &input.owner_user_id)
|
||||
}
|
||||
|
||||
pub fn validate_works_list_input(input: &BigFishWorksListInput) -> Result<(), BigFishFieldError> {
|
||||
if input.published_only {
|
||||
return Ok(());
|
||||
}
|
||||
if normalize_required_string(&input.owner_user_id).is_none() {
|
||||
return Err(BigFishFieldError::MissingOwnerUserId);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn validate_session_create_input(
|
||||
input: &BigFishSessionCreateInput,
|
||||
) -> Result<(), BigFishFieldError> {
|
||||
validate_session_owner(&input.session_id, &input.owner_user_id)?;
|
||||
if normalize_required_string(&input.welcome_message_id).is_none() {
|
||||
return Err(BigFishFieldError::MissingMessageId);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn validate_message_submit_input(
|
||||
input: &BigFishMessageSubmitInput,
|
||||
) -> Result<(), BigFishFieldError> {
|
||||
validate_session_owner(&input.session_id, &input.owner_user_id)?;
|
||||
if normalize_required_string(&input.user_message_id).is_none()
|
||||
|| normalize_required_string(&input.assistant_message_id).is_none()
|
||||
{
|
||||
return Err(BigFishFieldError::MissingMessageId);
|
||||
}
|
||||
if normalize_required_string(&input.user_message_text).is_none() {
|
||||
return Err(BigFishFieldError::MissingMessageText);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn validate_message_finalize_input(
|
||||
input: &BigFishMessageFinalizeInput,
|
||||
) -> Result<(), BigFishFieldError> {
|
||||
validate_session_owner(&input.session_id, &input.owner_user_id)
|
||||
}
|
||||
|
||||
pub fn validate_draft_compile_input(
|
||||
input: &BigFishDraftCompileInput,
|
||||
) -> Result<(), BigFishFieldError> {
|
||||
validate_session_owner(&input.session_id, &input.owner_user_id)
|
||||
}
|
||||
|
||||
pub fn validate_asset_generate_input(
|
||||
input: &BigFishAssetGenerateInput,
|
||||
draft: &BigFishGameDraft,
|
||||
) -> Result<(), BigFishFieldError> {
|
||||
validate_session_owner(&input.session_id, &input.owner_user_id)?;
|
||||
match input.asset_kind {
|
||||
BigFishAssetKind::LevelMainImage => validate_level(input.level, draft),
|
||||
BigFishAssetKind::LevelMotion => {
|
||||
validate_level(input.level, draft)?;
|
||||
match input.motion_key.as_deref() {
|
||||
Some("idle_float" | "move_swim") => Ok(()),
|
||||
_ => Err(BigFishFieldError::InvalidAssetKind),
|
||||
}
|
||||
}
|
||||
BigFishAssetKind::StageBackground => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn validate_publish_input(input: &BigFishPublishInput) -> Result<(), BigFishFieldError> {
|
||||
validate_session_owner(&input.session_id, &input.owner_user_id)
|
||||
}
|
||||
|
||||
pub fn validate_play_record_input(input: &BigFishPlayRecordInput) -> Result<(), BigFishFieldError> {
|
||||
if normalize_required_string(&input.session_id).is_none() {
|
||||
return Err(BigFishFieldError::MissingSessionId);
|
||||
}
|
||||
if normalize_required_string(&input.user_id).is_none() {
|
||||
return Err(BigFishFieldError::MissingOwnerUserId);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn validate_run_start_input(input: &BigFishRunStartInput) -> Result<(), BigFishFieldError> {
|
||||
validate_session_owner(&input.session_id, &input.owner_user_id)?;
|
||||
if normalize_required_string(&input.run_id).is_none() {
|
||||
return Err(BigFishFieldError::MissingRunId);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn validate_run_get_input(input: &BigFishRunGetInput) -> Result<(), BigFishFieldError> {
|
||||
if normalize_required_string(&input.run_id).is_none() {
|
||||
return Err(BigFishFieldError::MissingRunId);
|
||||
}
|
||||
if normalize_required_string(&input.owner_user_id).is_none() {
|
||||
return Err(BigFishFieldError::MissingOwnerUserId);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn validate_input_submit_input(
|
||||
input: &BigFishInputSubmitInput,
|
||||
) -> Result<(), BigFishFieldError> {
|
||||
if normalize_required_string(&input.run_id).is_none() {
|
||||
return Err(BigFishFieldError::MissingRunId);
|
||||
}
|
||||
if normalize_required_string(&input.owner_user_id).is_none() {
|
||||
return Err(BigFishFieldError::MissingOwnerUserId);
|
||||
}
|
||||
if !input.x.is_finite() || !input.y.is_finite() {
|
||||
return Err(BigFishFieldError::InvalidRuntimeInput);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn serialize_anchor_pack(anchor_pack: &BigFishAnchorPack) -> Result<String, serde_json::Error> {
|
||||
serde_json::to_string(anchor_pack)
|
||||
}
|
||||
|
||||
pub fn deserialize_anchor_pack(value: &str) -> Result<BigFishAnchorPack, serde_json::Error> {
|
||||
serde_json::from_str(value)
|
||||
}
|
||||
|
||||
pub fn serialize_draft(draft: &BigFishGameDraft) -> Result<String, serde_json::Error> {
|
||||
serde_json::to_string(draft)
|
||||
}
|
||||
|
||||
pub fn deserialize_draft(value: &str) -> Result<BigFishGameDraft, serde_json::Error> {
|
||||
serde_json::from_str(value)
|
||||
}
|
||||
|
||||
pub fn serialize_asset_coverage(
|
||||
coverage: &BigFishAssetCoverage,
|
||||
) -> Result<String, serde_json::Error> {
|
||||
serde_json::to_string(coverage)
|
||||
}
|
||||
|
||||
pub fn deserialize_asset_coverage(value: &str) -> Result<BigFishAssetCoverage, serde_json::Error> {
|
||||
serde_json::from_str(value)
|
||||
}
|
||||
|
||||
fn fallback_anchor_value(anchor: &BigFishAnchorItem, fallback: &str) -> String {
|
||||
normalize_required_string(&anchor.value).unwrap_or_else(|| fallback.to_string())
|
||||
}
|
||||
|
||||
fn build_level_blueprint(level: u32, level_count: u32, theme: &str) -> BigFishLevelBlueprint {
|
||||
let prey_window = (1..level)
|
||||
.rev()
|
||||
.take(2)
|
||||
.collect::<Vec<_>>()
|
||||
.into_iter()
|
||||
.rev()
|
||||
.collect();
|
||||
let threat_window = ((level + 1)..=(level + 2).min(level_count)).collect::<Vec<_>>();
|
||||
let size_ratio = 1.0 + (level.saturating_sub(1) as f32 * 0.22);
|
||||
let name = format!("{theme} L{level}");
|
||||
let one_line_fantasy = if level == level_count {
|
||||
"终局巨兽形态,获得即可通关".to_string()
|
||||
} else {
|
||||
format!("第 {level} 阶实体,继续吞噬同级和低级个体成长")
|
||||
};
|
||||
let text_description = if level == 1 {
|
||||
format!(
|
||||
"{name} 是这套 {theme} 等级阶梯的起点个体,体型最小、动作轻盈,会在谨慎试探中寻找第一个可吞噬目标。"
|
||||
)
|
||||
} else if level == level_count {
|
||||
format!(
|
||||
"{name} 是这套 {theme} 生态中的终局霸主形态,体格巨大、压迫感最强,一旦成型就代表本局成长链已经完成。"
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"{name} 是 {theme} 生态里的第 {level} 阶进化体,已经具备更鲜明的轮廓、猎食性和压迫感,会继续通过吞并同级与低级实体向上跃迁。"
|
||||
)
|
||||
};
|
||||
let visual_description = if level == 1 {
|
||||
format!(
|
||||
"{theme} 风格的小型初始鱼形生物,体态轻巧,轮廓圆润,局部带少量发光纹路或主题特征,明显呈现弱小但灵动的开局形象。"
|
||||
)
|
||||
} else if level == level_count {
|
||||
format!(
|
||||
"{theme} 风格的终局巨型鱼形霸主,体长与鳍面明显扩张,轮廓锋利或威严,层次细节最丰富,拥有一眼可辨识的终局统治感。"
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"{theme} 风格的第 {level} 级进化鱼形生物,相比上一阶段更大、更强、更成熟,身体主轮廓更清晰,局部装饰、鳍面结构和主题特征都更明显。"
|
||||
)
|
||||
};
|
||||
let idle_motion_description = if level == level_count {
|
||||
"待机时缓慢悬停,身体主体保持稳定,尾鳍与侧鳍做低频摆动,呈现强者从容压场的漂浮感。"
|
||||
.to_string()
|
||||
} else {
|
||||
format!(
|
||||
"待机时保持轻微漂浮与呼吸感摆动,尾鳍和侧鳍以小幅度节奏晃动,体现 Lv.{level} 生物在水中蓄势观察的状态。"
|
||||
)
|
||||
};
|
||||
let move_motion_description = if level == level_count {
|
||||
"移动时身体前倾,尾鳍和背鳍形成强力推进姿态,带出稳定而有压迫感的高速巡游动势。".to_string()
|
||||
} else {
|
||||
format!(
|
||||
"移动时身体向前游动,尾鳍形成清晰摆尾推进,整体节奏比待机更主动,体现 Lv.{level} 生物追逐猎物时的连续游动感。"
|
||||
)
|
||||
};
|
||||
BigFishLevelBlueprint {
|
||||
level,
|
||||
name,
|
||||
one_line_fantasy,
|
||||
text_description,
|
||||
silhouette_direction: format!(
|
||||
"体型约为初始的 {:.1} 倍,轮廓更清晰",
|
||||
1.0 + level as f32 * 0.22
|
||||
),
|
||||
size_ratio,
|
||||
visual_description: visual_description.clone(),
|
||||
visual_prompt_seed: format!(
|
||||
"{visual_description} 透明背景,单体完整入镜,适合作为竖屏吞噬成长玩法的等级主图。"
|
||||
),
|
||||
idle_motion_description: idle_motion_description.clone(),
|
||||
move_motion_description: move_motion_description.clone(),
|
||||
motion_prompt_seed: format!(
|
||||
"待机动作:{idle_motion_description} 移动动作:{move_motion_description}"
|
||||
),
|
||||
merge_source_level: if level == 1 { None } else { Some(level - 1) },
|
||||
prey_window,
|
||||
threat_window,
|
||||
is_final_level: level == level_count,
|
||||
}
|
||||
}
|
||||
|
||||
fn build_asset_prompt_snapshot(
|
||||
draft: &BigFishGameDraft,
|
||||
asset_kind: BigFishAssetKind,
|
||||
level: Option<u32>,
|
||||
motion_key: Option<&str>,
|
||||
) -> Result<String, BigFishFieldError> {
|
||||
match asset_kind {
|
||||
BigFishAssetKind::LevelMainImage => {
|
||||
let level = level.ok_or(BigFishFieldError::InvalidLevel)?;
|
||||
let blueprint = draft
|
||||
.levels
|
||||
.iter()
|
||||
.find(|item| item.level == level)
|
||||
.ok_or(BigFishFieldError::InvalidLevel)?;
|
||||
Ok(blueprint.visual_prompt_seed.clone())
|
||||
}
|
||||
BigFishAssetKind::LevelMotion => {
|
||||
let level = level.ok_or(BigFishFieldError::InvalidLevel)?;
|
||||
let blueprint = draft
|
||||
.levels
|
||||
.iter()
|
||||
.find(|item| item.level == level)
|
||||
.ok_or(BigFishFieldError::InvalidLevel)?;
|
||||
let motion_key = motion_key.ok_or(BigFishFieldError::InvalidAssetKind)?;
|
||||
let motion_description = match motion_key {
|
||||
"idle_float" => blueprint.idle_motion_description.as_str(),
|
||||
"move_swim" => blueprint.move_motion_description.as_str(),
|
||||
_ => return Err(BigFishFieldError::InvalidAssetKind),
|
||||
};
|
||||
Ok(format!(
|
||||
"{} 动作位:{}。{} 透明背景,单体完整入镜。",
|
||||
blueprint.motion_prompt_seed, motion_key, motion_description
|
||||
))
|
||||
}
|
||||
BigFishAssetKind::StageBackground => Ok(draft.background.background_prompt_seed.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
fn build_asset_slot_id(
|
||||
session_id: &str,
|
||||
asset_kind: BigFishAssetKind,
|
||||
level: Option<u32>,
|
||||
motion_key: Option<&str>,
|
||||
) -> String {
|
||||
let level_part = level
|
||||
.map(|value| value.to_string())
|
||||
.unwrap_or_else(|| "stage".to_string());
|
||||
let motion_part = motion_key.unwrap_or("main");
|
||||
format!(
|
||||
"{BIG_FISH_ASSET_SLOT_ID_PREFIX}{session_id}_{}_{}_{}",
|
||||
asset_kind.as_str(),
|
||||
level_part,
|
||||
motion_part
|
||||
)
|
||||
}
|
||||
|
||||
fn build_placeholder_asset_url(
|
||||
asset_kind: BigFishAssetKind,
|
||||
level: Option<u32>,
|
||||
seed_micros: i64,
|
||||
) -> String {
|
||||
let level_part = level
|
||||
.map(|value| format!("level-{value}"))
|
||||
.unwrap_or_else(|| "stage".to_string());
|
||||
format!(
|
||||
"/generated-big-fish/{}/{}/{}.png",
|
||||
asset_kind.as_str(),
|
||||
level_part,
|
||||
seed_micros
|
||||
)
|
||||
}
|
||||
|
||||
fn validate_session_owner(session_id: &str, owner_user_id: &str) -> Result<(), BigFishFieldError> {
|
||||
if normalize_required_string(session_id).is_none() {
|
||||
return Err(BigFishFieldError::MissingSessionId);
|
||||
}
|
||||
if normalize_required_string(owner_user_id).is_none() {
|
||||
return Err(BigFishFieldError::MissingOwnerUserId);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_level(level: Option<u32>, draft: &BigFishGameDraft) -> Result<(), BigFishFieldError> {
|
||||
match level {
|
||||
Some(value) if (1..=draft.runtime_params.level_count).contains(&value) => Ok(()),
|
||||
_ => Err(BigFishFieldError::InvalidLevel),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
//! 大鱼吃小鱼写入命令过渡落位。
|
||||
//! 大鱼吃小鱼写入命令。
|
||||
//!
|
||||
//! 用于表达创建会话、写入消息、更新资产槽和推进运行态等输入。
|
||||
|
||||
use crate::{BigFishGameDraft, domain::BigFishRuntimeSnapshot};
|
||||
use crate::domain::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
#[cfg(feature = "spacetime-types")]
|
||||
use spacetimedb::SpacetimeType;
|
||||
|
||||
/// 评估作品是否可以发布的纯领域命令。
|
||||
///
|
||||
@@ -36,3 +39,140 @@ pub struct SubmitBigFishInputCommand {
|
||||
pub submitted_at_micros: i64,
|
||||
pub current_snapshot: BigFishRuntimeSnapshot,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct BigFishWorksListInput {
|
||||
pub owner_user_id: String,
|
||||
pub published_only: bool,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct BigFishWorkDeleteInput {
|
||||
pub session_id: String,
|
||||
pub owner_user_id: String,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct BigFishWorksProcedureResult {
|
||||
pub ok: bool,
|
||||
pub items_json: Option<String>,
|
||||
pub error_message: Option<String>,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct BigFishSessionCreateInput {
|
||||
pub session_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub seed_text: String,
|
||||
pub welcome_message_id: String,
|
||||
pub welcome_message_text: String,
|
||||
pub created_at_micros: i64,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct BigFishSessionGetInput {
|
||||
pub session_id: String,
|
||||
pub owner_user_id: String,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct BigFishMessageSubmitInput {
|
||||
pub session_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub user_message_id: String,
|
||||
pub user_message_text: String,
|
||||
pub assistant_message_id: String,
|
||||
pub submitted_at_micros: i64,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct BigFishMessageFinalizeInput {
|
||||
pub session_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub assistant_message_id: Option<String>,
|
||||
pub assistant_reply_text: Option<String>,
|
||||
pub stage: BigFishCreationStage,
|
||||
pub progress_percent: u32,
|
||||
pub anchor_pack_json: String,
|
||||
pub error_message: Option<String>,
|
||||
pub updated_at_micros: i64,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct BigFishDraftCompileInput {
|
||||
pub session_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub draft_json: Option<String>,
|
||||
pub compiled_at_micros: i64,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct BigFishAssetGenerateInput {
|
||||
pub session_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub asset_kind: BigFishAssetKind,
|
||||
pub level: Option<u32>,
|
||||
pub motion_key: Option<String>,
|
||||
pub asset_url: Option<String>,
|
||||
pub generated_at_micros: i64,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct BigFishPublishInput {
|
||||
pub session_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub published_at_micros: i64,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct BigFishPlayRecordInput {
|
||||
pub session_id: String,
|
||||
pub user_id: String,
|
||||
pub elapsed_ms: u64,
|
||||
pub played_at_micros: i64,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct BigFishRunStartInput {
|
||||
pub run_id: String,
|
||||
pub session_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub started_at_micros: i64,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct BigFishRunGetInput {
|
||||
pub run_id: String,
|
||||
pub owner_user_id: String,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct BigFishInputSubmitInput {
|
||||
pub run_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub x: f32,
|
||||
pub y: f32,
|
||||
pub submitted_at_micros: i64,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct BigFishRunProcedureResult {
|
||||
pub ok: bool,
|
||||
pub run_json: Option<String>,
|
||||
pub error_message: Option<String>,
|
||||
}
|
||||
|
||||
@@ -1,12 +1,234 @@
|
||||
//! 大鱼吃小鱼领域模型过渡落位。
|
||||
//! 大鱼吃小鱼领域模型。
|
||||
//!
|
||||
//! 后续迁移创作会话、资产槽和运行态聚合时,只保留玩法状态与规则;
|
||||
//! 图片生成、OSS 与 HTTP handler 均留在 adapter 层。
|
||||
//! 保留创作会话、资产槽、发布门禁和运行态聚合的纯领域结构;图片生成、OSS 与 HTTP handler 均留在 adapter 层。
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
#[cfg(feature = "spacetime-types")]
|
||||
use spacetimedb::SpacetimeType;
|
||||
|
||||
pub const BIG_FISH_SESSION_ID_PREFIX: &str = "big-fish-session-";
|
||||
pub const BIG_FISH_MESSAGE_ID_PREFIX: &str = "big-fish-message-";
|
||||
pub const BIG_FISH_OPERATION_ID_PREFIX: &str = "big-fish-operation-";
|
||||
pub const BIG_FISH_ASSET_SLOT_ID_PREFIX: &str = "big-fish-asset-";
|
||||
pub const BIG_FISH_DEFAULT_LEVEL_COUNT: u32 = 8;
|
||||
pub const BIG_FISH_MIN_LEVEL_COUNT: u32 = 6;
|
||||
pub const BIG_FISH_MAX_LEVEL_COUNT: u32 = 12;
|
||||
pub const BIG_FISH_MERGE_COUNT_PER_UPGRADE: u32 = 3;
|
||||
pub const BIG_FISH_OFFSCREEN_CULL_SECONDS: f32 = 3.0;
|
||||
pub const BIG_FISH_TARGET_WILD_COUNT: usize = 12;
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum BigFishCreationStage {
|
||||
CollectingAnchors,
|
||||
DraftReady,
|
||||
AssetRefining,
|
||||
ReadyToPublish,
|
||||
Published,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum BigFishAnchorStatus {
|
||||
Confirmed,
|
||||
Inferred,
|
||||
Missing,
|
||||
Locked,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum BigFishAgentMessageRole {
|
||||
User,
|
||||
Assistant,
|
||||
System,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum BigFishAgentMessageKind {
|
||||
Chat,
|
||||
Summary,
|
||||
ActionResult,
|
||||
Warning,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum BigFishAssetKind {
|
||||
LevelMainImage,
|
||||
LevelMotion,
|
||||
StageBackground,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum BigFishAssetStatus {
|
||||
Missing,
|
||||
Ready,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct BigFishAnchorItem {
|
||||
pub key: String,
|
||||
pub label: String,
|
||||
pub value: String,
|
||||
pub status: BigFishAnchorStatus,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct BigFishAnchorPack {
|
||||
pub gameplay_promise: BigFishAnchorItem,
|
||||
pub ecology_visual_theme: BigFishAnchorItem,
|
||||
pub growth_ladder: BigFishAnchorItem,
|
||||
pub risk_tempo: BigFishAnchorItem,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct BigFishLevelBlueprint {
|
||||
pub level: u32,
|
||||
pub name: String,
|
||||
pub one_line_fantasy: String,
|
||||
pub text_description: String,
|
||||
pub silhouette_direction: String,
|
||||
pub size_ratio: f32,
|
||||
pub visual_description: String,
|
||||
pub visual_prompt_seed: String,
|
||||
pub idle_motion_description: String,
|
||||
pub move_motion_description: String,
|
||||
pub motion_prompt_seed: String,
|
||||
pub merge_source_level: Option<u32>,
|
||||
pub prey_window: Vec<u32>,
|
||||
pub threat_window: Vec<u32>,
|
||||
pub is_final_level: bool,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct BigFishBackgroundBlueprint {
|
||||
pub theme: String,
|
||||
pub color_mood: String,
|
||||
pub foreground_hints: String,
|
||||
pub midground_composition: String,
|
||||
pub background_depth: String,
|
||||
pub safe_play_area_hint: String,
|
||||
pub spawn_edge_hint: String,
|
||||
pub background_prompt_seed: String,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct BigFishRuntimeParams {
|
||||
pub level_count: u32,
|
||||
pub merge_count_per_upgrade: u32,
|
||||
pub spawn_target_count: u32,
|
||||
pub leader_move_speed: f32,
|
||||
pub follower_catch_up_speed: f32,
|
||||
pub offscreen_cull_seconds: f32,
|
||||
pub prey_spawn_delta_levels: Vec<u32>,
|
||||
pub threat_spawn_delta_levels: Vec<u32>,
|
||||
pub win_level: u32,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct BigFishGameDraft {
|
||||
pub title: String,
|
||||
pub subtitle: String,
|
||||
pub core_fun: String,
|
||||
pub ecology_theme: String,
|
||||
pub levels: Vec<BigFishLevelBlueprint>,
|
||||
pub background: BigFishBackgroundBlueprint,
|
||||
pub runtime_params: BigFishRuntimeParams,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct BigFishAgentMessageSnapshot {
|
||||
pub message_id: String,
|
||||
pub session_id: String,
|
||||
pub role: BigFishAgentMessageRole,
|
||||
pub kind: BigFishAgentMessageKind,
|
||||
pub text: String,
|
||||
pub created_at_micros: i64,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct BigFishAssetSlotSnapshot {
|
||||
pub slot_id: String,
|
||||
pub session_id: String,
|
||||
pub asset_kind: BigFishAssetKind,
|
||||
pub level: Option<u32>,
|
||||
pub motion_key: Option<String>,
|
||||
pub status: BigFishAssetStatus,
|
||||
pub asset_url: Option<String>,
|
||||
pub prompt_snapshot: String,
|
||||
pub updated_at_micros: i64,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct BigFishAssetCoverage {
|
||||
pub level_main_image_ready_count: u32,
|
||||
pub level_motion_ready_count: u32,
|
||||
pub background_ready: bool,
|
||||
pub required_level_count: u32,
|
||||
pub publish_ready: bool,
|
||||
pub blockers: Vec<String>,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct BigFishSessionSnapshot {
|
||||
pub session_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub seed_text: String,
|
||||
pub current_turn: u32,
|
||||
pub progress_percent: u32,
|
||||
pub stage: BigFishCreationStage,
|
||||
pub anchor_pack: BigFishAnchorPack,
|
||||
pub draft: Option<BigFishGameDraft>,
|
||||
pub asset_slots: Vec<BigFishAssetSlotSnapshot>,
|
||||
pub asset_coverage: BigFishAssetCoverage,
|
||||
pub messages: Vec<BigFishAgentMessageSnapshot>,
|
||||
pub last_assistant_reply: Option<String>,
|
||||
pub publish_ready: bool,
|
||||
pub created_at_micros: i64,
|
||||
pub updated_at_micros: i64,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct BigFishSessionProcedureResult {
|
||||
pub ok: bool,
|
||||
pub session: Option<BigFishSessionSnapshot>,
|
||||
pub error_message: Option<String>,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct BigFishWorkSummarySnapshot {
|
||||
pub work_id: String,
|
||||
pub source_session_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub title: String,
|
||||
pub subtitle: String,
|
||||
pub summary: String,
|
||||
pub cover_image_src: Option<String>,
|
||||
pub status: String,
|
||||
pub updated_at_micros: i64,
|
||||
pub publish_ready: bool,
|
||||
pub level_count: u32,
|
||||
pub level_main_image_ready_count: u32,
|
||||
pub level_motion_ready_count: u32,
|
||||
pub background_ready: bool,
|
||||
pub play_count: u32,
|
||||
}
|
||||
|
||||
/// 发布门禁的领域判定结果。
|
||||
///
|
||||
/// 这里不保存外部任务状态,只表达当前聚合快照是否满足发布条件。
|
||||
@@ -77,3 +299,66 @@ impl BigFishRunStatus {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BigFishCreationStage {
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::CollectingAnchors => "collecting_anchors",
|
||||
Self::DraftReady => "draft_ready",
|
||||
Self::AssetRefining => "asset_refining",
|
||||
Self::ReadyToPublish => "ready_to_publish",
|
||||
Self::Published => "published",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BigFishAnchorStatus {
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Confirmed => "confirmed",
|
||||
Self::Inferred => "inferred",
|
||||
Self::Missing => "missing",
|
||||
Self::Locked => "locked",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BigFishAgentMessageRole {
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::User => "user",
|
||||
Self::Assistant => "assistant",
|
||||
Self::System => "system",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BigFishAgentMessageKind {
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Chat => "chat",
|
||||
Self::Summary => "summary",
|
||||
Self::ActionResult => "action_result",
|
||||
Self::Warning => "warning",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BigFishAssetKind {
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::LevelMainImage => "level_main_image",
|
||||
Self::LevelMotion => "level_motion",
|
||||
Self::StageBackground => "stage_background",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BigFishAssetStatus {
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Missing => "missing",
|
||||
Self::Ready => "ready",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! 大鱼吃小鱼领域错误过渡落位。
|
||||
//! 大鱼吃小鱼领域错误。
|
||||
//!
|
||||
//! 错误只表达玩法规则失败,由 HTTP 和 SpacetimeDB adapter 分别映射展示。
|
||||
|
||||
@@ -27,3 +27,34 @@ impl fmt::Display for BigFishApplicationError {
|
||||
}
|
||||
|
||||
impl Error for BigFishApplicationError {}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum BigFishFieldError {
|
||||
MissingSessionId,
|
||||
MissingOwnerUserId,
|
||||
MissingMessageId,
|
||||
MissingMessageText,
|
||||
MissingDraft,
|
||||
InvalidLevel,
|
||||
InvalidAssetKind,
|
||||
MissingRunId,
|
||||
InvalidRuntimeInput,
|
||||
}
|
||||
|
||||
impl fmt::Display for BigFishFieldError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::MissingSessionId => f.write_str("big_fish.session_id 不能为空"),
|
||||
Self::MissingOwnerUserId => f.write_str("big_fish.owner_user_id 不能为空"),
|
||||
Self::MissingMessageId => f.write_str("big_fish.message_id 不能为空"),
|
||||
Self::MissingMessageText => f.write_str("big_fish.message_text 不能为空"),
|
||||
Self::MissingDraft => f.write_str("big_fish.draft 尚未编译"),
|
||||
Self::InvalidLevel => f.write_str("big_fish.level 不在合法等级范围内"),
|
||||
Self::InvalidAssetKind => f.write_str("big_fish.asset_kind 或动作位非法"),
|
||||
Self::MissingRunId => f.write_str("big_fish.run_id 不能为空"),
|
||||
Self::InvalidRuntimeInput => f.write_str("big_fish.runtime_input 非法"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for BigFishFieldError {}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! 大鱼吃小鱼领域事件过渡落位。
|
||||
//! 大鱼吃小鱼领域事件。
|
||||
//!
|
||||
//! 用于表达草稿变化、资产槽变化和运行态 tick 等事实。
|
||||
|
||||
|
||||
@@ -4,990 +4,11 @@ mod domain;
|
||||
mod errors;
|
||||
mod events;
|
||||
|
||||
pub use application::{
|
||||
BigFishRuntimeResult, EvaluateBigFishPublishReadinessResult, deserialize_runtime_snapshot,
|
||||
evaluate_publish_readiness, serialize_runtime_snapshot, start_big_fish_run,
|
||||
submit_big_fish_input,
|
||||
};
|
||||
pub use commands::{
|
||||
EvaluateBigFishPublishReadinessCommand, StartBigFishRunCommand, SubmitBigFishInputCommand,
|
||||
};
|
||||
pub use domain::{
|
||||
BigFishPublishReadiness, BigFishRunStatus, BigFishRuntimeEntitySnapshot,
|
||||
BigFishRuntimeSnapshot, BigFishVector2,
|
||||
};
|
||||
pub use errors::BigFishApplicationError;
|
||||
pub use events::BigFishDomainEvent;
|
||||
|
||||
use std::{error::Error, fmt};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use shared_kernel::normalize_required_string;
|
||||
#[cfg(feature = "spacetime-types")]
|
||||
use spacetimedb::SpacetimeType;
|
||||
|
||||
pub const BIG_FISH_SESSION_ID_PREFIX: &str = "big-fish-session-";
|
||||
pub const BIG_FISH_MESSAGE_ID_PREFIX: &str = "big-fish-message-";
|
||||
pub const BIG_FISH_OPERATION_ID_PREFIX: &str = "big-fish-operation-";
|
||||
pub const BIG_FISH_ASSET_SLOT_ID_PREFIX: &str = "big-fish-asset-";
|
||||
pub const BIG_FISH_DEFAULT_LEVEL_COUNT: u32 = 8;
|
||||
pub const BIG_FISH_MIN_LEVEL_COUNT: u32 = 6;
|
||||
pub const BIG_FISH_MAX_LEVEL_COUNT: u32 = 12;
|
||||
pub const BIG_FISH_MERGE_COUNT_PER_UPGRADE: u32 = 3;
|
||||
pub const BIG_FISH_OFFSCREEN_CULL_SECONDS: f32 = 3.0;
|
||||
pub const BIG_FISH_TARGET_WILD_COUNT: usize = 12;
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum BigFishCreationStage {
|
||||
CollectingAnchors,
|
||||
DraftReady,
|
||||
AssetRefining,
|
||||
ReadyToPublish,
|
||||
Published,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum BigFishAnchorStatus {
|
||||
Confirmed,
|
||||
Inferred,
|
||||
Missing,
|
||||
Locked,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum BigFishAgentMessageRole {
|
||||
User,
|
||||
Assistant,
|
||||
System,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum BigFishAgentMessageKind {
|
||||
Chat,
|
||||
Summary,
|
||||
ActionResult,
|
||||
Warning,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum BigFishAssetKind {
|
||||
LevelMainImage,
|
||||
LevelMotion,
|
||||
StageBackground,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum BigFishAssetStatus {
|
||||
Missing,
|
||||
Ready,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct BigFishAnchorItem {
|
||||
pub key: String,
|
||||
pub label: String,
|
||||
pub value: String,
|
||||
pub status: BigFishAnchorStatus,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct BigFishAnchorPack {
|
||||
pub gameplay_promise: BigFishAnchorItem,
|
||||
pub ecology_visual_theme: BigFishAnchorItem,
|
||||
pub growth_ladder: BigFishAnchorItem,
|
||||
pub risk_tempo: BigFishAnchorItem,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct BigFishLevelBlueprint {
|
||||
pub level: u32,
|
||||
pub name: String,
|
||||
pub one_line_fantasy: String,
|
||||
pub text_description: String,
|
||||
pub silhouette_direction: String,
|
||||
pub size_ratio: f32,
|
||||
pub visual_description: String,
|
||||
pub visual_prompt_seed: String,
|
||||
pub idle_motion_description: String,
|
||||
pub move_motion_description: String,
|
||||
pub motion_prompt_seed: String,
|
||||
pub merge_source_level: Option<u32>,
|
||||
pub prey_window: Vec<u32>,
|
||||
pub threat_window: Vec<u32>,
|
||||
pub is_final_level: bool,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct BigFishBackgroundBlueprint {
|
||||
pub theme: String,
|
||||
pub color_mood: String,
|
||||
pub foreground_hints: String,
|
||||
pub midground_composition: String,
|
||||
pub background_depth: String,
|
||||
pub safe_play_area_hint: String,
|
||||
pub spawn_edge_hint: String,
|
||||
pub background_prompt_seed: String,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct BigFishRuntimeParams {
|
||||
pub level_count: u32,
|
||||
pub merge_count_per_upgrade: u32,
|
||||
pub spawn_target_count: u32,
|
||||
pub leader_move_speed: f32,
|
||||
pub follower_catch_up_speed: f32,
|
||||
pub offscreen_cull_seconds: f32,
|
||||
pub prey_spawn_delta_levels: Vec<u32>,
|
||||
pub threat_spawn_delta_levels: Vec<u32>,
|
||||
pub win_level: u32,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct BigFishGameDraft {
|
||||
pub title: String,
|
||||
pub subtitle: String,
|
||||
pub core_fun: String,
|
||||
pub ecology_theme: String,
|
||||
pub levels: Vec<BigFishLevelBlueprint>,
|
||||
pub background: BigFishBackgroundBlueprint,
|
||||
pub runtime_params: BigFishRuntimeParams,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct BigFishAgentMessageSnapshot {
|
||||
pub message_id: String,
|
||||
pub session_id: String,
|
||||
pub role: BigFishAgentMessageRole,
|
||||
pub kind: BigFishAgentMessageKind,
|
||||
pub text: String,
|
||||
pub created_at_micros: i64,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct BigFishAssetSlotSnapshot {
|
||||
pub slot_id: String,
|
||||
pub session_id: String,
|
||||
pub asset_kind: BigFishAssetKind,
|
||||
pub level: Option<u32>,
|
||||
pub motion_key: Option<String>,
|
||||
pub status: BigFishAssetStatus,
|
||||
pub asset_url: Option<String>,
|
||||
pub prompt_snapshot: String,
|
||||
pub updated_at_micros: i64,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct BigFishAssetCoverage {
|
||||
pub level_main_image_ready_count: u32,
|
||||
pub level_motion_ready_count: u32,
|
||||
pub background_ready: bool,
|
||||
pub required_level_count: u32,
|
||||
pub publish_ready: bool,
|
||||
pub blockers: Vec<String>,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct BigFishSessionSnapshot {
|
||||
pub session_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub seed_text: String,
|
||||
pub current_turn: u32,
|
||||
pub progress_percent: u32,
|
||||
pub stage: BigFishCreationStage,
|
||||
pub anchor_pack: BigFishAnchorPack,
|
||||
pub draft: Option<BigFishGameDraft>,
|
||||
pub asset_slots: Vec<BigFishAssetSlotSnapshot>,
|
||||
pub asset_coverage: BigFishAssetCoverage,
|
||||
pub messages: Vec<BigFishAgentMessageSnapshot>,
|
||||
pub last_assistant_reply: Option<String>,
|
||||
pub publish_ready: bool,
|
||||
pub created_at_micros: i64,
|
||||
pub updated_at_micros: i64,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct BigFishSessionProcedureResult {
|
||||
pub ok: bool,
|
||||
pub session: Option<BigFishSessionSnapshot>,
|
||||
pub error_message: Option<String>,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct BigFishWorkSummarySnapshot {
|
||||
pub work_id: String,
|
||||
pub source_session_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub title: String,
|
||||
pub subtitle: String,
|
||||
pub summary: String,
|
||||
pub cover_image_src: Option<String>,
|
||||
pub status: String,
|
||||
pub updated_at_micros: i64,
|
||||
pub publish_ready: bool,
|
||||
pub level_count: u32,
|
||||
pub level_main_image_ready_count: u32,
|
||||
pub level_motion_ready_count: u32,
|
||||
pub background_ready: bool,
|
||||
pub play_count: u32,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct BigFishWorksListInput {
|
||||
pub owner_user_id: String,
|
||||
pub published_only: bool,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct BigFishWorkDeleteInput {
|
||||
pub session_id: String,
|
||||
pub owner_user_id: String,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct BigFishWorksProcedureResult {
|
||||
pub ok: bool,
|
||||
pub items_json: Option<String>,
|
||||
pub error_message: Option<String>,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct BigFishSessionCreateInput {
|
||||
pub session_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub seed_text: String,
|
||||
pub welcome_message_id: String,
|
||||
pub welcome_message_text: String,
|
||||
pub created_at_micros: i64,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct BigFishSessionGetInput {
|
||||
pub session_id: String,
|
||||
pub owner_user_id: String,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct BigFishMessageSubmitInput {
|
||||
pub session_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub user_message_id: String,
|
||||
pub user_message_text: String,
|
||||
pub assistant_message_id: String,
|
||||
pub submitted_at_micros: i64,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct BigFishMessageFinalizeInput {
|
||||
pub session_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub assistant_message_id: Option<String>,
|
||||
pub assistant_reply_text: Option<String>,
|
||||
pub stage: BigFishCreationStage,
|
||||
pub progress_percent: u32,
|
||||
pub anchor_pack_json: String,
|
||||
pub error_message: Option<String>,
|
||||
pub updated_at_micros: i64,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct BigFishDraftCompileInput {
|
||||
pub session_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub draft_json: Option<String>,
|
||||
pub compiled_at_micros: i64,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct BigFishAssetGenerateInput {
|
||||
pub session_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub asset_kind: BigFishAssetKind,
|
||||
pub level: Option<u32>,
|
||||
pub motion_key: Option<String>,
|
||||
pub asset_url: Option<String>,
|
||||
pub generated_at_micros: i64,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct BigFishPublishInput {
|
||||
pub session_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub published_at_micros: i64,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct BigFishPlayRecordInput {
|
||||
pub session_id: String,
|
||||
pub user_id: String,
|
||||
pub elapsed_ms: u64,
|
||||
pub played_at_micros: i64,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct BigFishRunStartInput {
|
||||
pub run_id: String,
|
||||
pub session_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub started_at_micros: i64,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct BigFishRunGetInput {
|
||||
pub run_id: String,
|
||||
pub owner_user_id: String,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct BigFishInputSubmitInput {
|
||||
pub run_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub x: f32,
|
||||
pub y: f32,
|
||||
pub submitted_at_micros: i64,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct BigFishRunProcedureResult {
|
||||
pub ok: bool,
|
||||
pub run_json: Option<String>,
|
||||
pub error_message: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum BigFishFieldError {
|
||||
MissingSessionId,
|
||||
MissingOwnerUserId,
|
||||
MissingMessageId,
|
||||
MissingMessageText,
|
||||
MissingDraft,
|
||||
InvalidLevel,
|
||||
InvalidAssetKind,
|
||||
MissingRunId,
|
||||
InvalidRuntimeInput,
|
||||
}
|
||||
|
||||
impl BigFishCreationStage {
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::CollectingAnchors => "collecting_anchors",
|
||||
Self::DraftReady => "draft_ready",
|
||||
Self::AssetRefining => "asset_refining",
|
||||
Self::ReadyToPublish => "ready_to_publish",
|
||||
Self::Published => "published",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BigFishAnchorStatus {
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Confirmed => "confirmed",
|
||||
Self::Inferred => "inferred",
|
||||
Self::Missing => "missing",
|
||||
Self::Locked => "locked",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BigFishAgentMessageRole {
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::User => "user",
|
||||
Self::Assistant => "assistant",
|
||||
Self::System => "system",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BigFishAgentMessageKind {
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Chat => "chat",
|
||||
Self::Summary => "summary",
|
||||
Self::ActionResult => "action_result",
|
||||
Self::Warning => "warning",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BigFishAssetKind {
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::LevelMainImage => "level_main_image",
|
||||
Self::LevelMotion => "level_motion",
|
||||
Self::StageBackground => "stage_background",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BigFishAssetStatus {
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Missing => "missing",
|
||||
Self::Ready => "ready",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn empty_anchor_pack() -> BigFishAnchorPack {
|
||||
BigFishAnchorPack {
|
||||
gameplay_promise: BigFishAnchorItem {
|
||||
key: "gameplayPromise".to_string(),
|
||||
label: "玩法承诺".to_string(),
|
||||
value: String::new(),
|
||||
status: BigFishAnchorStatus::Missing,
|
||||
},
|
||||
ecology_visual_theme: BigFishAnchorItem {
|
||||
key: "ecologyVisualTheme".to_string(),
|
||||
label: "生态与视觉母题".to_string(),
|
||||
value: String::new(),
|
||||
status: BigFishAnchorStatus::Missing,
|
||||
},
|
||||
growth_ladder: BigFishAnchorItem {
|
||||
key: "growthLadder".to_string(),
|
||||
label: "成长阶梯".to_string(),
|
||||
value: String::new(),
|
||||
status: BigFishAnchorStatus::Missing,
|
||||
},
|
||||
risk_tempo: BigFishAnchorItem {
|
||||
key: "riskTempo".to_string(),
|
||||
label: "风险节奏".to_string(),
|
||||
value: "平衡".to_string(),
|
||||
status: BigFishAnchorStatus::Inferred,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn infer_anchor_pack(seed_text: &str, latest_message: Option<&str>) -> BigFishAnchorPack {
|
||||
let source = normalize_required_string(latest_message.unwrap_or(seed_text))
|
||||
.or_else(|| normalize_required_string(seed_text))
|
||||
.unwrap_or_else(|| "深海弱小逆袭,逐级吞噬成长".to_string());
|
||||
let mut pack = empty_anchor_pack();
|
||||
pack.gameplay_promise.value = if source.contains("可爱") {
|
||||
"可爱生态成长".to_string()
|
||||
} else if source.contains("机械") {
|
||||
"机械微生物吞并进化".to_string()
|
||||
} else {
|
||||
"弱小逆袭和群体吞并".to_string()
|
||||
};
|
||||
pack.gameplay_promise.status = BigFishAnchorStatus::Inferred;
|
||||
pack.ecology_visual_theme.value = if source.contains("机械") {
|
||||
"机械微生物水域".to_string()
|
||||
} else if source.contains("梦") {
|
||||
"梦境纸鱼生态".to_string()
|
||||
} else {
|
||||
"深海生物生态".to_string()
|
||||
};
|
||||
pack.ecology_visual_theme.status = BigFishAnchorStatus::Inferred;
|
||||
pack.growth_ladder.value = "8 级连续进化,从幼小个体成长为终局巨兽".to_string();
|
||||
pack.growth_ladder.status = BigFishAnchorStatus::Inferred;
|
||||
pack.risk_tempo.value = if source.contains("爽") {
|
||||
"偏爽快".to_string()
|
||||
} else if source.contains("压迫") {
|
||||
"偏压迫".to_string()
|
||||
} else {
|
||||
"平衡".to_string()
|
||||
};
|
||||
pack
|
||||
}
|
||||
|
||||
pub fn compile_default_draft(anchor_pack: &BigFishAnchorPack) -> BigFishGameDraft {
|
||||
let level_count = BIG_FISH_DEFAULT_LEVEL_COUNT;
|
||||
let theme = fallback_anchor_value(&anchor_pack.ecology_visual_theme, "深海生物生态");
|
||||
let core_fun = fallback_anchor_value(&anchor_pack.gameplay_promise, "弱小逆袭和群体吞并");
|
||||
let risk_tempo = fallback_anchor_value(&anchor_pack.risk_tempo, "平衡");
|
||||
|
||||
let levels = (1..=level_count)
|
||||
.map(|level| build_level_blueprint(level, level_count, &theme))
|
||||
.collect();
|
||||
|
||||
BigFishGameDraft {
|
||||
title: format!("{theme} 大鱼吃小鱼"),
|
||||
subtitle: format!("{core_fun} · {risk_tempo}节奏"),
|
||||
core_fun,
|
||||
ecology_theme: theme.clone(),
|
||||
levels,
|
||||
background: BigFishBackgroundBlueprint {
|
||||
theme: theme.clone(),
|
||||
color_mood: "深蓝、青绿、带少量暖色生物光".to_string(),
|
||||
foreground_hints: "只保留少量漂浮颗粒和边缘水草,不遮挡中央操作区".to_string(),
|
||||
midground_composition: "中央留出大面积清晰活动区域,边缘只做出生缓冲层".to_string(),
|
||||
background_depth: "简洁纵深水域与极少量远处剪影".to_string(),
|
||||
safe_play_area_hint: "9:16 竖屏中央 80% 为主要活动区".to_string(),
|
||||
spawn_edge_hint: "四周边缘以少量暗礁或水草提示野生实体出生区".to_string(),
|
||||
background_prompt_seed: format!(
|
||||
"{theme},竖屏 9:16,全屏大场地游戏背景,元素少,中央开阔,无文字,无 UI 框"
|
||||
),
|
||||
},
|
||||
runtime_params: BigFishRuntimeParams {
|
||||
level_count,
|
||||
merge_count_per_upgrade: BIG_FISH_MERGE_COUNT_PER_UPGRADE,
|
||||
spawn_target_count: BIG_FISH_TARGET_WILD_COUNT as u32,
|
||||
leader_move_speed: 160.0,
|
||||
follower_catch_up_speed: 120.0,
|
||||
offscreen_cull_seconds: BIG_FISH_OFFSCREEN_CULL_SECONDS,
|
||||
prey_spawn_delta_levels: vec![1, 2],
|
||||
threat_spawn_delta_levels: vec![1, 2],
|
||||
win_level: level_count,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_asset_coverage(
|
||||
draft: Option<&BigFishGameDraft>,
|
||||
asset_slots: &[BigFishAssetSlotSnapshot],
|
||||
) -> BigFishAssetCoverage {
|
||||
let required_level_count = draft
|
||||
.map(|value| value.runtime_params.level_count)
|
||||
.unwrap_or(BIG_FISH_DEFAULT_LEVEL_COUNT);
|
||||
let main_ready = asset_slots
|
||||
.iter()
|
||||
.filter(|slot| {
|
||||
slot.asset_kind == BigFishAssetKind::LevelMainImage
|
||||
&& slot.status == BigFishAssetStatus::Ready
|
||||
})
|
||||
.count() as u32;
|
||||
let motion_ready = asset_slots
|
||||
.iter()
|
||||
.filter(|slot| {
|
||||
slot.asset_kind == BigFishAssetKind::LevelMotion
|
||||
&& slot.status == BigFishAssetStatus::Ready
|
||||
})
|
||||
.count() as u32;
|
||||
let background_ready = asset_slots.iter().any(|slot| {
|
||||
slot.asset_kind == BigFishAssetKind::StageBackground
|
||||
&& slot.status == BigFishAssetStatus::Ready
|
||||
});
|
||||
|
||||
let required_motion_count = required_level_count * 2;
|
||||
let mut blockers = Vec::new();
|
||||
if draft.is_none() {
|
||||
blockers.push("玩法草稿尚未编译".to_string());
|
||||
}
|
||||
if main_ready < required_level_count {
|
||||
blockers.push(format!(
|
||||
"还缺少 {} 个等级主图",
|
||||
required_level_count.saturating_sub(main_ready)
|
||||
));
|
||||
}
|
||||
if motion_ready < required_motion_count {
|
||||
blockers.push(format!(
|
||||
"还缺少 {} 个基础动作",
|
||||
required_motion_count.saturating_sub(motion_ready)
|
||||
));
|
||||
}
|
||||
if !background_ready {
|
||||
blockers.push("还缺少活动区域背景图".to_string());
|
||||
}
|
||||
|
||||
BigFishAssetCoverage {
|
||||
level_main_image_ready_count: main_ready,
|
||||
level_motion_ready_count: motion_ready,
|
||||
background_ready,
|
||||
required_level_count,
|
||||
publish_ready: blockers.is_empty(),
|
||||
blockers,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_generated_asset_slot(
|
||||
session_id: &str,
|
||||
draft: &BigFishGameDraft,
|
||||
asset_kind: BigFishAssetKind,
|
||||
level: Option<u32>,
|
||||
motion_key: Option<String>,
|
||||
asset_url: Option<String>,
|
||||
updated_at_micros: i64,
|
||||
) -> Result<BigFishAssetSlotSnapshot, BigFishFieldError> {
|
||||
let session_id =
|
||||
normalize_required_string(session_id).ok_or(BigFishFieldError::MissingSessionId)?;
|
||||
let prompt_snapshot =
|
||||
build_asset_prompt_snapshot(draft, asset_kind, level, motion_key.as_deref())?;
|
||||
let slot_id = build_asset_slot_id(&session_id, asset_kind, level, motion_key.as_deref());
|
||||
let resolved_asset_url = normalize_required_string(asset_url.as_deref().unwrap_or_default())
|
||||
.unwrap_or_else(|| build_placeholder_asset_url(asset_kind, level, updated_at_micros));
|
||||
|
||||
Ok(BigFishAssetSlotSnapshot {
|
||||
slot_id,
|
||||
session_id,
|
||||
asset_kind,
|
||||
level,
|
||||
motion_key,
|
||||
status: BigFishAssetStatus::Ready,
|
||||
asset_url: Some(resolved_asset_url),
|
||||
prompt_snapshot,
|
||||
updated_at_micros,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn validate_session_get_input(input: &BigFishSessionGetInput) -> Result<(), BigFishFieldError> {
|
||||
validate_session_owner(&input.session_id, &input.owner_user_id)
|
||||
}
|
||||
|
||||
pub fn validate_works_list_input(input: &BigFishWorksListInput) -> Result<(), BigFishFieldError> {
|
||||
if input.published_only {
|
||||
return Ok(());
|
||||
}
|
||||
if normalize_required_string(&input.owner_user_id).is_none() {
|
||||
return Err(BigFishFieldError::MissingOwnerUserId);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn validate_session_create_input(
|
||||
input: &BigFishSessionCreateInput,
|
||||
) -> Result<(), BigFishFieldError> {
|
||||
validate_session_owner(&input.session_id, &input.owner_user_id)?;
|
||||
if normalize_required_string(&input.welcome_message_id).is_none() {
|
||||
return Err(BigFishFieldError::MissingMessageId);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn validate_message_submit_input(
|
||||
input: &BigFishMessageSubmitInput,
|
||||
) -> Result<(), BigFishFieldError> {
|
||||
validate_session_owner(&input.session_id, &input.owner_user_id)?;
|
||||
if normalize_required_string(&input.user_message_id).is_none()
|
||||
|| normalize_required_string(&input.assistant_message_id).is_none()
|
||||
{
|
||||
return Err(BigFishFieldError::MissingMessageId);
|
||||
}
|
||||
if normalize_required_string(&input.user_message_text).is_none() {
|
||||
return Err(BigFishFieldError::MissingMessageText);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn validate_message_finalize_input(
|
||||
input: &BigFishMessageFinalizeInput,
|
||||
) -> Result<(), BigFishFieldError> {
|
||||
validate_session_owner(&input.session_id, &input.owner_user_id)
|
||||
}
|
||||
|
||||
pub fn validate_draft_compile_input(
|
||||
input: &BigFishDraftCompileInput,
|
||||
) -> Result<(), BigFishFieldError> {
|
||||
validate_session_owner(&input.session_id, &input.owner_user_id)
|
||||
}
|
||||
|
||||
pub fn validate_asset_generate_input(
|
||||
input: &BigFishAssetGenerateInput,
|
||||
draft: &BigFishGameDraft,
|
||||
) -> Result<(), BigFishFieldError> {
|
||||
validate_session_owner(&input.session_id, &input.owner_user_id)?;
|
||||
match input.asset_kind {
|
||||
BigFishAssetKind::LevelMainImage => validate_level(input.level, draft),
|
||||
BigFishAssetKind::LevelMotion => {
|
||||
validate_level(input.level, draft)?;
|
||||
match input.motion_key.as_deref() {
|
||||
Some("idle_float" | "move_swim") => Ok(()),
|
||||
_ => Err(BigFishFieldError::InvalidAssetKind),
|
||||
}
|
||||
}
|
||||
BigFishAssetKind::StageBackground => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn validate_publish_input(input: &BigFishPublishInput) -> Result<(), BigFishFieldError> {
|
||||
validate_session_owner(&input.session_id, &input.owner_user_id)
|
||||
}
|
||||
|
||||
pub fn validate_play_record_input(input: &BigFishPlayRecordInput) -> Result<(), BigFishFieldError> {
|
||||
if normalize_required_string(&input.session_id).is_none() {
|
||||
return Err(BigFishFieldError::MissingSessionId);
|
||||
}
|
||||
if normalize_required_string(&input.user_id).is_none() {
|
||||
return Err(BigFishFieldError::MissingOwnerUserId);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn validate_run_start_input(input: &BigFishRunStartInput) -> Result<(), BigFishFieldError> {
|
||||
validate_session_owner(&input.session_id, &input.owner_user_id)?;
|
||||
if normalize_required_string(&input.run_id).is_none() {
|
||||
return Err(BigFishFieldError::MissingRunId);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn validate_run_get_input(input: &BigFishRunGetInput) -> Result<(), BigFishFieldError> {
|
||||
if normalize_required_string(&input.run_id).is_none() {
|
||||
return Err(BigFishFieldError::MissingRunId);
|
||||
}
|
||||
if normalize_required_string(&input.owner_user_id).is_none() {
|
||||
return Err(BigFishFieldError::MissingOwnerUserId);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn validate_input_submit_input(
|
||||
input: &BigFishInputSubmitInput,
|
||||
) -> Result<(), BigFishFieldError> {
|
||||
if normalize_required_string(&input.run_id).is_none() {
|
||||
return Err(BigFishFieldError::MissingRunId);
|
||||
}
|
||||
if normalize_required_string(&input.owner_user_id).is_none() {
|
||||
return Err(BigFishFieldError::MissingOwnerUserId);
|
||||
}
|
||||
if !input.x.is_finite() || !input.y.is_finite() {
|
||||
return Err(BigFishFieldError::InvalidRuntimeInput);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn serialize_anchor_pack(anchor_pack: &BigFishAnchorPack) -> Result<String, serde_json::Error> {
|
||||
serde_json::to_string(anchor_pack)
|
||||
}
|
||||
|
||||
pub fn deserialize_anchor_pack(value: &str) -> Result<BigFishAnchorPack, serde_json::Error> {
|
||||
serde_json::from_str(value)
|
||||
}
|
||||
|
||||
pub fn serialize_draft(draft: &BigFishGameDraft) -> Result<String, serde_json::Error> {
|
||||
serde_json::to_string(draft)
|
||||
}
|
||||
|
||||
pub fn deserialize_draft(value: &str) -> Result<BigFishGameDraft, serde_json::Error> {
|
||||
serde_json::from_str(value)
|
||||
}
|
||||
|
||||
pub fn serialize_asset_coverage(
|
||||
coverage: &BigFishAssetCoverage,
|
||||
) -> Result<String, serde_json::Error> {
|
||||
serde_json::to_string(coverage)
|
||||
}
|
||||
|
||||
pub fn deserialize_asset_coverage(value: &str) -> Result<BigFishAssetCoverage, serde_json::Error> {
|
||||
serde_json::from_str(value)
|
||||
}
|
||||
|
||||
fn fallback_anchor_value(anchor: &BigFishAnchorItem, fallback: &str) -> String {
|
||||
normalize_required_string(&anchor.value).unwrap_or_else(|| fallback.to_string())
|
||||
}
|
||||
|
||||
fn build_level_blueprint(level: u32, level_count: u32, theme: &str) -> BigFishLevelBlueprint {
|
||||
let prey_window = (1..level)
|
||||
.rev()
|
||||
.take(2)
|
||||
.collect::<Vec<_>>()
|
||||
.into_iter()
|
||||
.rev()
|
||||
.collect();
|
||||
let threat_window = ((level + 1)..=(level + 2).min(level_count)).collect::<Vec<_>>();
|
||||
let size_ratio = 1.0 + (level.saturating_sub(1) as f32 * 0.22);
|
||||
let name = format!("{theme} L{level}");
|
||||
let one_line_fantasy = if level == level_count {
|
||||
"终局巨兽形态,获得即可通关".to_string()
|
||||
} else {
|
||||
format!("第 {level} 阶实体,继续吞噬同级和低级个体成长")
|
||||
};
|
||||
let text_description = if level == 1 {
|
||||
format!(
|
||||
"{name} 是这套 {theme} 等级阶梯的起点个体,体型最小、动作轻盈,会在谨慎试探中寻找第一个可吞噬目标。"
|
||||
)
|
||||
} else if level == level_count {
|
||||
format!(
|
||||
"{name} 是这套 {theme} 生态中的终局霸主形态,体格巨大、压迫感最强,一旦成型就代表本局成长链已经完成。"
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"{name} 是 {theme} 生态里的第 {level} 阶进化体,已经具备更鲜明的轮廓、猎食性和压迫感,会继续通过吞并同级与低级实体向上跃迁。"
|
||||
)
|
||||
};
|
||||
let visual_description = if level == 1 {
|
||||
format!(
|
||||
"{theme} 风格的小型初始鱼形生物,体态轻巧,轮廓圆润,局部带少量发光纹路或主题特征,明显呈现弱小但灵动的开局形象。"
|
||||
)
|
||||
} else if level == level_count {
|
||||
format!(
|
||||
"{theme} 风格的终局巨型鱼形霸主,体长与鳍面明显扩张,轮廓锋利或威严,层次细节最丰富,拥有一眼可辨识的终局统治感。"
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"{theme} 风格的第 {level} 级进化鱼形生物,相比上一阶段更大、更强、更成熟,身体主轮廓更清晰,局部装饰、鳍面结构和主题特征都更明显。"
|
||||
)
|
||||
};
|
||||
let idle_motion_description = if level == level_count {
|
||||
"待机时缓慢悬停,身体主体保持稳定,尾鳍与侧鳍做低频摆动,呈现强者从容压场的漂浮感。"
|
||||
.to_string()
|
||||
} else {
|
||||
format!(
|
||||
"待机时保持轻微漂浮与呼吸感摆动,尾鳍和侧鳍以小幅度节奏晃动,体现 Lv.{level} 生物在水中蓄势观察的状态。"
|
||||
)
|
||||
};
|
||||
let move_motion_description = if level == level_count {
|
||||
"移动时身体前倾,尾鳍和背鳍形成强力推进姿态,带出稳定而有压迫感的高速巡游动势。".to_string()
|
||||
} else {
|
||||
format!(
|
||||
"移动时身体向前游动,尾鳍形成清晰摆尾推进,整体节奏比待机更主动,体现 Lv.{level} 生物追逐猎物时的连续游动感。"
|
||||
)
|
||||
};
|
||||
BigFishLevelBlueprint {
|
||||
level,
|
||||
name,
|
||||
one_line_fantasy,
|
||||
text_description,
|
||||
silhouette_direction: format!(
|
||||
"体型约为初始的 {:.1} 倍,轮廓更清晰",
|
||||
1.0 + level as f32 * 0.22
|
||||
),
|
||||
size_ratio,
|
||||
visual_description: visual_description.clone(),
|
||||
visual_prompt_seed: format!(
|
||||
"{visual_description} 透明背景,单体完整入镜,适合作为竖屏吞噬成长玩法的等级主图。"
|
||||
),
|
||||
idle_motion_description: idle_motion_description.clone(),
|
||||
move_motion_description: move_motion_description.clone(),
|
||||
motion_prompt_seed: format!(
|
||||
"待机动作:{idle_motion_description} 移动动作:{move_motion_description}"
|
||||
),
|
||||
merge_source_level: if level == 1 { None } else { Some(level - 1) },
|
||||
prey_window,
|
||||
threat_window,
|
||||
is_final_level: level == level_count,
|
||||
}
|
||||
}
|
||||
|
||||
fn build_asset_prompt_snapshot(
|
||||
draft: &BigFishGameDraft,
|
||||
asset_kind: BigFishAssetKind,
|
||||
level: Option<u32>,
|
||||
motion_key: Option<&str>,
|
||||
) -> Result<String, BigFishFieldError> {
|
||||
match asset_kind {
|
||||
BigFishAssetKind::LevelMainImage => {
|
||||
let level = level.ok_or(BigFishFieldError::InvalidLevel)?;
|
||||
let blueprint = draft
|
||||
.levels
|
||||
.iter()
|
||||
.find(|item| item.level == level)
|
||||
.ok_or(BigFishFieldError::InvalidLevel)?;
|
||||
Ok(blueprint.visual_prompt_seed.clone())
|
||||
}
|
||||
BigFishAssetKind::LevelMotion => {
|
||||
let level = level.ok_or(BigFishFieldError::InvalidLevel)?;
|
||||
let blueprint = draft
|
||||
.levels
|
||||
.iter()
|
||||
.find(|item| item.level == level)
|
||||
.ok_or(BigFishFieldError::InvalidLevel)?;
|
||||
let motion_key = motion_key.ok_or(BigFishFieldError::InvalidAssetKind)?;
|
||||
let motion_description = match motion_key {
|
||||
"idle_float" => blueprint.idle_motion_description.as_str(),
|
||||
"move_swim" => blueprint.move_motion_description.as_str(),
|
||||
_ => return Err(BigFishFieldError::InvalidAssetKind),
|
||||
};
|
||||
Ok(format!(
|
||||
"{} 动作位:{}。{} 透明背景,单体完整入镜。",
|
||||
blueprint.motion_prompt_seed, motion_key, motion_description
|
||||
))
|
||||
}
|
||||
BigFishAssetKind::StageBackground => Ok(draft.background.background_prompt_seed.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
fn build_asset_slot_id(
|
||||
session_id: &str,
|
||||
asset_kind: BigFishAssetKind,
|
||||
level: Option<u32>,
|
||||
motion_key: Option<&str>,
|
||||
) -> String {
|
||||
let level_part = level
|
||||
.map(|value| value.to_string())
|
||||
.unwrap_or_else(|| "stage".to_string());
|
||||
let motion_part = motion_key.unwrap_or("main");
|
||||
format!(
|
||||
"{BIG_FISH_ASSET_SLOT_ID_PREFIX}{session_id}_{}_{}_{}",
|
||||
asset_kind.as_str(),
|
||||
level_part,
|
||||
motion_part
|
||||
)
|
||||
}
|
||||
|
||||
fn build_placeholder_asset_url(
|
||||
asset_kind: BigFishAssetKind,
|
||||
level: Option<u32>,
|
||||
seed_micros: i64,
|
||||
) -> String {
|
||||
let level_part = level
|
||||
.map(|value| format!("level-{value}"))
|
||||
.unwrap_or_else(|| "stage".to_string());
|
||||
format!(
|
||||
"/generated-big-fish/{}/{}/{}.png",
|
||||
asset_kind.as_str(),
|
||||
level_part,
|
||||
seed_micros
|
||||
)
|
||||
}
|
||||
|
||||
fn validate_session_owner(session_id: &str, owner_user_id: &str) -> Result<(), BigFishFieldError> {
|
||||
if normalize_required_string(session_id).is_none() {
|
||||
return Err(BigFishFieldError::MissingSessionId);
|
||||
}
|
||||
if normalize_required_string(owner_user_id).is_none() {
|
||||
return Err(BigFishFieldError::MissingOwnerUserId);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_level(level: Option<u32>, draft: &BigFishGameDraft) -> Result<(), BigFishFieldError> {
|
||||
match level {
|
||||
Some(value) if (1..=draft.runtime_params.level_count).contains(&value) => Ok(()),
|
||||
_ => Err(BigFishFieldError::InvalidLevel),
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for BigFishFieldError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::MissingSessionId => f.write_str("big_fish.session_id 不能为空"),
|
||||
Self::MissingOwnerUserId => f.write_str("big_fish.owner_user_id 不能为空"),
|
||||
Self::MissingMessageId => f.write_str("big_fish.message_id 不能为空"),
|
||||
Self::MissingMessageText => f.write_str("big_fish.message_text 不能为空"),
|
||||
Self::MissingDraft => f.write_str("big_fish.draft 尚未编译"),
|
||||
Self::InvalidLevel => f.write_str("big_fish.level 不在合法等级范围内"),
|
||||
Self::InvalidAssetKind => f.write_str("big_fish.asset_kind 或动作位非法"),
|
||||
Self::MissingRunId => f.write_str("big_fish.run_id 不能为空"),
|
||||
Self::InvalidRuntimeInput => f.write_str("big_fish.runtime_input 非法"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for BigFishFieldError {}
|
||||
pub use application::*;
|
||||
pub use commands::*;
|
||||
pub use domain::*;
|
||||
pub use errors::*;
|
||||
pub use events::*;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
Reference in New Issue
Block a user