a bit of progress
This commit is contained in:
+1116
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands\Game;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\Services\Game\CooldownService;
|
||||
use App\Models\Cooldown;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
class ProcessCooldowns extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'game:process-cooldowns';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Process and clean up expired cooldowns';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle(CooldownService $cooldownService): int
|
||||
{
|
||||
$this->info('Processing cooldowns...');
|
||||
|
||||
// Delete expired cooldowns
|
||||
$maxAge = now()->subHours(24); // Keep cooldowns for 24 hours max
|
||||
$deleted = Cooldown::where('last_action_at', '<', $maxAge)->delete();
|
||||
|
||||
$this->info("Deleted {$deleted} expired cooldown records.");
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,330 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Game;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Character;
|
||||
use App\Models\Attack as AttackModel;
|
||||
use App\Services\Game\CombatService;
|
||||
use App\Services\Game\HealthService;
|
||||
use App\Services\Game\JailService;
|
||||
use App\Services\Game\NotificationService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class AttackController extends Controller
|
||||
{
|
||||
protected CombatService $combatService;
|
||||
protected HealthService $healthService;
|
||||
protected JailService $jailService;
|
||||
protected NotificationService $notificationService;
|
||||
|
||||
public function __construct(
|
||||
CombatService $combatService,
|
||||
HealthService $healthService,
|
||||
JailService $jailService,
|
||||
NotificationService $notificationService
|
||||
) {
|
||||
$this->combatService = $combatService;
|
||||
$this->healthService = $healthService;
|
||||
$this->jailService = $jailService;
|
||||
$this->notificationService = $notificationService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the attack page with potential targets
|
||||
*/
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$character = $request->user()->character;
|
||||
|
||||
if (!$character) {
|
||||
return redirect()->route('game.character.create');
|
||||
}
|
||||
|
||||
// Check if character is in jail or hospital
|
||||
if ($character->isInJail() || $character->isInHospital() || !$character->canAct()) {
|
||||
return redirect()->route('game.dashboard')
|
||||
->with('error', 'You cannot attack while in jail, hospital, or unable to act.');
|
||||
}
|
||||
|
||||
// Get potential targets (other characters in same location, not in jail/hospital)
|
||||
$potentialTargets = Character::where('location_id', $character->location_id)
|
||||
->where('id', '!=', $character->id)
|
||||
->where('status', 'active')
|
||||
->where('health', '>', 0)
|
||||
->orderBy('level', 'desc')
|
||||
->get();
|
||||
|
||||
// Get attack cooldown
|
||||
$cooldownRemaining = $this->combatService->getAttackCooldownRemaining($character);
|
||||
|
||||
return view('game.attack.index', compact(
|
||||
'character',
|
||||
'potentialTargets',
|
||||
'cooldownRemaining'
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Show attack preview for a specific target
|
||||
*/
|
||||
public function preview(Request $request, Character $target): View
|
||||
{
|
||||
$attacker = $request->user()->character;
|
||||
|
||||
if (!$attacker) {
|
||||
return redirect()->route('game.character.create');
|
||||
}
|
||||
|
||||
// Check if can attack this target
|
||||
if (!$this->combatService->canAttack($attacker, $target)) {
|
||||
return redirect()->route('game.attack.index')
|
||||
->with('error', 'You cannot attack this target right now.');
|
||||
}
|
||||
|
||||
// Calculate preview stats
|
||||
$winProbability = $this->combatService->getWinProbability($attacker, $target);
|
||||
$hitChance = $this->combatService->getHitChance($attacker, $target);
|
||||
$attackerPower = $attacker->getAttackPowerAttribute();
|
||||
$defenderDefense = $target->getDefenseAttribute();
|
||||
$cooldownRemaining = $this->combatService->getAttackCooldownRemaining($attacker);
|
||||
|
||||
// Check for weapon and armor
|
||||
$attackerWeapon = $attacker->inventory()
|
||||
->where('equipped', true)
|
||||
->whereHas('item', function($q) {
|
||||
$q->where('type', 'weapon');
|
||||
})
|
||||
->first();
|
||||
|
||||
$defenderArmor = $target->inventory()
|
||||
->where('equipped', true)
|
||||
->whereHas('item', function($q) {
|
||||
$q->where('type', 'armor');
|
||||
})
|
||||
->first();
|
||||
|
||||
return view('game.attack.preview', compact(
|
||||
'attacker',
|
||||
'target',
|
||||
'winProbability',
|
||||
'hitChance',
|
||||
'attackerPower',
|
||||
'defenderDefense',
|
||||
'cooldownRemaining',
|
||||
'attackerWeapon',
|
||||
'defenderArmor'
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Process an attack on another character
|
||||
*/
|
||||
public function attack(Request $request, Character $target): RedirectResponse
|
||||
{
|
||||
$attacker = $request->user()->character;
|
||||
|
||||
if (!$attacker) {
|
||||
return redirect()->route('game.character.create');
|
||||
}
|
||||
|
||||
$isMugging = $request->has('mug') && $request->boolean('mug');
|
||||
|
||||
// Check if can attack this target
|
||||
if (!$this->combatService->canAttack($attacker, $target)) {
|
||||
$cooldown = $this->combatService->getAttackCooldownRemaining($attacker);
|
||||
if ($cooldown > 0) {
|
||||
return redirect()->route('game.attack.index')
|
||||
->with('error', "You must wait {$cooldown} seconds before attacking again.");
|
||||
}
|
||||
return redirect()->route('game.attack.index')
|
||||
->with('error', 'You cannot attack this target right now.');
|
||||
}
|
||||
|
||||
try {
|
||||
// Process the attack
|
||||
$result = $this->combatService->processAttack($attacker, $target, $isMugging);
|
||||
|
||||
// Record the attack
|
||||
$attackRecord = AttackModel::create([
|
||||
'attacker_id' => $attacker->id,
|
||||
'defender_id' => $target->id,
|
||||
'attacker_damage' => $result['attacker_damage'],
|
||||
'defender_damage' => $result['defender_damage'],
|
||||
'attacker_health_after' => $attacker->health,
|
||||
'defender_health_after' => $target->health,
|
||||
'result' => $result['result']->value,
|
||||
'money_stolen' => $result['money_stolen'],
|
||||
'experience_gained' => $result['experience_gained'],
|
||||
'was_critical' => $result['was_critical'],
|
||||
'was_mugging' => $result['was_mugging'],
|
||||
'attacker_weapon_id' => $attacker->inventory()
|
||||
->where('equipped', true)
|
||||
->whereHas('item', function($q) { $q->where('type', 'weapon'); })
|
||||
->first()?->item_id,
|
||||
'defender_armor_id' => $target->inventory()
|
||||
->where('equipped', true)
|
||||
->whereHas('item', function($q) { $q->where('type', 'armor'); })
|
||||
->first()?->item_id,
|
||||
'attacked_at' => now(),
|
||||
]);
|
||||
|
||||
// Handle death consequences
|
||||
if ($result['defender_died']) {
|
||||
// Defender died - send to hospital
|
||||
$this->healthService->sendToHospital($target);
|
||||
|
||||
// If not mugging, send attacker to jail for killing
|
||||
if (!$isMugging) {
|
||||
$sentenceLength = config('game.jail.sentence_length', 3600);
|
||||
$this->jailService->sendToJail($attacker, $sentenceLength);
|
||||
}
|
||||
}
|
||||
|
||||
if ($result['attacker_died']) {
|
||||
// Attacker died - send to hospital
|
||||
$this->healthService->sendToHospital($attacker);
|
||||
}
|
||||
|
||||
// Send notifications
|
||||
if ($result['result']->value === 'win') {
|
||||
if ($isMugging) {
|
||||
$this->notificationService->send($request->user(), 'mug_success', [
|
||||
'target_name' => $target->name,
|
||||
'money_stolen' => $result['money_stolen'],
|
||||
]);
|
||||
} else {
|
||||
$this->notificationService->send($request->user(), 'attack_win', [
|
||||
'target_name' => $target->name,
|
||||
'experience_gained' => $result['experience_gained'],
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
$this->notificationService->send($request->user(), 'attack_result', [
|
||||
'target_name' => $target->name,
|
||||
'result' => $result['result']->value,
|
||||
]);
|
||||
}
|
||||
|
||||
// Send notification to defender
|
||||
if ($target->user) {
|
||||
$this->notificationService->send($target->user, 'attacked', [
|
||||
'attacker_name' => $attacker->name,
|
||||
'result' => $result['result']->value,
|
||||
'damage_taken' => $result['attacker_damage'],
|
||||
]);
|
||||
}
|
||||
|
||||
$successMessage = match ($result['result']->value) {
|
||||
'win' => $isMugging ? "You mugged {$target->name} and stole $" . number_format($result['money_stolen']) . "!" : "You defeated {$target->name}!",
|
||||
'lose' => "You lost to {$target->name}!",
|
||||
'draw' => "Your attack against {$target->name} ended in a draw.",
|
||||
default => "Attack result: {$result['result']->label()}",
|
||||
};
|
||||
|
||||
return redirect()->route('game.attack.index')
|
||||
->with('success', $successMessage);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return redirect()->route('game.attack.index')
|
||||
->with('error', $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Place a hit/bounty on a character
|
||||
*/
|
||||
public function placeHit(Request $request, Character $target): RedirectResponse
|
||||
{
|
||||
$attacker = $request->user()->character;
|
||||
|
||||
if (!$attacker) {
|
||||
return redirect()->route('game.character.create');
|
||||
}
|
||||
|
||||
$validated = $request->validate([
|
||||
'amount' => ['required', 'integer', 'min:1000'],
|
||||
'reason' => ['nullable', 'string', 'max:255'],
|
||||
]);
|
||||
|
||||
$hitCost = config('game.attack.hit_list_cost', 5000);
|
||||
|
||||
if (!$attacker->hasMoney($validated['amount'] + $hitCost)) {
|
||||
return redirect()->back()
|
||||
->with('error', 'You need at least $' . number_format($validated['amount'] + $hitCost) . ' to place a hit.');
|
||||
}
|
||||
|
||||
// Place the hit - TODO: Implement Hit model
|
||||
$attacker->removeMoney($validated['amount'] + $hitCost);
|
||||
|
||||
$this->notificationService->send($request->user(), 'hit_placed', [
|
||||
'target_name' => $target->name,
|
||||
'amount' => $validated['amount'],
|
||||
]);
|
||||
|
||||
return redirect()->route('game.attack.index')
|
||||
->with('success', "Hit placed on {$target->name} for $" . number_format($validated['amount']) . "!");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recent attacks for a character
|
||||
*/
|
||||
public function history(Request $request): View
|
||||
{
|
||||
$character = $request->user()->character;
|
||||
|
||||
if (!$character) {
|
||||
return redirect()->route('game.character.create');
|
||||
}
|
||||
|
||||
// Attacks made by this character
|
||||
$myAttacks = AttackModel::where('attacker_id', $character->id)
|
||||
->with(['defender', 'defender.user'])
|
||||
->latest('attacked_at')
|
||||
->paginate(20);
|
||||
|
||||
// Attacks made against this character
|
||||
$attacksOnMe = AttackModel::where('defender_id', $character->id)
|
||||
->with(['attacker', 'attacker.user'])
|
||||
->latest('attacked_at')
|
||||
->paginate(20);
|
||||
|
||||
return view('game.attack.history', compact(
|
||||
'character',
|
||||
'myAttacks',
|
||||
'attacksOnMe'
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get characters who have attacked you recently (for revenge)
|
||||
*/
|
||||
public function revengeList(Request $request): View
|
||||
{
|
||||
$character = $request->user()->character;
|
||||
|
||||
if (!$character) {
|
||||
return redirect()->route('game.character.create');
|
||||
}
|
||||
|
||||
// Get characters who attacked this character and are still alive
|
||||
$revengeTargets = AttackModel::where('defender_id', $character->id)
|
||||
->where('attacker_id', '!=', $character->id)
|
||||
->whereHas('attacker', function($q) {
|
||||
$q->where('health', '>', 0)
|
||||
->where('status', 'active');
|
||||
})
|
||||
->with(['attacker', 'attacker.user'])
|
||||
->latest('attacked_at')
|
||||
->get()
|
||||
->unique('attacker_id')
|
||||
->pluck('attacker');
|
||||
|
||||
return view('game.attack.revenge', compact(
|
||||
'character',
|
||||
'revengeTargets'
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -3,9 +3,11 @@
|
||||
namespace App\Http\Controllers\Game;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Game\CharacterDepositRequest;
|
||||
use App\Http\Requests\Game\CharacterWithdrawRequest;
|
||||
use App\Models\Character;
|
||||
use App\Models\Location;
|
||||
use App\Models\ActivityLog;
|
||||
use App\Services\Game\ActivityLogService;
|
||||
use App\Services\Game\EconomyService;
|
||||
use App\Services\Game\EnergyService;
|
||||
use App\Services\Game\HealthService;
|
||||
@@ -21,13 +23,20 @@ class CharacterController extends Controller
|
||||
protected EnergyService $energyService;
|
||||
protected HealthService $healthService;
|
||||
protected NotificationService $notificationService;
|
||||
protected ActivityLogService $activityLogService;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->economyService = new EconomyService();
|
||||
$this->energyService = new EnergyService();
|
||||
$this->healthService = new HealthService();
|
||||
$this->notificationService = new NotificationService();
|
||||
public function __construct(
|
||||
EconomyService $economyService,
|
||||
EnergyService $energyService,
|
||||
HealthService $healthService,
|
||||
NotificationService $notificationService,
|
||||
ActivityLogService $activityLogService
|
||||
) {
|
||||
$this->economyService = $economyService;
|
||||
$this->energyService = $energyService;
|
||||
$this->healthService = $healthService;
|
||||
$this->notificationService = $notificationService;
|
||||
$this->activityLogService = $activityLogService;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -85,7 +94,7 @@ class CharacterController extends Controller
|
||||
]);
|
||||
|
||||
// Log character creation
|
||||
ActivityLog::logCharacterCreated($character);
|
||||
$this->activityLogService->logCharacterCreated($character);
|
||||
|
||||
return redirect()->route('game.dashboard')
|
||||
->with('success', 'Character created successfully! Welcome to the game.');
|
||||
@@ -144,15 +153,11 @@ class CharacterController extends Controller
|
||||
/**
|
||||
* Deposit money to bank
|
||||
*/
|
||||
public function deposit(Request $request): RedirectResponse
|
||||
public function deposit(CharacterDepositRequest $request): RedirectResponse
|
||||
{
|
||||
$character = $request->user()->character;
|
||||
|
||||
$validated = $request->validate([
|
||||
'amount' => ['required', 'integer', 'min:1'],
|
||||
]);
|
||||
|
||||
if ($this->economyService->depositToBank($character, $validated['amount'])) {
|
||||
if ($this->economyService->depositToBank($character, $request->amount)) {
|
||||
return redirect()->back()
|
||||
->with('success', 'Money deposited to bank successfully!');
|
||||
}
|
||||
@@ -164,15 +169,11 @@ class CharacterController extends Controller
|
||||
/**
|
||||
* Withdraw money from bank
|
||||
*/
|
||||
public function withdraw(Request $request): RedirectResponse
|
||||
public function withdraw(CharacterWithdrawRequest $request): RedirectResponse
|
||||
{
|
||||
$character = $request->user()->character;
|
||||
|
||||
$validated = $request->validate([
|
||||
'amount' => ['required', 'integer', 'min:1'],
|
||||
]);
|
||||
|
||||
if ($this->economyService->withdrawFromBank($character, $validated['amount'])) {
|
||||
if ($this->economyService->withdrawFromBank($character, $request->amount)) {
|
||||
return redirect()->back()
|
||||
->with('success', 'Money withdrawn from bank successfully!');
|
||||
}
|
||||
|
||||
@@ -18,11 +18,14 @@ class CrimeController extends Controller
|
||||
protected EnergyService $energyService;
|
||||
protected NotificationService $notificationService;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->crimeService = new CrimeService();
|
||||
$this->energyService = new EnergyService();
|
||||
$this->notificationService = new NotificationService();
|
||||
public function __construct(
|
||||
CrimeService $crimeService,
|
||||
EnergyService $energyService,
|
||||
NotificationService $notificationService
|
||||
) {
|
||||
$this->crimeService = $crimeService;
|
||||
$this->energyService = $energyService;
|
||||
$this->notificationService = $notificationService;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Http\Controllers\Game;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Game\TrainRequest;
|
||||
use App\Services\Game\TrainingService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
@@ -12,9 +13,9 @@ class GymController extends Controller
|
||||
{
|
||||
protected TrainingService $trainingService;
|
||||
|
||||
public function __construct()
|
||||
public function __construct(TrainingService $trainingService)
|
||||
{
|
||||
$this->trainingService = new TrainingService();
|
||||
$this->trainingService = $trainingService;
|
||||
}
|
||||
|
||||
public function index(Request $request): View
|
||||
@@ -35,10 +36,8 @@ class GymController extends Controller
|
||||
return view('game.gym.index', compact('character', 'stats', 'energyCost', 'dailyCap'));
|
||||
}
|
||||
|
||||
public function train(Request $request): RedirectResponse
|
||||
public function train(TrainRequest $request): RedirectResponse
|
||||
{
|
||||
$request->validate(['stat' => 'required|in:strength,defense,speed,dexterity']);
|
||||
|
||||
$character = $request->user()->character;
|
||||
$result = $this->trainingService->train($character, $request->stat);
|
||||
|
||||
|
||||
@@ -14,9 +14,9 @@ class InventoryController extends Controller
|
||||
{
|
||||
protected EconomyService $economyService;
|
||||
|
||||
public function __construct()
|
||||
public function __construct(EconomyService $economyService)
|
||||
{
|
||||
$this->economyService = new EconomyService();
|
||||
$this->economyService = $economyService;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -11,9 +11,9 @@ class JailController extends Controller
|
||||
{
|
||||
protected JailService $jailService;
|
||||
|
||||
public function __construct()
|
||||
public function __construct(JailService $jailService)
|
||||
{
|
||||
$this->jailService = new JailService();
|
||||
$this->jailService = $jailService;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware\Game;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class CheckAlive
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
// Check if user is authenticated
|
||||
if (!$request->user()) {
|
||||
return redirect()->route('login');
|
||||
}
|
||||
|
||||
// Check if user has a character
|
||||
$character = $request->user()->character;
|
||||
|
||||
if (!$character) {
|
||||
return redirect()->route('game.character.create');
|
||||
}
|
||||
|
||||
// Check if character is alive and can act
|
||||
if (!$character->isAlive() || !$character->canAct()) {
|
||||
if ($character->isInHospital()) {
|
||||
return redirect()->route('game.hospital');
|
||||
}
|
||||
if ($character->isDead()) {
|
||||
return redirect()->route('game.hospital');
|
||||
}
|
||||
return redirect()->route('game.dashboard')
|
||||
->with('error', 'Your character cannot perform actions right now.');
|
||||
}
|
||||
|
||||
// Character is alive and can act, proceed with request
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware\Game;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class CheckCooldown
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
|
||||
*/
|
||||
public function handle(Request $request, Closure $next, string $actionType): Response
|
||||
{
|
||||
// Check if user is authenticated
|
||||
if (!$request->user()) {
|
||||
return redirect()->route('login');
|
||||
}
|
||||
|
||||
// Check if user has a character
|
||||
$character = $request->user()->character;
|
||||
|
||||
if (!$character) {
|
||||
return redirect()->route('game.character.create');
|
||||
}
|
||||
|
||||
$cooldownService = app(\App\Services\Game\CooldownService::class);
|
||||
|
||||
if ($cooldownService->isOnCooldown($character, $actionType)) {
|
||||
$remaining = $cooldownService->getRemainingCooldown($character, $actionType);
|
||||
return redirect()->back()
|
||||
->with('error', "You must wait {$remaining} seconds before performing this action again.");
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Game;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class AttackRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'mug' => ['sometimes', 'boolean'],
|
||||
'hit' => ['sometimes', 'boolean'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get custom messages for validator errors.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'mug.boolean' => 'The mug option must be a boolean value.',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Game;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class AttemptCrimeRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'crime_id' => ['required', 'exists:crimes,id'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get custom messages for validator errors.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'crime_id.required' => 'You must select a crime to attempt.',
|
||||
'crime_id.exists' => 'The selected crime does not exist.',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Game;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class CharacterDepositRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'amount' => ['required', 'integer', 'min:1', 'max:1000000'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get custom messages for validator errors.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'amount.required' => 'You must enter an amount to deposit.',
|
||||
'amount.integer' => 'The amount must be a whole number.',
|
||||
'amount.min' => 'The minimum deposit amount is :min.',
|
||||
'amount.max' => 'The maximum deposit amount is :max.',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Game;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class CharacterWithdrawRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'amount' => ['required', 'integer', 'min:1', 'max:1000000'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get custom messages for validator errors.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'amount.required' => 'You must enter an amount to withdraw.',
|
||||
'amount.integer' => 'The amount must be a whole number.',
|
||||
'amount.min' => 'The minimum withdrawal amount is :min.',
|
||||
'amount.max' => 'The maximum withdrawal amount is :max.',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Game;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class TrainRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'stat' => ['required', 'string', 'in:strength,defense,speed,dexterity'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get custom messages for validator errors.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'stat.required' => 'You must select a stat to train.',
|
||||
'stat.string' => 'The stat must be a string.',
|
||||
'stat.in' => 'The selected stat is invalid. Valid stats are: strength, defense, speed, dexterity.',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Enums\AttackResult;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
* @property int $attacker_id
|
||||
* @property int $defender_id
|
||||
* @property int $attacker_damage
|
||||
* @property int $defender_damage
|
||||
* @property int $attacker_health_after
|
||||
* @property int $defender_health_after
|
||||
* @property AttackResult $result
|
||||
* @property int $money_stolen
|
||||
* @property int $experience_gained
|
||||
* @property bool $was_critical
|
||||
* @property bool $was_mugging
|
||||
* @property int|null $attacker_weapon_id
|
||||
* @property int|null $defender_armor_id
|
||||
* @property \Illuminate\Support\Carbon $attacked_at
|
||||
* @property \Illuminate\Support\Carbon $created_at
|
||||
* @property \Illuminate\Support\Carbon $updated_at
|
||||
*/
|
||||
class Attack extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'attacker_id',
|
||||
'defender_id',
|
||||
'attacker_damage',
|
||||
'defender_damage',
|
||||
'attacker_health_after',
|
||||
'defender_health_after',
|
||||
'result',
|
||||
'money_stolen',
|
||||
'experience_gained',
|
||||
'was_critical',
|
||||
'was_mugging',
|
||||
'attacker_weapon_id',
|
||||
'defender_armor_id',
|
||||
'attacked_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'result' => AttackResult::class,
|
||||
'was_critical' => 'boolean',
|
||||
'was_mugging' => 'boolean',
|
||||
'attacked_at' => 'datetime',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the attacker character.
|
||||
*/
|
||||
public function attacker(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Character::class, 'attacker_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the defender character.
|
||||
*/
|
||||
public function defender(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Character::class, 'defender_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the weapon used by attacker.
|
||||
*/
|
||||
public function attackerWeapon(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Item::class, 'attacker_weapon_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the armor used by defender.
|
||||
*/
|
||||
public function defenderArmor(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Item::class, 'defender_armor_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this was a successful attack (attacker won)
|
||||
*/
|
||||
public function isWin(): bool
|
||||
{
|
||||
return $this->result === AttackResult::WIN;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this was a loss (attacker lost)
|
||||
*/
|
||||
public function isLose(): bool
|
||||
{
|
||||
return $this->result === AttackResult::LOSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this was a draw
|
||||
*/
|
||||
public function isDraw(): bool
|
||||
{
|
||||
return $this->result === AttackResult::DRAW;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if attacker escaped
|
||||
*/
|
||||
public function isEscape(): bool
|
||||
{
|
||||
return $this->result === AttackResult::ESCAPE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this attack resulted in defender's death
|
||||
*/
|
||||
public function defenderDied(): bool
|
||||
{
|
||||
return $this->defender_health_after <= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this attack resulted in attacker's death
|
||||
*/
|
||||
public function attackerDied(): bool
|
||||
{
|
||||
return $this->attacker_health_after <= 0;
|
||||
}
|
||||
}
|
||||
@@ -114,6 +114,22 @@ class Character extends Model
|
||||
return $this->hasMany(CrimeAttempt::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the attacks made by this character.
|
||||
*/
|
||||
public function attacks(): HasMany
|
||||
{
|
||||
return $this->hasMany(Attack::class, 'attacker_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the attacks received by this character.
|
||||
*/
|
||||
public function attacksReceived(): HasMany
|
||||
{
|
||||
return $this->hasMany(Attack::class, 'defender_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the activity logs for the character.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
* @property int $character_id
|
||||
* @property string $action_type
|
||||
* @property \Illuminate\Support\Carbon $last_action_at
|
||||
* @property \Illuminate\Support\Carbon $created_at
|
||||
* @property \Illuminate\Support\Carbon $updated_at
|
||||
*/
|
||||
class Cooldown extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'character_id',
|
||||
'action_type',
|
||||
'last_action_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'last_action_at' => 'datetime',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the character that owns this cooldown.
|
||||
*/
|
||||
public function character(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Character::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this cooldown has expired
|
||||
*/
|
||||
public function hasExpired(): bool
|
||||
{
|
||||
$cooldownSeconds = config("game.cooldowns.{$this->action_type}", config('game.cooldowns.default', 60));
|
||||
$secondsSinceAction = now()->diffInSeconds($this->last_action_at);
|
||||
return $secondsSinceAction >= $cooldownSeconds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get remaining cooldown time in seconds
|
||||
*/
|
||||
public function getRemainingSecondsAttribute(): int
|
||||
{
|
||||
$cooldownSeconds = config("game.cooldowns.{$this->action_type}", config('game.cooldowns.default', 60));
|
||||
$secondsSinceAction = now()->diffInSeconds($this->last_action_at);
|
||||
return max(0, $cooldownSeconds - $secondsSinceAction);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the cooldown to now
|
||||
*/
|
||||
public function refresh(): void
|
||||
{
|
||||
$this->last_action_at = now();
|
||||
$this->save();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -18,6 +18,8 @@ return Application::configure(basePath: dirname(__DIR__))
|
||||
'check.energy' => \App\Http\Middleware\Game\CheckEnergy::class,
|
||||
'check.jail' => \App\Http\Middleware\Game\CheckInJail::class,
|
||||
'check.hospital' => \App\Http\Middleware\Game\CheckInHospital::class,
|
||||
'check.alive' => \App\Http\Middleware\Game\CheckAlive::class,
|
||||
'check.cooldown' => \App\Http\Middleware\Game\CheckCooldown::class,
|
||||
]);
|
||||
|
||||
// Game middleware group
|
||||
|
||||
@@ -69,6 +69,20 @@ return [
|
||||
'hit_list_cost' => 5000, // Cost to place a hit
|
||||
],
|
||||
|
||||
// Attack system configuration
|
||||
'attack' => [
|
||||
'base_damage' => 5,
|
||||
'damage_variation' => 0.20, // +/- 20% random variation
|
||||
'crit_chance' => 0.10, // 10% chance for critical hit
|
||||
'crit_multiplier' => 2.0, // Critical hits do 2x damage
|
||||
'defense_reduction' => 0.5, // Each defense point reduces damage by 0.5
|
||||
'min_damage' => 1, // Minimum damage per hit
|
||||
'attack_cooldown' => 300, // 5 minutes between attacks
|
||||
'hit_list_cost' => 5000, // Cost to place a hit
|
||||
'mug_chance' => 0.30, // 30% chance to mug instead of hospitalize
|
||||
'mug_percent' => 0.20, // Steal 20% of victim's cash
|
||||
],
|
||||
|
||||
// Crime system configuration
|
||||
'crime' => [
|
||||
'success_rate' => 0.70, // 70% base success rate
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('attacks', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('attacker_id')->constrained('characters')->onDelete('cascade');
|
||||
$table->foreignId('defender_id')->constrained('characters')->onDelete('cascade');
|
||||
$table->integer('attacker_damage')->default(0);
|
||||
$table->integer('defender_damage')->default(0);
|
||||
$table->integer('attacker_health_after')->default(0);
|
||||
$table->integer('defender_health_after')->default(0);
|
||||
$table->string('result')->default('draw'); // win, lose, draw, escape
|
||||
$table->integer('money_stolen')->default(0);
|
||||
$table->integer('experience_gained')->default(0);
|
||||
$table->boolean('was_critical')->default(false);
|
||||
$table->boolean('was_mugging')->default(false);
|
||||
$table->foreignId('attacker_weapon_id')->nullable()->constrained('items')->onDelete('set null');
|
||||
$table->foreignId('defender_armor_id')->nullable()->constrained('items')->onDelete('set null');
|
||||
$table->timestamp('attacked_at')->useCurrent();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index('attacker_id');
|
||||
$table->index('defender_id');
|
||||
$table->index('attacked_at');
|
||||
$table->index('result');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('attacks');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
// Add indexes to characters table
|
||||
Schema::table('characters', function (Blueprint $table) {
|
||||
$table->index('user_id');
|
||||
$table->index('location_id');
|
||||
$table->index('faction_id');
|
||||
$table->index('status');
|
||||
$table->index('level');
|
||||
$table->index('health');
|
||||
$table->index('energy');
|
||||
$table->index('last_action_at');
|
||||
$table->index('in_hospital_until');
|
||||
$table->index('in_jail_until');
|
||||
});
|
||||
|
||||
// Add indexes to crimes table
|
||||
Schema::table('crimes', function (Blueprint $table) {
|
||||
$table->index('name');
|
||||
$table->index('slug');
|
||||
$table->index('difficulty');
|
||||
$table->index('location_id');
|
||||
$table->index('requires_level');
|
||||
});
|
||||
|
||||
// Add indexes to crime_attempts table
|
||||
Schema::table('crime_attempts', function (Blueprint $table) {
|
||||
$table->index('character_id');
|
||||
$table->index('crime_id');
|
||||
$table->index('success');
|
||||
$table->index('attempted_at');
|
||||
});
|
||||
|
||||
// Add indexes to inventories table
|
||||
Schema::table('inventories', function (Blueprint $table) {
|
||||
$table->index('character_id');
|
||||
$table->index('item_id');
|
||||
$table->index('equipped');
|
||||
});
|
||||
|
||||
// Add indexes to items table
|
||||
Schema::table('items', function (Blueprint $table) {
|
||||
$table->index('name');
|
||||
$table->index('slug');
|
||||
$table->index('type');
|
||||
$table->index('requires_level');
|
||||
$table->index('is_tradeable');
|
||||
$table->index('is_usable');
|
||||
});
|
||||
|
||||
// Add indexes to locations table
|
||||
Schema::table('locations', function (Blueprint $table) {
|
||||
$table->index('name');
|
||||
$table->index('slug');
|
||||
});
|
||||
|
||||
// Add indexes to notifications table
|
||||
Schema::table('notifications', function (Blueprint $table) {
|
||||
$table->index('user_id');
|
||||
$table->index('type');
|
||||
$table->index('read_at');
|
||||
$table->index('created_at');
|
||||
});
|
||||
|
||||
// Add indexes to activity_logs table
|
||||
Schema::table('activity_logs', function (Blueprint $table) {
|
||||
$table->index('character_id');
|
||||
$table->index('action');
|
||||
$table->index('created_at');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
// We don't remove indexes in down() as it's generally safe to keep them
|
||||
// and removing them might cause issues with existing data
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('cooldowns', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('character_id')->constrained('characters')->onDelete('cascade');
|
||||
$table->string('action_type'); // attack, crime, travel, train, etc.
|
||||
$table->timestamp('last_action_at')->useCurrent();
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['character_id', 'action_type']);
|
||||
$table->index('character_id');
|
||||
$table->index('action_type');
|
||||
$table->index('last_action_at');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('cooldowns');
|
||||
}
|
||||
};
|
||||
@@ -64,6 +64,16 @@ Route::middleware(['auth', 'verify.character'])->group(function () {
|
||||
Route::get('/', [GymController::class, 'index'])->name('index');
|
||||
Route::post('/train', [GymController::class, 'train'])->name('train')->middleware(['check.energy:gym']);
|
||||
});
|
||||
|
||||
// Attack routes
|
||||
Route::prefix('game/attack')->name('game.attack.')->group(function () {
|
||||
Route::get('/', [\App\Http\Controllers\Game\AttackController::class, 'index'])->name('index')->middleware(['check.alive']);
|
||||
Route::get('/preview/{target}', [\App\Http\Controllers\Game\AttackController::class, 'preview'])->name('preview')->middleware(['check.alive']);
|
||||
Route::post('/{target}', [\App\Http\Controllers\Game\AttackController::class, 'attack'])->name('attack')->middleware(['check.alive', 'check.cooldown:attack']);
|
||||
Route::post('/hit/{target}', [\App\Http\Controllers\Game\AttackController::class, 'placeHit'])->name('hit')->middleware(['check.alive']);
|
||||
Route::get('/history', [\App\Http\Controllers\Game\AttackController::class, 'history'])->name('history')->middleware(['verify.character']);
|
||||
Route::get('/revenge', [\App\Http\Controllers\Game\AttackController::class, 'revengeList'])->name('revenge')->middleware(['verify.character']);
|
||||
});
|
||||
});
|
||||
|
||||
// Hospital route (for when character is in hospital)
|
||||
|
||||
Reference in New Issue
Block a user