a bit of progress
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Game;
|
||||
|
||||
use App\Models\Character;
|
||||
use App\Models\Crime;
|
||||
|
||||
class ActivityLogService
|
||||
{
|
||||
/**
|
||||
* Log a generic activity
|
||||
*/
|
||||
public function log(Character $character, string $action, ?string $description = null, ?string $ipAddress = null, ?string $userAgent = null): void
|
||||
{
|
||||
\App\Models\ActivityLog::log($character, $action, $description, $ipAddress, $userAgent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a crime attempt
|
||||
*/
|
||||
public function logCrimeAttempt(Character $character, Crime $crime, bool $success, int $reward = 0): void
|
||||
{
|
||||
\App\Models\ActivityLog::logCrimeAttempt($character, $crime, $success, $reward);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log money transaction
|
||||
*/
|
||||
public function logMoneyTransaction(Character $character, string $type, int $amount, string $reason): void
|
||||
{
|
||||
\App\Models\ActivityLog::logMoneyTransaction($character, $type, $amount, $reason);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log level up
|
||||
*/
|
||||
public function logLevelUp(Character $character, int $newLevel): void
|
||||
{
|
||||
\App\Models\ActivityLog::logLevelUp($character, $newLevel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log character creation
|
||||
*/
|
||||
public function logCharacterCreated(Character $character): void
|
||||
{
|
||||
\App\Models\ActivityLog::logCharacterCreated($character);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Game;
|
||||
|
||||
use App\Models\Character;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
class CooldownService
|
||||
{
|
||||
/**
|
||||
* Check if a character is on cooldown for a specific action type
|
||||
*/
|
||||
public function isOnCooldown(Character $character, string $actionType): bool
|
||||
{
|
||||
$cooldownSeconds = $this->getCooldownSeconds($actionType);
|
||||
|
||||
if ($cooldownSeconds <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$lastActionTime = $this->getLastActionTime($character, $actionType);
|
||||
|
||||
if (!$lastActionTime) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$secondsSinceAction = now()->diffInSeconds($lastActionTime);
|
||||
return $secondsSinceAction < $cooldownSeconds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get remaining cooldown time in seconds
|
||||
*/
|
||||
public function getRemainingCooldown(Character $character, string $actionType): int
|
||||
{
|
||||
$cooldownSeconds = $this->getCooldownSeconds($actionType);
|
||||
|
||||
if ($cooldownSeconds <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$lastActionTime = $this->getLastActionTime($character, $actionType);
|
||||
|
||||
if (!$lastActionTime) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$secondsSinceAction = now()->diffInSeconds($lastActionTime);
|
||||
return max(0, $cooldownSeconds - $secondsSinceAction);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get formatted cooldown time (e.g., "2m 30s")
|
||||
*/
|
||||
public function getFormattedCooldown(Character $character, string $actionType): string
|
||||
{
|
||||
$remaining = $this->getRemainingCooldown($character, $actionType);
|
||||
|
||||
if ($remaining <= 0) {
|
||||
return 'Ready';
|
||||
}
|
||||
|
||||
$minutes = (int) ($remaining / 60);
|
||||
$seconds = $remaining % 60;
|
||||
|
||||
if ($minutes > 0) {
|
||||
return "{$minutes}m {$seconds}s";
|
||||
}
|
||||
|
||||
return "{$seconds}s";
|
||||
}
|
||||
|
||||
/**
|
||||
* Record an action to start the cooldown
|
||||
*/
|
||||
public function recordAction(Character $character, string $actionType): void
|
||||
{
|
||||
$cooldownModel = $this->getCooldownModel($character, $actionType);
|
||||
|
||||
// Update or create the cooldown record
|
||||
$cooldownModel::updateOrCreate(
|
||||
[
|
||||
'character_id' => $character->id,
|
||||
'action_type' => $actionType,
|
||||
],
|
||||
[
|
||||
'last_action_at' => now(),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the cooldown duration in seconds for an action type
|
||||
*/
|
||||
protected function getCooldownSeconds(string $actionType): int
|
||||
{
|
||||
return config("game.cooldowns.{$actionType}", config('game.cooldowns.default', 60));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the last action time for a character and action type
|
||||
*/
|
||||
protected function getLastActionTime(Character $character, string $actionType): ?Carbon
|
||||
{
|
||||
$cooldownModel = $this->getCooldownModel($character, $actionType);
|
||||
|
||||
$cooldown = $cooldownModel::where('character_id', $character->id)
|
||||
->where('action_type', $actionType)
|
||||
->first();
|
||||
|
||||
return $cooldown?->last_action_at;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the cooldown model class for an action type
|
||||
*/
|
||||
protected function getCooldownModel(Character $character, string $actionType): string
|
||||
{
|
||||
// For now, we'll use a simple approach with a cooldowns table
|
||||
// In the future, this could be extended to support different cooldown types
|
||||
return \App\Models\Cooldown::class;
|
||||
}
|
||||
}
|
||||
@@ -5,21 +5,25 @@ namespace App\Services\Game;
|
||||
use App\Models\Character;
|
||||
use App\Models\Crime;
|
||||
use App\Models\CrimeAttempt;
|
||||
use App\Models\ActivityLog;
|
||||
use App\Enums\CharacterStatus;
|
||||
use App\Services\Game\NerveService;
|
||||
|
||||
class CrimeService
|
||||
{
|
||||
protected EnergyService $energyService;
|
||||
protected EconomyService $economyService;
|
||||
protected NerveService $nerveService;
|
||||
protected ActivityLogService $activityLogService;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->energyService = new EnergyService();
|
||||
$this->economyService = new EconomyService();
|
||||
$this->nerveService = new NerveService();
|
||||
public function __construct(
|
||||
EnergyService $energyService,
|
||||
EconomyService $economyService,
|
||||
NerveService $nerveService,
|
||||
ActivityLogService $activityLogService
|
||||
) {
|
||||
$this->energyService = $energyService;
|
||||
$this->economyService = $economyService;
|
||||
$this->nerveService = $nerveService;
|
||||
$this->activityLogService = $activityLogService;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -63,7 +67,7 @@ class CrimeService
|
||||
$character->addExperience($experience);
|
||||
|
||||
// Log success
|
||||
ActivityLog::logCrimeAttempt($character, $crime, true, $reward);
|
||||
$this->activityLogService->logCrimeAttempt($character, $crime, true, $reward);
|
||||
} else {
|
||||
// Failed crime - take health penalty
|
||||
$healthPenalty = (int) ($character->max_health * config('game.crime.failure_penalty', 0.10));
|
||||
@@ -75,7 +79,7 @@ class CrimeService
|
||||
}
|
||||
|
||||
// Log failure
|
||||
ActivityLog::logCrimeAttempt($character, $crime, false, 0);
|
||||
$this->activityLogService->logCrimeAttempt($character, $crime, false, 0);
|
||||
}
|
||||
|
||||
// Create crime attempt record
|
||||
|
||||
@@ -6,6 +6,13 @@ use App\Models\Character;
|
||||
|
||||
class EconomyService
|
||||
{
|
||||
protected ActivityLogService $activityLogService;
|
||||
|
||||
public function __construct(ActivityLogService $activityLogService)
|
||||
{
|
||||
$this->activityLogService = $activityLogService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transfer money between characters
|
||||
*/
|
||||
@@ -23,8 +30,8 @@ class EconomyService
|
||||
$to->addMoney($amount);
|
||||
|
||||
// Log the transaction
|
||||
ActivityLog::logMoneyTransaction($from, 'spent', $amount, "Transfer to {$to->name}: {$reason}");
|
||||
ActivityLog::logMoneyTransaction($to, 'earned', $amount, "Transfer from {$from->name}: {$reason}");
|
||||
$this->activityLogService->logMoneyTransaction($from, 'spent', $amount, "Transfer to {$to->name}: {$reason}");
|
||||
$this->activityLogService->logMoneyTransaction($to, 'earned', $amount, "Transfer from {$from->name}: {$reason}");
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -37,7 +44,7 @@ class EconomyService
|
||||
$character->addMoney($amount);
|
||||
|
||||
if ($amount > 0 && $reason) {
|
||||
ActivityLog::logMoneyTransaction($character, 'earned', $amount, $reason);
|
||||
$this->activityLogService->logMoneyTransaction($character, 'earned', $amount, $reason);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,7 +60,7 @@ class EconomyService
|
||||
$character->removeMoney($amount);
|
||||
|
||||
if ($amount > 0 && $reason) {
|
||||
ActivityLog::logMoneyTransaction($character, 'spent', $amount, $reason);
|
||||
$this->activityLogService->logMoneyTransaction($character, 'spent', $amount, $reason);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
Reference in New Issue
Block a user