Close DDD cleanup and tests-support closure
This commit is contained in:
@@ -1,3 +1,177 @@
|
||||
//! 运行时物品应用编排过渡落位。
|
||||
//! 运行时物品应用编排。
|
||||
//!
|
||||
//! 这里只返回奖励结果、记录快照和待写入背包事件。
|
||||
|
||||
use crate::commands::TreasureResolveInput;
|
||||
use crate::domain::{
|
||||
RuntimeItemEquipmentSlot, RuntimeItemRewardItemRarity, RuntimeItemRewardItemSnapshot,
|
||||
TreasureRecordSnapshot,
|
||||
};
|
||||
use crate::errors::TreasureFieldError;
|
||||
use module_inventory::{
|
||||
InventoryEquipmentSlot, InventoryItemRarity, InventoryItemSnapshot, InventoryItemSourceKind,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use shared_kernel::{
|
||||
normalize_optional_string as normalize_shared_optional_string, normalize_required_string,
|
||||
normalize_string_list as normalize_shared_string_list,
|
||||
};
|
||||
#[cfg(feature = "spacetime-types")]
|
||||
use spacetimedb::SpacetimeType;
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct TreasureRecordProcedureResult {
|
||||
pub ok: bool,
|
||||
pub record: Option<TreasureRecordSnapshot>,
|
||||
pub error_message: Option<String>,
|
||||
}
|
||||
|
||||
pub fn build_treasure_record_snapshot(
|
||||
input: TreasureResolveInput,
|
||||
) -> Result<TreasureRecordSnapshot, TreasureFieldError> {
|
||||
validate_treasure_input(&input)?;
|
||||
|
||||
Ok(TreasureRecordSnapshot {
|
||||
treasure_record_id: input.treasure_record_id,
|
||||
runtime_session_id: input.runtime_session_id,
|
||||
story_session_id: input.story_session_id,
|
||||
actor_user_id: input.actor_user_id,
|
||||
encounter_id: input.encounter_id,
|
||||
encounter_name: input.encounter_name,
|
||||
scene_id: normalize_optional_value(input.scene_id),
|
||||
scene_name: normalize_optional_value(input.scene_name),
|
||||
action: input.action,
|
||||
reward_items: input
|
||||
.reward_items
|
||||
.into_iter()
|
||||
.map(normalize_reward_item)
|
||||
.collect::<Result<Vec<_>, _>>()?,
|
||||
reward_hp: input.reward_hp,
|
||||
reward_mana: input.reward_mana,
|
||||
reward_currency: input.reward_currency,
|
||||
story_hint: normalize_optional_value(input.story_hint),
|
||||
created_at_micros: input.created_at_micros,
|
||||
updated_at_micros: input.updated_at_micros,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn build_inventory_item_snapshot_from_reward_item(
|
||||
treasure_record_id: &str,
|
||||
reward_item: RuntimeItemRewardItemSnapshot,
|
||||
) -> Result<InventoryItemSnapshot, TreasureFieldError> {
|
||||
let treasure_record_id = normalize_required_value(
|
||||
treasure_record_id.to_string(),
|
||||
TreasureFieldError::MissingTreasureRecordId,
|
||||
)?;
|
||||
let reward_item = normalize_reward_item(reward_item)?;
|
||||
|
||||
Ok(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_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_reward_item_equipment_slot),
|
||||
source_kind: InventoryItemSourceKind::TreasureReward,
|
||||
source_reference_id: Some(treasure_record_id),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn normalize_reward_item_snapshot(
|
||||
reward_item: RuntimeItemRewardItemSnapshot,
|
||||
) -> Result<RuntimeItemRewardItemSnapshot, TreasureFieldError> {
|
||||
normalize_reward_item(reward_item)
|
||||
}
|
||||
|
||||
fn validate_treasure_input(input: &TreasureResolveInput) -> Result<(), TreasureFieldError> {
|
||||
if input.treasure_record_id.trim().is_empty() {
|
||||
return Err(TreasureFieldError::MissingTreasureRecordId);
|
||||
}
|
||||
if input.runtime_session_id.trim().is_empty() {
|
||||
return Err(TreasureFieldError::MissingRuntimeSessionId);
|
||||
}
|
||||
if input.story_session_id.trim().is_empty() {
|
||||
return Err(TreasureFieldError::MissingStorySessionId);
|
||||
}
|
||||
if input.actor_user_id.trim().is_empty() {
|
||||
return Err(TreasureFieldError::MissingActorUserId);
|
||||
}
|
||||
if input.encounter_id.trim().is_empty() {
|
||||
return Err(TreasureFieldError::MissingEncounterId);
|
||||
}
|
||||
if input.encounter_name.trim().is_empty() {
|
||||
return Err(TreasureFieldError::MissingEncounterName);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn normalize_optional_value(value: Option<String>) -> Option<String> {
|
||||
normalize_shared_optional_string(value)
|
||||
}
|
||||
|
||||
fn normalize_reward_item(
|
||||
mut item: RuntimeItemRewardItemSnapshot,
|
||||
) -> Result<RuntimeItemRewardItemSnapshot, TreasureFieldError> {
|
||||
item.item_id = normalize_required_value(item.item_id, TreasureFieldError::MissingRewardItemId)?;
|
||||
item.category =
|
||||
normalize_required_value(item.category, TreasureFieldError::MissingRewardItemCategory)?;
|
||||
item.item_name =
|
||||
normalize_required_value(item.item_name, TreasureFieldError::MissingRewardItemName)?;
|
||||
item.description = normalize_optional_value(item.description);
|
||||
if item.quantity == 0 {
|
||||
return Err(TreasureFieldError::InvalidRewardItemQuantity);
|
||||
}
|
||||
if !item.stackable && item.quantity != 1 {
|
||||
return Err(TreasureFieldError::RewardNonStackableItemMustStaySingleQuantity);
|
||||
}
|
||||
if item.equipment_slot_id.is_some() && item.stackable {
|
||||
return Err(TreasureFieldError::RewardEquipmentItemCannotStack);
|
||||
}
|
||||
item.tags = normalize_string_list(item.tags);
|
||||
item.stack_key = if item.stackable {
|
||||
normalize_required_value(
|
||||
item.stack_key,
|
||||
TreasureFieldError::MissingRewardItemStackKey,
|
||||
)?
|
||||
} else {
|
||||
normalize_optional_value(Some(item.stack_key)).unwrap_or_else(|| item.item_id.clone())
|
||||
};
|
||||
Ok(item)
|
||||
}
|
||||
|
||||
fn normalize_required_value(
|
||||
value: String,
|
||||
error: TreasureFieldError,
|
||||
) -> Result<String, TreasureFieldError> {
|
||||
normalize_required_string(value).ok_or(error)
|
||||
}
|
||||
|
||||
fn normalize_string_list(values: Vec<String>) -> Vec<String> {
|
||||
normalize_shared_string_list(values)
|
||||
}
|
||||
|
||||
fn map_reward_item_rarity(rarity: RuntimeItemRewardItemRarity) -> InventoryItemRarity {
|
||||
match rarity {
|
||||
RuntimeItemRewardItemRarity::Common => InventoryItemRarity::Common,
|
||||
RuntimeItemRewardItemRarity::Uncommon => InventoryItemRarity::Uncommon,
|
||||
RuntimeItemRewardItemRarity::Rare => InventoryItemRarity::Rare,
|
||||
RuntimeItemRewardItemRarity::Epic => InventoryItemRarity::Epic,
|
||||
RuntimeItemRewardItemRarity::Legendary => InventoryItemRarity::Legendary,
|
||||
}
|
||||
}
|
||||
|
||||
fn map_reward_item_equipment_slot(slot: RuntimeItemEquipmentSlot) -> InventoryEquipmentSlot {
|
||||
match slot {
|
||||
RuntimeItemEquipmentSlot::Weapon => InventoryEquipmentSlot::Weapon,
|
||||
RuntimeItemEquipmentSlot::Armor => InventoryEquipmentSlot::Armor,
|
||||
RuntimeItemEquipmentSlot::Relic => InventoryEquipmentSlot::Relic,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,29 @@
|
||||
//! 运行时物品写入命令过渡落位。
|
||||
//! 运行时物品写入命令。
|
||||
//!
|
||||
//! 用于表达宝箱检查、开启、离开和奖励记录等输入。
|
||||
|
||||
use crate::domain::{RuntimeItemRewardItemSnapshot, TreasureInteractionAction};
|
||||
use serde::{Deserialize, Serialize};
|
||||
#[cfg(feature = "spacetime-types")]
|
||||
use spacetimedb::SpacetimeType;
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct TreasureResolveInput {
|
||||
pub treasure_record_id: String,
|
||||
pub runtime_session_id: String,
|
||||
pub story_session_id: String,
|
||||
pub actor_user_id: String,
|
||||
pub encounter_id: String,
|
||||
pub encounter_name: String,
|
||||
pub scene_id: Option<String>,
|
||||
pub scene_name: Option<String>,
|
||||
pub action: TreasureInteractionAction,
|
||||
pub reward_items: Vec<RuntimeItemRewardItemSnapshot>,
|
||||
pub reward_hp: u32,
|
||||
pub reward_mana: u32,
|
||||
pub reward_currency: u32,
|
||||
pub story_hint: Option<String>,
|
||||
pub created_at_micros: i64,
|
||||
pub updated_at_micros: i64,
|
||||
}
|
||||
|
||||
@@ -1,4 +1,71 @@
|
||||
//! 运行时物品领域模型过渡落位。
|
||||
//! 运行时物品领域模型。
|
||||
//!
|
||||
//! 后续迁移宝箱、奇遇和奖励物品规则时,只保留奖励生成与记录规则;
|
||||
//! 背包落库由外层事务 adapter 编排。
|
||||
//! 本文件承载宝箱、奇遇和奖励物品的稳定值对象;背包落库由外层事务 adapter 编排。
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
#[cfg(feature = "spacetime-types")]
|
||||
use spacetimedb::SpacetimeType;
|
||||
|
||||
pub const TREASURE_RECORD_ID_PREFIX: &str = "treasure_";
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum TreasureInteractionAction {
|
||||
Inspect,
|
||||
Leave,
|
||||
Secure,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct RuntimeItemRewardItemSnapshot {
|
||||
pub item_id: String,
|
||||
pub category: String,
|
||||
pub item_name: String,
|
||||
pub description: Option<String>,
|
||||
pub quantity: u32,
|
||||
pub rarity: RuntimeItemRewardItemRarity,
|
||||
pub tags: Vec<String>,
|
||||
pub stackable: bool,
|
||||
pub stack_key: String,
|
||||
pub equipment_slot_id: Option<RuntimeItemEquipmentSlot>,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum RuntimeItemRewardItemRarity {
|
||||
Common,
|
||||
Uncommon,
|
||||
Rare,
|
||||
Epic,
|
||||
Legendary,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum RuntimeItemEquipmentSlot {
|
||||
Weapon,
|
||||
Armor,
|
||||
Relic,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct TreasureRecordSnapshot {
|
||||
pub treasure_record_id: String,
|
||||
pub runtime_session_id: String,
|
||||
pub story_session_id: String,
|
||||
pub actor_user_id: String,
|
||||
pub encounter_id: String,
|
||||
pub encounter_name: String,
|
||||
pub scene_id: Option<String>,
|
||||
pub scene_name: Option<String>,
|
||||
pub action: TreasureInteractionAction,
|
||||
pub reward_items: Vec<RuntimeItemRewardItemSnapshot>,
|
||||
pub reward_hp: u32,
|
||||
pub reward_mana: u32,
|
||||
pub reward_currency: u32,
|
||||
pub story_hint: Option<String>,
|
||||
pub created_at_micros: i64,
|
||||
pub updated_at_micros: i64,
|
||||
}
|
||||
|
||||
@@ -1,3 +1,62 @@
|
||||
//! 运行时物品领域错误过渡落位。
|
||||
//! 运行时物品领域错误。
|
||||
//!
|
||||
//! 错误只表达物品/奇遇规则失败,例如 encounter 缺失或奖励字段非法。
|
||||
|
||||
use std::{error::Error, fmt};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum TreasureFieldError {
|
||||
MissingTreasureRecordId,
|
||||
MissingRuntimeSessionId,
|
||||
MissingStorySessionId,
|
||||
MissingActorUserId,
|
||||
MissingEncounterId,
|
||||
MissingEncounterName,
|
||||
MissingRewardItemId,
|
||||
MissingRewardItemCategory,
|
||||
MissingRewardItemName,
|
||||
InvalidRewardItemQuantity,
|
||||
MissingRewardItemStackKey,
|
||||
RewardEquipmentItemCannotStack,
|
||||
RewardNonStackableItemMustStaySingleQuantity,
|
||||
}
|
||||
|
||||
impl fmt::Display for TreasureFieldError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::MissingTreasureRecordId => {
|
||||
f.write_str("treasure_record.treasure_record_id 不能为空")
|
||||
}
|
||||
Self::MissingRuntimeSessionId => {
|
||||
f.write_str("treasure_record.runtime_session_id 不能为空")
|
||||
}
|
||||
Self::MissingStorySessionId => f.write_str("treasure_record.story_session_id 不能为空"),
|
||||
Self::MissingActorUserId => f.write_str("treasure_record.actor_user_id 不能为空"),
|
||||
Self::MissingEncounterId => f.write_str("treasure_record.encounter_id 不能为空"),
|
||||
Self::MissingEncounterName => f.write_str("treasure_record.encounter_name 不能为空"),
|
||||
Self::MissingRewardItemId => {
|
||||
f.write_str("treasure_record.reward_items[].item_id 不能为空")
|
||||
}
|
||||
Self::MissingRewardItemCategory => {
|
||||
f.write_str("treasure_record.reward_items[].category 不能为空")
|
||||
}
|
||||
Self::MissingRewardItemName => {
|
||||
f.write_str("treasure_record.reward_items[].item_name 不能为空")
|
||||
}
|
||||
Self::InvalidRewardItemQuantity => {
|
||||
f.write_str("treasure_record.reward_items[].quantity 必须大于 0")
|
||||
}
|
||||
Self::MissingRewardItemStackKey => {
|
||||
f.write_str("treasure_record.reward_items[].stack_key 不能为空")
|
||||
}
|
||||
Self::RewardEquipmentItemCannotStack => {
|
||||
f.write_str("treasure_record.reward_items[] 可装备物品不能标记为 stackable")
|
||||
}
|
||||
Self::RewardNonStackableItemMustStaySingleQuantity => {
|
||||
f.write_str("treasure_record.reward_items[] 不可堆叠物品必须固定为单槽位单数量")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for TreasureFieldError {}
|
||||
|
||||
@@ -1,3 +1,32 @@
|
||||
//! 运行时物品领域事件过渡落位。
|
||||
//! 运行时物品领域事件。
|
||||
//!
|
||||
//! 用于表达宝箱已结算、奖励物品已生成和资源奖励待入账等事实。
|
||||
|
||||
use crate::domain::RuntimeItemRewardItemSnapshot;
|
||||
use serde::{Deserialize, Serialize};
|
||||
#[cfg(feature = "spacetime-types")]
|
||||
use spacetimedb::SpacetimeType;
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum RuntimeItemDomainEvent {
|
||||
TreasureResolved(RuntimeItemTreasureResolvedEvent),
|
||||
TreasureRewardItemsGenerated(RuntimeItemTreasureRewardItemsGeneratedEvent),
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct RuntimeItemTreasureResolvedEvent {
|
||||
pub treasure_record_id: String,
|
||||
pub runtime_session_id: String,
|
||||
pub story_session_id: String,
|
||||
pub occurred_at_micros: i64,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct RuntimeItemTreasureRewardItemsGeneratedEvent {
|
||||
pub treasure_record_id: String,
|
||||
pub reward_items: Vec<RuntimeItemRewardItemSnapshot>,
|
||||
pub occurred_at_micros: i64,
|
||||
}
|
||||
|
||||
@@ -4,321 +4,16 @@ mod domain;
|
||||
mod errors;
|
||||
mod events;
|
||||
|
||||
use std::{error::Error, fmt};
|
||||
|
||||
use module_inventory::{
|
||||
InventoryEquipmentSlot, InventoryItemRarity, InventoryItemSnapshot, InventoryItemSourceKind,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use shared_kernel::{
|
||||
normalize_optional_string as normalize_shared_optional_string, normalize_required_string,
|
||||
normalize_string_list as normalize_shared_string_list,
|
||||
};
|
||||
#[cfg(feature = "spacetime-types")]
|
||||
use spacetimedb::SpacetimeType;
|
||||
|
||||
pub const TREASURE_RECORD_ID_PREFIX: &str = "treasure_";
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum TreasureInteractionAction {
|
||||
Inspect,
|
||||
Leave,
|
||||
Secure,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct RuntimeItemRewardItemSnapshot {
|
||||
pub item_id: String,
|
||||
pub category: String,
|
||||
pub item_name: String,
|
||||
pub description: Option<String>,
|
||||
pub quantity: u32,
|
||||
pub rarity: RuntimeItemRewardItemRarity,
|
||||
pub tags: Vec<String>,
|
||||
pub stackable: bool,
|
||||
pub stack_key: String,
|
||||
pub equipment_slot_id: Option<RuntimeItemEquipmentSlot>,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum RuntimeItemRewardItemRarity {
|
||||
Common,
|
||||
Uncommon,
|
||||
Rare,
|
||||
Epic,
|
||||
Legendary,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum RuntimeItemEquipmentSlot {
|
||||
Weapon,
|
||||
Armor,
|
||||
Relic,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct TreasureResolveInput {
|
||||
pub treasure_record_id: String,
|
||||
pub runtime_session_id: String,
|
||||
pub story_session_id: String,
|
||||
pub actor_user_id: String,
|
||||
pub encounter_id: String,
|
||||
pub encounter_name: String,
|
||||
pub scene_id: Option<String>,
|
||||
pub scene_name: Option<String>,
|
||||
pub action: TreasureInteractionAction,
|
||||
pub reward_items: Vec<RuntimeItemRewardItemSnapshot>,
|
||||
pub reward_hp: u32,
|
||||
pub reward_mana: u32,
|
||||
pub reward_currency: u32,
|
||||
pub story_hint: Option<String>,
|
||||
pub created_at_micros: i64,
|
||||
pub updated_at_micros: i64,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct TreasureRecordSnapshot {
|
||||
pub treasure_record_id: String,
|
||||
pub runtime_session_id: String,
|
||||
pub story_session_id: String,
|
||||
pub actor_user_id: String,
|
||||
pub encounter_id: String,
|
||||
pub encounter_name: String,
|
||||
pub scene_id: Option<String>,
|
||||
pub scene_name: Option<String>,
|
||||
pub action: TreasureInteractionAction,
|
||||
pub reward_items: Vec<RuntimeItemRewardItemSnapshot>,
|
||||
pub reward_hp: u32,
|
||||
pub reward_mana: u32,
|
||||
pub reward_currency: u32,
|
||||
pub story_hint: Option<String>,
|
||||
pub created_at_micros: i64,
|
||||
pub updated_at_micros: i64,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct TreasureRecordProcedureResult {
|
||||
pub ok: bool,
|
||||
pub record: Option<TreasureRecordSnapshot>,
|
||||
pub error_message: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum TreasureFieldError {
|
||||
MissingTreasureRecordId,
|
||||
MissingRuntimeSessionId,
|
||||
MissingStorySessionId,
|
||||
MissingActorUserId,
|
||||
MissingEncounterId,
|
||||
MissingEncounterName,
|
||||
MissingRewardItemId,
|
||||
MissingRewardItemCategory,
|
||||
MissingRewardItemName,
|
||||
InvalidRewardItemQuantity,
|
||||
MissingRewardItemStackKey,
|
||||
RewardEquipmentItemCannotStack,
|
||||
RewardNonStackableItemMustStaySingleQuantity,
|
||||
}
|
||||
|
||||
pub fn build_treasure_record_snapshot(
|
||||
input: TreasureResolveInput,
|
||||
) -> Result<TreasureRecordSnapshot, TreasureFieldError> {
|
||||
validate_treasure_input(&input)?;
|
||||
|
||||
Ok(TreasureRecordSnapshot {
|
||||
treasure_record_id: input.treasure_record_id,
|
||||
runtime_session_id: input.runtime_session_id,
|
||||
story_session_id: input.story_session_id,
|
||||
actor_user_id: input.actor_user_id,
|
||||
encounter_id: input.encounter_id,
|
||||
encounter_name: input.encounter_name,
|
||||
scene_id: normalize_optional_value(input.scene_id),
|
||||
scene_name: normalize_optional_value(input.scene_name),
|
||||
action: input.action,
|
||||
reward_items: input
|
||||
.reward_items
|
||||
.into_iter()
|
||||
.map(normalize_reward_item)
|
||||
.collect::<Result<Vec<_>, _>>()?,
|
||||
reward_hp: input.reward_hp,
|
||||
reward_mana: input.reward_mana,
|
||||
reward_currency: input.reward_currency,
|
||||
story_hint: normalize_optional_value(input.story_hint),
|
||||
created_at_micros: input.created_at_micros,
|
||||
updated_at_micros: input.updated_at_micros,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn build_inventory_item_snapshot_from_reward_item(
|
||||
treasure_record_id: &str,
|
||||
reward_item: RuntimeItemRewardItemSnapshot,
|
||||
) -> Result<InventoryItemSnapshot, TreasureFieldError> {
|
||||
let treasure_record_id = normalize_required_value(
|
||||
treasure_record_id.to_string(),
|
||||
TreasureFieldError::MissingTreasureRecordId,
|
||||
)?;
|
||||
let reward_item = normalize_reward_item(reward_item)?;
|
||||
|
||||
Ok(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_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_reward_item_equipment_slot),
|
||||
source_kind: InventoryItemSourceKind::TreasureReward,
|
||||
source_reference_id: Some(treasure_record_id),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn normalize_reward_item_snapshot(
|
||||
reward_item: RuntimeItemRewardItemSnapshot,
|
||||
) -> Result<RuntimeItemRewardItemSnapshot, TreasureFieldError> {
|
||||
normalize_reward_item(reward_item)
|
||||
}
|
||||
|
||||
fn validate_treasure_input(input: &TreasureResolveInput) -> Result<(), TreasureFieldError> {
|
||||
if input.treasure_record_id.trim().is_empty() {
|
||||
return Err(TreasureFieldError::MissingTreasureRecordId);
|
||||
}
|
||||
if input.runtime_session_id.trim().is_empty() {
|
||||
return Err(TreasureFieldError::MissingRuntimeSessionId);
|
||||
}
|
||||
if input.story_session_id.trim().is_empty() {
|
||||
return Err(TreasureFieldError::MissingStorySessionId);
|
||||
}
|
||||
if input.actor_user_id.trim().is_empty() {
|
||||
return Err(TreasureFieldError::MissingActorUserId);
|
||||
}
|
||||
if input.encounter_id.trim().is_empty() {
|
||||
return Err(TreasureFieldError::MissingEncounterId);
|
||||
}
|
||||
if input.encounter_name.trim().is_empty() {
|
||||
return Err(TreasureFieldError::MissingEncounterName);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn normalize_optional_value(value: Option<String>) -> Option<String> {
|
||||
normalize_shared_optional_string(value)
|
||||
}
|
||||
|
||||
fn normalize_reward_item(
|
||||
mut item: RuntimeItemRewardItemSnapshot,
|
||||
) -> Result<RuntimeItemRewardItemSnapshot, TreasureFieldError> {
|
||||
item.item_id = normalize_required_value(item.item_id, TreasureFieldError::MissingRewardItemId)?;
|
||||
item.category =
|
||||
normalize_required_value(item.category, TreasureFieldError::MissingRewardItemCategory)?;
|
||||
item.item_name =
|
||||
normalize_required_value(item.item_name, TreasureFieldError::MissingRewardItemName)?;
|
||||
item.description = normalize_optional_value(item.description);
|
||||
if item.quantity == 0 {
|
||||
return Err(TreasureFieldError::InvalidRewardItemQuantity);
|
||||
}
|
||||
if !item.stackable && item.quantity != 1 {
|
||||
return Err(TreasureFieldError::RewardNonStackableItemMustStaySingleQuantity);
|
||||
}
|
||||
if item.equipment_slot_id.is_some() && item.stackable {
|
||||
return Err(TreasureFieldError::RewardEquipmentItemCannotStack);
|
||||
}
|
||||
item.tags = normalize_string_list(item.tags);
|
||||
item.stack_key = if item.stackable {
|
||||
normalize_required_value(
|
||||
item.stack_key,
|
||||
TreasureFieldError::MissingRewardItemStackKey,
|
||||
)?
|
||||
} else {
|
||||
normalize_optional_value(Some(item.stack_key)).unwrap_or_else(|| item.item_id.clone())
|
||||
};
|
||||
Ok(item)
|
||||
}
|
||||
|
||||
fn normalize_required_value(
|
||||
value: String,
|
||||
error: TreasureFieldError,
|
||||
) -> Result<String, TreasureFieldError> {
|
||||
normalize_required_string(value).ok_or(error)
|
||||
}
|
||||
|
||||
fn normalize_string_list(values: Vec<String>) -> Vec<String> {
|
||||
normalize_shared_string_list(values)
|
||||
}
|
||||
|
||||
fn map_reward_item_rarity(rarity: RuntimeItemRewardItemRarity) -> InventoryItemRarity {
|
||||
match rarity {
|
||||
RuntimeItemRewardItemRarity::Common => InventoryItemRarity::Common,
|
||||
RuntimeItemRewardItemRarity::Uncommon => InventoryItemRarity::Uncommon,
|
||||
RuntimeItemRewardItemRarity::Rare => InventoryItemRarity::Rare,
|
||||
RuntimeItemRewardItemRarity::Epic => InventoryItemRarity::Epic,
|
||||
RuntimeItemRewardItemRarity::Legendary => InventoryItemRarity::Legendary,
|
||||
}
|
||||
}
|
||||
|
||||
fn map_reward_item_equipment_slot(slot: RuntimeItemEquipmentSlot) -> InventoryEquipmentSlot {
|
||||
match slot {
|
||||
RuntimeItemEquipmentSlot::Weapon => InventoryEquipmentSlot::Weapon,
|
||||
RuntimeItemEquipmentSlot::Armor => InventoryEquipmentSlot::Armor,
|
||||
RuntimeItemEquipmentSlot::Relic => InventoryEquipmentSlot::Relic,
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for TreasureFieldError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::MissingTreasureRecordId => {
|
||||
f.write_str("treasure_record.treasure_record_id 不能为空")
|
||||
}
|
||||
Self::MissingRuntimeSessionId => {
|
||||
f.write_str("treasure_record.runtime_session_id 不能为空")
|
||||
}
|
||||
Self::MissingStorySessionId => f.write_str("treasure_record.story_session_id 不能为空"),
|
||||
Self::MissingActorUserId => f.write_str("treasure_record.actor_user_id 不能为空"),
|
||||
Self::MissingEncounterId => f.write_str("treasure_record.encounter_id 不能为空"),
|
||||
Self::MissingEncounterName => f.write_str("treasure_record.encounter_name 不能为空"),
|
||||
Self::MissingRewardItemId => {
|
||||
f.write_str("treasure_record.reward_items[].item_id 不能为空")
|
||||
}
|
||||
Self::MissingRewardItemCategory => {
|
||||
f.write_str("treasure_record.reward_items[].category 不能为空")
|
||||
}
|
||||
Self::MissingRewardItemName => {
|
||||
f.write_str("treasure_record.reward_items[].item_name 不能为空")
|
||||
}
|
||||
Self::InvalidRewardItemQuantity => {
|
||||
f.write_str("treasure_record.reward_items[].quantity 必须大于 0")
|
||||
}
|
||||
Self::MissingRewardItemStackKey => {
|
||||
f.write_str("treasure_record.reward_items[].stack_key 不能为空")
|
||||
}
|
||||
Self::RewardEquipmentItemCannotStack => {
|
||||
f.write_str("treasure_record.reward_items[] 可装备物品不能标记为 stackable")
|
||||
}
|
||||
Self::RewardNonStackableItemMustStaySingleQuantity => {
|
||||
f.write_str("treasure_record.reward_items[] 不可堆叠物品必须固定为单槽位单数量")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for TreasureFieldError {}
|
||||
pub use application::*;
|
||||
pub use commands::*;
|
||||
pub use domain::*;
|
||||
pub use errors::*;
|
||||
pub use events::*;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use module_inventory::{InventoryEquipmentSlot, InventoryItemRarity, InventoryItemSourceKind};
|
||||
|
||||
#[test]
|
||||
fn build_treasure_record_snapshot_accepts_minimal_contract() {
|
||||
|
||||
Reference in New Issue
Block a user