itemService = $itemService; $this->chestContentLogic = new ChestContentLogic(); $this->itemLogic = new ItemLogic(); $this->pityTimeLogic = new PityTimeLogic(); } /** * 开启宝箱 * * @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 = Item::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 = $this->chestContentLogic->getAdjustedWeight($content, $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 = $this->chestContentLogic->getRandomQuantity($content); // 如果是物品组,随机选择一个物品 if ($this->chestContentLogic->isGroupContent($content)) { $group = $content->group; $groupLogic = new \App\Module\GameItems\Logics\Group(); $item = $groupLogic->getRandomItem($group); 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, ] ); $this->pityTimeLogic->incrementCount($pityTime); } /** * 重置保底计数 * * @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, ] ); $this->pityTimeLogic->resetCount($pityTime); } /** * 获取宝箱内容预览 * * @param int $chestId 宝箱ID * @return array 宝箱内容预览 */ public function getChestContentPreview(int $chestId): array { // 获取宝箱信息 $chest = Item::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, ]; } }