feat(jump-hop): redesign sling platform gameplay
This commit is contained in:
@@ -1,15 +1,15 @@
|
||||
use super::*;
|
||||
use crate::mapper::{
|
||||
map_jump_hop_agent_session_procedure_result, map_jump_hop_gallery_card_view_row,
|
||||
map_jump_hop_run_procedure_result, map_jump_hop_work_procedure_result,
|
||||
map_jump_hop_works_procedure_result,
|
||||
map_jump_hop_leaderboard_procedure_result, map_jump_hop_run_procedure_result,
|
||||
map_jump_hop_work_procedure_result, map_jump_hop_works_procedure_result,
|
||||
};
|
||||
use shared_contracts::jump_hop::{
|
||||
JumpHopActionRequest, JumpHopActionResponse, JumpHopActionType, JumpHopCharacterAsset,
|
||||
JumpHopDifficulty, JumpHopDraftResponse, JumpHopGalleryResponse, JumpHopGenerationStatus,
|
||||
JumpHopJumpRequest, JumpHopRestartRunRequest, JumpHopRuntimeRunSnapshotResponse,
|
||||
JumpHopSessionSnapshotResponse, JumpHopStartRunRequest, JumpHopStylePreset, JumpHopTileAsset,
|
||||
JumpHopTileType, JumpHopWorkProfileResponse,
|
||||
JumpHopJumpRequest, JumpHopLeaderboardResponse, JumpHopRestartRunRequest,
|
||||
JumpHopRuntimeRunSnapshotResponse, JumpHopSessionSnapshotResponse, JumpHopStartRunRequest,
|
||||
JumpHopStylePreset, JumpHopWorkProfileResponse,
|
||||
};
|
||||
use shared_kernel::build_prefixed_uuid_id;
|
||||
|
||||
@@ -229,7 +229,7 @@ impl SpacetimeClient {
|
||||
let work = self
|
||||
.get_jump_hop_work_profile(profile_id, String::new())
|
||||
.await?;
|
||||
validate_jump_hop_runtime_ready(&work)?;
|
||||
validate_jump_hop_runtime_ready(&work, "published")?;
|
||||
Ok(work)
|
||||
}
|
||||
|
||||
@@ -242,13 +242,15 @@ impl SpacetimeClient {
|
||||
let work = self
|
||||
.get_jump_hop_work_profile(profile_id.clone(), String::new())
|
||||
.await?;
|
||||
validate_jump_hop_runtime_ready(&work)?;
|
||||
let runtime_mode = normalize_jump_hop_runtime_mode(payload.runtime_mode.as_deref());
|
||||
validate_jump_hop_runtime_ready(&work, runtime_mode)?;
|
||||
let run_id = build_prefixed_uuid_id("jump-hop-run-");
|
||||
let procedure_input = JumpHopRunStartInput {
|
||||
client_event_id: format!("{run_id}:start"),
|
||||
run_id,
|
||||
owner_user_id,
|
||||
profile_id,
|
||||
runtime_mode: runtime_mode.to_string(),
|
||||
started_at_ms: current_unix_micros().div_euclid(1000),
|
||||
};
|
||||
self.start_jump_hop_run_with_input(procedure_input).await
|
||||
@@ -303,7 +305,9 @@ impl SpacetimeClient {
|
||||
let procedure_input = JumpHopRunJumpInput {
|
||||
run_id,
|
||||
owner_user_id,
|
||||
charge_ms: payload.charge_ms,
|
||||
drag_distance: payload.drag_distance,
|
||||
drag_vector_x: payload.drag_vector_x,
|
||||
drag_vector_y: payload.drag_vector_y,
|
||||
client_event_id: payload.client_event_id,
|
||||
jumped_at_ms: current_unix_micros().div_euclid(1000),
|
||||
};
|
||||
@@ -396,13 +400,39 @@ impl SpacetimeClient {
|
||||
self.get_jump_hop_work_profile(card.profile_id, String::new())
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_jump_hop_leaderboard(
|
||||
&self,
|
||||
profile_id: String,
|
||||
viewer_player_id: String,
|
||||
) -> Result<JumpHopLeaderboardResponse, SpacetimeClientError> {
|
||||
let procedure_input = JumpHopLeaderboardGetInput {
|
||||
profile_id,
|
||||
viewer_player_id,
|
||||
limit: 50,
|
||||
};
|
||||
|
||||
self.call_after_connect("get_jump_hop_leaderboard", move |connection, sender| {
|
||||
connection.procedures().get_jump_hop_leaderboard_then(
|
||||
procedure_input,
|
||||
move |_, result| {
|
||||
let mapped = result
|
||||
.map_err(SpacetimeClientError::from_sdk_error)
|
||||
.and_then(map_jump_hop_leaderboard_procedure_result);
|
||||
send_once(&sender, mapped);
|
||||
},
|
||||
);
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_jump_hop_runtime_ready(
|
||||
work: &JumpHopWorkProfileResponse,
|
||||
runtime_mode: &str,
|
||||
) -> Result<(), SpacetimeClientError> {
|
||||
let status = work.summary.publication_status.trim().to_ascii_lowercase();
|
||||
if status != "published" {
|
||||
if runtime_mode == "published" && status != "published" {
|
||||
return Err(SpacetimeClientError::validation_failed(
|
||||
"jump-hop runtime 只能启动已发布作品",
|
||||
));
|
||||
@@ -412,11 +442,11 @@ fn validate_jump_hop_runtime_ready(
|
||||
"jump-hop runtime 需要 ready 状态作品",
|
||||
));
|
||||
}
|
||||
validate_jump_hop_character_asset_ready(&work.character_asset, "character_asset")?;
|
||||
validate_jump_hop_character_asset_ready(&work.tile_atlas_asset, "tile_atlas_asset")?;
|
||||
if work.tile_assets.is_empty() {
|
||||
validate_jump_hop_default_character_ready(work)?;
|
||||
validate_jump_hop_tile_atlas_asset_ready(&work.tile_atlas_asset, "tile_atlas_asset")?;
|
||||
if work.tile_assets.len() < 25 {
|
||||
return Err(SpacetimeClientError::validation_failed(
|
||||
"jump-hop runtime 缺少地块资产",
|
||||
"jump-hop runtime 需要 25 个地块资产",
|
||||
));
|
||||
}
|
||||
for (index, asset) in work.tile_assets.iter().enumerate() {
|
||||
@@ -437,7 +467,34 @@ fn validate_jump_hop_runtime_ready(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_jump_hop_character_asset_ready(
|
||||
fn normalize_jump_hop_runtime_mode(value: Option<&str>) -> &'static str {
|
||||
if value
|
||||
.map(|value| value.trim().eq_ignore_ascii_case("draft"))
|
||||
.unwrap_or(false)
|
||||
{
|
||||
"draft"
|
||||
} else {
|
||||
"published"
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_jump_hop_default_character_ready(
|
||||
work: &JumpHopWorkProfileResponse,
|
||||
) -> Result<(), SpacetimeClientError> {
|
||||
let Some(default_character) = work.default_character.as_ref() else {
|
||||
return Err(SpacetimeClientError::validation_failed(
|
||||
"jump-hop runtime 缺少内置默认角色配置",
|
||||
));
|
||||
};
|
||||
if default_character.model_kind.trim() != "builtin-three" {
|
||||
return Err(SpacetimeClientError::validation_failed(
|
||||
"jump-hop runtime 默认角色必须使用 builtin-three",
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_jump_hop_tile_atlas_asset_ready(
|
||||
asset: &JumpHopCharacterAsset,
|
||||
field: &str,
|
||||
) -> Result<(), SpacetimeClientError> {
|
||||
@@ -475,7 +532,6 @@ enum JumpHopActionProcedure {
|
||||
#[derive(Clone, Copy)]
|
||||
enum JumpHopDraftMergeScope {
|
||||
CompileDraft,
|
||||
RegenerateCharacter,
|
||||
RegenerateTiles,
|
||||
UpdateWorkMeta,
|
||||
UpdateDifficulty,
|
||||
@@ -484,7 +540,6 @@ enum JumpHopDraftMergeScope {
|
||||
#[derive(Clone, Copy)]
|
||||
enum JumpHopAssetRefresh {
|
||||
Preserve,
|
||||
Character,
|
||||
Tiles,
|
||||
}
|
||||
|
||||
@@ -496,12 +551,18 @@ fn build_jump_hop_action_plan(
|
||||
) -> Result<(JumpHopActionProcedure, JumpHopDraftResponse), SpacetimeClientError> {
|
||||
let scope = match payload.action_type {
|
||||
JumpHopActionType::CompileDraft => JumpHopDraftMergeScope::CompileDraft,
|
||||
JumpHopActionType::RegenerateCharacter => JumpHopDraftMergeScope::RegenerateCharacter,
|
||||
JumpHopActionType::RegenerateTiles => JumpHopDraftMergeScope::RegenerateTiles,
|
||||
JumpHopActionType::UpdateWorkMeta => JumpHopDraftMergeScope::UpdateWorkMeta,
|
||||
JumpHopActionType::UpdateDifficulty => JumpHopDraftMergeScope::UpdateDifficulty,
|
||||
};
|
||||
let mut draft = merge_action_into_draft(current.draft.clone(), payload, scope)?;
|
||||
let mut base_draft = current.draft.clone();
|
||||
if matches!(payload.action_type, JumpHopActionType::RegenerateTiles) {
|
||||
if let Some(draft) = base_draft.as_mut() {
|
||||
draft.tile_atlas_asset = None;
|
||||
draft.tile_assets.clear();
|
||||
}
|
||||
}
|
||||
let mut draft = merge_action_into_draft(base_draft, payload, scope)?;
|
||||
let profile_id = resolve_jump_hop_profile_id(&draft, &payload.action_type)?;
|
||||
draft.profile_id = Some(profile_id.clone());
|
||||
|
||||
@@ -514,16 +575,6 @@ fn build_jump_hop_action_plan(
|
||||
JumpHopAssetRefresh::Preserve,
|
||||
now_micros,
|
||||
)?),
|
||||
JumpHopActionType::RegenerateCharacter => {
|
||||
JumpHopActionProcedure::Compile(build_compile_input(
|
||||
current,
|
||||
owner_user_id,
|
||||
&profile_id,
|
||||
&mut draft,
|
||||
JumpHopAssetRefresh::Character,
|
||||
now_micros,
|
||||
)?)
|
||||
}
|
||||
JumpHopActionType::RegenerateTiles => JumpHopActionProcedure::Compile(build_compile_input(
|
||||
current,
|
||||
owner_user_id,
|
||||
@@ -563,6 +614,13 @@ fn merge_action_into_draft(
|
||||
{
|
||||
draft.work_title = value.trim().to_string();
|
||||
}
|
||||
if let Some(value) = payload
|
||||
.theme_text
|
||||
.as_ref()
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
{
|
||||
draft.theme_text = value.trim().chars().take(60).collect();
|
||||
}
|
||||
if let Some(value) = payload.work_description.as_ref() {
|
||||
draft.work_description = value.trim().to_string();
|
||||
}
|
||||
@@ -590,10 +648,7 @@ fn merge_action_into_draft(
|
||||
.filter(|value| !value.is_empty());
|
||||
}
|
||||
}
|
||||
if matches!(
|
||||
scope,
|
||||
JumpHopDraftMergeScope::CompileDraft | JumpHopDraftMergeScope::RegenerateCharacter
|
||||
) {
|
||||
if matches!(scope, JumpHopDraftMergeScope::CompileDraft) {
|
||||
if let Some(value) = payload
|
||||
.character_prompt
|
||||
.as_ref()
|
||||
@@ -622,10 +677,7 @@ fn merge_action_into_draft(
|
||||
{
|
||||
draft.profile_id = Some(profile_id.to_string());
|
||||
}
|
||||
if matches!(
|
||||
scope,
|
||||
JumpHopDraftMergeScope::CompileDraft | JumpHopDraftMergeScope::RegenerateCharacter
|
||||
) {
|
||||
if matches!(scope, JumpHopDraftMergeScope::CompileDraft) {
|
||||
if let Some(asset) = payload.character_asset.clone() {
|
||||
draft.character_asset = Some(asset);
|
||||
}
|
||||
@@ -665,28 +717,19 @@ fn build_compile_input(
|
||||
refresh: JumpHopAssetRefresh,
|
||||
now_micros: i64,
|
||||
) -> Result<JumpHopDraftCompileInput, SpacetimeClientError> {
|
||||
let force_character = matches!(refresh, JumpHopAssetRefresh::Character);
|
||||
let force_tiles = matches!(refresh, JumpHopAssetRefresh::Tiles);
|
||||
if force_character {
|
||||
draft.character_asset = None;
|
||||
}
|
||||
if force_tiles {
|
||||
draft.tile_atlas_asset = None;
|
||||
draft.tile_assets.clear();
|
||||
}
|
||||
let character_asset = draft.character_asset.clone().ok_or_else(|| {
|
||||
SpacetimeClientError::validation_failed(
|
||||
"jump-hop compile-draft 缺少真实角色资产,请先由 api-server 生成并持久化 asset_object",
|
||||
)
|
||||
})?;
|
||||
let character_asset = draft.character_asset.clone().unwrap_or_else(|| {
|
||||
build_jump_hop_default_character_asset(profile_id, draft.theme_text.as_str())
|
||||
});
|
||||
draft.character_asset = Some(character_asset.clone());
|
||||
draft.default_character = Some(default_jump_hop_default_character());
|
||||
let tile_atlas_asset = draft.tile_atlas_asset.clone().ok_or_else(|| {
|
||||
SpacetimeClientError::validation_failed(
|
||||
"jump-hop compile-draft 缺少真实地块图集资产,请先由 api-server 生成并持久化 asset_object",
|
||||
)
|
||||
})?;
|
||||
let tile_assets = if draft.tile_assets.is_empty() {
|
||||
let tile_assets = if draft.tile_assets.len() < 25 {
|
||||
return Err(SpacetimeClientError::validation_failed(
|
||||
"jump-hop compile-draft 缺少真实地块资产,请先由 api-server 生成并持久化 asset_object",
|
||||
"jump-hop compile-draft 需要 25 个真实地块资产,请先由 api-server 生成并持久化 asset_object",
|
||||
));
|
||||
} else {
|
||||
draft.tile_assets.clone()
|
||||
@@ -705,7 +748,7 @@ fn build_compile_input(
|
||||
work_title: draft.work_title.clone(),
|
||||
work_description: draft.work_description.clone(),
|
||||
theme_tags_json: Some(json_string(&draft.theme_tags)?),
|
||||
theme_text: Some(draft.work_title.clone()),
|
||||
theme_text: Some(draft.theme_text.clone()),
|
||||
difficulty: Some(difficulty_to_str(&draft.difficulty).to_string()),
|
||||
style_preset: Some(style_to_str(&draft.style_preset).to_string()),
|
||||
character_prompt: Some(draft.character_prompt.clone()),
|
||||
@@ -785,13 +828,15 @@ fn default_draft() -> JumpHopDraftResponse {
|
||||
template_id: JUMP_HOP_TEMPLATE_ID.to_string(),
|
||||
template_name: JUMP_HOP_TEMPLATE_NAME.to_string(),
|
||||
profile_id: None,
|
||||
theme_text: JUMP_HOP_TEMPLATE_NAME.to_string(),
|
||||
work_title: JUMP_HOP_TEMPLATE_NAME.to_string(),
|
||||
work_description: "俯视角跳跃闯关".to_string(),
|
||||
theme_tags: vec!["跳一跳".to_string(), "休闲".to_string()],
|
||||
difficulty: JumpHopDifficulty::Standard,
|
||||
style_preset: JumpHopStylePreset::MinimalBlocks,
|
||||
character_prompt: "俯视角可爱主角,透明背景".to_string(),
|
||||
tile_prompt: "等距立体地块图集".to_string(),
|
||||
default_character: Some(default_jump_hop_default_character()),
|
||||
character_prompt: "内置默认 3D 角色".to_string(),
|
||||
tile_prompt: "跳一跳主题的俯视角清爽游戏化立体感平台素材".to_string(),
|
||||
end_mood_prompt: None,
|
||||
character_asset: None,
|
||||
tile_atlas_asset: None,
|
||||
@@ -804,7 +849,7 @@ fn default_draft() -> JumpHopDraftResponse {
|
||||
|
||||
fn build_config_json(draft: &JumpHopDraftResponse) -> Result<String, SpacetimeClientError> {
|
||||
serde_json::to_string(&serde_json::json!({
|
||||
"themeText": draft.work_title,
|
||||
"themeText": draft.theme_text,
|
||||
"difficulty": difficulty_to_str(&draft.difficulty),
|
||||
"stylePreset": style_to_str(&draft.style_preset),
|
||||
"characterPrompt": draft.character_prompt,
|
||||
@@ -814,94 +859,6 @@ fn build_config_json(draft: &JumpHopDraftResponse) -> Result<String, SpacetimeCl
|
||||
.map_err(SpacetimeClientError::validation_failed)
|
||||
}
|
||||
|
||||
fn ensure_character_asset(
|
||||
existing: Option<JumpHopCharacterAsset>,
|
||||
profile_id: &str,
|
||||
prompt: &str,
|
||||
force_new: bool,
|
||||
now_micros: i64,
|
||||
) -> JumpHopCharacterAsset {
|
||||
if !force_new {
|
||||
if let Some(asset) = existing {
|
||||
return asset;
|
||||
}
|
||||
}
|
||||
let revision = force_new.then_some(now_micros);
|
||||
let suffix = asset_revision_suffix(revision);
|
||||
JumpHopCharacterAsset {
|
||||
asset_id: format!("{profile_id}-character{suffix}"),
|
||||
image_src: format!("/generated-jump-hop-assets/{profile_id}/character{suffix}.png"),
|
||||
image_object_key: format!("generated-jump-hop-assets/{profile_id}/character{suffix}.png"),
|
||||
asset_object_id: format!("{profile_id}-character{suffix}-object"),
|
||||
generation_provider: "deterministic-placeholder".to_string(),
|
||||
prompt: prompt.to_string(),
|
||||
width: 768,
|
||||
height: 768,
|
||||
}
|
||||
}
|
||||
|
||||
fn ensure_tile_atlas_asset(
|
||||
existing: Option<JumpHopCharacterAsset>,
|
||||
profile_id: &str,
|
||||
prompt: &str,
|
||||
force_new: bool,
|
||||
now_micros: i64,
|
||||
) -> JumpHopCharacterAsset {
|
||||
if !force_new {
|
||||
if let Some(asset) = existing {
|
||||
return asset;
|
||||
}
|
||||
}
|
||||
let revision = force_new.then_some(now_micros);
|
||||
let suffix = asset_revision_suffix(revision);
|
||||
JumpHopCharacterAsset {
|
||||
asset_id: format!("{profile_id}-tile-atlas{suffix}"),
|
||||
image_src: format!("/generated-jump-hop-assets/{profile_id}/tile-atlas{suffix}.png"),
|
||||
image_object_key: format!("generated-jump-hop-assets/{profile_id}/tile-atlas{suffix}.png"),
|
||||
asset_object_id: format!("{profile_id}-tile-atlas{suffix}-object"),
|
||||
generation_provider: "deterministic-placeholder".to_string(),
|
||||
prompt: prompt.to_string(),
|
||||
width: 1024,
|
||||
height: 1024,
|
||||
}
|
||||
}
|
||||
|
||||
fn ensure_tile_assets(
|
||||
existing: Vec<JumpHopTileAsset>,
|
||||
profile_id: &str,
|
||||
force_new: bool,
|
||||
now_micros: i64,
|
||||
) -> Vec<JumpHopTileAsset> {
|
||||
if !force_new && !existing.is_empty() {
|
||||
return existing;
|
||||
}
|
||||
let suffix = asset_revision_suffix(force_new.then_some(now_micros));
|
||||
[
|
||||
JumpHopTileType::Start,
|
||||
JumpHopTileType::Normal,
|
||||
JumpHopTileType::Target,
|
||||
JumpHopTileType::Finish,
|
||||
JumpHopTileType::Bonus,
|
||||
JumpHopTileType::Accent,
|
||||
]
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(index, tile_type)| JumpHopTileAsset {
|
||||
tile_type,
|
||||
image_src: format!("/generated-jump-hop-assets/{profile_id}/tiles/{index}{suffix}.png"),
|
||||
image_object_key: format!(
|
||||
"generated-jump-hop-assets/{profile_id}/tiles/{index}{suffix}.png"
|
||||
),
|
||||
asset_object_id: format!("{profile_id}-tile-{index}{suffix}-object"),
|
||||
source_atlas_cell: format!("cell-{index}{suffix}"),
|
||||
visual_width: 256,
|
||||
visual_height: 192,
|
||||
top_surface_radius: 42.0,
|
||||
landing_radius: 34.0,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn resolve_cover_composite(
|
||||
draft: &JumpHopDraftResponse,
|
||||
profile_id: &str,
|
||||
@@ -926,6 +883,22 @@ fn resolve_cover_composite(
|
||||
))
|
||||
}
|
||||
|
||||
fn build_jump_hop_default_character_asset(
|
||||
profile_id: &str,
|
||||
theme_text: &str,
|
||||
) -> JumpHopCharacterAsset {
|
||||
JumpHopCharacterAsset {
|
||||
asset_id: format!("{profile_id}-builtin-character"),
|
||||
image_src: "builtin://jump-hop/default-character".to_string(),
|
||||
image_object_key: String::new(),
|
||||
asset_object_id: format!("{profile_id}-builtin-character"),
|
||||
generation_provider: "builtin-three".to_string(),
|
||||
prompt: format!("内置默认 3D 角色:{}", theme_text.trim()),
|
||||
width: 0,
|
||||
height: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn asset_revision_suffix(revision: Option<i64>) -> String {
|
||||
revision
|
||||
.filter(|value| *value > 0)
|
||||
@@ -957,6 +930,16 @@ fn style_to_str(value: &JumpHopStylePreset) -> &'static str {
|
||||
}
|
||||
}
|
||||
|
||||
fn default_jump_hop_default_character() -> shared_contracts::jump_hop::JumpHopDefaultCharacter {
|
||||
shared_contracts::jump_hop::JumpHopDefaultCharacter {
|
||||
character_id: "jump-hop-default-runner".to_string(),
|
||||
display_name: "默认角色".to_string(),
|
||||
model_kind: "builtin-three".to_string(),
|
||||
body_color: "#f59e0b".to_string(),
|
||||
accent_color: "#2563eb".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -968,8 +951,9 @@ mod tests {
|
||||
const NOW_MICROS: i64 = 1_763_456_789_000_000;
|
||||
|
||||
#[test]
|
||||
fn jump_hop_action_compile_draft_builds_compile_input_with_assets() {
|
||||
let session = session_with_draft(draft_without_assets());
|
||||
fn jump_hop_action_compile_draft_builds_compile_input_with_25_tile_assets_and_builtin_character()
|
||||
{
|
||||
let session = session_with_draft(draft_without_character_asset());
|
||||
let payload = action(JumpHopActionType::CompileDraft);
|
||||
|
||||
let (plan, draft) =
|
||||
@@ -987,7 +971,7 @@ mod tests {
|
||||
.character_asset_json
|
||||
.as_deref()
|
||||
.unwrap_or("")
|
||||
.contains("-character")
|
||||
.contains("builtin-three")
|
||||
);
|
||||
assert!(
|
||||
input
|
||||
@@ -1001,59 +985,19 @@ mod tests {
|
||||
.tile_assets_json
|
||||
.as_deref()
|
||||
.unwrap_or("")
|
||||
.contains("tile-0-object")
|
||||
.contains("old-tile-25-object")
|
||||
);
|
||||
assert_eq!(draft.tile_assets.len(), 25);
|
||||
assert_eq!(draft.generation_status, JumpHopGenerationStatus::Ready);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn jump_hop_action_regenerate_character_replaces_only_character_asset_input() {
|
||||
let session = session_with_draft(draft_with_assets());
|
||||
let mut payload = action(JumpHopActionType::RegenerateCharacter);
|
||||
payload.character_prompt = Some("新的主角提示词".to_string());
|
||||
|
||||
let (plan, _draft) =
|
||||
build_jump_hop_action_plan(&session, OWNER_USER_ID, &payload, NOW_MICROS)
|
||||
.expect("regenerate-character should build plan");
|
||||
|
||||
let JumpHopActionProcedure::Compile(input) = plan else {
|
||||
panic!("regenerate-character should call compile_jump_hop_draft");
|
||||
};
|
||||
assert!(
|
||||
!input
|
||||
.character_asset_json
|
||||
.as_deref()
|
||||
.unwrap_or("")
|
||||
.contains("old-character")
|
||||
);
|
||||
assert!(
|
||||
input
|
||||
.character_asset_json
|
||||
.as_deref()
|
||||
.unwrap_or("")
|
||||
.contains(&NOW_MICROS.to_string())
|
||||
);
|
||||
assert!(
|
||||
input
|
||||
.tile_atlas_asset_json
|
||||
.as_deref()
|
||||
.unwrap_or("")
|
||||
.contains("old-tile-atlas")
|
||||
);
|
||||
assert!(
|
||||
input
|
||||
.tile_assets_json
|
||||
.as_deref()
|
||||
.unwrap_or("")
|
||||
.contains("old-normal-tile")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn jump_hop_action_regenerate_tiles_replaces_only_tile_asset_input() {
|
||||
let session = session_with_draft(draft_with_assets());
|
||||
let mut payload = action(JumpHopActionType::RegenerateTiles);
|
||||
payload.tile_prompt = Some("新的地块提示词".to_string());
|
||||
payload.tile_atlas_asset = Some(tile_atlas_asset("new-tile-atlas", NOW_MICROS));
|
||||
payload.tile_assets = Some(tile_assets("new", 25));
|
||||
|
||||
let (plan, _draft) =
|
||||
build_jump_hop_action_plan(&session, OWNER_USER_ID, &payload, NOW_MICROS)
|
||||
@@ -1067,7 +1011,7 @@ mod tests {
|
||||
.character_asset_json
|
||||
.as_deref()
|
||||
.unwrap_or("")
|
||||
.contains("old-character")
|
||||
.contains("builtin-three")
|
||||
);
|
||||
assert!(
|
||||
!input
|
||||
@@ -1081,24 +1025,43 @@ mod tests {
|
||||
.tile_assets_json
|
||||
.as_deref()
|
||||
.unwrap_or("")
|
||||
.contains("old-normal-tile")
|
||||
.contains("old-tile-01-object")
|
||||
);
|
||||
assert!(
|
||||
input
|
||||
.tile_atlas_asset_json
|
||||
.as_deref()
|
||||
.unwrap_or("")
|
||||
.contains(&NOW_MICROS.to_string())
|
||||
.contains("new-tile-atlas")
|
||||
);
|
||||
assert!(
|
||||
input
|
||||
.tile_assets_json
|
||||
.as_deref()
|
||||
.unwrap_or("")
|
||||
.contains(&NOW_MICROS.to_string())
|
||||
.contains("new-tile-25-object")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn jump_hop_action_compile_draft_persists_theme_text_separately_from_title() {
|
||||
let session = session_with_draft(draft_without_character_asset());
|
||||
let mut payload = action(JumpHopActionType::CompileDraft);
|
||||
payload.theme_text = Some(" 森林蘑菇跳台 ".to_string());
|
||||
payload.work_title = Some("自动标题".to_string());
|
||||
|
||||
let (plan, draft) =
|
||||
build_jump_hop_action_plan(&session, OWNER_USER_ID, &payload, NOW_MICROS)
|
||||
.expect("compile-draft should build plan");
|
||||
|
||||
let JumpHopActionProcedure::Compile(input) = plan else {
|
||||
panic!("compile-draft should call compile_jump_hop_draft");
|
||||
};
|
||||
assert_eq!(draft.theme_text, "森林蘑菇跳台");
|
||||
assert_eq!(input.theme_text.as_deref(), Some("森林蘑菇跳台"));
|
||||
assert_eq!(input.work_title, "自动标题");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn jump_hop_action_update_work_meta_builds_update_input_without_asset_compile() {
|
||||
let session = session_with_draft(draft_with_assets());
|
||||
@@ -1143,20 +1106,22 @@ mod tests {
|
||||
.character_asset
|
||||
.as_ref()
|
||||
.map(|asset| asset.asset_id.as_str()),
|
||||
Some("old-character")
|
||||
Some("jump-hop-profile-test-builtin-character")
|
||||
);
|
||||
assert_eq!(
|
||||
draft
|
||||
.tile_assets
|
||||
.first()
|
||||
.map(|asset| asset.asset_object_id.as_str()),
|
||||
Some("old-normal-tile-object")
|
||||
Some("old-tile-01-object")
|
||||
);
|
||||
}
|
||||
|
||||
fn action(action_type: JumpHopActionType) -> JumpHopActionRequest {
|
||||
JumpHopActionRequest {
|
||||
action_type,
|
||||
profile_id: None,
|
||||
theme_text: None,
|
||||
work_title: None,
|
||||
work_description: None,
|
||||
theme_tags: None,
|
||||
@@ -1165,6 +1130,10 @@ mod tests {
|
||||
character_prompt: None,
|
||||
tile_prompt: None,
|
||||
end_mood_prompt: None,
|
||||
character_asset: None,
|
||||
tile_atlas_asset: None,
|
||||
tile_assets: None,
|
||||
cover_composite: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1179,9 +1148,11 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
fn draft_without_assets() -> JumpHopDraftResponse {
|
||||
fn draft_without_character_asset() -> JumpHopDraftResponse {
|
||||
JumpHopDraftResponse {
|
||||
profile_id: None,
|
||||
tile_atlas_asset: Some(tile_atlas_asset("old-tile-atlas", 0)),
|
||||
tile_assets: tile_assets("old", 25),
|
||||
..base_draft()
|
||||
}
|
||||
}
|
||||
@@ -1189,37 +1160,9 @@ mod tests {
|
||||
fn draft_with_assets() -> JumpHopDraftResponse {
|
||||
JumpHopDraftResponse {
|
||||
profile_id: Some(PROFILE_ID.to_string()),
|
||||
character_asset: Some(JumpHopCharacterAsset {
|
||||
asset_id: "old-character".to_string(),
|
||||
image_src: "/generated-jump-hop-assets/old-character.png".to_string(),
|
||||
image_object_key: "generated-jump-hop-assets/old-character.png".to_string(),
|
||||
asset_object_id: "old-character-object".to_string(),
|
||||
generation_provider: "old-provider".to_string(),
|
||||
prompt: "旧角色提示词".to_string(),
|
||||
width: 768,
|
||||
height: 768,
|
||||
}),
|
||||
tile_atlas_asset: Some(JumpHopCharacterAsset {
|
||||
asset_id: "old-tile-atlas".to_string(),
|
||||
image_src: "/generated-jump-hop-assets/old-tile-atlas.png".to_string(),
|
||||
image_object_key: "generated-jump-hop-assets/old-tile-atlas.png".to_string(),
|
||||
asset_object_id: "old-tile-atlas-object".to_string(),
|
||||
generation_provider: "old-provider".to_string(),
|
||||
prompt: "旧地块提示词".to_string(),
|
||||
width: 1024,
|
||||
height: 1024,
|
||||
}),
|
||||
tile_assets: vec![JumpHopTileAsset {
|
||||
tile_type: JumpHopTileType::Normal,
|
||||
image_src: "/generated-jump-hop-assets/old-normal-tile.png".to_string(),
|
||||
image_object_key: "generated-jump-hop-assets/old-normal-tile.png".to_string(),
|
||||
asset_object_id: "old-normal-tile-object".to_string(),
|
||||
source_atlas_cell: "old-cell".to_string(),
|
||||
visual_width: 256,
|
||||
visual_height: 192,
|
||||
top_surface_radius: 42.0,
|
||||
landing_radius: 34.0,
|
||||
}],
|
||||
character_asset: Some(build_jump_hop_default_character_asset(PROFILE_ID, "旧主题")),
|
||||
tile_atlas_asset: Some(tile_atlas_asset("old-tile-atlas", 0)),
|
||||
tile_assets: tile_assets("old", 25),
|
||||
path: Some(sample_jump_hop_path()),
|
||||
cover_composite: Some("/generated-jump-hop-assets/old-cover.png".to_string()),
|
||||
generation_status: JumpHopGenerationStatus::Ready,
|
||||
@@ -1227,16 +1170,58 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
fn tile_atlas_asset(asset_id: &str, revision: i64) -> JumpHopCharacterAsset {
|
||||
let suffix = asset_revision_suffix((revision > 0).then_some(revision));
|
||||
JumpHopCharacterAsset {
|
||||
asset_id: asset_id.to_string(),
|
||||
image_src: format!("/generated-jump-hop-assets/{asset_id}{suffix}.png"),
|
||||
image_object_key: format!("generated-jump-hop-assets/{asset_id}{suffix}.png"),
|
||||
asset_object_id: format!("{asset_id}-object"),
|
||||
generation_provider: "vector-engine-image2".to_string(),
|
||||
prompt: "旧地块提示词".to_string(),
|
||||
width: 1024,
|
||||
height: 1024,
|
||||
}
|
||||
}
|
||||
|
||||
fn tile_assets(prefix: &str, count: usize) -> Vec<JumpHopTileAsset> {
|
||||
(0..count)
|
||||
.map(|index| JumpHopTileAsset {
|
||||
tile_type: if index == 0 {
|
||||
JumpHopTileType::Start
|
||||
} else {
|
||||
JumpHopTileType::Normal
|
||||
},
|
||||
tile_id: Some(format!("tile-{:02}", index + 1)),
|
||||
image_src: format!("/generated-jump-hop-assets/{prefix}-tile-{}.png", index + 1),
|
||||
image_object_key: format!(
|
||||
"generated-jump-hop-assets/{prefix}-tile-{}.png",
|
||||
index + 1
|
||||
),
|
||||
asset_object_id: format!("{prefix}-tile-{:02}-object", index + 1),
|
||||
source_atlas_cell: format!("row-{}-col-{}", index / 5 + 1, index % 5 + 1),
|
||||
atlas_row: Some(index as u32 / 5 + 1),
|
||||
atlas_col: Some(index as u32 % 5 + 1),
|
||||
visual_width: 256,
|
||||
visual_height: 192,
|
||||
top_surface_radius: 42.0,
|
||||
landing_radius: 34.0,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn base_draft() -> JumpHopDraftResponse {
|
||||
JumpHopDraftResponse {
|
||||
template_id: JUMP_HOP_TEMPLATE_ID.to_string(),
|
||||
template_name: JUMP_HOP_TEMPLATE_NAME.to_string(),
|
||||
profile_id: None,
|
||||
theme_text: "旧主题".to_string(),
|
||||
work_title: "旧标题".to_string(),
|
||||
work_description: "旧描述".to_string(),
|
||||
theme_tags: vec!["旧标签".to_string()],
|
||||
difficulty: JumpHopDifficulty::Standard,
|
||||
style_preset: JumpHopStylePreset::MinimalBlocks,
|
||||
default_character: Some(default_jump_hop_default_character()),
|
||||
character_prompt: "旧角色提示词".to_string(),
|
||||
tile_prompt: "旧地块提示词".to_string(),
|
||||
end_mood_prompt: None,
|
||||
|
||||
Reference in New Issue
Block a user