disasters)) { return collect(); } $disasters = collect($crop->disasters); return $disasters->map(function ($disaster) { return DisasterInfoDto::fromArray($disaster); }); } catch (\Exception $e) { Log::error('获取作物灾害信息失败', [ 'crop_id' => $cropId, 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString() ]); return collect(); } } /** * 获取作物的活跃灾害 * * @param int $cropId * @return Collection */ public function getActiveDisasters(int $cropId): Collection { try { $crop = FarmCrop::find($cropId); if (!$crop || empty($crop->disasters)) { return collect(); } $disasters = collect($crop->disasters)->filter(function ($disaster) { return ($disaster['status'] ?? '') === 'active'; }); return $disasters->map(function ($disaster) { return DisasterInfoDto::fromArray($disaster); }); } catch (\Exception $e) { Log::error('获取作物活跃灾害失败', [ 'crop_id' => $cropId, 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString() ]); return collect(); } } /** * 生成灾害 * * @param int $cropId * @return DisasterInfoDto|null */ public function generateDisaster(int $cropId): ?DisasterInfoDto { try { $crop = FarmCrop::find($cropId); if (!$crop) { throw new \Exception('作物不存在'); } /** * @var FarmLand $land */ $land = $crop->land; if (!$land) { throw new \Exception('土地不存在'); } // 只在发芽期、生长期和果实期生成灾害 if (!in_array($crop->growth_stage, [ GROWTH_STAGE::SPROUT, GROWTH_STAGE::GROWTH, GROWTH_STAGE::FRUIT ])) { return null; } // 注意:根据文档,作物灾害和土地无关,不需要检查土地状态 $userId = $crop->user_id; $seed = $crop->seed; // 获取种子的灾害抵抗属性 $disasterResistance = $seed->disaster_resistance ?? []; // 获取土地的灾害抵抗属性(数据库存储百分比,需要除以100转换为小数) $landDisasterResistance = ($land->landType->disaster_resistance ?? 0) / 100; // 检查用户是否有有效的神灵加持 $activeBuffs = FarmGodBuff::where('user_id', $userId) ->where('expire_time', '>', now()) ->pluck('buff_type') ->toArray(); // 尝试生成灾害(支持多种灾害) $disasterInfos = $this->tryGenerateDisasterForCrop($crop, $disasterResistance, $landDisasterResistance, $activeBuffs); if (!empty($disasterInfos)) { $disasterDtos = $this->applyDisastersToCrop($crop, $disasterInfos); Log::info('灾害生成成功', [ 'user_id' => $userId, 'crop_id' => $crop->id, 'land_id' => $land->id, 'disaster_count' => count($disasterInfos), 'disaster_types' => array_column($disasterInfos, 'type') ]); return $disasterDtos[0] ?? null; // 返回第一个灾害信息保持兼容性 } return null; } catch (\Exception $e) { \UCore\Helper\Logger::exception('灾害生成失败', $e, [ 'crop_id' => $cropId, ]); return null; } } /** * 尝试为作物生成灾害(支持多种灾害同时发生) * * @param FarmCrop $crop * @param mixed $disasterResistance * @param float $landDisasterResistance * @param array $activeBuffs * @return array 生成的灾害信息数组 */ private function tryGenerateDisasterForCrop(FarmCrop $crop, $disasterResistance, float $landDisasterResistance, array $activeBuffs): array { // 灾害类型及其基础概率 $disasterTypes = DisasterService::getRate(); $generatedDisasters = []; // 对每种灾害类型都进行判定 foreach ($disasterTypes as $disasterType => $baseProb) { // 计算最终概率,考虑种子抵抗、土地抵抗和神灵加持 $seedResistance = 0; if ($disasterResistance) { if (is_object($disasterResistance) && method_exists($disasterResistance, 'getResistance')) { // 如果是 DisasterResistanceCast 对象(数据库存储百分比,需要除以100转换为小数) $seedResistance = $disasterResistance->getResistance($disasterType) / 100; } elseif (is_array($disasterResistance)) { // 如果是数组格式(数据库存储百分比,需要除以100转换为小数) $seedResistance = ($disasterResistance[DisasterService::getDisasterKey($disasterType)] ?? 0) / 100; } } $finalProb = $baseProb - $seedResistance - $landDisasterResistance; // 如果有对应的神灵加持,则不生成该类型的灾害 $buffType = DISASTER_TYPE::getPreventBuffType($disasterType); $hasGodBuff = $buffType && in_array($buffType, $activeBuffs); if ($hasGodBuff) { $finalProb = 0; } // 确保概率在有效范围内 $finalProb = max(0, min(1, $finalProb)); // 生成随机数 $randomNumber = mt_rand(1, 100); $threshold = $finalProb * 100; // 详细的debug日志 Log::debug('灾害生成详细信息', [ 'crop_id' => $crop->id, 'user_id' => $crop->user_id, 'growth_stage' => $crop->growth_stage->valueInt(), 'disaster_type' => $disasterType, 'disaster_type_name' => DISASTER_TYPE::getName($disasterType), 'base_probability' => $baseProb, 'seed_resistance' => $seedResistance, 'land_resistance' => $landDisasterResistance, 'active_buffs' => $activeBuffs, 'god_buff_type' => $buffType, 'has_god_buff' => $hasGodBuff, 'final_probability' => $finalProb, 'threshold' => $threshold, 'random_number' => $randomNumber, 'will_generate' => $randomNumber <= $threshold, 'disaster_resistance_type' => is_object($disasterResistance) ? get_class($disasterResistance) : gettype($disasterResistance), 'disaster_resistance_raw' => is_object($disasterResistance) ? 'object' : $disasterResistance ]); // 随机决定是否生成该类型灾害 if ($randomNumber <= $threshold) { $disasterInfo = [ 'type' => $disasterType, 'generated_ts' => now()->toDateTimeString(), 'status' => 'active' ]; $generatedDisasters[] = $disasterInfo; Log::debug('灾害生成成功', [ 'crop_id' => $crop->id, 'disaster_type' => $disasterType, 'disaster_type_name' => DISASTER_TYPE::getName($disasterType), 'final_probability' => $finalProb ]); } else { Log::info('灾害未生成', [ 'crop_id' => $crop->id, 'disaster_type' => $disasterType, 'disaster_type_name' => DISASTER_TYPE::getName($disasterType), 'reason' => '随机数超过阈值', 'random_number' => $randomNumber, 'threshold' => $threshold, 'final_probability' => $finalProb ]); } } return $generatedDisasters; } /** * 将多个灾害应用到作物上 * * @param FarmCrop $crop * @param array $disasterInfos * @return array */ private function applyDisastersToCrop(FarmCrop $crop, array $disasterInfos): array { if (empty($disasterInfos)) { return []; } // 更新作物灾害信息 $disasters = $crop->disasters ?? []; $disasters = array_merge($disasters, $disasterInfos); $crop->disasters = $disasters; // TODO: 根据文档,作物灾害和土地无关,此处更新土地状态可能需要重新评估 // 更新土地状态为灾害 $land = $crop->land; $oldLandStatus = $land->status; $land->status = LAND_STATUS::DISASTER->value; // 保存更改 $crop->save(); $land->save(); // 如果土地状态发生了变化,触发土地状态变更事件 if ($oldLandStatus !== $land->status) { event(new \App\Module\Farm\Events\LandStatusChangedEvent($crop->user_id, $land->id, $oldLandStatus, $land->status)); } // 触发灾害生成事件 $disasterDtos = []; foreach ($disasterInfos as $disasterInfo) { event(new DisasterGeneratedEvent($crop->user_id, $crop, $disasterInfo['type'], $disasterInfo)); $disasterDtos[] = DisasterInfoDto::fromArray($disasterInfo); // 记录灾害产生事件 FarmCropLog::logDisasterOccurred($crop->user_id, $crop->land_id, $crop->id, $crop->seed_id, [ 'disaster_type' => $disasterInfo['type'], 'disaster_info' => $disasterInfo, 'growth_stage' => $crop->growth_stage->value, 'land_type' => $land->land_type ?? 1, 'old_land_status' => $oldLandStatus, 'new_land_status' => $land->status, 'generated_at' => $disasterInfo['generated_ts'] ]); } return $disasterDtos; } /** * 批量获取需要检查灾害的作物并处理 * * @param int|null $checkIntervalMinutes 检查间隔(分钟) * @return array 生成结果统计 */ public function generateDisasterBatchs(?int $checkIntervalMinutes = null): array { if ($checkIntervalMinutes === null) { $checkIntervalMinutes = DisasterService::getCheckInterval(); } $checkTime = now()->subMinutes($checkIntervalMinutes); // 获取需要检查灾害的作物: // 1. 必须在发芽期、生长期或果实期 // 2. 当前阶段可以产生灾害 (can_disaster = 1) // 3. 满足时间检查条件(首次检查或超过检查间隔) $crops = FarmCrop::whereIn('growth_stage', [GROWTH_STAGE::SPROUT, GROWTH_STAGE::GROWTH, GROWTH_STAGE::FRUIT]) ->where('can_disaster', true) ->where(function ($query) use ($checkTime) { $query->whereNull('last_disaster_check_time') ->orWhere('last_disaster_check_time', '<', $checkTime); }) ->with(['land.landType', 'seed', 'user.buffs' => function ($query) { $query->where('expire_time', '>', now()); }]) ->get(); $totalCount = $crops->count(); $generatedCount = 0; $skippedCount = 0; foreach ($crops as $crop) { $result = $this->generateDisasters($crop); if ($result === 'generated') { $generatedCount++; } elseif ($result === 'skipped') { $skippedCount++; } } return [ 'total' => $totalCount, 'generated' => $generatedCount, 'skipped' => $skippedCount ]; } /** * 为单个作物生成灾害 * * @param FarmCrop $crop * @return string 处理结果:'generated', 'skipped', 'failed' */ public function generateDisasters(FarmCrop $crop): string { try { // 1. 先进行基础检查和计算(不在事务中,减少锁时间) // 检查用户是否存在 if (!$crop->user) { Log::warning('作物关联的用户不存在,跳过灾害生成', [ 'crop_id' => $crop->id, 'user_id' => $crop->user_id ]); return 'skipped'; } // 注意:根据文档,作物灾害和土地无关,不需要检查土地状态 // 获取相关数据并计算灾害 $disasterResistance = $crop->seed->disaster_resistance ?? null; $landDisasterResistance = ($crop->land->landType->disaster_resistance ?? 0) / 100; $activeBuffs = $crop->user->buffs->pluck('buff_type')->toArray(); // 尝试生成灾害(支持多种灾害) $disasterInfos = $this->tryGenerateDisasterForCrop($crop, $disasterResistance, $landDisasterResistance, $activeBuffs); // 2. 开启事务进行有锁更新 return DB::transaction(function () use ($crop, $disasterInfos) { // 使用行锁重新获取作物,确保数据一致性 $lockedCrop = FarmCrop::where('id', $crop->id) ->lockForUpdate() ->first(); // 如果作物不存在或已被软删除,跳过处理 if (!$lockedCrop || $lockedCrop->trashed()) { Log::info('作物已被删除,跳过灾害生成', [ 'crop_id' => $crop->id, 'user_id' => $crop->user_id ]); return 'skipped'; } // 更新检查时间 $lockedCrop->last_disaster_check_time = now(); // 如果有灾害需要生成,应用到作物 if (!empty($disasterInfos)) { // 应用灾害到作物 $this->applyDisastersToCrop($lockedCrop, $disasterInfos); // 生成灾害后,设置当前阶段不能再产生灾害 $lockedCrop->can_disaster = false; $lockedCrop->save(); Log::info('单个作物灾害生成成功', [ 'crop_id' => $lockedCrop->id, 'user_id' => $lockedCrop->user_id, 'disaster_count' => count($disasterInfos), 'disaster_types' => array_column($disasterInfos, 'type') ]); return 'generated'; } else { // 没有生成灾害,但需要保存检查时间 $lockedCrop->save(); return 'checked'; } }); } catch (\Exception $e) { Log::error('单个作物灾害生成失败', [ 'crop_id' => $crop->id, 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString() ]); return 'failed'; } } /** * 批量检查并生成灾害(兼容性方法) * * @param int|null $checkIntervalMinutes 检查间隔(分钟) * @return array 生成结果统计 */ public function batchGenerateDisasters(?int $checkIntervalMinutes = null): array { return $this->generateDisasterBatchs($checkIntervalMinutes); } }