Close DDD refactor and remove generated asset proxy
This commit is contained in:
@@ -116,6 +116,8 @@ fn upsert_asset_entity_binding(
|
||||
}
|
||||
};
|
||||
|
||||
emit_asset_entity_binding_changed_event(ctx, &snapshot);
|
||||
|
||||
Ok(snapshot)
|
||||
}
|
||||
|
||||
@@ -133,3 +135,37 @@ fn build_asset_entity_binding_row(snapshot: &AssetEntityBindingSnapshot) -> Asse
|
||||
updated_at: Timestamp::from_micros_since_unix_epoch(snapshot.updated_at_micros),
|
||||
}
|
||||
}
|
||||
|
||||
fn emit_asset_entity_binding_changed_event(
|
||||
ctx: &ReducerContext,
|
||||
snapshot: &AssetEntityBindingSnapshot,
|
||||
) {
|
||||
let event = AssetEntityBindingChangedEvent {
|
||||
binding_id: snapshot.binding_id.clone(),
|
||||
asset_object_id: snapshot.asset_object_id.clone(),
|
||||
entity_kind: snapshot.entity_kind.clone(),
|
||||
entity_id: snapshot.entity_id.clone(),
|
||||
slot: snapshot.slot.clone(),
|
||||
asset_kind: snapshot.asset_kind.clone(),
|
||||
owner_user_id: snapshot.owner_user_id.clone(),
|
||||
profile_id: snapshot.profile_id.clone(),
|
||||
occurred_at_micros: snapshot.updated_at_micros,
|
||||
};
|
||||
|
||||
ctx.db.asset_event().insert(AssetEvent {
|
||||
event_id: format!(
|
||||
"assetevt_{}_{}_binding",
|
||||
event.binding_id, event.occurred_at_micros
|
||||
),
|
||||
asset_object_id: event.asset_object_id,
|
||||
binding_id: Some(event.binding_id),
|
||||
event_kind: AssetEventKind::EntityBindingChanged,
|
||||
asset_kind: event.asset_kind,
|
||||
owner_user_id: event.owner_user_id,
|
||||
profile_id: event.profile_id,
|
||||
entity_kind: Some(event.entity_kind),
|
||||
entity_id: Some(event.entity_id),
|
||||
slot: Some(event.slot),
|
||||
occurred_at: Timestamp::from_micros_since_unix_epoch(event.occurred_at_micros),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5,6 +5,39 @@ const ASSET_HISTORY_CHARACTER_VISUAL_KIND: &str = "character_visual";
|
||||
const ASSET_HISTORY_SCENE_IMAGE_KIND: &str = "scene_image";
|
||||
const ASSET_HISTORY_PUZZLE_COVER_IMAGE_KIND: &str = "puzzle_cover_image";
|
||||
|
||||
/// 资产事件类型。
|
||||
///
|
||||
/// 事件表只承接订阅端和审计所需的轻量事实,正式资产状态仍以
|
||||
/// `asset_object` 和 `asset_entity_binding` 为准。
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
pub enum AssetEventKind {
|
||||
ObjectConfirmed,
|
||||
EntityBindingChanged,
|
||||
}
|
||||
|
||||
#[spacetimedb::table(
|
||||
accessor = asset_event,
|
||||
public,
|
||||
event,
|
||||
index(accessor = by_asset_event_asset_object_id, btree(columns = [asset_object_id])),
|
||||
index(accessor = by_asset_event_owner_user_id, btree(columns = [owner_user_id])),
|
||||
index(accessor = by_asset_event_profile_id, btree(columns = [profile_id]))
|
||||
)]
|
||||
pub struct AssetEvent {
|
||||
#[primary_key]
|
||||
pub(crate) event_id: String,
|
||||
pub(crate) asset_object_id: String,
|
||||
pub(crate) binding_id: Option<String>,
|
||||
pub(crate) event_kind: AssetEventKind,
|
||||
pub(crate) asset_kind: String,
|
||||
pub(crate) owner_user_id: Option<String>,
|
||||
pub(crate) profile_id: Option<String>,
|
||||
pub(crate) entity_kind: Option<String>,
|
||||
pub(crate) entity_id: Option<String>,
|
||||
pub(crate) slot: Option<String>,
|
||||
pub(crate) occurred_at: Timestamp,
|
||||
}
|
||||
|
||||
#[spacetimedb::table(
|
||||
accessor = asset_object,
|
||||
index(accessor = by_bucket_object_key, btree(columns = [bucket, object_key]))
|
||||
@@ -151,6 +184,8 @@ pub(crate) fn upsert_asset_object(
|
||||
}
|
||||
};
|
||||
|
||||
emit_asset_object_confirmed_event(ctx, &snapshot);
|
||||
|
||||
Ok(snapshot)
|
||||
}
|
||||
|
||||
@@ -232,3 +267,34 @@ fn build_asset_object_row(snapshot: &AssetObjectUpsertSnapshot) -> AssetObject {
|
||||
updated_at: Timestamp::from_micros_since_unix_epoch(snapshot.updated_at_micros),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn emit_asset_object_confirmed_event(
|
||||
ctx: &ReducerContext,
|
||||
snapshot: &AssetObjectUpsertSnapshot,
|
||||
) {
|
||||
let event = AssetObjectConfirmedEvent {
|
||||
asset_object_id: snapshot.asset_object_id.clone(),
|
||||
asset_kind: snapshot.asset_kind.clone(),
|
||||
owner_user_id: snapshot.owner_user_id.clone(),
|
||||
profile_id: snapshot.profile_id.clone(),
|
||||
entity_id: snapshot.entity_id.clone(),
|
||||
occurred_at_micros: snapshot.updated_at_micros,
|
||||
};
|
||||
|
||||
ctx.db.asset_event().insert(AssetEvent {
|
||||
event_id: format!(
|
||||
"assetevt_{}_{}_confirmed",
|
||||
event.asset_object_id, event.occurred_at_micros
|
||||
),
|
||||
asset_object_id: event.asset_object_id,
|
||||
binding_id: None,
|
||||
event_kind: AssetEventKind::ObjectConfirmed,
|
||||
asset_kind: event.asset_kind,
|
||||
owner_user_id: event.owner_user_id,
|
||||
profile_id: event.profile_id,
|
||||
entity_kind: None,
|
||||
entity_id: event.entity_id,
|
||||
slot: None,
|
||||
occurred_at: Timestamp::from_micros_since_unix_epoch(event.occurred_at_micros),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -6,6 +6,10 @@ use module_quest::{
|
||||
acknowledge_quest_completion as acknowledge_quest_record_completion,
|
||||
apply_quest_signal as apply_quest_record_signal,
|
||||
};
|
||||
use module_story::{
|
||||
RpgGameplaySettlementPlan, build_combat_victory_settlement_plan,
|
||||
build_quest_turn_in_settlement_plan, build_treasure_settlement_plan,
|
||||
};
|
||||
|
||||
#[spacetimedb::table(accessor = player_progression)]
|
||||
pub struct PlayerProgression {
|
||||
@@ -619,37 +623,11 @@ fn resolve_battle_state_record(
|
||||
.battle_state()
|
||||
.insert(build_battle_state_row(result.snapshot.clone()));
|
||||
|
||||
if result.outcome == CombatOutcome::Victory {
|
||||
grant_battle_reward_items(ctx, &result.snapshot)?;
|
||||
|
||||
if result.snapshot.experience_reward > 0 {
|
||||
let updated_player = upsert_player_progression_after_grant_tx(
|
||||
ctx,
|
||||
PlayerProgressionGrantInput {
|
||||
user_id: result.snapshot.actor_user_id.clone(),
|
||||
amount: result.snapshot.experience_reward,
|
||||
source: PlayerProgressionGrantSource::HostileNpc,
|
||||
updated_at_micros: result.snapshot.updated_at_micros,
|
||||
},
|
||||
)?;
|
||||
|
||||
// 章节计划可能尚未初始化;此时不能阻断战斗胜利结算,只跳过章节账本写入。
|
||||
try_update_chapter_progression_ledger_tx(
|
||||
ctx,
|
||||
result.snapshot.actor_user_id.clone(),
|
||||
result.snapshot.chapter_id.clone(),
|
||||
ChapterProgressionLedgerInput {
|
||||
user_id: result.snapshot.actor_user_id.clone(),
|
||||
chapter_id: result.snapshot.chapter_id.clone().unwrap_or_default(),
|
||||
granted_quest_xp: 0,
|
||||
granted_hostile_xp: result.snapshot.experience_reward,
|
||||
hostile_defeat_increment: 1,
|
||||
level_at_exit: Some(updated_player.level),
|
||||
updated_at_micros: result.snapshot.updated_at_micros,
|
||||
},
|
||||
)?;
|
||||
}
|
||||
}
|
||||
apply_rpg_gameplay_settlement_plan(
|
||||
ctx,
|
||||
build_combat_victory_settlement_plan(&result.snapshot),
|
||||
Some(result.snapshot.battle_state_id.as_str()),
|
||||
)?;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
@@ -1094,35 +1072,11 @@ pub fn turn_in_quest(ctx: &ReducerContext, input: QuestTurnInInput) -> Result<()
|
||||
next.updated_at_micros,
|
||||
);
|
||||
|
||||
let reward_experience = next.reward.experience.unwrap_or(0);
|
||||
grant_quest_reward_items(ctx, &next)?;
|
||||
if reward_experience > 0 {
|
||||
let updated_player = upsert_player_progression_after_grant_tx(
|
||||
ctx,
|
||||
PlayerProgressionGrantInput {
|
||||
user_id: next.actor_user_id.clone(),
|
||||
amount: reward_experience,
|
||||
source: PlayerProgressionGrantSource::Quest,
|
||||
updated_at_micros: next.updated_at_micros,
|
||||
},
|
||||
)?;
|
||||
|
||||
// 章节计划缺失时先保持任务交付成功,避免成长联动反向阻断 quest 主链。
|
||||
try_update_chapter_progression_ledger_tx(
|
||||
ctx,
|
||||
next.actor_user_id.clone(),
|
||||
next.chapter_id.clone(),
|
||||
ChapterProgressionLedgerInput {
|
||||
user_id: next.actor_user_id.clone(),
|
||||
chapter_id: next.chapter_id.clone().unwrap_or_default(),
|
||||
granted_quest_xp: reward_experience,
|
||||
granted_hostile_xp: 0,
|
||||
hostile_defeat_increment: 0,
|
||||
level_at_exit: Some(updated_player.level),
|
||||
updated_at_micros: next.updated_at_micros,
|
||||
},
|
||||
)?;
|
||||
}
|
||||
apply_rpg_gameplay_settlement_plan(
|
||||
ctx,
|
||||
build_quest_turn_in_settlement_plan(&next),
|
||||
Some(next.quest_id.as_str()),
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1199,57 +1153,15 @@ fn upsert_treasure_record(
|
||||
.treasure_record()
|
||||
.insert(build_treasure_record_row(&snapshot, created_at, updated_at));
|
||||
|
||||
grant_treasure_reward_items_to_inventory(ctx, &snapshot)?;
|
||||
apply_rpg_gameplay_settlement_plan(
|
||||
ctx,
|
||||
build_treasure_settlement_plan(&snapshot).map_err(|error| error.to_string())?,
|
||||
Some(snapshot.treasure_record_id.as_str()),
|
||||
)?;
|
||||
|
||||
Ok(snapshot)
|
||||
}
|
||||
|
||||
fn grant_treasure_reward_items_to_inventory(
|
||||
ctx: &ReducerContext,
|
||||
snapshot: &TreasureRecordSnapshot,
|
||||
) -> Result<(), String> {
|
||||
for (index, reward_item) in snapshot.reward_items.iter().cloned().enumerate() {
|
||||
let inventory_item = build_inventory_item_snapshot_from_reward_item(
|
||||
&snapshot.treasure_record_id,
|
||||
reward_item,
|
||||
)
|
||||
.map_err(|error| error.to_string())?;
|
||||
let slot_id = build_treasure_inventory_slot_id(&snapshot.treasure_record_id, index);
|
||||
let mutation_id = build_treasure_inventory_mutation_id(&snapshot.treasure_record_id, index);
|
||||
|
||||
apply_inventory_mutation_tx(
|
||||
ctx,
|
||||
InventoryMutationInput {
|
||||
mutation_id,
|
||||
runtime_session_id: snapshot.runtime_session_id.clone(),
|
||||
story_session_id: Some(snapshot.story_session_id.clone()),
|
||||
actor_user_id: snapshot.actor_user_id.clone(),
|
||||
mutation: InventoryMutation::GrantItem(module_inventory::GrantInventoryItemInput {
|
||||
slot_id,
|
||||
item: inventory_item,
|
||||
}),
|
||||
updated_at_micros: snapshot.updated_at_micros,
|
||||
},
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn build_treasure_inventory_slot_id(treasure_record_id: &str, reward_index: usize) -> String {
|
||||
format!(
|
||||
"{}{}_{}",
|
||||
INVENTORY_SLOT_ID_PREFIX, treasure_record_id, reward_index
|
||||
)
|
||||
}
|
||||
|
||||
fn build_treasure_inventory_mutation_id(treasure_record_id: &str, reward_index: usize) -> String {
|
||||
format!(
|
||||
"{}{}_{}",
|
||||
INVENTORY_MUTATION_ID_PREFIX, treasure_record_id, reward_index
|
||||
)
|
||||
}
|
||||
|
||||
fn build_treasure_record_row(
|
||||
snapshot: &TreasureRecordSnapshot,
|
||||
created_at: Timestamp,
|
||||
@@ -1496,214 +1408,69 @@ fn build_inventory_slot_snapshot_from_row(row: &InventorySlot) -> InventorySlotS
|
||||
}
|
||||
}
|
||||
|
||||
fn grant_quest_reward_items(
|
||||
fn apply_rpg_gameplay_settlement_plan(
|
||||
ctx: &ReducerContext,
|
||||
snapshot: &QuestRecordSnapshot,
|
||||
plan: RpgGameplaySettlementPlan,
|
||||
inventory_source_reference_id: Option<&str>,
|
||||
) -> Result<(), String> {
|
||||
if !ctx
|
||||
.db
|
||||
.inventory_slot()
|
||||
.iter()
|
||||
.filter(|row| {
|
||||
row.runtime_session_id == snapshot.runtime_session_id
|
||||
&& row.actor_user_id == snapshot.actor_user_id
|
||||
})
|
||||
.all(|row| row.source_reference_id.as_deref() != Some(snapshot.quest_id.as_str()))
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
if !plan.inventory_mutations.is_empty() {
|
||||
if let Some(source_reference_id) = inventory_source_reference_id {
|
||||
if inventory_reward_source_already_granted(ctx, &plan, source_reference_id) {
|
||||
return apply_progression_and_chapter_settlement(ctx, plan);
|
||||
}
|
||||
}
|
||||
|
||||
for (index, reward_item) in snapshot.reward.items.clone().into_iter().enumerate() {
|
||||
let inventory_item =
|
||||
build_inventory_item_snapshot_from_quest_reward_item(&snapshot.quest_id, reward_item);
|
||||
grant_inventory_item_to_actor(
|
||||
ctx,
|
||||
&snapshot.runtime_session_id,
|
||||
snapshot.story_session_id.clone(),
|
||||
&snapshot.actor_user_id,
|
||||
inventory_item,
|
||||
build_reward_seed(snapshot.updated_at_micros, index),
|
||||
snapshot.updated_at_micros,
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn grant_battle_reward_items(
|
||||
ctx: &ReducerContext,
|
||||
snapshot: &BattleStateSnapshot,
|
||||
) -> Result<(), String> {
|
||||
if snapshot.reward_items.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if !ctx
|
||||
.db
|
||||
.inventory_slot()
|
||||
.iter()
|
||||
.filter(|row| {
|
||||
row.runtime_session_id == snapshot.runtime_session_id
|
||||
&& row.actor_user_id == snapshot.actor_user_id
|
||||
})
|
||||
.all(|row| row.source_reference_id.as_deref() != Some(snapshot.battle_state_id.as_str()))
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
for (index, reward_item) in snapshot.reward_items.clone().into_iter().enumerate() {
|
||||
let inventory_item = build_inventory_item_snapshot_from_battle_reward_item(
|
||||
&snapshot.battle_state_id,
|
||||
reward_item,
|
||||
);
|
||||
grant_inventory_item_to_actor(
|
||||
ctx,
|
||||
&snapshot.runtime_session_id,
|
||||
Some(snapshot.story_session_id.clone()),
|
||||
&snapshot.actor_user_id,
|
||||
inventory_item,
|
||||
build_reward_seed(snapshot.updated_at_micros, index),
|
||||
snapshot.updated_at_micros,
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn grant_inventory_item_to_actor(
|
||||
ctx: &ReducerContext,
|
||||
runtime_session_id: &str,
|
||||
story_session_id: Option<String>,
|
||||
actor_user_id: &str,
|
||||
item: InventoryItemSnapshot,
|
||||
seed_micros: i64,
|
||||
updated_at_micros: i64,
|
||||
) -> Result<(), String> {
|
||||
let current_slots = ctx
|
||||
.db
|
||||
.inventory_slot()
|
||||
.iter()
|
||||
.filter(|row| {
|
||||
row.runtime_session_id == runtime_session_id && row.actor_user_id == actor_user_id
|
||||
})
|
||||
.map(|row| build_inventory_slot_snapshot_from_row(&row))
|
||||
.collect::<Vec<_>>();
|
||||
let slot_id = generate_inventory_slot_id(seed_micros);
|
||||
let mutation_id = generate_inventory_mutation_id(seed_micros);
|
||||
let outcome = apply_inventory_slot_mutation(
|
||||
current_slots,
|
||||
InventoryMutationInput {
|
||||
mutation_id,
|
||||
runtime_session_id: runtime_session_id.to_string(),
|
||||
story_session_id,
|
||||
actor_user_id: actor_user_id.to_string(),
|
||||
mutation: InventoryMutation::GrantItem(GrantInventoryItemInput { slot_id, item }),
|
||||
updated_at_micros,
|
||||
},
|
||||
)
|
||||
.map_err(|error| error.to_string())?;
|
||||
|
||||
for removed_slot_id in outcome.removed_slot_ids {
|
||||
ctx.db.inventory_slot().slot_id().delete(&removed_slot_id);
|
||||
}
|
||||
for slot in outcome.next_slots {
|
||||
ctx.db.inventory_slot().slot_id().delete(&slot.slot_id);
|
||||
ctx.db
|
||||
.inventory_slot()
|
||||
.insert(build_inventory_slot_row(slot));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn build_inventory_item_snapshot_from_battle_reward_item(
|
||||
battle_state_id: &str,
|
||||
reward_item: RuntimeItemRewardItemSnapshot,
|
||||
) -> InventoryItemSnapshot {
|
||||
InventoryItemSnapshot {
|
||||
item_id: reward_item.item_id,
|
||||
category: reward_item.category,
|
||||
name: reward_item.item_name,
|
||||
description: reward_item.description,
|
||||
quantity: reward_item.quantity,
|
||||
rarity: map_runtime_reward_item_rarity(reward_item.rarity),
|
||||
tags: reward_item.tags,
|
||||
stackable: reward_item.stackable,
|
||||
stack_key: reward_item.stack_key,
|
||||
equipment_slot_id: reward_item
|
||||
.equipment_slot_id
|
||||
.map(map_runtime_reward_equipment_slot),
|
||||
source_kind: InventoryItemSourceKind::CombatDrop,
|
||||
source_reference_id: Some(battle_state_id.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
fn build_inventory_item_snapshot_from_quest_reward_item(
|
||||
quest_id: &str,
|
||||
reward_item: QuestRewardItem,
|
||||
) -> InventoryItemSnapshot {
|
||||
InventoryItemSnapshot {
|
||||
item_id: reward_item.item_id,
|
||||
category: reward_item.category,
|
||||
name: reward_item.name,
|
||||
description: reward_item.description,
|
||||
quantity: reward_item.quantity,
|
||||
rarity: map_quest_reward_item_rarity(reward_item.rarity),
|
||||
tags: reward_item.tags,
|
||||
stackable: reward_item.stackable,
|
||||
stack_key: reward_item.stack_key,
|
||||
equipment_slot_id: reward_item
|
||||
.equipment_slot_id
|
||||
.map(map_quest_reward_equipment_slot),
|
||||
source_kind: InventoryItemSourceKind::QuestReward,
|
||||
source_reference_id: Some(quest_id.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
fn map_quest_reward_item_rarity(rarity: QuestRewardItemRarity) -> InventoryItemRarity {
|
||||
match rarity {
|
||||
QuestRewardItemRarity::Common => InventoryItemRarity::Common,
|
||||
QuestRewardItemRarity::Uncommon => InventoryItemRarity::Uncommon,
|
||||
QuestRewardItemRarity::Rare => InventoryItemRarity::Rare,
|
||||
QuestRewardItemRarity::Epic => InventoryItemRarity::Epic,
|
||||
QuestRewardItemRarity::Legendary => InventoryItemRarity::Legendary,
|
||||
}
|
||||
}
|
||||
|
||||
fn map_runtime_reward_item_rarity(
|
||||
rarity: module_runtime_item::RuntimeItemRewardItemRarity,
|
||||
) -> InventoryItemRarity {
|
||||
match rarity {
|
||||
module_runtime_item::RuntimeItemRewardItemRarity::Common => InventoryItemRarity::Common,
|
||||
module_runtime_item::RuntimeItemRewardItemRarity::Uncommon => InventoryItemRarity::Uncommon,
|
||||
module_runtime_item::RuntimeItemRewardItemRarity::Rare => InventoryItemRarity::Rare,
|
||||
module_runtime_item::RuntimeItemRewardItemRarity::Epic => InventoryItemRarity::Epic,
|
||||
module_runtime_item::RuntimeItemRewardItemRarity::Legendary => {
|
||||
InventoryItemRarity::Legendary
|
||||
for mutation in plan.inventory_mutations.clone() {
|
||||
apply_inventory_mutation_tx(ctx, mutation)?;
|
||||
}
|
||||
}
|
||||
|
||||
apply_progression_and_chapter_settlement(ctx, plan)
|
||||
}
|
||||
|
||||
fn map_quest_reward_equipment_slot(slot: QuestRewardEquipmentSlot) -> InventoryEquipmentSlot {
|
||||
match slot {
|
||||
QuestRewardEquipmentSlot::Weapon => InventoryEquipmentSlot::Weapon,
|
||||
QuestRewardEquipmentSlot::Armor => InventoryEquipmentSlot::Armor,
|
||||
QuestRewardEquipmentSlot::Relic => InventoryEquipmentSlot::Relic,
|
||||
fn inventory_reward_source_already_granted(
|
||||
ctx: &ReducerContext,
|
||||
plan: &RpgGameplaySettlementPlan,
|
||||
source_reference_id: &str,
|
||||
) -> bool {
|
||||
let Some(first_mutation) = plan.inventory_mutations.first() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
ctx.db
|
||||
.inventory_slot()
|
||||
.iter()
|
||||
.filter(|row| {
|
||||
row.runtime_session_id == first_mutation.runtime_session_id
|
||||
&& row.actor_user_id == first_mutation.actor_user_id
|
||||
})
|
||||
.any(|row| row.source_reference_id.as_deref() == Some(source_reference_id))
|
||||
}
|
||||
|
||||
fn apply_progression_and_chapter_settlement(
|
||||
ctx: &ReducerContext,
|
||||
plan: RpgGameplaySettlementPlan,
|
||||
) -> Result<(), String> {
|
||||
let updated_player = match plan.progression_grant {
|
||||
Some(input) => Some(upsert_player_progression_after_grant_tx(ctx, input)?),
|
||||
None => None,
|
||||
};
|
||||
|
||||
if let Some(mut input) = plan.chapter_ledger {
|
||||
if let Some(player) = updated_player {
|
||||
input.level_at_exit = Some(player.level);
|
||||
}
|
||||
|
||||
// 章节计划可能尚未初始化;此时不能反向阻断战斗或任务主链。
|
||||
try_update_chapter_progression_ledger_tx(
|
||||
ctx,
|
||||
input.user_id.clone(),
|
||||
Some(input.chapter_id.clone()),
|
||||
input,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
fn map_runtime_reward_equipment_slot(
|
||||
slot: module_runtime_item::RuntimeItemEquipmentSlot,
|
||||
) -> InventoryEquipmentSlot {
|
||||
match slot {
|
||||
module_runtime_item::RuntimeItemEquipmentSlot::Weapon => InventoryEquipmentSlot::Weapon,
|
||||
module_runtime_item::RuntimeItemEquipmentSlot::Armor => InventoryEquipmentSlot::Armor,
|
||||
module_runtime_item::RuntimeItemEquipmentSlot::Relic => InventoryEquipmentSlot::Relic,
|
||||
}
|
||||
}
|
||||
|
||||
fn build_reward_seed(updated_at_micros: i64, index: usize) -> i64 {
|
||||
updated_at_micros.saturating_add(index as i64 + 1)
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn build_story_session_snapshot_from_row(row: &StorySession) -> StorySessionSnapshot {
|
||||
|
||||
@@ -139,6 +139,7 @@ macro_rules! migration_tables {
|
||||
custom_world_gallery_entry,
|
||||
asset_object,
|
||||
asset_entity_binding,
|
||||
asset_event,
|
||||
puzzle_agent_session,
|
||||
puzzle_agent_message,
|
||||
puzzle_work_profile,
|
||||
|
||||
@@ -1080,10 +1080,22 @@ fn start_puzzle_run_tx(
|
||||
.profile_id()
|
||||
.find(&input.profile_id)
|
||||
.ok_or_else(|| "入口拼图作品不存在".to_string())?;
|
||||
if entry_profile_row.publication_status != PuzzlePublicationStatus::Published {
|
||||
// 结果页试玩允许作者启动自己的草稿 run;公开入口仍必须保持已发布状态。
|
||||
let is_owner_draft_preview = entry_profile_row.publication_status
|
||||
== PuzzlePublicationStatus::Draft
|
||||
&& entry_profile_row.owner_user_id == input.owner_user_id;
|
||||
if entry_profile_row.publication_status != PuzzlePublicationStatus::Published
|
||||
&& !is_owner_draft_preview
|
||||
{
|
||||
return Err("入口拼图作品未发布".to_string());
|
||||
}
|
||||
let entry_profile = build_puzzle_work_profile_from_row(&entry_profile_row)?;
|
||||
if entry_profile.cover_image_src.is_none() {
|
||||
return Err("入口拼图作品缺少正式图片".to_string());
|
||||
}
|
||||
if entry_profile.theme_tags.is_empty() {
|
||||
return Err("入口拼图作品缺少标签".to_string());
|
||||
}
|
||||
let mut run =
|
||||
start_run(input.run_id.clone(), &entry_profile, 0).map_err(|error| error.to_string())?;
|
||||
let current_grid_size = run.current_grid_size;
|
||||
@@ -1102,13 +1114,15 @@ fn start_puzzle_run_tx(
|
||||
)
|
||||
.map(|value| value.profile_id.clone());
|
||||
|
||||
increment_puzzle_profile_play_count(ctx, &entry_profile_row, input.started_at_micros);
|
||||
upsert_puzzle_profile_played_work(
|
||||
ctx,
|
||||
&input.owner_user_id,
|
||||
&entry_profile_row,
|
||||
input.started_at_micros,
|
||||
)?;
|
||||
if entry_profile_row.publication_status == PuzzlePublicationStatus::Published {
|
||||
increment_puzzle_profile_play_count(ctx, &entry_profile_row, input.started_at_micros);
|
||||
upsert_puzzle_profile_played_work(
|
||||
ctx,
|
||||
&input.owner_user_id,
|
||||
&entry_profile_row,
|
||||
input.started_at_micros,
|
||||
)?;
|
||||
}
|
||||
insert_puzzle_runtime_run(ctx, &run, &input.owner_user_id, input.started_at_micros)?;
|
||||
Ok(run)
|
||||
}
|
||||
@@ -1246,6 +1260,23 @@ fn submit_puzzle_leaderboard_entry_tx(
|
||||
if current_level.grid_size != input.grid_size {
|
||||
return Err("提交成绩的网格规格与当前关卡不匹配".to_string());
|
||||
}
|
||||
let current_profile_row = ctx
|
||||
.db
|
||||
.puzzle_work_profile()
|
||||
.profile_id()
|
||||
.find(&input.profile_id)
|
||||
.ok_or_else(|| "提交成绩的拼图作品不存在".to_string())?;
|
||||
if current_profile_row.publication_status != PuzzlePublicationStatus::Published {
|
||||
hydrate_puzzle_leaderboard_entries(
|
||||
ctx,
|
||||
&mut run,
|
||||
&input.owner_user_id,
|
||||
&input.profile_id,
|
||||
input.grid_size,
|
||||
);
|
||||
replace_puzzle_runtime_run(ctx, &row, &run, input.submitted_at_micros);
|
||||
return Ok(run);
|
||||
}
|
||||
|
||||
let nickname = input.nickname.trim();
|
||||
if nickname.is_empty() {
|
||||
|
||||
Reference in New Issue
Block a user