331 lines
12 KiB
PHP
331 lines
12 KiB
PHP
<?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'
|
||
|
|
));
|
||
|
|
}
|
||
|
|
}
|