CropLogic.php 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702
  1. <?php
  2. namespace App\Module\Farm\Logics;
  3. use App\Module\Farm\Dtos\CropInfoDto;
  4. use App\Module\Farm\Dtos\HarvestResultDto;
  5. use App\Module\Farm\Enums\GROWTH_STAGE;
  6. use App\Module\Farm\Enums\LAND_STATUS;
  7. use App\Module\Farm\Events\CropGrowthStageChangedEvent;
  8. use App\Module\Farm\Events\CropHarvestedEvent;
  9. use App\Module\Farm\Events\CropPlantedEvent;
  10. use App\Module\Farm\Events\DisasterClearedEvent;
  11. use App\Module\Farm\Models\FarmCrop;
  12. use App\Module\Farm\Models\FarmHarvestLog;
  13. use App\Module\Farm\Models\FarmLand;
  14. use App\Module\Farm\Models\FarmSeed;
  15. use App\Module\Farm\Models\FarmSeedOutput;
  16. use Illuminate\Support\Facades\DB;
  17. use Illuminate\Support\Facades\Log;
  18. /**
  19. * 作物管理逻辑
  20. */
  21. class CropLogic
  22. {
  23. /**
  24. * 获取作物信息
  25. *
  26. * @param int $cropId
  27. * @return CropInfoDto|null
  28. */
  29. public function getCropInfo(int $cropId): ?CropInfoDto
  30. {
  31. try {
  32. $crop = FarmCrop::find($cropId);
  33. if (!$crop) {
  34. return null;
  35. }
  36. return CropInfoDto::fromModel($crop);
  37. } catch (\Exception $e) {
  38. Log::error('获取作物信息失败', [
  39. 'crop_id' => $cropId,
  40. 'error' => $e->getMessage(),
  41. 'trace' => $e->getTraceAsString()
  42. ]);
  43. return null;
  44. }
  45. }
  46. /**
  47. * 获取土地上的作物信息
  48. *
  49. * @param int $landId
  50. * @return CropInfoDto|null
  51. */
  52. public function getCropByLandId(int $landId): ?CropInfoDto
  53. {
  54. try {
  55. $crop = FarmCrop::where('land_id', $landId)->first();
  56. if (!$crop) {
  57. return null;
  58. }
  59. return CropInfoDto::fromModel($crop);
  60. } catch (\Exception $e) {
  61. Log::error('获取土地作物信息失败', [
  62. 'land_id' => $landId,
  63. 'error' => $e->getMessage(),
  64. 'trace' => $e->getTraceAsString()
  65. ]);
  66. return null;
  67. }
  68. }
  69. /**
  70. * 种植作物
  71. *
  72. * @param int $userId
  73. * @param int $landId
  74. * @param int $seedId
  75. * @return CropInfoDto|null
  76. */
  77. public function plantCrop(int $userId, int $landId, int $seedId): ?CropInfoDto
  78. {
  79. try {
  80. // 开启事务
  81. DB::beginTransaction();
  82. // 获取土地信息
  83. $land = FarmLand::where('id', $landId)
  84. ->where('user_id', $userId)
  85. ->first();
  86. if (!$land) {
  87. throw new \Exception('土地不存在');
  88. }
  89. // 检查土地状态
  90. if ($land->status !== LAND_STATUS::IDLE->value) {
  91. throw new \Exception('土地状态不允许种植');
  92. }
  93. // 获取种子信息
  94. $seed = FarmSeed::find($seedId);
  95. if (!$seed) {
  96. throw new \Exception('种子不存在');
  97. }
  98. // 创建作物记录
  99. $crop = new FarmCrop();
  100. $crop->land_id = $landId;
  101. $crop->user_id = $userId;
  102. $crop->seed_id = $seedId;
  103. $crop->plant_time = now();
  104. $crop->growth_stage = GROWTH_STAGE::SEED->value;
  105. $crop->stage_end_time = now()->addSeconds($seed->seed_time);
  106. $crop->disasters = [];
  107. $crop->fertilized = false;
  108. $crop->save();
  109. // 更新土地状态
  110. $land->status = LAND_STATUS::PLANTING->value;
  111. $land->save();
  112. // 提交事务
  113. DB::commit();
  114. // 触发作物种植事件
  115. event(new CropPlantedEvent($userId, $land, $crop));
  116. Log::info('作物种植成功', [
  117. 'user_id' => $userId,
  118. 'land_id' => $landId,
  119. 'seed_id' => $seedId,
  120. 'crop_id' => $crop->id
  121. ]);
  122. return CropInfoDto::fromModel($crop);
  123. } catch (\Exception $e) {
  124. // 回滚事务
  125. DB::rollBack();
  126. Log::error('作物种植失败', [
  127. 'user_id' => $userId,
  128. 'land_id' => $landId,
  129. 'seed_id' => $seedId,
  130. 'error' => $e->getMessage(),
  131. 'trace' => $e->getTraceAsString()
  132. ]);
  133. return null;
  134. }
  135. }
  136. /**
  137. * 收获作物
  138. *
  139. * @param int $userId
  140. * @param int $landId
  141. * @return HarvestResultDto|null
  142. */
  143. public function harvestCrop(int $userId, int $landId): ?HarvestResultDto
  144. {
  145. try {
  146. // 开启事务
  147. DB::beginTransaction();
  148. // 获取土地信息
  149. $land = FarmLand::where('id', $landId)
  150. ->where('user_id', $userId)
  151. ->first();
  152. if (!$land) {
  153. throw new \Exception('土地不存在');
  154. }
  155. // 检查土地状态
  156. if ($land->status !== LAND_STATUS::HARVESTABLE->value) {
  157. throw new \Exception('土地状态不允许收获');
  158. }
  159. // 获取作物信息
  160. $crop = FarmCrop::where('land_id', $landId)->first();
  161. if (!$crop) {
  162. throw new \Exception('作物不存在');
  163. }
  164. // 检查作物生长阶段
  165. if ($crop->growth_stage !== GROWTH_STAGE::MATURE->value) {
  166. throw new \Exception('作物未成熟,不能收获');
  167. }
  168. // 获取种子信息
  169. $seed = $crop->seed;
  170. if (!$seed) {
  171. throw new \Exception('种子信息不存在');
  172. }
  173. // 随机选择产出物品
  174. $outputInfo = $this->getRandomOutput($seed->id);
  175. $outputItemId = $outputInfo['item_id'];
  176. $outputAmount = mt_rand($outputInfo['min_amount'], $outputInfo['max_amount']);
  177. // 创建收获记录
  178. $harvestLog = new FarmHarvestLog();
  179. $harvestLog->user_id = $userId;
  180. $harvestLog->land_id = $landId;
  181. $harvestLog->crop_id = $crop->id;
  182. $harvestLog->seed_id = $seed->id;
  183. $harvestLog->output_amount = $outputAmount;
  184. $harvestLog->harvest_time = now();
  185. $harvestLog->created_at = now();
  186. $harvestLog->save();
  187. // 删除作物记录
  188. $crop->delete();
  189. // 更新土地状态
  190. $land->status = LAND_STATUS::IDLE;
  191. $land->save();
  192. // 提交事务
  193. DB::commit();
  194. // 触发作物收获事件
  195. event(new CropHarvestedEvent($userId, $land, $crop, $harvestLog, $outputItemId, $outputAmount));
  196. Log::info('作物收获成功', [
  197. 'user_id' => $userId,
  198. 'land_id' => $landId,
  199. 'crop_id' => $crop->id,
  200. 'seed_id' => $seed->id,
  201. 'output_item_id' => $outputItemId,
  202. 'output_amount' => $outputAmount,
  203. 'harvest_log_id' => $harvestLog->id
  204. ]);
  205. return HarvestResultDto::fromModel($harvestLog, $outputItemId);
  206. } catch (\Exception $e) {
  207. // 回滚事务
  208. DB::rollBack();
  209. Log::error('作物收获失败', [
  210. 'user_id' => $userId,
  211. 'land_id' => $landId,
  212. 'error' => $e->getMessage(),
  213. 'trace' => $e->getTraceAsString()
  214. ]);
  215. return null;
  216. }
  217. }
  218. /**
  219. * 使用化肥
  220. *
  221. * @param int $userId
  222. * @param int $landId
  223. * @return bool
  224. */
  225. public function useFertilizer(int $userId, int $landId): bool
  226. {
  227. try {
  228. // 获取土地信息
  229. $land = FarmLand::where('id', $landId)
  230. ->where('user_id', $userId)
  231. ->first();
  232. if (!$land) {
  233. throw new \Exception('土地不存在');
  234. }
  235. // 检查土地状态
  236. if ($land->status !== LAND_STATUS::PLANTING) {
  237. throw new \Exception('土地状态不允许使用化肥');
  238. }
  239. // 获取作物信息
  240. $crop = FarmCrop::where('land_id', $landId)->first();
  241. if (!$crop) {
  242. throw new \Exception('作物不存在');
  243. }
  244. // 检查作物生长阶段
  245. if (!GROWTH_STAGE::canUseFertilizer($crop->growth_stage)) {
  246. throw new \Exception('当前生长阶段不能使用化肥');
  247. }
  248. // 检查是否已经使用过化肥
  249. if ($crop->fertilized) {
  250. throw new \Exception('当前阶段已经使用过化肥');
  251. }
  252. // 更新作物信息
  253. $crop->fertilized = true;
  254. // 缩短当前阶段时间(假设化肥效果是缩短30%的时间)
  255. if ($crop->stage_end_time) {
  256. $currentTime = now();
  257. $endTime = $crop->stage_end_time;
  258. $remainingTime = $currentTime->diffInSeconds($endTime, false);
  259. if ($remainingTime > 0) {
  260. $reducedTime = (int)($remainingTime * 0.3);
  261. $crop->stage_end_time = $endTime->subSeconds($reducedTime);
  262. }
  263. }
  264. $crop->save();
  265. Log::info('使用化肥成功', [
  266. 'user_id' => $userId,
  267. 'land_id' => $landId,
  268. 'crop_id' => $crop->id,
  269. 'growth_stage' => $crop->growth_stage,
  270. 'stage_end_time' => $crop->stage_end_time
  271. ]);
  272. return true;
  273. } catch (\Exception $e) {
  274. Log::error('使用化肥失败', [
  275. 'user_id' => $userId,
  276. 'land_id' => $landId,
  277. 'error' => $e->getMessage(),
  278. 'trace' => $e->getTraceAsString()
  279. ]);
  280. return false;
  281. }
  282. }
  283. /**
  284. * 清理灾害
  285. *
  286. * @param int $userId
  287. * @param int $landId
  288. * @param int $disasterType
  289. * @return bool
  290. */
  291. public function clearDisaster(int $userId, int $landId, int $disasterType): bool
  292. {
  293. try {
  294. // 获取土地信息
  295. $land = FarmLand::where('id', $landId)
  296. ->where('user_id', $userId)
  297. ->first();
  298. if (!$land) {
  299. throw new \Exception('土地不存在');
  300. }
  301. // 检查土地状态
  302. if ($land->status !== LAND_STATUS::DISASTER->value) {
  303. throw new \Exception('土地没有灾害');
  304. }
  305. // 获取作物信息
  306. $crop = FarmCrop::where('land_id', $landId)->first();
  307. if (!$crop) {
  308. throw new \Exception('作物不存在');
  309. }
  310. // 检查灾害是否存在
  311. $disasters = $crop->disasters ?? [];
  312. $disasterIndex = -1;
  313. $disasterInfo = null;
  314. foreach ($disasters as $index => $disaster) {
  315. if (($disaster['type'] ?? 0) == $disasterType && ($disaster['status'] ?? '') === 'active') {
  316. $disasterIndex = $index;
  317. $disasterInfo = $disaster;
  318. break;
  319. }
  320. }
  321. if ($disasterIndex === -1 || !$disasterInfo) {
  322. throw new \Exception('指定类型的灾害不存在');
  323. }
  324. // 更新灾害状态
  325. $disasters[$disasterIndex]['status'] = 'cleared';
  326. $disasters[$disasterIndex]['cleared_at'] = now()->toDateTimeString();
  327. $crop->disasters = $disasters;
  328. // 检查是否还有其他活跃灾害
  329. $hasActiveDisaster = false;
  330. foreach ($disasters as $disaster) {
  331. if (($disaster['status'] ?? '') === 'active') {
  332. $hasActiveDisaster = true;
  333. break;
  334. }
  335. }
  336. // 如果没有其他活跃灾害,更新土地状态
  337. if (!$hasActiveDisaster) {
  338. $land->status = LAND_STATUS::PLANTING->value;
  339. }
  340. // 保存更改
  341. $crop->save();
  342. $land->save();
  343. // 触发灾害清理事件
  344. event(new DisasterClearedEvent($userId, $crop, $disasterType, $disasterInfo));
  345. Log::info('灾害清理成功', [
  346. 'user_id' => $userId,
  347. 'land_id' => $landId,
  348. 'crop_id' => $crop->id,
  349. 'disaster_type' => $disasterType
  350. ]);
  351. return true;
  352. } catch (\Exception $e) {
  353. Log::error('灾害清理失败', [
  354. 'user_id' => $userId,
  355. 'land_id' => $landId,
  356. 'disaster_type' => $disasterType,
  357. 'error' => $e->getMessage(),
  358. 'trace' => $e->getTraceAsString()
  359. ]);
  360. return false;
  361. }
  362. }
  363. /**
  364. * 铲除作物
  365. *
  366. * @param int $userId
  367. * @param int $landId
  368. * @return bool
  369. */
  370. public function removeCrop(int $userId, int $landId): bool
  371. {
  372. try {
  373. // 开启事务
  374. DB::beginTransaction();
  375. // 获取土地信息
  376. $land = FarmLand::where('id', $landId)
  377. ->where('user_id', $userId)
  378. ->first();
  379. if (!$land) {
  380. throw new \Exception('土地不存在');
  381. }
  382. // 检查土地状态
  383. if ($land->status === LAND_STATUS::IDLE->value) {
  384. throw new \Exception('土地上没有作物');
  385. }
  386. // 获取作物信息
  387. $crop = FarmCrop::where('land_id', $landId)->first();
  388. if (!$crop) {
  389. // 如果没有作物但土地状态不是空闲,修正土地状态
  390. $land->status = LAND_STATUS::IDLE->value;
  391. $land->save();
  392. DB::commit();
  393. return true;
  394. }
  395. // 删除作物记录
  396. $crop->delete();
  397. // 更新土地状态
  398. $land->status = LAND_STATUS::IDLE->value;
  399. $land->save();
  400. // 提交事务
  401. DB::commit();
  402. Log::info('铲除作物成功', [
  403. 'user_id' => $userId,
  404. 'land_id' => $landId,
  405. 'crop_id' => $crop->id
  406. ]);
  407. return true;
  408. } catch (\Exception $e) {
  409. // 回滚事务
  410. DB::rollBack();
  411. Log::error('铲除作物失败', [
  412. 'user_id' => $userId,
  413. 'land_id' => $landId,
  414. 'error' => $e->getMessage(),
  415. 'trace' => $e->getTraceAsString()
  416. ]);
  417. return false;
  418. }
  419. }
  420. /**
  421. * 更新作物生长阶段
  422. *
  423. * @param int $cropId
  424. * @return bool
  425. */
  426. public function updateGrowthStage(int $cropId): bool
  427. {
  428. try {
  429. // 获取作物信息
  430. $crop = FarmCrop::find($cropId);
  431. if (!$crop) {
  432. throw new \Exception('作物不存在');
  433. }
  434. // 检查是否需要更新
  435. if (!$crop->stage_end_time || $crop->stage_end_time > now()) {
  436. return false;
  437. }
  438. // 获取当前生长阶段
  439. $oldStage = $crop->growth_stage;
  440. // 计算新的生长阶段
  441. $newStage = $this->calculateNextStage($crop);
  442. // 如果阶段没有变化,不需要更新
  443. if ($newStage === $oldStage) {
  444. return false;
  445. }
  446. // 计算新阶段的结束时间
  447. $stageEndTime = $this->calculateStageEndTime($crop, $newStage);
  448. // 更新作物信息
  449. $crop->growth_stage = $newStage;
  450. $crop->stage_end_time = $stageEndTime;
  451. $crop->fertilized = false; // 重置施肥状态
  452. $crop->save();
  453. // 触发生长阶段变更事件
  454. event(new CropGrowthStageChangedEvent($crop->user_id, $crop, $oldStage, $newStage));
  455. Log::info('作物生长阶段更新成功', [
  456. 'crop_id' => $cropId,
  457. 'user_id' => $crop->user_id,
  458. 'old_stage' => $oldStage,
  459. 'new_stage' => $newStage,
  460. 'stage_end_time' => $stageEndTime
  461. ]);
  462. return true;
  463. } catch (\Exception $e) {
  464. Log::error('作物生长阶段更新失败', [
  465. 'crop_id' => $cropId,
  466. 'error' => $e->getMessage(),
  467. 'trace' => $e->getTraceAsString()
  468. ]);
  469. return false;
  470. }
  471. }
  472. /**
  473. * 计算下一个生长阶段
  474. *
  475. * @param FarmCrop $crop
  476. * @return int
  477. */
  478. private function calculateNextStage(FarmCrop $crop): int
  479. {
  480. $currentStage = $crop->growth_stage;
  481. // 如果当前是成熟期,且超过一定时间,则进入枯萎期
  482. if ($currentStage === GROWTH_STAGE::MATURE) {
  483. // 成熟期持续时间,默认为24小时
  484. $matureDuration = 24 * 60 * 60;
  485. // 如果成熟期已经超过指定时间,则进入枯萎期
  486. if ($crop->stage_end_time && now()->diffInSeconds($crop->stage_end_time->subSeconds($matureDuration)) > $matureDuration) {
  487. return GROWTH_STAGE::WITHERED;
  488. }
  489. return GROWTH_STAGE::MATURE;
  490. }
  491. // 正常阶段递增
  492. return $currentStage + 1;
  493. }
  494. /**
  495. * 计算阶段结束时间
  496. *
  497. * @param FarmCrop $crop
  498. * @param int $stage
  499. * @return \Carbon\Carbon|null
  500. */
  501. private function calculateStageEndTime(FarmCrop $crop, int $stage)
  502. {
  503. $seed = $crop->seed;
  504. if (!$seed) {
  505. return null;
  506. }
  507. $now = now();
  508. switch ($stage) {
  509. case GROWTH_STAGE::SEED:
  510. return $now->addSeconds($seed->seed_time);
  511. case GROWTH_STAGE::SPROUT:
  512. return $now->addSeconds($seed->sprout_time);
  513. case GROWTH_STAGE::GROWTH:
  514. return $now->addSeconds($seed->growth_time);
  515. case GROWTH_STAGE::MATURE:
  516. // 成熟期持续24小时后进入枯萎期
  517. return $now->addHours(24);
  518. case GROWTH_STAGE::WITHERED:
  519. // 枯萎期没有结束时间
  520. return null;
  521. default:
  522. return null;
  523. }
  524. }
  525. /**
  526. * 获取随机产出
  527. *
  528. * @param int $seedId
  529. * @return array
  530. */
  531. private function getRandomOutput(int $seedId): array
  532. {
  533. // 获取种子的所有产出配置
  534. $outputs = FarmSeedOutput::where('seed_id', $seedId)->get();
  535. if ($outputs->isEmpty()) {
  536. // 如果没有产出配置,使用种子的默认产出
  537. $seed = FarmSeed::find($seedId);
  538. return [
  539. 'item_id' => $seed->item_id,
  540. 'min_amount' => $seed->min_output,
  541. 'max_amount' => $seed->max_output,
  542. ];
  543. }
  544. // 按概率排序
  545. $outputs = $outputs->sortByDesc('probability');
  546. // 获取默认产出
  547. $defaultOutput = $outputs->firstWhere('is_default', true);
  548. // 随机选择产出
  549. $random = mt_rand(1, 100);
  550. $cumulativeProbability = 0;
  551. foreach ($outputs as $output) {
  552. $cumulativeProbability += $output->probability;
  553. if ($random <= $cumulativeProbability) {
  554. return [
  555. 'item_id' => $output->item_id,
  556. 'min_amount' => $output->min_amount,
  557. 'max_amount' => $output->max_amount,
  558. ];
  559. }
  560. }
  561. // 如果随机值超过了所有概率之和,使用默认产出
  562. if ($defaultOutput) {
  563. return [
  564. 'item_id' => $defaultOutput->item_id,
  565. 'min_amount' => $defaultOutput->min_amount,
  566. 'max_amount' => $defaultOutput->max_amount,
  567. ];
  568. }
  569. // 如果没有默认产出,使用第一个产出
  570. $firstOutput = $outputs->first();
  571. return [
  572. 'item_id' => $firstOutput->item_id,
  573. 'min_amount' => $firstOutput->min_amount,
  574. 'max_amount' => $firstOutput->max_amount,
  575. ];
  576. }
  577. }