a bit of progress
This commit is contained in:
@@ -0,0 +1,301 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Game;
|
||||
|
||||
use App\Models\Character;
|
||||
use App\Models\Item;
|
||||
use App\Enums\AttackResult;
|
||||
|
||||
class CombatService
|
||||
{
|
||||
protected HealthService $healthService;
|
||||
protected EconomyService $economyService;
|
||||
protected ActivityLogService $activityLogService;
|
||||
protected CooldownService $cooldownService;
|
||||
|
||||
public function __construct(
|
||||
HealthService $healthService,
|
||||
EconomyService $economyService,
|
||||
ActivityLogService $activityLogService,
|
||||
CooldownService $cooldownService
|
||||
) {
|
||||
$this->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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user