1
This commit is contained in:
@@ -1145,6 +1145,12 @@ fn normalize_migration_row(table_name: &str, value: &serde_json::Value) -> serde
|
||||
object
|
||||
.entry("like_count".to_string())
|
||||
.or_insert_with(|| serde_json::Value::from(0));
|
||||
object
|
||||
.entry("point_incentive_total_half_points".to_string())
|
||||
.or_insert_with(|| serde_json::Value::from(0));
|
||||
object
|
||||
.entry("point_incentive_claimed_points".to_string())
|
||||
.or_insert_with(|| serde_json::Value::from(0));
|
||||
// 中文注释:拼图多关卡字段晚于旧作品表加入,旧迁移包留空并由读取层补出首关。
|
||||
object
|
||||
.entry("levels_json".to_string())
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use crate::runtime::{
|
||||
ProfilePlayedWorkUpsertInput, PublicWorkLikeRecordInput, PublicWorkPlayRecordInput,
|
||||
ProfileSaveArchiveUpsertInput,
|
||||
add_profile_observed_play_time, count_recent_public_work_plays, record_public_work_like,
|
||||
record_public_work_play, upsert_profile_played_work, upsert_profile_save_archive,
|
||||
ProfilePlayedWorkUpsertInput, ProfileSaveArchiveUpsertInput, PublicWorkLikeRecordInput,
|
||||
PublicWorkPlayRecordInput, add_profile_observed_play_time, count_recent_public_work_plays,
|
||||
grant_profile_wallet_points, record_public_work_like, record_public_work_play,
|
||||
upsert_profile_played_work, upsert_profile_save_archive,
|
||||
};
|
||||
use module_puzzle::{
|
||||
PUZZLE_MAX_TAG_COUNT, PUZZLE_NEXT_LEVEL_MODE_NONE, PUZZLE_NEXT_LEVEL_MODE_SAME_WORK,
|
||||
@@ -16,19 +16,23 @@ use module_puzzle::{
|
||||
PuzzleRunNextLevelInput, PuzzleRunPauseInput, PuzzleRunProcedureResult, PuzzleRunPropInput,
|
||||
PuzzleRunSnapshot, PuzzleRunStartInput, PuzzleRunSwapInput, PuzzleRuntimeLevelStatus,
|
||||
PuzzleSelectCoverImageInput, PuzzleWorkDeleteInput, PuzzleWorkGetInput,
|
||||
PuzzleWorkLikeRecordInput as PuzzleWorkLikeInput, PuzzleWorkProcedureResult, PuzzleWorkProfile,
|
||||
PuzzleWorkRemixInput, PuzzleWorkUpsertInput, PuzzleWorksListInput, PuzzleWorksProcedureResult,
|
||||
apply_publish_overrides_to_draft, apply_selected_candidate, build_form_draft_from_seed,
|
||||
build_result_preview, compile_result_draft_from_seed, create_work_profile, infer_anchor_pack,
|
||||
normalize_puzzle_draft, normalize_puzzle_levels, normalize_theme_tags, publish_work_profile,
|
||||
replace_puzzle_level, resolve_puzzle_grid_size, select_next_profiles,
|
||||
selected_profile_level_after_runtime_level, selected_puzzle_level, tag_similarity_score,
|
||||
PuzzleWorkLikeRecordInput as PuzzleWorkLikeInput, PuzzleWorkPointIncentiveClaimInput,
|
||||
PuzzleWorkProcedureResult, PuzzleWorkProfile, PuzzleWorkRemixInput, PuzzleWorkUpsertInput,
|
||||
PuzzleWorksListInput, PuzzleWorksProcedureResult, apply_publish_overrides_to_draft,
|
||||
apply_selected_candidate, build_form_draft_from_seed, build_result_preview,
|
||||
compile_result_draft_from_seed, create_work_profile, infer_anchor_pack, normalize_puzzle_draft,
|
||||
normalize_puzzle_levels, normalize_theme_tags, publish_work_profile, replace_puzzle_level,
|
||||
select_next_profiles, selected_profile_level_after_runtime_level, selected_puzzle_level,
|
||||
tag_similarity_score,
|
||||
};
|
||||
use module_runtime::RuntimeProfileWalletLedgerSourceType;
|
||||
use serde_json::from_str as json_from_str;
|
||||
use serde_json::json;
|
||||
use serde_json::to_string as json_to_string;
|
||||
use spacetimedb::{ProcedureContext, Table, Timestamp, TxContext};
|
||||
|
||||
const PUZZLE_POINT_INCENTIVE_DEFAULT_U64: u64 = 0;
|
||||
|
||||
/// 拼图 Agent session 真相表。
|
||||
/// 当前只保存结构化字段与 JSON 草稿,不提前拆出更多编辑态子表。
|
||||
#[spacetimedb::table(
|
||||
@@ -98,6 +102,10 @@ pub struct PuzzleWorkProfileRow {
|
||||
remix_count: u32,
|
||||
#[default(0)]
|
||||
like_count: u32,
|
||||
#[default(PUZZLE_POINT_INCENTIVE_DEFAULT_U64)]
|
||||
point_incentive_total_half_points: u64,
|
||||
#[default(PUZZLE_POINT_INCENTIVE_DEFAULT_U64)]
|
||||
point_incentive_claimed_points: u64,
|
||||
}
|
||||
|
||||
/// 运行态 run 快照表。
|
||||
@@ -595,6 +603,25 @@ pub fn use_puzzle_runtime_prop(
|
||||
}
|
||||
}
|
||||
|
||||
#[spacetimedb::procedure]
|
||||
pub fn claim_puzzle_work_point_incentive(
|
||||
ctx: &mut ProcedureContext,
|
||||
input: PuzzleWorkPointIncentiveClaimInput,
|
||||
) -> PuzzleWorkProcedureResult {
|
||||
match ctx.try_with_tx(|tx| claim_puzzle_work_point_incentive_tx(tx, input.clone())) {
|
||||
Ok(item) => PuzzleWorkProcedureResult {
|
||||
ok: true,
|
||||
item_json: Some(serialize_json(&item)),
|
||||
error_message: None,
|
||||
},
|
||||
Err(message) => PuzzleWorkProcedureResult {
|
||||
ok: false,
|
||||
item_json: None,
|
||||
error_message: Some(message),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[spacetimedb::procedure]
|
||||
pub fn submit_puzzle_leaderboard_entry(
|
||||
ctx: &mut ProcedureContext,
|
||||
@@ -1186,6 +1213,8 @@ fn update_puzzle_work_tx(
|
||||
play_count: row.play_count,
|
||||
remix_count: row.remix_count,
|
||||
like_count: row.like_count,
|
||||
point_incentive_total_half_points: row.point_incentive_total_half_points,
|
||||
point_incentive_claimed_points: row.point_incentive_claimed_points,
|
||||
anchor_pack_json: row.anchor_pack_json.clone(),
|
||||
publish_ready: build_result_preview(&preview_draft, Some(&row.author_display_name))
|
||||
.publish_ready,
|
||||
@@ -1341,6 +1370,8 @@ fn record_puzzle_work_like_tx(
|
||||
play_count: row.play_count,
|
||||
remix_count: row.remix_count,
|
||||
like_count: row.like_count.saturating_add(1),
|
||||
point_incentive_total_half_points: row.point_incentive_total_half_points,
|
||||
point_incentive_claimed_points: row.point_incentive_claimed_points,
|
||||
anchor_pack_json: row.anchor_pack_json.clone(),
|
||||
publish_ready: row.publish_ready,
|
||||
created_at: row.created_at,
|
||||
@@ -1427,6 +1458,8 @@ fn remix_puzzle_work_tx(
|
||||
play_count: source.play_count,
|
||||
remix_count: source.remix_count.saturating_add(1),
|
||||
like_count: source.like_count,
|
||||
point_incentive_total_half_points: source.point_incentive_total_half_points,
|
||||
point_incentive_claimed_points: source.point_incentive_claimed_points,
|
||||
anchor_pack_json: source.anchor_pack_json.clone(),
|
||||
publish_ready: source.publish_ready,
|
||||
created_at: source.created_at,
|
||||
@@ -1492,6 +1525,8 @@ fn remix_puzzle_work_tx(
|
||||
play_count: 0,
|
||||
remix_count: 0,
|
||||
like_count: 0,
|
||||
point_incentive_total_half_points: 0,
|
||||
point_incentive_claimed_points: 0,
|
||||
anchor_pack_json: serialize_json(&source_profile.anchor_pack),
|
||||
publish_ready: true,
|
||||
created_at: remixed_at,
|
||||
@@ -1531,13 +1566,20 @@ fn start_puzzle_run_tx(
|
||||
return Err("入口拼图作品未发布".to_string());
|
||||
}
|
||||
let mut entry_profile = build_puzzle_work_profile_from_row(&entry_profile_row)?;
|
||||
let mut cleared_level_count = 0;
|
||||
if let Some(level) = selected_profile_level(&entry_profile, input.level_id.as_deref())? {
|
||||
cleared_level_count =
|
||||
module_puzzle::resolve_restart_cleared_level_count(&entry_profile, &level.level_id);
|
||||
entry_profile = profile_for_single_level(&entry_profile, &level);
|
||||
}
|
||||
let started_at_ms = micros_to_millis(input.started_at_micros);
|
||||
let mut run =
|
||||
module_puzzle::start_run_at(input.run_id.clone(), &entry_profile, 0, started_at_ms)
|
||||
.map_err(|error| error.to_string())?;
|
||||
let mut run = module_puzzle::start_run_at(
|
||||
input.run_id.clone(),
|
||||
&entry_profile,
|
||||
cleared_level_count,
|
||||
started_at_ms,
|
||||
)
|
||||
.map_err(|error| error.to_string())?;
|
||||
let current_grid_size = run.current_grid_size;
|
||||
let current_profile_id = entry_profile.profile_id.clone();
|
||||
hydrate_puzzle_leaderboard_entries(
|
||||
@@ -1682,26 +1724,40 @@ fn advance_puzzle_next_level_tx(
|
||||
.find(¤t_level.profile_id)
|
||||
.ok_or_else(|| "当前拼图作品不存在".to_string())?;
|
||||
let current_profile = build_puzzle_work_profile_from_row(¤t_profile_row)?;
|
||||
let next_profile = selected_profile_level_after_runtime_level(¤t_profile, current_level)
|
||||
.map(|level| profile_for_single_level(¤t_profile, &level))
|
||||
.or_else(|| {
|
||||
let candidates = list_published_puzzle_profiles(ctx).ok()?;
|
||||
select_next_profiles(
|
||||
¤t_profile,
|
||||
¤t_run.played_profile_ids,
|
||||
&candidates,
|
||||
1,
|
||||
)
|
||||
.into_iter()
|
||||
.next()
|
||||
.cloned()
|
||||
})
|
||||
let same_work_next_profile =
|
||||
selected_profile_level_after_runtime_level(¤t_profile, current_level)
|
||||
.map(|level| profile_for_single_level(¤t_profile, &level));
|
||||
let similar_work_next_profile = if same_work_next_profile.is_none() {
|
||||
let candidates = list_published_puzzle_profiles(ctx)?;
|
||||
select_next_profiles(
|
||||
¤t_profile,
|
||||
¤t_run.played_profile_ids,
|
||||
&candidates,
|
||||
1,
|
||||
)
|
||||
.into_iter()
|
||||
.next()
|
||||
.cloned()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let next_profile = same_work_next_profile
|
||||
.as_ref()
|
||||
.or(similar_work_next_profile.as_ref())
|
||||
.ok_or_else(|| "没有可用的下一关候选".to_string())?;
|
||||
let mut next_run = module_puzzle::advance_next_level_at(
|
||||
¤t_run,
|
||||
&next_profile,
|
||||
micros_to_millis(input.advanced_at_micros),
|
||||
)
|
||||
let mut next_run = if same_work_next_profile.is_some() {
|
||||
module_puzzle::advance_next_level_at(
|
||||
¤t_run,
|
||||
next_profile,
|
||||
micros_to_millis(input.advanced_at_micros),
|
||||
)
|
||||
} else {
|
||||
module_puzzle::advance_to_new_work_first_level_at(
|
||||
¤t_run,
|
||||
next_profile,
|
||||
micros_to_millis(input.advanced_at_micros),
|
||||
)
|
||||
}
|
||||
.map_err(|error| error.to_string())?;
|
||||
let next_grid_size = next_run.current_grid_size;
|
||||
let next_profile_id = next_profile.profile_id.clone();
|
||||
@@ -1805,6 +1861,19 @@ fn use_puzzle_runtime_prop_tx(
|
||||
};
|
||||
let mut hydrated_run = next_run;
|
||||
refresh_next_level_handoff(ctx, &mut hydrated_run)?;
|
||||
if let Some(profile_id) = hydrated_run
|
||||
.current_level
|
||||
.as_ref()
|
||||
.map(|level| level.profile_id.clone())
|
||||
{
|
||||
accrue_puzzle_point_incentive(
|
||||
ctx,
|
||||
&profile_id,
|
||||
&input.owner_user_id,
|
||||
input.spent_points,
|
||||
input.used_at_micros,
|
||||
)?;
|
||||
}
|
||||
replace_puzzle_runtime_run(ctx, &row, &hydrated_run, input.used_at_micros);
|
||||
if let Some((profile_id, grid_size)) = hydrated_run
|
||||
.current_level
|
||||
@@ -1822,6 +1891,86 @@ fn use_puzzle_runtime_prop_tx(
|
||||
Ok(hydrated_run)
|
||||
}
|
||||
|
||||
fn claim_puzzle_work_point_incentive_tx(
|
||||
ctx: &TxContext,
|
||||
input: PuzzleWorkPointIncentiveClaimInput,
|
||||
) -> Result<PuzzleWorkProfile, String> {
|
||||
let profile_id = input.profile_id.trim();
|
||||
let owner_user_id = input.owner_user_id.trim();
|
||||
if profile_id.is_empty() || owner_user_id.is_empty() {
|
||||
return Err("拼图积分激励参数不能为空".to_string());
|
||||
}
|
||||
|
||||
let row = ctx
|
||||
.db
|
||||
.puzzle_work_profile()
|
||||
.profile_id()
|
||||
.find(&profile_id.to_string())
|
||||
.ok_or_else(|| "拼图作品不存在".to_string())?;
|
||||
if row.owner_user_id != owner_user_id {
|
||||
return Err("无权领取该作品的积分激励".to_string());
|
||||
}
|
||||
|
||||
let claimable_points = puzzle_point_incentive_claimable_points(
|
||||
row.point_incentive_total_half_points,
|
||||
row.point_incentive_claimed_points,
|
||||
);
|
||||
if claimable_points == 0 {
|
||||
return Err("暂无可领取积分激励".to_string());
|
||||
}
|
||||
|
||||
let claimed_at = Timestamp::from_micros_since_unix_epoch(input.claimed_at_micros);
|
||||
let next_row = PuzzleWorkProfileRow {
|
||||
profile_id: row.profile_id.clone(),
|
||||
work_id: row.work_id.clone(),
|
||||
owner_user_id: row.owner_user_id.clone(),
|
||||
source_session_id: row.source_session_id.clone(),
|
||||
author_display_name: row.author_display_name.clone(),
|
||||
work_title: row.work_title.clone(),
|
||||
work_description: row.work_description.clone(),
|
||||
level_name: row.level_name.clone(),
|
||||
summary: row.summary.clone(),
|
||||
theme_tags_json: row.theme_tags_json.clone(),
|
||||
cover_image_src: row.cover_image_src.clone(),
|
||||
cover_asset_id: row.cover_asset_id.clone(),
|
||||
levels_json: row.levels_json.clone(),
|
||||
publication_status: row.publication_status,
|
||||
play_count: row.play_count,
|
||||
remix_count: row.remix_count,
|
||||
like_count: row.like_count,
|
||||
point_incentive_total_half_points: row.point_incentive_total_half_points,
|
||||
point_incentive_claimed_points: row
|
||||
.point_incentive_claimed_points
|
||||
.saturating_add(claimable_points),
|
||||
anchor_pack_json: row.anchor_pack_json.clone(),
|
||||
publish_ready: row.publish_ready,
|
||||
created_at: row.created_at,
|
||||
updated_at: claimed_at,
|
||||
published_at: row.published_at,
|
||||
};
|
||||
replace_puzzle_work_profile(ctx, &row, next_row);
|
||||
|
||||
grant_profile_wallet_points(
|
||||
ctx,
|
||||
owner_user_id,
|
||||
claimable_points,
|
||||
RuntimeProfileWalletLedgerSourceType::PuzzleAuthorIncentiveClaim,
|
||||
&format!(
|
||||
"puzzle_author_incentive_claim:{}:{}:{}",
|
||||
profile_id, owner_user_id, input.claimed_at_micros
|
||||
),
|
||||
claimed_at,
|
||||
)?;
|
||||
|
||||
let updated = ctx
|
||||
.db
|
||||
.puzzle_work_profile()
|
||||
.profile_id()
|
||||
.find(&profile_id.to_string())
|
||||
.ok_or_else(|| "拼图积分激励领取更新失败".to_string())?;
|
||||
build_puzzle_work_profile_from_row(&updated)
|
||||
}
|
||||
|
||||
fn submit_puzzle_leaderboard_entry_tx(
|
||||
ctx: &TxContext,
|
||||
input: PuzzleLeaderboardSubmitInput,
|
||||
@@ -1835,7 +1984,7 @@ fn submit_puzzle_leaderboard_entry_tx(
|
||||
if input.profile_id.trim().is_empty() {
|
||||
return Err("提交成绩的拼图作品不能为空".to_string());
|
||||
}
|
||||
if input.grid_size != 3 && input.grid_size != 4 {
|
||||
if !module_puzzle::is_supported_puzzle_grid_size(input.grid_size) {
|
||||
return Err("提交成绩的网格规格无效".to_string());
|
||||
}
|
||||
let matches_service_level =
|
||||
@@ -2002,6 +2151,8 @@ fn build_puzzle_work_profile_from_row_without_recent_count(
|
||||
play_count: row.play_count,
|
||||
remix_count: row.remix_count,
|
||||
like_count: row.like_count,
|
||||
point_incentive_total_half_points: row.point_incentive_total_half_points,
|
||||
point_incentive_claimed_points: row.point_incentive_claimed_points,
|
||||
recent_play_count_7d: 0,
|
||||
publish_ready: row.publish_ready,
|
||||
anchor_pack: deserialize_anchor_pack(&row.anchor_pack_json)?,
|
||||
@@ -2108,6 +2259,8 @@ fn upsert_puzzle_draft_work_profile(
|
||||
profile.play_count = existing.play_count;
|
||||
profile.remix_count = existing.remix_count;
|
||||
profile.like_count = existing.like_count;
|
||||
profile.point_incentive_total_half_points = existing.point_incentive_total_half_points;
|
||||
profile.point_incentive_claimed_points = existing.point_incentive_claimed_points;
|
||||
return upsert_puzzle_work_profile(ctx, profile);
|
||||
}
|
||||
let profile = create_work_profile(
|
||||
@@ -2286,6 +2439,12 @@ fn upsert_puzzle_work_profile(ctx: &TxContext, profile: PuzzleWorkProfile) -> Re
|
||||
play_count: existing.play_count.max(profile.play_count),
|
||||
remix_count: existing.remix_count.max(profile.remix_count),
|
||||
like_count: existing.like_count.max(profile.like_count),
|
||||
point_incentive_total_half_points: existing
|
||||
.point_incentive_total_half_points
|
||||
.max(profile.point_incentive_total_half_points),
|
||||
point_incentive_claimed_points: existing
|
||||
.point_incentive_claimed_points
|
||||
.max(profile.point_incentive_claimed_points),
|
||||
anchor_pack_json: serialize_json(&profile.anchor_pack),
|
||||
publish_ready: profile.publish_ready,
|
||||
created_at: existing.created_at,
|
||||
@@ -2316,6 +2475,8 @@ fn upsert_puzzle_work_profile(ctx: &TxContext, profile: PuzzleWorkProfile) -> Re
|
||||
play_count: profile.play_count,
|
||||
remix_count: profile.remix_count,
|
||||
like_count: profile.like_count,
|
||||
point_incentive_total_half_points: profile.point_incentive_total_half_points,
|
||||
point_incentive_claimed_points: profile.point_incentive_claimed_points,
|
||||
anchor_pack_json: serialize_json(&profile.anchor_pack),
|
||||
publish_ready: profile.publish_ready,
|
||||
created_at: Timestamp::from_micros_since_unix_epoch(profile.updated_at_micros),
|
||||
@@ -2375,7 +2536,7 @@ fn replace_puzzle_runtime_run(
|
||||
.unwrap_or_else(|| current.current_profile_id.clone()),
|
||||
cleared_level_count: run.cleared_level_count,
|
||||
current_level_index: run.current_level_index,
|
||||
current_grid_size: resolve_puzzle_grid_size(run.cleared_level_count),
|
||||
current_grid_size: run.current_grid_size,
|
||||
played_profile_ids_json: serialize_json(&run.played_profile_ids),
|
||||
previous_level_tags_json: serialize_json(&run.previous_level_tags),
|
||||
snapshot_json: serialize_json(run),
|
||||
@@ -2403,16 +2564,17 @@ fn upsert_puzzle_profile_save_archive(
|
||||
return Ok(());
|
||||
};
|
||||
let world_key = format!("puzzle:{}", run.entry_profile_id);
|
||||
let target = resolve_puzzle_archive_target(ctx, run, current_level)?;
|
||||
|
||||
// 中文注释:拼图存档只保存恢复入口所需的最小运行态索引,棋盘真相继续放在 puzzle_runtime_run。
|
||||
let game_state_json = json_to_string(&json!({
|
||||
"runtimeKind": "puzzle",
|
||||
"runId": run.run_id,
|
||||
"entryProfileId": run.entry_profile_id,
|
||||
"currentProfileId": current_level.profile_id,
|
||||
"currentLevelIndex": current_level.level_index,
|
||||
"currentLevelId": current_level.level_id,
|
||||
"status": current_level.status.as_str(),
|
||||
"currentProfileId": target.profile_id,
|
||||
"currentLevelIndex": target.level_index,
|
||||
"currentLevelId": target.level_id,
|
||||
"status": target.status.as_str(),
|
||||
}))
|
||||
.unwrap_or_else(|_| "{}".to_string());
|
||||
|
||||
@@ -2421,13 +2583,13 @@ fn upsert_puzzle_profile_save_archive(
|
||||
ProfileSaveArchiveUpsertInput {
|
||||
user_id: user_id.to_string(),
|
||||
world_key,
|
||||
owner_user_id: resolve_puzzle_current_owner_user_id(ctx, ¤t_level.profile_id),
|
||||
owner_user_id: target.owner_user_id,
|
||||
profile_id: Some(run.entry_profile_id.clone()),
|
||||
world_type: Some("PUZZLE".to_string()),
|
||||
world_name: current_level.level_name.clone(),
|
||||
subtitle: format!("第 {} 关", current_level.level_index),
|
||||
summary_text: puzzle_archive_summary_text(current_level.status),
|
||||
cover_image_src: current_level.cover_image_src.clone(),
|
||||
world_name: target.level_name,
|
||||
subtitle: format!("第 {} 关", target.level_index),
|
||||
summary_text: puzzle_archive_summary_text(target.status),
|
||||
cover_image_src: target.cover_image_src,
|
||||
bottom_tab: "puzzle".to_string(),
|
||||
game_state_json,
|
||||
current_story_json: None,
|
||||
@@ -2436,6 +2598,88 @@ fn upsert_puzzle_profile_save_archive(
|
||||
)
|
||||
}
|
||||
|
||||
struct PuzzleArchiveTarget {
|
||||
profile_id: String,
|
||||
level_index: u32,
|
||||
level_id: Option<String>,
|
||||
level_name: String,
|
||||
status: PuzzleRuntimeLevelStatus,
|
||||
cover_image_src: Option<String>,
|
||||
owner_user_id: Option<String>,
|
||||
}
|
||||
|
||||
fn resolve_puzzle_archive_target(
|
||||
ctx: &TxContext,
|
||||
run: &PuzzleRunSnapshot,
|
||||
current_level: &module_puzzle::PuzzleRuntimeLevelSnapshot,
|
||||
) -> Result<PuzzleArchiveTarget, String> {
|
||||
let owner_user_id = resolve_puzzle_current_owner_user_id(ctx, ¤t_level.profile_id);
|
||||
if current_level.status != PuzzleRuntimeLevelStatus::Cleared {
|
||||
return Ok(PuzzleArchiveTarget {
|
||||
profile_id: current_level.profile_id.clone(),
|
||||
level_index: current_level.level_index,
|
||||
level_id: current_level.level_id.clone(),
|
||||
level_name: current_level.level_name.clone(),
|
||||
status: current_level.status,
|
||||
cover_image_src: current_level.cover_image_src.clone(),
|
||||
owner_user_id,
|
||||
});
|
||||
}
|
||||
|
||||
let Some(next_level_id) = run
|
||||
.next_level_id
|
||||
.as_deref()
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
else {
|
||||
return Ok(PuzzleArchiveTarget {
|
||||
profile_id: current_level.profile_id.clone(),
|
||||
level_index: current_level.level_index,
|
||||
level_id: current_level.level_id.clone(),
|
||||
level_name: current_level.level_name.clone(),
|
||||
status: current_level.status,
|
||||
cover_image_src: current_level.cover_image_src.clone(),
|
||||
owner_user_id,
|
||||
});
|
||||
};
|
||||
if run.next_level_profile_id.as_deref() != Some(current_level.profile_id.as_str())
|
||||
|| run.next_level_mode != PUZZLE_NEXT_LEVEL_MODE_SAME_WORK
|
||||
{
|
||||
return Ok(PuzzleArchiveTarget {
|
||||
profile_id: current_level.profile_id.clone(),
|
||||
level_index: current_level.level_index,
|
||||
level_id: current_level.level_id.clone(),
|
||||
level_name: current_level.level_name.clone(),
|
||||
status: current_level.status,
|
||||
cover_image_src: current_level.cover_image_src.clone(),
|
||||
owner_user_id,
|
||||
});
|
||||
}
|
||||
|
||||
let current_profile = build_puzzle_work_profile_from_row(
|
||||
&ctx.db
|
||||
.puzzle_work_profile()
|
||||
.profile_id()
|
||||
.find(¤t_level.profile_id)
|
||||
.ok_or_else(|| "当前拼图作品不存在".to_string())?,
|
||||
)?;
|
||||
let next_level = current_profile
|
||||
.levels
|
||||
.iter()
|
||||
.find(|level| level.level_id == next_level_id)
|
||||
.cloned()
|
||||
.ok_or_else(|| "下一关拼图关卡不存在".to_string())?;
|
||||
|
||||
Ok(PuzzleArchiveTarget {
|
||||
profile_id: current_profile.profile_id,
|
||||
level_index: current_level.level_index.saturating_add(1),
|
||||
level_id: Some(next_level.level_id),
|
||||
level_name: next_level.level_name,
|
||||
status: PuzzleRuntimeLevelStatus::Playing,
|
||||
cover_image_src: next_level.cover_image_src,
|
||||
owner_user_id,
|
||||
})
|
||||
}
|
||||
|
||||
fn resolve_puzzle_current_owner_user_id(ctx: &TxContext, profile_id: &str) -> Option<String> {
|
||||
ctx.db
|
||||
.puzzle_work_profile()
|
||||
@@ -2453,6 +2697,72 @@ fn puzzle_archive_summary_text(status: PuzzleRuntimeLevelStatus) -> String {
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn puzzle_point_incentive_claimable_points(total_half_points: u64, claimed_points: u64) -> u64 {
|
||||
total_half_points
|
||||
.saturating_div(2)
|
||||
.saturating_sub(claimed_points)
|
||||
}
|
||||
|
||||
fn accrue_puzzle_point_incentive(
|
||||
ctx: &TxContext,
|
||||
profile_id: &str,
|
||||
player_user_id: &str,
|
||||
spent_points: u64,
|
||||
updated_at_micros: i64,
|
||||
) -> Result<(), String> {
|
||||
if spent_points == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let Some(row) = ctx
|
||||
.db
|
||||
.puzzle_work_profile()
|
||||
.profile_id()
|
||||
.find(&profile_id.to_string())
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
if row.publication_status != PuzzlePublicationStatus::Published
|
||||
|| row.owner_user_id == player_user_id
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
replace_puzzle_work_profile(
|
||||
ctx,
|
||||
&row,
|
||||
PuzzleWorkProfileRow {
|
||||
profile_id: row.profile_id.clone(),
|
||||
work_id: row.work_id.clone(),
|
||||
owner_user_id: row.owner_user_id.clone(),
|
||||
source_session_id: row.source_session_id.clone(),
|
||||
author_display_name: row.author_display_name.clone(),
|
||||
work_title: row.work_title.clone(),
|
||||
work_description: row.work_description.clone(),
|
||||
level_name: row.level_name.clone(),
|
||||
summary: row.summary.clone(),
|
||||
theme_tags_json: row.theme_tags_json.clone(),
|
||||
cover_image_src: row.cover_image_src.clone(),
|
||||
cover_asset_id: row.cover_asset_id.clone(),
|
||||
levels_json: row.levels_json.clone(),
|
||||
publication_status: row.publication_status,
|
||||
play_count: row.play_count,
|
||||
remix_count: row.remix_count,
|
||||
like_count: row.like_count,
|
||||
point_incentive_total_half_points: row
|
||||
.point_incentive_total_half_points
|
||||
.saturating_add(spent_points),
|
||||
point_incentive_claimed_points: row.point_incentive_claimed_points,
|
||||
anchor_pack_json: row.anchor_pack_json.clone(),
|
||||
publish_ready: row.publish_ready,
|
||||
created_at: row.created_at,
|
||||
updated_at: Timestamp::from_micros_since_unix_epoch(updated_at_micros),
|
||||
published_at: row.published_at,
|
||||
},
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn increment_puzzle_profile_play_count(
|
||||
ctx: &TxContext,
|
||||
row: &PuzzleWorkProfileRow,
|
||||
@@ -2479,6 +2789,8 @@ fn increment_puzzle_profile_play_count(
|
||||
play_count: row.play_count.saturating_add(1),
|
||||
remix_count: row.remix_count,
|
||||
like_count: row.like_count,
|
||||
point_incentive_total_half_points: row.point_incentive_total_half_points,
|
||||
point_incentive_claimed_points: row.point_incentive_claimed_points,
|
||||
anchor_pack_json: row.anchor_pack_json.clone(),
|
||||
publish_ready: row.publish_ready,
|
||||
created_at: row.created_at,
|
||||
@@ -2841,7 +3153,7 @@ mod tests {
|
||||
}];
|
||||
|
||||
replace_generated_candidate(
|
||||
&mut draft,
|
||||
&mut draft.candidates,
|
||||
vec![PuzzleGeneratedImageCandidate {
|
||||
candidate_id: "session-1-candidate-2".to_string(),
|
||||
image_src: "/generated-puzzle-assets/session-1/new/cover.png".to_string(),
|
||||
@@ -2866,11 +3178,14 @@ mod tests {
|
||||
owner_user_id: "owner-a".to_string(),
|
||||
source_session_id: None,
|
||||
author_display_name: "作者".to_string(),
|
||||
work_title: "A".to_string(),
|
||||
work_description: String::new(),
|
||||
level_name: "A".to_string(),
|
||||
summary: String::new(),
|
||||
theme_tags: vec!["雨夜".to_string(), "猫咪".to_string()],
|
||||
cover_image_src: Some("/a.png".to_string()),
|
||||
cover_asset_id: Some("asset-a".to_string()),
|
||||
levels: Vec::new(),
|
||||
publication_status: PuzzlePublicationStatus::Published,
|
||||
updated_at_micros: 1,
|
||||
published_at_micros: Some(1),
|
||||
@@ -2878,6 +3193,8 @@ mod tests {
|
||||
recent_play_count_7d: 0,
|
||||
remix_count: 0,
|
||||
like_count: 0,
|
||||
point_incentive_total_half_points: 0,
|
||||
point_incentive_claimed_points: 0,
|
||||
publish_ready: true,
|
||||
anchor_pack: empty_anchor_pack(),
|
||||
};
|
||||
@@ -2885,10 +3202,13 @@ mod tests {
|
||||
owner_user_id: "owner-a".to_string(),
|
||||
profile_id: "profile-b".to_string(),
|
||||
work_id: "work-b".to_string(),
|
||||
work_title: "B".to_string(),
|
||||
work_description: String::new(),
|
||||
level_name: "B".to_string(),
|
||||
theme_tags: vec!["雨夜".to_string(), "蒸汽城市".to_string()],
|
||||
cover_image_src: Some("/b.png".to_string()),
|
||||
cover_asset_id: Some("asset-b".to_string()),
|
||||
levels: Vec::new(),
|
||||
publication_status: PuzzlePublicationStatus::Published,
|
||||
updated_at_micros: 2,
|
||||
published_at_micros: Some(2),
|
||||
@@ -2896,6 +3216,8 @@ mod tests {
|
||||
recent_play_count_7d: 0,
|
||||
remix_count: 0,
|
||||
like_count: 0,
|
||||
point_incentive_total_half_points: 0,
|
||||
point_incentive_claimed_points: 0,
|
||||
publish_ready: true,
|
||||
anchor_pack: empty_anchor_pack(),
|
||||
source_session_id: None,
|
||||
|
||||
@@ -2120,6 +2120,24 @@ fn apply_profile_wallet_delta(
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn grant_profile_wallet_points(
|
||||
ctx: &ReducerContext,
|
||||
user_id: &str,
|
||||
amount_delta: u64,
|
||||
source_type: RuntimeProfileWalletLedgerSourceType,
|
||||
ledger_id: &str,
|
||||
created_at: Timestamp,
|
||||
) -> Result<u64, String> {
|
||||
apply_profile_wallet_delta(
|
||||
ctx,
|
||||
user_id,
|
||||
amount_delta,
|
||||
source_type,
|
||||
ledger_id,
|
||||
created_at,
|
||||
)
|
||||
}
|
||||
|
||||
fn apply_profile_wallet_adjustment(
|
||||
ctx: &ReducerContext,
|
||||
input: RuntimeProfileWalletAdjustmentInput,
|
||||
|
||||
Reference in New Issue
Block a user