a bit of progress

This commit is contained in:
2026-07-04 17:09:55 +03:00
parent f6e7d37428
commit 082b532d52
29 changed files with 2734 additions and 47 deletions
+301
View File
@@ -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));
}
}