healthService = $healthService; $this->economyService = $economyService; $this->activityLogService = $activityLogService; $this->cooldownService = $cooldownService; } /** * Process an attack between two characters * * @return array{ * result: AttackResult, * attacker_damage: int, * defender_damage: int, * money_stolen: int, * experience_gained: int, * was_critical: bool, * was_mugging: bool, * attacker_died: bool, * defender_died: bool * } */ public function processAttack(Character $attacker, Character $defender, bool $isMugging = false): array { // Check if characters can fight if (!$attacker->canAct() || !$defender->canAct()) { throw new \Exception('One or both characters cannot perform actions.'); } if ($attacker->id === $defender->id) { throw new \Exception('Cannot attack yourself.'); } // Determine initiative (who attacks first) $attackerFirst = $this->determineInitiative($attacker, $defender); $attackerDamage = 0; $defenderDamage = 0; $wasCritical = false; $wasMugging = $isMugging; if ($attackerFirst) { // Attacker goes first $attackerDamage = $this->calculateDamage($attacker, $defender); $defender->takeDamage($attackerDamage); // Check if defender is still alive if ($defender->isAlive()) { $defenderDamage = $this->calculateDamage($defender, $attacker); $attacker->takeDamage($defenderDamage); } } else { // Defender goes first $defenderDamage = $this->calculateDamage($defender, $attacker); $attacker->takeDamage($defenderDamage); // Check if attacker is still alive if ($attacker->isAlive()) { $attackerDamage = $this->calculateDamage($attacker, $defender); $defender->takeDamage($attackerDamage); } } // Determine result $result = $this->determineResult($attacker, $defender, $isMugging); // Calculate rewards $moneyStolen = 0; $experienceGained = 0; if ($result === AttackResult::WIN) { // Attacker wins if ($isMugging) { $mugPercent = config('game.attack.mug_percent', 0.20); $moneyStolen = (int) ($defender->money * $mugPercent); $this->economyService->removeMoney($defender, $moneyStolen, 'Mugged by ' . $attacker->name); $this->economyService->addMoney($attacker, $moneyStolen, 'Mugged ' . $defender->name); } $experienceGained = $this->calculateExperienceReward($attacker, $defender, true); $attacker->addExperience($experienceGained); } elseif ($result === AttackResult::LOSE) { // Defender wins (attacker lost) $experienceGained = $this->calculateExperienceReward($attacker, $defender, false); $attacker->addExperience($experienceGained); } else { // Draw or escape - small experience reward $experienceGained = (int) (config('game.combat.base_damage', 5) * 2); $attacker->addExperience($experienceGained); } // Log the attack $this->activityLogService->log( $attacker, 'attack', "Attacked {$defender->name} - Result: {$result->label()}" ); $this->activityLogService->log( $defender, 'attacked', "Was attacked by {$attacker->name} - Result: " . ($result === AttackResult::LOSE ? 'Defended' : $result->label()) ); // Record the cooldown $this->cooldownService->recordAction($attacker, 'attack'); return [ 'result' => $result, 'attacker_damage' => $attackerDamage, 'defender_damage' => $defenderDamage, 'money_stolen' => $moneyStolen, 'experience_gained' => $experienceGained, 'was_critical' => $wasCritical, 'was_mugging' => $wasMugging, 'attacker_died' => !$attacker->isAlive(), 'defender_died' => !$defender->isAlive(), ]; } /** * Determine which character attacks first based on speed */ protected function determineInitiative(Character $attacker, Character $defender): bool { $baseChance = 0.5; // 50% base chance $speedBonus = $attacker->speed * config('game.skills.speed.attack_first_chance', 0.02); $randomVariation = random_int(0, 20) / 100 - 0.1; // +/- 10% $attackerChance = $baseChance + $speedBonus + $randomVariation; return random_int(0, 9999) / 10000 <= $attackerChance; } /** * Calculate damage dealt by attacker to defender */ protected function calculateDamage(Character $attacker, Character $defender): int { // Get attacker's attack power (includes strength and weapon bonus) $baseDamage = $attacker->getAttackPowerAttribute(); // Apply damage variation $variation = 1 + (random_int(0, 4000) / 10000 - 0.2); // +/- 20% $damage = (int) ($baseDamage * $variation); // Check for critical hit $critChance = config('game.combat.crit_chance', 0.10) + ($attacker->dexterity * config('game.skills.dexterity.crit_chance', 0.005)); $isCritical = random_int(0, 9999) / 10000 <= $critChance; if ($isCritical) { $critMultiplier = config('game.combat.crit_multiplier', 2.0); $damage = (int) ($damage * $critMultiplier); } // Apply defender's defense reduction $defenseReduction = $defender->getDefenseAttribute() * config('game.combat.defense_reduction', 0.5); $actualDamage = max(config('game.combat.min_damage', 1), $damage - $defenseReduction); return $actualDamage; } /** * Determine the result of the attack */ protected function determineResult(Character $attacker, Character $defender, bool $isMugging): AttackResult { if (!$attacker->isAlive()) { return AttackResult::LOSE; // Attacker died } if (!$defender->isAlive()) { return AttackResult::WIN; // Defender died, attacker wins } // If both are alive, it's a draw return AttackResult::DRAW; } /** * Calculate experience reward for an attack */ protected function calculateExperienceReward(Character $attacker, Character $defender, bool $won): int { $baseExp = 50; if ($won) { // More experience for winning $levelDifference = max(0, $defender->level - $attacker->level); $expMultiplier = 1 + ($levelDifference * 0.1); // 10% more per level difference return (int) ($baseExp * 2 * $expMultiplier); // Base 100 for win } else { // Less experience for losing $levelDifference = max(0, $attacker->level - $defender->level); $expMultiplier = 1 - ($levelDifference * 0.05); // 5% less per level difference return (int) ($baseExp * $expMultiplier); // Base 50 for loss } } /** * Check if a character can attack another character */ public function canAttack(Character $attacker, Character $defender): bool { // Cannot attack self if ($attacker->id === $defender->id) { return false; } // Both must be able to act if (!$attacker->canAct() || !$defender->canAct()) { return false; } // Check if attacker is in cooldown if ($this->cooldownService->isOnCooldown($attacker, 'attack')) { return false; } // Check if defender is in jail or hospital (can still attack them) // But cannot attack if attacker is in jail or hospital if ($attacker->isInJail() || $attacker->isInHospital()) { return false; } return true; } /** * Get remaining cooldown time for attack (delegated to CooldownService) */ public function getAttackCooldownRemaining(Character $character): int { return $this->cooldownService->getRemainingCooldown($character, 'attack'); } /** * Get the chance to hit for display purposes */ public function getHitChance(Character $attacker, Character $defender): float { // Calculate base hit chance $baseChance = 0.70; // Speed affects hit chance $speedBonus = $attacker->speed * 0.01; // +1% per speed point // Defense affects opponent's chance to hit $defensePenalty = $defender->defense * 0.005; // -0.5% per defense point $hitChance = $baseChance + $speedBonus - $defensePenalty; return max(0.10, min(0.95, $hitChance)); } /** * Get estimated win probability */ public function getWinProbability(Character $attacker, Character $defender): float { // Simple estimation based on attack and defense $attackerPower = $attacker->getAttackPowerAttribute() + ($attacker->health * 0.1); $defenderPower = $defender->getDefenseAttribute() + ($defender->health * 0.1); $totalPower = $attackerPower + $defenderPower; if ($totalPower <= 0) { return 0.5; } $winProbability = $attackerPower / $totalPower; // Add speed initiative bonus $speedDiff = $attacker->speed - $defender->speed; $winProbability += $speedDiff * 0.005; // +0.5% per speed point difference return max(0.05, min(0.95, $winProbability)); } }