> ## Documentation Index
> Fetch the complete documentation index at: https://devdocs.xbox.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Economy v2 中的掉落表和随机战利品

> 使用 Title Data、Azure Functions 和 ExecuteInventoryOperations 在 PlayFab Economy v2 中构建加权随机战利品掉落，以原子方式授予物品。

<Info>
  Economy v2 现已正式发布。如需支持和反馈，请访问 [PlayFab 论坛](https://community.playfab.com)。
</Info>

Economy v1 (Legacy) 有一个内置的 [Drop Tables](/services/playfab/economy-monetization/economy/tutorials/drop-tables) 功能，可让你直接在 Game Manager 中定义加权随机物品分布。Economy v2 不包括掉落表作为原生功能，但你可以使用 [Azure Functions](https://learn.microsoft.com/en-us/azure/azure-functions/functions-overview) 结合 [ExecuteInventoryOperations](https://learn.microsoft.com/en-us/rest/api/playfab/economy/inventory/execute-inventory-operations) API 实现相同的行为——并具有更多的灵活性。

本教程向你介绍如何构建一个完整的随机战利品系统，其功能是：

* 在 [Title Data](/services/playfab/live-service-management/game-configuration/titledata) 中使用加权物品池定义战利品表。
* 使用 Azure Function 从战利品表中滚出随机物品。
* 在单个调用中原子地扣除容器令牌并授予随机选择的物品。

## 工作原理

1. **在 Title Data 中定义战利品表**：将掉落表配置（物品 ID、权重、数量）作为 JSON 存储在 Title Data 中，这样你就可以更新它们而无需重新部署代码。
2. **将容器表示为虚拟货币**：使用与固定内容容器相同的[容器令牌模式](/services/playfab/economy-monetization/economy-v2/tutorials/containers-with-fixed-contents)：一种作为未打开的战利品箱的虚拟货币。
3. **调用 Azure Function 来打开容器**：函数读取战利品表，运行加权随机选择，然后调用 `ExecuteInventoryOperations` 原子地扣除 1 个容器令牌并添加随机选择的物品。

## 先决条件

* 一个 [PlayFab 开发者账户](https://developer.playfab.com/en-us/sign-up)。
* 在 [Game Manager](https://developer.playfab.com/) 中创建的 title。
* 已发布的目录物品，你可以作为战利品奖励授予。
* 代表容器的虚拟货币（有关设置说明，请参见 [带有固定内容的容器](/services/playfab/economy-monetization/economy-v2/tutorials/containers-with-fixed-contents#step-1-create-a-virtual-currency-for-the-container)）。
* 一个连接到你的 PlayFab title 的 [Azure Functions](/services/playfab/live-service-management/service-gateway/automation/cloudscript-af/quickstart) 项目。

## 步骤 1：在 Title Data 中定义战利品表

在 [Title Data](/services/playfab/live-service-management/game-configuration/titledata) 中存储战利品表定义，以便设计师可以在没有代码更改的情况下更新掉落率。

### 战利品表结构示例

创建一个名为 `LootTables` 的 Title Data 键，其 JSON 值为：

```json theme={null}
{
  "TreasureChest": {
    "Rolls": 1,
    "Items": [
      { "ItemId": "{{CommonSwordID}}", "Weight": 1000, "MinQuantity": 1, "MaxQuantity": 1 },
      { "ItemId": "{{UncommonSwordID}}", "Weight": 100, "MinQuantity": 1, "MaxQuantity": 1 },
      { "ItemId": "{{RareSwordID}}", "Weight": 10, "MinQuantity": 1, "MaxQuantity": 1 },
      { "ItemId": "{{LegendarySwordID}}", "Weight": 1, "MinQuantity": 1, "MaxQuantity": 1 }
    ]
  },
  "SupplyCrate": {
    "Rolls": 3,
    "Items": [
      { "ItemId": "{{HealthPotionID}}", "Weight": 50, "MinQuantity": 1, "MaxQuantity": 5 },
      { "ItemId": "{{ManaPotionID}}", "Weight": 50, "MinQuantity": 1, "MaxQuantity": 3 },
      { "ItemId": "{{GoldCurrencyID}}", "Weight": 80, "MinQuantity": 10, "MaxQuantity": 100 },
      { "ItemId": "{{RareGemID}}", "Weight": 5, "MinQuantity": 1, "MaxQuantity": 1 }
    ]
  }
}
```

每个战利品表包括以下属性：

| 属性                            | 描述                       |
| ----------------------------- | ------------------------ |
| **Rolls**                     | 打开容器时在表上滚动的次数。           |
| **Items**                     | 可能的奖励池，每个都有权重和数量范围。      |
| **Weight**                    | 相对概率。较高的权重意味着物品掉落的可能性更大。 |
| **MinQuantity / MaxQuantity** | 每次滚动授予该物品数量的范围。          |

### 设置 Title Data

#### Game Manager

1. 转到 **Content** > **Title Data**。
2. 创建一个名为 `LootTables` 的键。
3. 将你的 JSON 战利品表配置粘贴为值。
4. 选择 **Save**。

#### API

使用 [SetTitleData](https://learn.microsoft.com/en-us/rest/api/playfab/server/title-wide-data-management/set-title-data)：

```json theme={null}
{
  "Key": "LootTables",
  "Value": "{\"TreasureChest\":{\"Rolls\":1,\"Items\":[...]}}"
}
```

***

## 步骤 2：创建 Azure Function

创建一个 Azure Function，当玩家想要打开容器时你的游戏客户端调用它。此函数：

1. 验证玩家至少拥有一个容器令牌。
2. 从 Title Data 读取战利品表。
3. 运行加权随机选择。
4. 调用 `ExecuteInventoryOperations` 原子地扣除令牌并授予奖励。

### 数据模型

```csharp theme={null}
public class OpenContainerRequest
{
    public string LootTableId { get; set; }  // e.g., "TreasureChest"
}

public class LootTable
{
    public int Rolls { get; set; }
    public List<LootTableEntry> Items { get; set; }
}

public class LootTableEntry
{
    public string ItemId { get; set; }
    public int Weight { get; set; }
    public int MinQuantity { get; set; }
    public int MaxQuantity { get; set; }
}
```

### 加权随机选择

```csharp theme={null}
public static LootTableEntry RollOnTable(LootTable table, Random rng)
{
    int totalWeight = table.Items.Sum(e => e.Weight);
    int roll = rng.Next(0, totalWeight);

    int cumulative = 0;
    foreach (var entry in table.Items)
    {
        cumulative += entry.Weight;
        if (roll < cumulative)
            return entry;
    }

    // Fallback (shouldn't reach here)
    return table.Items.Last();
}
```

### 完整的 Azure Function

```csharp theme={null}
[FunctionName("OpenLootContainer")]
public static async Task<dynamic> Run(
    [HttpTrigger(AuthorizationLevel.Function, "post", Route = null)] HttpRequest req,
    ILogger log)
{
    // 1. Parse the PlayFab context and request
    var context = JsonConvert.DeserializeObject<FunctionExecutionContext<dynamic>>(
        await req.ReadAsStringAsync());
    var args = context.FunctionArgument != null
        ? JsonConvert.DeserializeObject<OpenContainerRequest>(
            context.FunctionArgument.ToString())
        : null;

    if (args == null || string.IsNullOrEmpty(args.LootTableId))
        return new { Error = "LootTableId is required." };

    var playerEntity = context.CallerEntityProfile.Entity;
    var settings = new PlayFabApiSettings
    {
        TitleId = context.TitleAuthenticationContext.Id,
        DeveloperSecretKey = Environment.GetEnvironmentVariable("PLAYFAB_DEV_SECRET_KEY")
    };
    var authContext = new PlayFabAuthenticationContext
    {
        EntityToken = context.TitleAuthenticationContext.EntityToken
    };

    var serverApi = new PlayFabServerInstanceAPI(settings);
    var economyApi = new PlayFabEconomyInstanceAPI(settings, authContext);

    // 2. Read the loot table from Title Data
    var titleDataResult = await serverApi.GetTitleDataAsync(new GetTitleDataRequest
    {
        Keys = new List<string> { "LootTables" }
    });

    if (!titleDataResult.Result.Data.ContainsKey("LootTables"))
        return new { Error = "LootTables not found in Title Data." };

    var allTables = JsonConvert.DeserializeObject<Dictionary<string, LootTable>>(
        titleDataResult.Result.Data["LootTables"]);

    if (!allTables.ContainsKey(args.LootTableId))
        return new { Error = $"Loot table '{args.LootTableId}' not found." };

    var lootTable = allTables[args.LootTableId];

    // 3. Roll for rewards
    var rng = new Random();
    var rewards = new Dictionary<string, int>(); // ItemId -> total quantity

    for (int i = 0; i < lootTable.Rolls; i++)
    {
        var entry = RollOnTable(lootTable, rng);
        int quantity = rng.Next(entry.MinQuantity, entry.MaxQuantity + 1);

        if (rewards.ContainsKey(entry.ItemId))
            rewards[entry.ItemId] += quantity;
        else
            rewards[entry.ItemId] = quantity;
    }

    // 4. Build the atomic operation: subtract 1 container token + add rewards
    var operations = new List<InventoryOperation>();

    // Subtract the container token
    // The container currency ID must match the LootTableId mapping
    // Store this mapping in Title Data or use a naming convention
    operations.Add(new InventoryOperation
    {
        Subtract = new SubtractInventoryItemsOperation
        {
            Item = new InventoryItemReference
            {
                Id = GetContainerCurrencyId(args.LootTableId) // Your mapping logic
            },
            Amount = 1
        }
    });

    // Add each reward
    foreach (var reward in rewards)
    {
        operations.Add(new InventoryOperation
        {
            Add = new AddInventoryItemsOperation
            {
                Item = new InventoryItemReference { Id = reward.Key },
                Amount = reward.Value
            }
        });
    }

    // 5. Execute atomically
    var executeResult = await economyApi.ExecuteInventoryOperationsAsync(
        new ExecuteInventoryOperationsRequest
        {
            Entity = new EntityKey
            {
                Id = playerEntity.Id,
                Type = playerEntity.Type
            },
            Operations = operations,
            IdempotencyId = Guid.NewGuid().ToString()
        });

    if (executeResult.Error != null)
    {
        log.LogError($"Failed to execute operations: {executeResult.Error.ErrorMessage}");
        return new { Error = executeResult.Error.ErrorMessage };
    }

    log.LogInformation($"Player {playerEntity.Id} opened {args.LootTableId}, received: " +
        string.Join(", ", rewards.Select(r => $"{r.Value}x {r.Key}")));

    return new { Rewards = rewards, TransactionIds = executeResult.Result.TransactionIds };
}
```

<Note>
  `GetContainerCurrencyId` 帮助函数将战利品表名称映射到其相应的容器货币 ID。你可以将此映射与战利品表一起存储在 Title Data 中，或使用命名约定。
</Note>

## 步骤 3：向玩家授予容器令牌

通过游戏玩法、商店购买或奖励向玩家授予容器令牌。使用 [带有固定内容的容器](/services/playfab/economy-monetization/economy-v2/tutorials/containers-with-fixed-contents#step-3-grant-container-tokens-to-players) 中描述的相同方法。

## 步骤 4：从你的游戏客户端调用函数

当玩家想要打开容器时，通过使用 [ExecuteFunction](https://learn.microsoft.com/en-us/rest/api/playfab/cloudscript/server-side-cloud-script/execute-function) 调用 Azure Function：

```json theme={null}
{
    "FunctionName": "OpenLootContainer",
    "FunctionParameter": {
        "LootTableId": "TreasureChest"
    },
    "GeneratePlayStreamEvent": true
}
```

响应包含随机选择的奖励：

```json theme={null}
{
    "FunctionResult": {
        "Rewards": {
            "{{UncommonSwordID}}": 1
        },
        "TransactionIds": ["txn-abc-123"]
    }
}
```

## 战利品表设计模式

### 稀有度层级

具有指数递减权重的经典稀有度系统：

```json theme={null}
{
  "Items": [
    { "ItemId": "CommonItem",     "Weight": 1000, "MinQuantity": 1, "MaxQuantity": 1 },
    { "ItemId": "UncommonItem",   "Weight": 100,  "MinQuantity": 1, "MaxQuantity": 1 },
    { "ItemId": "RareItem",       "Weight": 10,   "MinQuantity": 1, "MaxQuantity": 1 },
    { "ItemId": "LegendaryItem",  "Weight": 1,    "MinQuantity": 1, "MaxQuantity": 1 }
  ]
}
```

掉落概率：

* Common: \~90.0%
* Uncommon: \~9.0%
* Rare: \~0.9%
* Legendary: \~0.09%

### 多次滚动与混合奖励

一个补给箱，从混合池中给出三个随机物品：

```json theme={null}
{
  "Rolls": 3,
  "Items": [
    { "ItemId": "{{HealthPotionID}}", "Weight": 40, "MinQuantity": 1, "MaxQuantity": 3 },
    { "ItemId": "{{ManaPotionID}}",   "Weight": 40, "MinQuantity": 1, "MaxQuantity": 3 },
    { "ItemId": "{{GoldCurrencyID}}", "Weight": 30, "MinQuantity": 10, "MaxQuantity": 50 },
    { "ItemId": "{{RareGemID}}",      "Weight": 5,  "MinQuantity": 1, "MaxQuantity": 1 }
  ]
}
```

### 保底 + 随机（保底系统）

对于"每 10 次打开保底稀有或更好"的模式，在[显示属性](/services/playfab/economy-monetization/economy-v2/inventory#display-properties)或[玩家数据](/services/playfab/player-progression/player-data/quickstart) 中跟踪玩家的打开次数，并在达到阈值时在 Azure Function 中覆盖随机选择。

### 嵌套掉落表

一个战利品表可以按 ID 引用另一个战利品表。你的 Azure Function 递归地解析引用：

```json theme={null}
{
  "BossDropTable": {
    "Rolls": 2,
    "Items": [
      { "ItemId": "TABLE:WeaponDropTable", "Weight": 50, "MinQuantity": 1, "MaxQuantity": 1 },
      { "ItemId": "TABLE:ArmorDropTable",  "Weight": 30, "MinQuantity": 1, "MaxQuantity": 1 },
      { "ItemId": "{{GoldCurrencyID}}",    "Weight": 80, "MinQuantity": 50, "MaxQuantity": 200 }
    ]
  }
}
```

函数检查 `ItemId` 是否以 `TABLE:` 开头，如果是，则在引用的表上滚动，而不是直接授予物品。

## 安全注意事项

* **始终在服务器端运行战利品逻辑**，在 Azure Function 中。永远不要相信客户端来确定滚出了哪些物品。此方法可防止操纵。
* **使用 `IdempotencyId`** 在 `ExecuteInventoryOperations` 调用上，以防止如果函数被重试时的重复授予。有关更多信息，请参见 [幂等交易和重试](/services/playfab/economy-monetization/economy-v2/tutorials/idempotent-transactions-and-retries)。
* **通过在扣除之前检查玩家的库存来验证容器所有权**。如果玩家没有足够的令牌，`ExecuteInventoryOperations` 中的 `Subtract` 操作将失败，这会回滚整个批处理。
* **记录所有滚动**用于审计。函数的返回值和 PlayStream 事件提供了每个玩家收到内容的记录。

## 另请参阅

* [带有固定内容的容器](/services/playfab/economy-monetization/economy-v2/tutorials/containers-with-fixed-contents)
* [ExecuteInventoryOperations API 参考](https://learn.microsoft.com/en-us/rest/api/playfab/economy/inventory/execute-inventory-operations)
* [幂等交易和重试](/services/playfab/economy-monetization/economy-v2/tutorials/idempotent-transactions-and-retries)
* [Title Data](/services/playfab/live-service-management/game-configuration/titledata)
* [Azure Functions 快速入门](/services/playfab/live-service-management/service-gateway/automation/cloudscript-af/quickstart)
* [Drop tables (Legacy Economy)](/services/playfab/economy-monetization/economy/tutorials/drop-tables)
* [Economy v2 概览](/services/playfab/economy-monetization/economy-v2/overview)


## Related topics

- [Economy（旧版）中的掉落表](/zh-CN/services/playfab/economy-monetization/economy/tutorials/drop-tables.md)
- [Economy v2 中带有固定内容的容器](/zh-CN/services/playfab/economy-monetization/economy-v2/tutorials/containers-with-fixed-contents.md)
- [Economy v2 概览](/zh-CN/services/playfab/economy-monetization/economy-v2/overview.md)
- [目录（旧版 Economy）](/zh-CN/services/playfab/economy-monetization/economy/items/catalogs.md)
- [XR-017 游戏分级](/zh-CN/publishing/certification/xr/xr-017.md)
