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
+49
View File
@@ -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);
}
}
+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));
}
}
+123
View File
@@ -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;
}
}
+13 -9
View File
@@ -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
+11 -4
View File
@@ -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;