heartbeatConfig(); // 2. 获取 AI 实体 $user = User::where('username', 'AI小班长')->first(); if (! $user) { return Command::SUCCESS; } // 心跳开始前,若手上金币已高于 100 万,则先把超出的部分转入银行。 if ((int) ($user->jjb ?? 0) > AiFinanceService::AVAILABLE_GOLD_RESERVE) { $this->aiFinance->bankExcessGold($user); } // 2.5 自动每日签到(今日已签时 claim() 幂等返回,不重复发奖) if ($this->performDailySignIn($user)) { // 签到可能发放经验、金币或魅力,后续心跳计算必须基于最新余额。 $user->refresh(); } // 3. 常规心跳经验与金币发放 // (模拟前端每30-60秒发一次心跳的过程,此处每分钟跑一次,发放单人心跳奖励) $expGain = $this->parseRewardValue($config['exp_per_heartbeat']); if ($expGain > 0) { $expMultiplier = $this->vipService->getExpMultiplier($user); $actualExpGain = (int) round($expGain * $expMultiplier); $user->exp_num += $actualExpGain; } $jjbGain = $this->parseRewardValue($config['jjb_per_heartbeat']); if ($jjbGain > 0) { $jjbMultiplier = $this->vipService->getJjbMultiplier($user); $actualJjbGain = (int) round($jjbGain * $jjbMultiplier); $user->jjb = ($user->jjb ?? 0) + $actualJjbGain; } $user->save(); // 4. 重算等级(基础心跳升级) $superLevel = (int) $config['superlevel']; $leveledUp = $this->calculateNewLevel($user, $superLevel); // 5. 随机事件触发 $eventChance = (int) $config['auto_event_chance']; if ($eventChance > 0 && rand(1, 100) <= $eventChance) { $autoEvent = Autoact::randomEvent(); if ($autoEvent) { $hasCurrencyChange = false; // 执行随机事件的金钱经验惩奖 if ($autoEvent->exp_change !== 0) { $this->currencyService->change( $user, 'exp', $autoEvent->exp_change, CurrencySource::AUTO_EVENT, "随机事件:{$autoEvent->text_body}", 1 ); $hasCurrencyChange = true; } if ($autoEvent->jjb_change !== 0) { $this->currencyService->change( $user, 'gold', $autoEvent->jjb_change, CurrencySource::AUTO_EVENT, "随机事件:{$autoEvent->text_body}", 1 ); $hasCurrencyChange = true; } if ($hasCurrencyChange) { $user->refresh(); } // 重新计算等级 if ($this->calculateNewLevel($user, $superLevel)) { $leveledUp = true; } // 广播随机事件 $this->broadcastSystemMessage( '星海小博士', $autoEvent->renderText($user->username), match ($autoEvent->event_type) { 'good' => '#16a34a', 'bad' => '#dc2626', default => '#7c3aed', } ); } } // 6. 如果由于心跳或事件导致了升级,广播升级消息 if ($leveledUp) { $this->broadcastSystemMessage( '系统传音', "🌟 天道酬勤!恭喜侠客【{$user->username}】挂机苦修,境界突破至 LV.{$user->user_level}!", '#d97706', '大声宣告' ); } // 7. 钓鱼小游戏随机参与逻辑 $fishingEnabled = $config['chatbot_fishing_enabled'] === '1'; $fishingChance = (int) $config['chatbot_fishing_chance']; // 默认 100% 概率,保持原有配置默认值。 if ($fishingEnabled && $fishingChance > 0 && rand(1, 100) <= $fishingChance) { $fishingConfig = GameConfig::forGame('fishing'); if ($fishingConfig?->enabled) { $cost = (int) ($fishingConfig->params['fishing_cost'] ?? $config['fishing_cost']); // 常规小游戏只使用当前手上金币,不再自动从银行补到 100 万。 if ($this->aiFinance->prepareSpend($user, $cost)) { // 先扣除抛竿费用,再派发延迟收竿任务,避免当前心跳等待钓鱼结果。 $this->currencyService->change( $user, 'gold', -$cost, CurrencySource::FISHING_COST, "AI小班长钓鱼抛竿消耗 {$cost} 金币", 1, ); // 模拟玩家等待时间 $waitMin = (int) ($fishingConfig->params['fishing_wait_min'] ?? $config['fishing_wait_min']); $waitMax = (int) ($fishingConfig->params['fishing_wait_max'] ?? $config['fishing_wait_max']); $waitTime = rand($waitMin, $waitMax); // 延迟派发收竿事件(AI目前统一将事件播报到房间 1,或者拿 active room ids) $activeRoomIds = $this->chatState->getAllActiveRoomIds(); $roomId = ! empty($activeRoomIds) ? $activeRoomIds[0] : 1; AiFishingJob::dispatch($user, $roomId)->delay(now()->addSeconds($waitTime)); } } } // 心跳结束后,若新增金币让手上金额超过 100 万,则把超出的部分重新转回银行。 if ((int) ($user->jjb ?? 0) > AiFinanceService::AVAILABLE_GOLD_RESERVE) { $this->aiFinance->bankExcessGold($user); } $elapsedMs = (int) round((microtime(true) - $startedAt) * 1000); $this->info("AI心跳完成,耗时 {$elapsedMs}ms。"); return Command::SUCCESS; } /** * 读取本轮心跳需要的系统配置,避免命令流程中重复触发配置读取。 * * @return array */ private function heartbeatConfig(): array { return [ 'exp_per_heartbeat' => Sysparam::getValue('exp_per_heartbeat', '1'), 'jjb_per_heartbeat' => Sysparam::getValue('jjb_per_heartbeat', '0'), 'superlevel' => Sysparam::getValue('superlevel', '100'), 'auto_event_chance' => Sysparam::getValue('auto_event_chance', '10'), 'chatbot_fishing_enabled' => Sysparam::getValue('chatbot_fishing_enabled', '0'), 'chatbot_fishing_chance' => Sysparam::getValue('chatbot_fishing_chance', '100'), 'fishing_cost' => Sysparam::getValue('fishing_cost', '5'), 'fishing_wait_min' => Sysparam::getValue('fishing_wait_min', '8'), 'fishing_wait_max' => Sysparam::getValue('fishing_wait_max', '15'), ]; } /** * 计算并更新用户等级 */ private function calculateNewLevel(User $user, int $superLevel): bool { $oldLevel = $user->user_level; if ($oldLevel >= $superLevel) { return false; // 管理员不自动升降级 } $newLevel = Sysparam::calculateLevel($user->exp_num); if ($newLevel !== $oldLevel && $newLevel < $superLevel) { $user->user_level = $newLevel; $user->save(); return $newLevel > $oldLevel; } return false; } /** * 解析配置的奖励范围,如 "1" 或 "1-5" */ private function parseRewardValue(string $raw): int { $raw = trim($raw); if (str_contains($raw, '-')) { [$min, $max] = explode('-', $raw, 2); return rand((int) $min, (int) $max); } return (int) $raw; } /** * 尝试为 AI小班长 执行今日签到,成功时广播签到通知。 */ private function performDailySignIn(User $user): bool { // 先检查今日是否已签,避免每分钟都调用事务 $alreadySigned = DailySignIn::query() ->where('user_id', $user->id) ->whereDate('sign_in_date', today()) ->exists(); if ($alreadySigned) { return false; } // 获取活跃房间作为签到归属(默认房间 1) $activeRoomIds = $this->chatState->getAllActiveRoomIds(); $roomId = ! empty($activeRoomIds) ? (int) $activeRoomIds[0] : 1; $dailySignIn = $this->signInService->claim($user, $roomId); // 仅当本次心跳实际完成签到时才广播(幂等保护) if (! $dailySignIn->wasRecentlyCreated) { return false; } $rewardParts = []; if ($dailySignIn->gold_reward > 0) { $rewardParts[] = $dailySignIn->gold_reward.' 金币'; } if ($dailySignIn->exp_reward > 0) { $rewardParts[] = $dailySignIn->exp_reward.' 经验'; } if ($dailySignIn->charm_reward > 0) { $rewardParts[] = $dailySignIn->charm_reward.' 魅力'; } $rewardText = $rewardParts === [] ? '签到记录' : implode(' + ', $rewardParts); $identityText = $dailySignIn->identity_badge_name ? ',获得身份 '.e($dailySignIn->identity_badge_name) : ''; $content = '【'.e($user->username).'】完成今日签到,连续签到 ' .$dailySignIn->streak_days.' 天,获得 '.$rewardText.$identityText.'。'; $this->broadcastSystemMessage('系统传音', $content, '#0f766e'); return true; } /** * 往所有活跃房间发送系统广播消息 */ private function broadcastSystemMessage(string $fromUser, string $content, string $color, string $action = ''): void { $activeRoomIds = $this->chatState->getAllActiveRoomIds(); if (empty($activeRoomIds)) { $activeRoomIds = [1]; } foreach ($activeRoomIds as $roomId) { $sysMsg = [ 'id' => $this->chatState->nextMessageId($roomId), 'room_id' => $roomId, 'from_user' => $fromUser, 'to_user' => '大家', 'content' => $content, 'is_secret' => false, 'font_color' => $color, 'action' => $action, 'sent_at' => now()->toDateTimeString(), ]; $this->chatState->pushMessage($roomId, $sysMsg); broadcast(new MessageSent($roomId, $sysMsg)); SaveMessageJob::dispatch($sysMsg); } } }