feat(jump-hop): redesign sling platform gameplay

This commit is contained in:
2026-06-03 22:21:00 +08:00
parent 40ef89aeb5
commit 7d2d67a3f5
59 changed files with 6930 additions and 1973 deletions

View File

@@ -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,

View File

@@ -171,8 +171,8 @@ pub(crate) use self::inventory::{
};
pub(crate) use self::jump_hop::{
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,
};
pub(crate) use self::match3d::{
map_match3d_agent_session_procedure_result, map_match3d_click_item_procedure_result,

View File

@@ -163,6 +163,7 @@ mod tests {
let row = BarkBattleGalleryViewRow {
work_id: "BB-33333333".to_string(),
owner_user_id: "user-3".to_string(),
author_display_name: "声浪玩家".to_string(),
source_draft_id: Some("bark-battle-draft-3".to_string()),
config_version: 1,
ruleset_version: "bark-battle-ruleset-v1".to_string(),

View File

@@ -1,10 +1,11 @@
use super::*;
pub use shared_contracts::jump_hop::{
JumpHopActionRequest, JumpHopActionResponse, JumpHopActionType, JumpHopCharacterAsset,
JumpHopDifficulty, JumpHopDraftResponse, JumpHopGalleryCardResponse,
JumpHopDefaultCharacter, JumpHopDifficulty, JumpHopDraftResponse, JumpHopGalleryCardResponse,
JumpHopGalleryDetailResponse, JumpHopGalleryResponse, JumpHopGenerationStatus,
JumpHopJumpRequest, JumpHopJumpResponse, JumpHopJumpResult, JumpHopLastJump, JumpHopPath,
JumpHopPlatform, JumpHopRestartRunRequest, JumpHopRunResponse, JumpHopRunStatus,
JumpHopJumpRequest, JumpHopJumpResponse, JumpHopJumpResult, JumpHopLastJump,
JumpHopLeaderboardEntry, JumpHopLeaderboardResponse, JumpHopPath, JumpHopPlatform,
JumpHopRestartRunRequest, JumpHopRunResponse, JumpHopRunStatus,
JumpHopRuntimeRunSnapshotResponse, JumpHopScoring, JumpHopSessionResponse,
JumpHopSessionSnapshotResponse, JumpHopStartRunRequest, JumpHopStylePreset, JumpHopTileAsset,
JumpHopTileType, JumpHopWorkDetailResponse, JumpHopWorkMutationResponse,
@@ -61,6 +62,25 @@ pub(crate) fn map_jump_hop_run_procedure_result(
Ok(map_jump_hop_run_snapshot(run))
}
pub(crate) fn map_jump_hop_leaderboard_procedure_result(
result: JumpHopLeaderboardProcedureResult,
) -> Result<JumpHopLeaderboardResponse, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
Ok(JumpHopLeaderboardResponse {
profile_id: result.profile_id,
items: result
.items
.into_iter()
.map(map_jump_hop_leaderboard_entry_snapshot)
.collect(),
viewer_best: result
.viewer_best
.map(map_jump_hop_leaderboard_entry_snapshot),
})
}
pub(crate) fn map_jump_hop_gallery_card_view_row(
row: JumpHopGalleryCardViewRow,
) -> JumpHopGalleryCardResponse {
@@ -70,6 +90,7 @@ pub(crate) fn map_jump_hop_gallery_card_view_row(
profile_id: row.profile_id,
owner_user_id: row.owner_user_id,
author_display_name: row.author_display_name,
theme_text: row.work_title.clone(),
work_title: row.work_title,
work_description: row.work_description,
cover_image_src: empty_string_to_none(row.cover_image_src),
@@ -108,11 +129,13 @@ fn map_jump_hop_work_snapshot(
template_id: "jump-hop".to_string(),
template_name: "跳一跳".to_string(),
profile_id: Some(snapshot.profile_id.clone()),
theme_text: snapshot.work_title.clone(),
work_title: snapshot.work_title.clone(),
work_description: snapshot.work_description.clone(),
theme_tags: snapshot.theme_tags.clone(),
difficulty: parse_difficulty(&snapshot.difficulty),
style_preset: parse_style_preset(&snapshot.style_preset),
default_character: Some(default_jump_hop_character()),
character_prompt: snapshot.character_prompt.clone(),
tile_prompt: snapshot.tile_prompt.clone(),
end_mood_prompt: snapshot.end_mood_prompt.clone(),
@@ -143,6 +166,7 @@ fn map_jump_hop_work_snapshot(
profile_id: snapshot.profile_id,
owner_user_id: snapshot.owner_user_id,
source_session_id: empty_string_to_none(snapshot.source_session_id),
theme_text: snapshot.work_title.clone(),
work_title: snapshot.work_title,
work_description: snapshot.work_description,
theme_tags: snapshot.theme_tags,
@@ -159,6 +183,7 @@ fn map_jump_hop_work_snapshot(
},
draft,
path: map_jump_hop_path(snapshot.path),
default_character: Some(default_jump_hop_character()),
character_asset,
tile_atlas_asset,
tile_assets: snapshot
@@ -170,15 +195,18 @@ fn map_jump_hop_work_snapshot(
}
fn map_jump_hop_draft_snapshot(snapshot: JumpHopDraftSnapshot) -> JumpHopDraftResponse {
let theme_text = snapshot.work_title.clone();
JumpHopDraftResponse {
template_id: snapshot.template_id,
template_name: snapshot.template_name,
profile_id: snapshot.profile_id,
theme_text,
work_title: snapshot.work_title,
work_description: snapshot.work_description,
theme_tags: snapshot.theme_tags,
difficulty: parse_difficulty(&snapshot.difficulty),
style_preset: parse_style_preset(&snapshot.style_preset),
default_character: Some(default_jump_hop_character()),
character_prompt: snapshot.character_prompt,
tile_prompt: snapshot.tile_prompt,
end_mood_prompt: snapshot.end_mood_prompt,
@@ -211,10 +239,13 @@ fn map_character_asset(snapshot: JumpHopCharacterAssetSnapshot) -> JumpHopCharac
fn map_tile_asset(snapshot: JumpHopTileAssetSnapshot) -> JumpHopTileAsset {
JumpHopTileAsset {
tile_type: parse_tile_type(&snapshot.tile_type),
tile_id: snapshot.tile_id,
image_src: snapshot.image_src,
image_object_key: snapshot.image_object_key,
asset_object_id: snapshot.asset_object_id,
source_atlas_cell: snapshot.source_atlas_cell,
atlas_row: snapshot.atlas_row,
atlas_col: snapshot.atlas_col,
visual_width: snapshot.visual_width,
visual_height: snapshot.visual_height,
top_surface_radius: snapshot.top_surface_radius,
@@ -263,6 +294,8 @@ fn map_jump_hop_run_snapshot(snapshot: JumpHopRunSnapshot) -> JumpHopRuntimeRunS
crate::module_bindings::JumpHopRunStatus::Playing => JumpHopRunStatus::Playing,
},
current_platform_index: snapshot.current_platform_index,
successful_jump_count: snapshot.current_platform_index,
duration_ms: jump_hop_duration_ms(snapshot.started_at_ms, snapshot.finished_at_ms),
score: snapshot.score,
combo: snapshot.combo,
path: map_jump_hop_path(snapshot.path),
@@ -286,6 +319,34 @@ fn map_jump_hop_run_snapshot(snapshot: JumpHopRunSnapshot) -> JumpHopRuntimeRunS
}
}
fn map_jump_hop_leaderboard_entry_snapshot(
snapshot: JumpHopLeaderboardEntrySnapshot,
) -> JumpHopLeaderboardEntry {
JumpHopLeaderboardEntry {
rank: snapshot.rank,
player_id: snapshot.player_id,
successful_jump_count: snapshot.successful_jump_count,
duration_ms: snapshot.duration_ms,
updated_at: format_timestamp_micros(snapshot.updated_at_micros),
}
}
fn default_jump_hop_character() -> JumpHopDefaultCharacter {
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(),
}
}
fn jump_hop_duration_ms(started_at_ms: u64, finished_at_ms: Option<u64>) -> u64 {
finished_at_ms
.unwrap_or(started_at_ms)
.saturating_sub(started_at_ms)
}
fn parse_difficulty(value: &str) -> JumpHopDifficulty {
match value {
"easy" => JumpHopDifficulty::Easy,

View File

@@ -280,7 +280,7 @@ pub(crate) fn build_creation_entry_config_record_from_rows(
},
creation_types: creation_types
.into_iter()
.map(|item| module_runtime::CreationEntryTypeSnapshot {
.map(|item| normalize_creation_entry_type_snapshot(module_runtime::CreationEntryTypeSnapshot {
id: item.id,
title: item.title,
subtitle: item.subtitle,
@@ -299,7 +299,7 @@ pub(crate) fn build_creation_entry_config_record_from_rows(
),
category_sort_order: item.category_sort_order,
updated_at_micros: item.updated_at.to_micros_since_unix_epoch(),
})
}))
.collect(),
updated_at_micros: header.updated_at.to_micros_since_unix_epoch(),
},
@@ -332,19 +332,21 @@ fn map_creation_entry_config_snapshot(
creation_types: snapshot
.creation_types
.into_iter()
.map(|item| module_runtime::CreationEntryTypeSnapshot {
id: item.id,
title: item.title,
subtitle: item.subtitle,
badge: item.badge,
image_src: item.image_src,
visible: item.visible,
open: item.open,
sort_order: item.sort_order,
category_id: item.category_id,
category_label: item.category_label,
category_sort_order: item.category_sort_order,
updated_at_micros: item.updated_at_micros,
.map(|item| {
normalize_creation_entry_type_snapshot(module_runtime::CreationEntryTypeSnapshot {
id: item.id,
title: item.title,
subtitle: item.subtitle,
badge: item.badge,
image_src: item.image_src,
visible: item.visible,
open: item.open,
sort_order: item.sort_order,
category_id: item.category_id,
category_label: item.category_label,
category_sort_order: item.category_sort_order,
updated_at_micros: item.updated_at_micros,
})
})
.collect(),
updated_at_micros: snapshot.updated_at_micros,
@@ -358,6 +360,138 @@ fn creation_entry_text_or_default(value: Option<String>, default_value: &str) ->
.unwrap_or_else(|| default_value.to_string())
}
fn normalize_creation_entry_type_snapshot(
item: module_runtime::CreationEntryTypeSnapshot,
) -> module_runtime::CreationEntryTypeSnapshot {
// 中文注释:旧库里残留的跳一跳系统默认入口行仍会从订阅缓存命中,这里统一做读模型纠偏,
// 这样无论走订阅缓存还是 procedure 回退,创作页都只会看到新的跳一跳入口口径。
if item.id == "jump-hop"
&& item.title == "跳一跳"
&& item.subtitle == "俯视角跳跃闯关"
&& item.badge == "可创建"
&& item.image_src == "/creation-type-references/puzzle.webp"
&& item.visible
&& item.open
&& item.sort_order == 45
{
return module_runtime::CreationEntryTypeSnapshot {
subtitle: "主题驱动平台跳跃".to_string(),
image_src: "/creation-type-references/jump-hop.webp".to_string(),
..item
};
}
item
}
#[cfg(test)]
mod tests {
use super::*;
use spacetimedb_sdk::Timestamp;
fn build_creation_entry_header() -> CreationEntryConfig {
CreationEntryConfig {
config_id: "creation-entry-config".to_string(),
start_title: "新建作品".to_string(),
start_description: "选择模板后进入对应的创作表单。".to_string(),
start_idle_badge: "模板 Tab".to_string(),
start_busy_badge: "正在开启".to_string(),
modal_title: "选择创作类型".to_string(),
modal_description: "先选玩法类型,再进入对应创作工作台。".to_string(),
updated_at: Timestamp::from_micros_since_unix_epoch(1_000_000),
event_title: None,
event_description: None,
event_cover_image_src: None,
event_prize_pool_mud_points: 0,
event_starts_at_text: None,
event_ends_at_text: None,
}
}
fn build_old_jump_hop_row() -> CreationEntryTypeConfig {
CreationEntryTypeConfig {
id: "jump-hop".to_string(),
title: "跳一跳".to_string(),
subtitle: "俯视角跳跃闯关".to_string(),
badge: "可创建".to_string(),
image_src: "/creation-type-references/puzzle.webp".to_string(),
visible: true,
open: true,
sort_order: 45,
updated_at: Timestamp::from_micros_since_unix_epoch(2_000_000),
category_id: Some("recommended".to_string()),
category_label: Some("热门推荐".to_string()),
category_sort_order: 20,
}
}
#[test]
fn build_creation_entry_config_record_from_rows_normalizes_old_jump_hop_row() {
let record = build_creation_entry_config_record_from_rows(
build_creation_entry_header(),
vec![build_old_jump_hop_row()],
);
let jump_hop = record
.creation_types
.iter()
.find(|item| item.id == "jump-hop")
.expect("should contain jump-hop");
assert_eq!(jump_hop.subtitle, "主题驱动平台跳跃");
assert_eq!(jump_hop.image_src, "/creation-type-references/jump-hop.webp");
}
#[test]
fn map_creation_entry_config_snapshot_normalizes_old_jump_hop_snapshot() {
let record = map_creation_entry_config_snapshot(CreationEntryConfigSnapshot {
config_id: "creation-entry-config".to_string(),
start_card: CreationEntryStartCardSnapshot {
title: "新建作品".to_string(),
description: "选择模板后进入对应的创作表单。".to_string(),
idle_badge: "模板 Tab".to_string(),
busy_badge: "正在开启".to_string(),
},
type_modal: CreationEntryTypeModalSnapshot {
title: "选择创作类型".to_string(),
description: "先选玩法类型,再进入对应创作工作台。".to_string(),
},
event_banner: CreationEntryEventBannerSnapshot {
title: "主题创作赛".to_string(),
description: "用温暖的色彩,捏出秋天的故事。".to_string(),
cover_image_src: "/branding/taonier-logo-spiral-reference-concepts/taonier-spiral-bouncy-clay.png".to_string(),
prize_pool_mud_points: 58_000,
starts_at_text: "2024.10.20 10:00".to_string(),
ends_at_text: "2024.11.20 23:59".to_string(),
},
creation_types: vec![CreationEntryTypeSnapshot {
id: "jump-hop".to_string(),
title: "跳一跳".to_string(),
subtitle: "俯视角跳跃闯关".to_string(),
badge: "可创建".to_string(),
image_src: "/creation-type-references/puzzle.webp".to_string(),
visible: true,
open: true,
sort_order: 45,
category_id: "recommended".to_string(),
category_label: "热门推荐".to_string(),
category_sort_order: 20,
updated_at_micros: 2_000_000,
}],
updated_at_micros: 1_000_000,
});
let jump_hop = record
.creation_types
.iter()
.find(|item| item.id == "jump-hop")
.expect("should contain jump-hop");
assert_eq!(jump_hop.subtitle, "主题驱动平台跳跃");
assert_eq!(jump_hop.image_src, "/creation-type-references/jump-hop.webp");
}
}
pub(crate) fn map_runtime_setting_procedure_result(
result: RuntimeSettingProcedureResult,
) -> Result<RuntimeSettingsRecord, SpacetimeClientError> {

View File

@@ -365,6 +365,7 @@ pub mod get_custom_world_gallery_detail_by_code_procedure;
pub mod get_custom_world_gallery_detail_procedure;
pub mod get_custom_world_library_detail_procedure;
pub mod get_jump_hop_agent_session_procedure;
pub mod get_jump_hop_leaderboard_procedure;
pub mod get_jump_hop_run_procedure;
pub mod get_jump_hop_work_profile_procedure;
pub mod get_match_3_d_agent_session_procedure;
@@ -433,6 +434,11 @@ pub mod jump_hop_gallery_view_table;
pub mod jump_hop_jump_procedure;
pub mod jump_hop_jump_result_kind_type;
pub mod jump_hop_last_jump_type;
pub mod jump_hop_leaderboard_entry_row_type;
pub mod jump_hop_leaderboard_entry_snapshot_type;
pub mod jump_hop_leaderboard_entry_table;
pub mod jump_hop_leaderboard_get_input_type;
pub mod jump_hop_leaderboard_procedure_result_type;
pub mod jump_hop_path_type;
pub mod jump_hop_platform_type;
pub mod jump_hop_run_get_input_type;
@@ -1404,6 +1410,7 @@ pub use get_custom_world_gallery_detail_by_code_procedure::get_custom_world_gall
pub use get_custom_world_gallery_detail_procedure::get_custom_world_gallery_detail;
pub use get_custom_world_library_detail_procedure::get_custom_world_library_detail;
pub use get_jump_hop_agent_session_procedure::get_jump_hop_agent_session;
pub use get_jump_hop_leaderboard_procedure::get_jump_hop_leaderboard;
pub use get_jump_hop_run_procedure::get_jump_hop_run;
pub use get_jump_hop_work_profile_procedure::get_jump_hop_work_profile;
pub use get_match_3_d_agent_session_procedure::get_match_3_d_agent_session;
@@ -1472,6 +1479,11 @@ pub use jump_hop_gallery_view_table::*;
pub use jump_hop_jump_procedure::jump_hop_jump;
pub use jump_hop_jump_result_kind_type::JumpHopJumpResultKind;
pub use jump_hop_last_jump_type::JumpHopLastJump;
pub use jump_hop_leaderboard_entry_row_type::JumpHopLeaderboardEntryRow;
pub use jump_hop_leaderboard_entry_snapshot_type::JumpHopLeaderboardEntrySnapshot;
pub use jump_hop_leaderboard_entry_table::*;
pub use jump_hop_leaderboard_get_input_type::JumpHopLeaderboardGetInput;
pub use jump_hop_leaderboard_procedure_result_type::JumpHopLeaderboardProcedureResult;
pub use jump_hop_path_type::JumpHopPath;
pub use jump_hop_platform_type::JumpHopPlatform;
pub use jump_hop_run_get_input_type::JumpHopRunGetInput;
@@ -2400,6 +2412,7 @@ pub struct DbUpdate {
jump_hop_event: __sdk::TableUpdate<JumpHopEventRow>,
jump_hop_gallery_card_view: __sdk::TableUpdate<JumpHopGalleryCardViewRow>,
jump_hop_gallery_view: __sdk::TableUpdate<JumpHopGalleryViewRow>,
jump_hop_leaderboard_entry: __sdk::TableUpdate<JumpHopLeaderboardEntryRow>,
jump_hop_runtime_run: __sdk::TableUpdate<JumpHopRuntimeRunRow>,
jump_hop_work_profile: __sdk::TableUpdate<JumpHopWorkProfileRow>,
match_3_d_agent_message: __sdk::TableUpdate<Match3DAgentMessageRow>,
@@ -2614,6 +2627,9 @@ impl TryFrom<__ws::v2::TransactionUpdate> for DbUpdate {
"jump_hop_gallery_view" => db_update.jump_hop_gallery_view.append(
jump_hop_gallery_view_table::parse_table_update(table_update)?,
),
"jump_hop_leaderboard_entry" => db_update.jump_hop_leaderboard_entry.append(
jump_hop_leaderboard_entry_table::parse_table_update(table_update)?,
),
"jump_hop_runtime_run" => db_update.jump_hop_runtime_run.append(
jump_hop_runtime_run_table::parse_table_update(table_update)?,
),
@@ -3043,6 +3059,12 @@ impl __sdk::DbUpdate for DbUpdate {
diff.jump_hop_event = cache
.apply_diff_to_table::<JumpHopEventRow>("jump_hop_event", &self.jump_hop_event)
.with_updates_by_pk(|row| &row.event_id);
diff.jump_hop_leaderboard_entry = cache
.apply_diff_to_table::<JumpHopLeaderboardEntryRow>(
"jump_hop_leaderboard_entry",
&self.jump_hop_leaderboard_entry,
)
.with_updates_by_pk(|row| &row.entry_id);
diff.jump_hop_runtime_run = cache
.apply_diff_to_table::<JumpHopRuntimeRunRow>(
"jump_hop_runtime_run",
@@ -3528,6 +3550,9 @@ impl __sdk::DbUpdate for DbUpdate {
"jump_hop_gallery_view" => db_update
.jump_hop_gallery_view
.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
"jump_hop_leaderboard_entry" => db_update
.jump_hop_leaderboard_entry
.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
"jump_hop_runtime_run" => db_update
.jump_hop_runtime_run
.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
@@ -3871,6 +3896,9 @@ impl __sdk::DbUpdate for DbUpdate {
"jump_hop_gallery_view" => db_update
.jump_hop_gallery_view
.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
"jump_hop_leaderboard_entry" => db_update
.jump_hop_leaderboard_entry
.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
"jump_hop_runtime_run" => db_update
.jump_hop_runtime_run
.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
@@ -4130,6 +4158,7 @@ pub struct AppliedDiff<'r> {
jump_hop_event: __sdk::TableAppliedDiff<'r, JumpHopEventRow>,
jump_hop_gallery_card_view: __sdk::TableAppliedDiff<'r, JumpHopGalleryCardViewRow>,
jump_hop_gallery_view: __sdk::TableAppliedDiff<'r, JumpHopGalleryViewRow>,
jump_hop_leaderboard_entry: __sdk::TableAppliedDiff<'r, JumpHopLeaderboardEntryRow>,
jump_hop_runtime_run: __sdk::TableAppliedDiff<'r, JumpHopRuntimeRunRow>,
jump_hop_work_profile: __sdk::TableAppliedDiff<'r, JumpHopWorkProfileRow>,
match_3_d_agent_message: __sdk::TableAppliedDiff<'r, Match3DAgentMessageRow>,
@@ -4422,6 +4451,11 @@ impl<'r> __sdk::AppliedDiff<'r> for AppliedDiff<'r> {
&self.jump_hop_gallery_view,
event,
);
callbacks.invoke_table_row_callbacks::<JumpHopLeaderboardEntryRow>(
"jump_hop_leaderboard_entry",
&self.jump_hop_leaderboard_entry,
event,
);
callbacks.invoke_table_row_callbacks::<JumpHopRuntimeRunRow>(
"jump_hop_runtime_run",
&self.jump_hop_runtime_run,
@@ -5444,6 +5478,7 @@ impl __sdk::SpacetimeModule for RemoteModule {
jump_hop_event_table::register_table(client_cache);
jump_hop_gallery_card_view_table::register_table(client_cache);
jump_hop_gallery_view_table::register_table(client_cache);
jump_hop_leaderboard_entry_table::register_table(client_cache);
jump_hop_runtime_run_table::register_table(client_cache);
jump_hop_work_profile_table::register_table(client_cache);
match_3_d_agent_message_table::register_table(client_cache);
@@ -5556,6 +5591,7 @@ impl __sdk::SpacetimeModule for RemoteModule {
"jump_hop_event",
"jump_hop_gallery_card_view",
"jump_hop_gallery_view",
"jump_hop_leaderboard_entry",
"jump_hop_runtime_run",
"jump_hop_work_profile",
"match_3_d_agent_message",

View File

@@ -0,0 +1,59 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
use super::jump_hop_leaderboard_get_input_type::JumpHopLeaderboardGetInput;
use super::jump_hop_leaderboard_procedure_result_type::JumpHopLeaderboardProcedureResult;
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
struct GetJumpHopLeaderboardArgs {
pub input: JumpHopLeaderboardGetInput,
}
impl __sdk::InModule for GetJumpHopLeaderboardArgs {
type Module = super::RemoteModule;
}
#[allow(non_camel_case_types)]
/// Extension trait for access to the procedure `get_jump_hop_leaderboard`.
///
/// Implemented for [`super::RemoteProcedures`].
pub trait get_jump_hop_leaderboard {
fn get_jump_hop_leaderboard(&self, input: JumpHopLeaderboardGetInput) {
self.get_jump_hop_leaderboard_then(input, |_, _| {});
}
fn get_jump_hop_leaderboard_then(
&self,
input: JumpHopLeaderboardGetInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<JumpHopLeaderboardProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
);
}
impl get_jump_hop_leaderboard for super::RemoteProcedures {
fn get_jump_hop_leaderboard_then(
&self,
input: JumpHopLeaderboardGetInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<JumpHopLeaderboardProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, JumpHopLeaderboardProcedureResult>(
"get_jump_hop_leaderboard",
GetJumpHopLeaderboardArgs { input },
__callback,
);
}
}

View File

@@ -0,0 +1,72 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
pub struct JumpHopLeaderboardEntryRow {
pub entry_id: String,
pub profile_id: String,
pub player_id: String,
pub successful_jump_count: u32,
pub duration_ms: u64,
pub run_id: String,
pub updated_at: __sdk::Timestamp,
}
impl __sdk::InModule for JumpHopLeaderboardEntryRow {
type Module = super::RemoteModule;
}
/// Column accessor struct for the table `JumpHopLeaderboardEntryRow`.
///
/// Provides typed access to columns for query building.
pub struct JumpHopLeaderboardEntryRowCols {
pub entry_id: __sdk::__query_builder::Col<JumpHopLeaderboardEntryRow, String>,
pub profile_id: __sdk::__query_builder::Col<JumpHopLeaderboardEntryRow, String>,
pub player_id: __sdk::__query_builder::Col<JumpHopLeaderboardEntryRow, String>,
pub successful_jump_count: __sdk::__query_builder::Col<JumpHopLeaderboardEntryRow, u32>,
pub duration_ms: __sdk::__query_builder::Col<JumpHopLeaderboardEntryRow, u64>,
pub run_id: __sdk::__query_builder::Col<JumpHopLeaderboardEntryRow, String>,
pub updated_at: __sdk::__query_builder::Col<JumpHopLeaderboardEntryRow, __sdk::Timestamp>,
}
impl __sdk::__query_builder::HasCols for JumpHopLeaderboardEntryRow {
type Cols = JumpHopLeaderboardEntryRowCols;
fn cols(table_name: &'static str) -> Self::Cols {
JumpHopLeaderboardEntryRowCols {
entry_id: __sdk::__query_builder::Col::new(table_name, "entry_id"),
profile_id: __sdk::__query_builder::Col::new(table_name, "profile_id"),
player_id: __sdk::__query_builder::Col::new(table_name, "player_id"),
successful_jump_count: __sdk::__query_builder::Col::new(
table_name,
"successful_jump_count",
),
duration_ms: __sdk::__query_builder::Col::new(table_name, "duration_ms"),
run_id: __sdk::__query_builder::Col::new(table_name, "run_id"),
updated_at: __sdk::__query_builder::Col::new(table_name, "updated_at"),
}
}
}
/// Indexed column accessor struct for the table `JumpHopLeaderboardEntryRow`.
///
/// Provides typed access to indexed columns for query building.
pub struct JumpHopLeaderboardEntryRowIxCols {
pub entry_id: __sdk::__query_builder::IxCol<JumpHopLeaderboardEntryRow, String>,
pub profile_id: __sdk::__query_builder::IxCol<JumpHopLeaderboardEntryRow, String>,
}
impl __sdk::__query_builder::HasIxCols for JumpHopLeaderboardEntryRow {
type IxCols = JumpHopLeaderboardEntryRowIxCols;
fn ix_cols(table_name: &'static str) -> Self::IxCols {
JumpHopLeaderboardEntryRowIxCols {
entry_id: __sdk::__query_builder::IxCol::new(table_name, "entry_id"),
profile_id: __sdk::__query_builder::IxCol::new(table_name, "profile_id"),
}
}
}
impl __sdk::__query_builder::CanBeLookupTable for JumpHopLeaderboardEntryRow {}

View File

@@ -0,0 +1,19 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
pub struct JumpHopLeaderboardEntrySnapshot {
pub rank: u32,
pub player_id: String,
pub successful_jump_count: u32,
pub duration_ms: u64,
pub updated_at_micros: i64,
}
impl __sdk::InModule for JumpHopLeaderboardEntrySnapshot {
type Module = super::RemoteModule;
}

View File

@@ -0,0 +1,166 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)]
use super::jump_hop_leaderboard_entry_row_type::JumpHopLeaderboardEntryRow;
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
/// Table handle for the table `jump_hop_leaderboard_entry`.
///
/// Obtain a handle from the [`JumpHopLeaderboardEntryTableAccess::jump_hop_leaderboard_entry`] method on [`super::RemoteTables`],
/// like `ctx.db.jump_hop_leaderboard_entry()`.
///
/// Users are encouraged not to explicitly reference this type,
/// but to directly chain method calls,
/// like `ctx.db.jump_hop_leaderboard_entry().on_insert(...)`.
pub struct JumpHopLeaderboardEntryTableHandle<'ctx> {
imp: __sdk::TableHandle<JumpHopLeaderboardEntryRow>,
ctx: std::marker::PhantomData<&'ctx super::RemoteTables>,
}
#[allow(non_camel_case_types)]
/// Extension trait for access to the table `jump_hop_leaderboard_entry`.
///
/// Implemented for [`super::RemoteTables`].
pub trait JumpHopLeaderboardEntryTableAccess {
#[allow(non_snake_case)]
/// Obtain a [`JumpHopLeaderboardEntryTableHandle`], which mediates access to the table `jump_hop_leaderboard_entry`.
fn jump_hop_leaderboard_entry(&self) -> JumpHopLeaderboardEntryTableHandle<'_>;
}
impl JumpHopLeaderboardEntryTableAccess for super::RemoteTables {
fn jump_hop_leaderboard_entry(&self) -> JumpHopLeaderboardEntryTableHandle<'_> {
JumpHopLeaderboardEntryTableHandle {
imp: self
.imp
.get_table::<JumpHopLeaderboardEntryRow>("jump_hop_leaderboard_entry"),
ctx: std::marker::PhantomData,
}
}
}
pub struct JumpHopLeaderboardEntryInsertCallbackId(__sdk::CallbackId);
pub struct JumpHopLeaderboardEntryDeleteCallbackId(__sdk::CallbackId);
impl<'ctx> __sdk::Table for JumpHopLeaderboardEntryTableHandle<'ctx> {
type Row = JumpHopLeaderboardEntryRow;
type EventContext = super::EventContext;
fn count(&self) -> u64 {
self.imp.count()
}
fn iter(&self) -> impl Iterator<Item = JumpHopLeaderboardEntryRow> + '_ {
self.imp.iter()
}
type InsertCallbackId = JumpHopLeaderboardEntryInsertCallbackId;
fn on_insert(
&self,
callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static,
) -> JumpHopLeaderboardEntryInsertCallbackId {
JumpHopLeaderboardEntryInsertCallbackId(self.imp.on_insert(Box::new(callback)))
}
fn remove_on_insert(&self, callback: JumpHopLeaderboardEntryInsertCallbackId) {
self.imp.remove_on_insert(callback.0)
}
type DeleteCallbackId = JumpHopLeaderboardEntryDeleteCallbackId;
fn on_delete(
&self,
callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static,
) -> JumpHopLeaderboardEntryDeleteCallbackId {
JumpHopLeaderboardEntryDeleteCallbackId(self.imp.on_delete(Box::new(callback)))
}
fn remove_on_delete(&self, callback: JumpHopLeaderboardEntryDeleteCallbackId) {
self.imp.remove_on_delete(callback.0)
}
}
pub struct JumpHopLeaderboardEntryUpdateCallbackId(__sdk::CallbackId);
impl<'ctx> __sdk::TableWithPrimaryKey for JumpHopLeaderboardEntryTableHandle<'ctx> {
type UpdateCallbackId = JumpHopLeaderboardEntryUpdateCallbackId;
fn on_update(
&self,
callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static,
) -> JumpHopLeaderboardEntryUpdateCallbackId {
JumpHopLeaderboardEntryUpdateCallbackId(self.imp.on_update(Box::new(callback)))
}
fn remove_on_update(&self, callback: JumpHopLeaderboardEntryUpdateCallbackId) {
self.imp.remove_on_update(callback.0)
}
}
/// Access to the `entry_id` unique index on the table `jump_hop_leaderboard_entry`,
/// which allows point queries on the field of the same name
/// via the [`JumpHopLeaderboardEntryEntryIdUnique::find`] method.
///
/// Users are encouraged not to explicitly reference this type,
/// but to directly chain method calls,
/// like `ctx.db.jump_hop_leaderboard_entry().entry_id().find(...)`.
pub struct JumpHopLeaderboardEntryEntryIdUnique<'ctx> {
imp: __sdk::UniqueConstraintHandle<JumpHopLeaderboardEntryRow, String>,
phantom: std::marker::PhantomData<&'ctx super::RemoteTables>,
}
impl<'ctx> JumpHopLeaderboardEntryTableHandle<'ctx> {
/// Get a handle on the `entry_id` unique index on the table `jump_hop_leaderboard_entry`.
pub fn entry_id(&self) -> JumpHopLeaderboardEntryEntryIdUnique<'ctx> {
JumpHopLeaderboardEntryEntryIdUnique {
imp: self.imp.get_unique_constraint::<String>("entry_id"),
phantom: std::marker::PhantomData,
}
}
}
impl<'ctx> JumpHopLeaderboardEntryEntryIdUnique<'ctx> {
/// Find the subscribed row whose `entry_id` column value is equal to `col_val`,
/// if such a row is present in the client cache.
pub fn find(&self, col_val: &String) -> Option<JumpHopLeaderboardEntryRow> {
self.imp.find(col_val)
}
}
#[doc(hidden)]
pub(super) fn register_table(client_cache: &mut __sdk::ClientCache<super::RemoteModule>) {
let _table =
client_cache.get_or_make_table::<JumpHopLeaderboardEntryRow>("jump_hop_leaderboard_entry");
_table.add_unique_constraint::<String>("entry_id", |row| &row.entry_id);
}
#[doc(hidden)]
pub(super) fn parse_table_update(
raw_updates: __ws::v2::TableUpdate,
) -> __sdk::Result<__sdk::TableUpdate<JumpHopLeaderboardEntryRow>> {
__sdk::TableUpdate::parse_table_update(raw_updates).map_err(|e| {
__sdk::InternalError::failed_parse("TableUpdate<JumpHopLeaderboardEntryRow>", "TableUpdate")
.with_cause(e)
.into()
})
}
#[allow(non_camel_case_types)]
/// Extension trait for query builder access to the table `JumpHopLeaderboardEntryRow`.
///
/// Implemented for [`__sdk::QueryTableAccessor`].
pub trait jump_hop_leaderboard_entryQueryTableAccess {
#[allow(non_snake_case)]
/// Get a query builder for the table `JumpHopLeaderboardEntryRow`.
fn jump_hop_leaderboard_entry(
&self,
) -> __sdk::__query_builder::Table<JumpHopLeaderboardEntryRow>;
}
impl jump_hop_leaderboard_entryQueryTableAccess for __sdk::QueryTableAccessor {
fn jump_hop_leaderboard_entry(
&self,
) -> __sdk::__query_builder::Table<JumpHopLeaderboardEntryRow> {
__sdk::__query_builder::Table::new("jump_hop_leaderboard_entry")
}
}

View File

@@ -0,0 +1,17 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
pub struct JumpHopLeaderboardGetInput {
pub profile_id: String,
pub viewer_player_id: String,
pub limit: u32,
}
impl __sdk::InModule for JumpHopLeaderboardGetInput {
type Module = super::RemoteModule;
}

View File

@@ -0,0 +1,21 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
use super::jump_hop_leaderboard_entry_snapshot_type::JumpHopLeaderboardEntrySnapshot;
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
pub struct JumpHopLeaderboardProcedureResult {
pub ok: bool,
pub profile_id: String,
pub items: Vec<JumpHopLeaderboardEntrySnapshot>,
pub viewer_best: Option<JumpHopLeaderboardEntrySnapshot>,
pub error_message: Option<String>,
}
impl __sdk::InModule for JumpHopLeaderboardProcedureResult {
type Module = super::RemoteModule;
}

View File

@@ -9,7 +9,9 @@ use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
pub struct JumpHopRunJumpInput {
pub run_id: String,
pub owner_user_id: String,
pub charge_ms: u32,
pub drag_distance: f32,
pub drag_vector_x: Option<f32>,
pub drag_vector_y: Option<f32>,
pub client_event_id: String,
pub jumped_at_ms: i64,
}

View File

@@ -10,6 +10,7 @@ pub struct JumpHopRunStartInput {
pub run_id: String,
pub owner_user_id: String,
pub profile_id: String,
pub runtime_mode: String,
pub client_event_id: String,
pub started_at_ms: i64,
}

View File

@@ -8,10 +8,13 @@ use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
#[sats(crate = __lib)]
pub struct JumpHopTileAssetSnapshot {
pub tile_type: String,
pub tile_id: Option<String>,
pub image_src: String,
pub image_object_key: String,
pub asset_object_id: String,
pub source_atlas_cell: String,
pub atlas_row: Option<u32>,
pub atlas_col: Option<u32>,
pub visual_width: u32,
pub visual_height: u32,
pub top_surface_radius: f32,

View File

@@ -801,6 +801,7 @@ mod tests {
const SESSION_ID: &str = "wooden-fish-session-test";
const OWNER_USER_ID: &str = "user-test";
const AUTHOR_DISPLAY_NAME: &str = "木鱼作者";
const PROFILE_ID: &str = "wooden-fish-profile-test";
const NOW_MICROS: i64 = 1_763_456_789_000_000;
@@ -814,7 +815,13 @@ mod tests {
payload.hit_sound_asset = Some(generated_hit_sound_asset("generated-compile-sound"));
let (plan, draft) =
build_wooden_fish_action_plan(&session, OWNER_USER_ID, &payload, NOW_MICROS)
build_wooden_fish_action_plan(
&session,
OWNER_USER_ID,
AUTHOR_DISPLAY_NAME,
&payload,
NOW_MICROS,
)
.expect("compile-draft should build plan");
let WoodenFishActionProcedure::Compile(input) = plan else {
@@ -863,7 +870,13 @@ mod tests {
payload.back_button_asset = Some(generated_back_button_asset("generated-compile-back"));
let error =
match build_wooden_fish_action_plan(&session, OWNER_USER_ID, &payload, NOW_MICROS) {
match build_wooden_fish_action_plan(
&session,
OWNER_USER_ID,
AUTHOR_DISPLAY_NAME,
&payload,
NOW_MICROS,
) {
Ok(_) => panic!("compile-draft should not synthesize fake hit sound assets"),
Err(error) => error,
};
@@ -884,7 +897,13 @@ mod tests {
payload.back_button_asset = Some(generated_back_button_asset("generated-compile-back"));
let error =
match build_wooden_fish_action_plan(&session, OWNER_USER_ID, &payload, NOW_MICROS) {
match build_wooden_fish_action_plan(
&session,
OWNER_USER_ID,
AUTHOR_DISPLAY_NAME,
&payload,
NOW_MICROS,
) {
Ok(_) => panic!("compile-draft should not publish without background asset"),
Err(error) => error,
};
@@ -905,7 +924,13 @@ mod tests {
payload.hit_sound_asset = Some(generated_hit_sound_asset("generated-compile-sound"));
let error =
match build_wooden_fish_action_plan(&session, OWNER_USER_ID, &payload, NOW_MICROS) {
match build_wooden_fish_action_plan(
&session,
OWNER_USER_ID,
AUTHOR_DISPLAY_NAME,
&payload,
NOW_MICROS,
) {
Ok(_) => panic!("compile-draft should not publish without back button asset"),
Err(error) => error,
};
@@ -927,7 +952,13 @@ mod tests {
payload.back_button_asset = Some(generated_back_button_asset("generated-back"));
let (plan, _draft) =
build_wooden_fish_action_plan(&session, OWNER_USER_ID, &payload, NOW_MICROS)
build_wooden_fish_action_plan(
&session,
OWNER_USER_ID,
AUTHOR_DISPLAY_NAME,
&payload,
NOW_MICROS,
)
.expect("regenerate-hit-object should build plan");
let WoodenFishActionProcedure::Compile(input) = plan else {
@@ -988,7 +1019,13 @@ mod tests {
]);
let (plan, draft) =
build_wooden_fish_action_plan(&session, OWNER_USER_ID, &payload, NOW_MICROS)
build_wooden_fish_action_plan(
&session,
OWNER_USER_ID,
AUTHOR_DISPLAY_NAME,
&payload,
NOW_MICROS,
)
.expect("update-floating-words should build plan");
let WoodenFishActionProcedure::Update(input) = plan else {