| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459 |
- <?php
- namespace App\Module\GameItems\Services;
- use App\Module\GameItems\Enums\ITEM_TYPE;
- use App\Module\GameItems\Models\ItemChestContent;
- use App\Module\GameItems\Models\ItemChestOpenLog;
- use App\Module\GameItems\Models\ItemItem;
- use App\Module\GameItems\Models\ItemPityTime;
- use Exception;
- use Illuminate\Support\Collection;
- use Illuminate\Support\Facades\DB;
- class ChestService
- {
- /**
- * @var ItemService
- */
- protected $itemService;
- /**
- * 构造函数
- *
- * @param ItemService $itemService
- */
- public function __construct(ItemService $itemService)
- {
- $this->itemService = $itemService;
- }
- /**
- * 开启宝箱
- *
- * @param int $userId 用户ID
- * @param int $chestId 宝箱ID
- * @param int $quantity 开启数量
- * @param array $options 选项
- * @return array 开启结果
- * @throws Exception
- */
- public function openChest(int $userId, int $chestId, int $quantity = 1, array $options = []): array
- {
- // 获取宝箱信息
- $chest = ItemItem::findOrFail($chestId);
- // 检查是否为宝箱类型
- if ($chest->type != ITEM_TYPE::OPENABLE) {
- throw new Exception("物品 {$chestId} 不是宝箱类型");
- }
- // 获取宝箱内容配置
- $chestContents = ItemChestContent::where('chest_id', $chestId)->get();
- if ($chestContents->isEmpty()) {
- throw new Exception("宝箱 {$chestId} 没有配置内容");
- }
- // 开始事务
- DB::beginTransaction();
- try {
- // 消耗宝箱
- $consumeResult = $this->itemService->consumeItem(
- $userId,
- $chestId,
- null,
- $quantity,
- [
- 'source_type' => 'chest_open',
- 'source_id' => $chestId,
- 'details' => ['quantity' => $quantity],
- 'ip_address' => $options['ip_address'] ?? null,
- 'device_info' => $options['device_info'] ?? null,
- ]
- );
- // 获取宝箱掉落物品数量范围
- $minDropCount = $chest->numeric_attributes['min_drop_count'] ?? 1;
- $maxDropCount = $chest->numeric_attributes['max_drop_count'] ?? 1;
- $allResults = [];
- $allPityTriggered = false;
- $pityContentId = null;
- // 循环开启指定数量的宝箱
- for ($i = 0; $i < $quantity; $i++) {
- // 随机决定本次开启掉落的物品数量
- $dropCount = mt_rand($minDropCount, $maxDropCount);
- // 获取开启结果
- $openResult = $this->getChestOpenResult($userId, $chestId, $chestContents, $dropCount);
- // 添加物品到用户背包
- foreach ($openResult['items'] as $item) {
- $this->itemService->addItem(
- $userId,
- $item['item_id'],
- $item['quantity'],
- [
- 'source_type' => 'chest_open',
- 'source_id' => $chestId,
- 'details' => [
- 'chest_id' => $chestId,
- 'is_pity' => $item['is_pity'] ?? false,
- ],
- 'ip_address' => $options['ip_address'] ?? null,
- 'device_info' => $options['device_info'] ?? null,
- ]
- );
- }
- // 记录本次开启结果
- $allResults[] = $openResult['items'];
- // 更新保底触发状态
- if ($openResult['pity_triggered']) {
- $allPityTriggered = true;
- $pityContentId = $openResult['pity_content_id'];
- }
- }
- // 记录宝箱开启日志
- $openLog = ItemChestOpenLog::create([
- 'user_id' => $userId,
- 'chest_id' => $chestId,
- 'quantity' => $quantity,
- 'results' => $allResults,
- 'pity_triggered' => $allPityTriggered,
- 'pity_content_id' => $pityContentId,
- 'open_time' => now(),
- 'ip_address' => $options['ip_address'] ?? null,
- 'device_info' => $options['device_info'] ?? null,
- ]);
- DB::commit();
- return [
- 'success' => true,
- 'chest_id' => $chestId,
- 'quantity' => $quantity,
- 'results' => $allResults,
- 'pity_triggered' => $allPityTriggered,
- 'log_id' => $openLog->id,
- ];
- } catch (Exception $e) {
- DB::rollBack();
- throw $e;
- }
- }
- /**
- * 获取宝箱开启结果
- *
- * @param int $userId 用户ID
- * @param int $chestId 宝箱ID
- * @param Collection $chestContents 宝箱内容配置
- * @param int $dropCount 掉落物品数量
- * @return array 开启结果
- */
- protected function getChestOpenResult(int $userId, int $chestId, Collection $chestContents, int $dropCount): array
- {
- $items = [];
- $selectedContentIds = [];
- $pityTriggered = false;
- $pityContentId = null;
- // 获取用户保底计数
- $pityTimes = ItemPityTime::where('user_id', $userId)
- ->where('chest_id', $chestId)
- ->get()
- ->keyBy('chest_content_id');
- // 计算调整后的权重
- $contentsWithAdjustedWeight = $chestContents->map(function ($content) use ($pityTimes) {
- $pityCount = 0;
- if (isset($pityTimes[$content->id])) {
- $pityCount = $pityTimes[$content->id]->current_count;
- }
- $adjustedWeight = $content->getAdjustedWeight($pityCount);
- return [
- 'content' => $content,
- 'adjusted_weight' => $adjustedWeight,
- 'pity_count' => $pityCount,
- ];
- });
- // 检查是否有达到保底次数的内容
- $guaranteedContents = $contentsWithAdjustedWeight->filter(function ($item) {
- return $item['content']->pity_count > 0 && $item['pity_count'] >= $item['content']->pity_count;
- });
- // 如果有达到保底次数的内容,优先选择
- if ($guaranteedContents->isNotEmpty()) {
- $guaranteedContent = $guaranteedContents->first();
- $content = $guaranteedContent['content'];
- // 添加到结果
- $items[] = $this->processChestContent($content, true);
- $selectedContentIds[] = $content->id;
- // 重置保底计数
- $this->resetPityCount($userId, $chestId, $content->id);
- // 更新保底触发状态
- $pityTriggered = true;
- $pityContentId = $content->id;
- // 减少掉落数量
- $dropCount--;
- }
- // 如果还需要掉落物品,继续随机选择
- if ($dropCount > 0) {
- // 计算总权重
- $totalWeight = $contentsWithAdjustedWeight->sum('adjusted_weight');
- // 随机选择指定数量的物品
- for ($i = 0; $i < $dropCount; $i++) {
- // 过滤掉已选择的内容(如果不允许重复)
- $availableContents = $contentsWithAdjustedWeight->filter(function ($item) use ($selectedContentIds) {
- return !in_array($item['content']->id, $selectedContentIds) || $item['content']->allow_duplicate;
- });
- if ($availableContents->isEmpty()) {
- break;
- }
- // 重新计算总权重
- $availableTotalWeight = $availableContents->sum('adjusted_weight');
- // 随机选择
- $randomValue = mt_rand(1, (int)($availableTotalWeight * 1000)) / 1000;
- $currentWeight = 0;
- $selectedContent = null;
- foreach ($availableContents as $item) {
- $currentWeight += $item['adjusted_weight'];
- if ($randomValue <= $currentWeight) {
- $selectedContent = $item['content'];
- break;
- }
- }
- // 如果没有选中(可能由于浮点数精度问题),选择第一个
- if (!$selectedContent && $availableContents->isNotEmpty()) {
- $selectedContent = $availableContents->first()['content'];
- }
- if ($selectedContent) {
- // 添加到结果
- $items[] = $this->processChestContent($selectedContent, false);
- $selectedContentIds[] = $selectedContent->id;
- // 增加保底计数
- $this->incrementPityCount($userId, $chestId, $selectedContent->id);
- }
- }
- }
- // 增加其他内容的保底计数
- foreach ($chestContents as $content) {
- if (!in_array($content->id, $selectedContentIds) && $content->pity_count > 0) {
- $this->incrementPityCount($userId, $chestId, $content->id);
- }
- }
- return [
- 'items' => $items,
- 'pity_triggered' => $pityTriggered,
- 'pity_content_id' => $pityContentId,
- ];
- }
- /**
- * 处理宝箱内容
- *
- * @param ItemChestContent $content 宝箱内容
- * @param bool $isPity 是否为保底触发
- * @return array 处理结果
- */
- protected function processChestContent(ItemChestContent $content, bool $isPity): array
- {
- // 获取随机数量
- $quantity = $content->getRandomQuantity();
- // 如果是物品组,随机选择一个物品
- if ($content->isGroupContent()) {
- $group = $content->group;
- $item = $group->getRandomItem();
- if (!$item) {
- throw new Exception("物品组 {$group->id} 没有配置物品");
- }
- return [
- 'item_id' => $item->id,
- 'quantity' => $quantity,
- 'is_pity' => $isPity,
- 'content_id' => $content->id,
- ];
- }
- // 直接返回物品
- return [
- 'item_id' => $content->item_id,
- 'quantity' => $quantity,
- 'is_pity' => $isPity,
- 'content_id' => $content->id,
- ];
- }
- /**
- * 增加保底计数
- *
- * @param int $userId 用户ID
- * @param int $chestId 宝箱ID
- * @param int $contentId 内容ID
- * @return void
- */
- protected function incrementPityCount(int $userId, int $chestId, int $contentId): void
- {
- $pityTime = ItemPityTime::firstOrCreate(
- [
- 'user_id' => $userId,
- 'chest_id' => $chestId,
- 'chest_content_id' => $contentId,
- ],
- [
- 'current_count' => 0,
- ]
- );
- $pityTime->incrementCount();
- }
- /**
- * 重置保底计数
- *
- * @param int $userId 用户ID
- * @param int $chestId 宝箱ID
- * @param int $contentId 内容ID
- * @return void
- */
- protected function resetPityCount(int $userId, int $chestId, int $contentId): void
- {
- $pityTime = ItemPityTime::firstOrCreate(
- [
- 'user_id' => $userId,
- 'chest_id' => $chestId,
- 'chest_content_id' => $contentId,
- ],
- [
- 'current_count' => 0,
- ]
- );
- $pityTime->resetCount();
- }
- /**
- * 获取宝箱内容预览
- *
- * @param int $chestId 宝箱ID
- * @return array 宝箱内容预览
- */
- public function getChestContentPreview(int $chestId): array
- {
- // 获取宝箱信息
- $chest = ItemItem::findOrFail($chestId);
- // 检查是否为宝箱类型
- if ($chest->type != ITEM_TYPE::OPENABLE) {
- throw new Exception("物品 {$chestId} 不是宝箱类型");
- }
- // 获取宝箱内容配置
- $chestContents = ItemChestContent::where('chest_id', $chestId)
- ->with(['item', 'group.groupItems.item'])
- ->get();
- if ($chestContents->isEmpty()) {
- return [
- 'chest_id' => $chestId,
- 'chest_name' => $chest->name,
- 'contents' => [],
- ];
- }
- // 计算总权重
- $totalWeight = $chestContents->sum('weight');
- // 处理宝箱内容
- $contents = [];
- foreach ($chestContents as $content) {
- // 计算概率
- $probability = ($totalWeight > 0) ? ($content->weight / $totalWeight * 100) : 0;
- if ($content->isGroupContent()) {
- // 处理物品组
- $group = $content->group;
- $groupItems = [];
- // 计算物品组内总权重
- $groupTotalWeight = $group->groupItems->sum('weight');
- foreach ($group->groupItems as $groupItem) {
- // 计算物品在组内的概率
- $groupItemProbability = ($groupTotalWeight > 0) ? ($groupItem->weight / $groupTotalWeight * 100) : 0;
- // 计算物品的综合概率
- $combinedProbability = $probability * $groupItemProbability / 100;
- $groupItems[] = [
- 'item_id' => $groupItem->item_id,
- 'item_name' => $groupItem->item->name,
- 'item_icon' => $groupItem->item->icon,
- 'group_probability' => $groupItemProbability,
- 'combined_probability' => $combinedProbability,
- ];
- }
- $contents[] = [
- 'content_id' => $content->id,
- 'type' => 'group',
- 'group_id' => $group->id,
- 'group_name' => $group->name,
- 'min_quantity' => $content->min_quantity,
- 'max_quantity' => $content->max_quantity,
- 'probability' => $probability,
- 'pity_count' => $content->pity_count,
- 'items' => $groupItems,
- ];
- } else {
- // 处理单个物品
- $item = $content->item;
- $contents[] = [
- 'content_id' => $content->id,
- 'type' => 'item',
- 'item_id' => $item->id,
- 'item_name' => $item->name,
- 'item_icon' => $item->icon,
- 'min_quantity' => $content->min_quantity,
- 'max_quantity' => $content->max_quantity,
- 'probability' => $probability,
- 'pity_count' => $content->pity_count,
- ];
- }
- }
- return [
- 'chest_id' => $chestId,
- 'chest_name' => $chest->name,
- 'min_drop_count' => $chest->numeric_attributes['min_drop_count'] ?? 1,
- 'max_drop_count' => $chest->numeric_attributes['max_drop_count'] ?? 1,
- 'contents' => $contents,
- ];
- }
- }
|