Adds a new port district on the north-east coast with walkable quay, container yard, terminal, gantry cranes, and berths. Includes AI-driven port master (Capitão do Porto) and dock workers for ship dispatch, plus full frontend rendering of ships, boats, and trucks.
719 lines
24 KiB
PHP
Executable File
719 lines
24 KiB
PHP
Executable File
<?php
|
||
|
||
namespace App\Http\Controllers\Web;
|
||
|
||
use App\Http\Controllers\Controller;
|
||
use App\Models\Building;
|
||
use App\Models\Character;
|
||
use App\Models\City;
|
||
use App\Models\InventoryItem;
|
||
use App\Models\Job;
|
||
use App\Models\Transaction;
|
||
use App\Support\Airports;
|
||
use App\Support\Beach;
|
||
use App\Support\CityPlazas;
|
||
use App\Support\MissionTracker;
|
||
use App\Support\Port;
|
||
use Illuminate\Http\JsonResponse;
|
||
use Illuminate\Http\Request;
|
||
use Illuminate\Support\Facades\DB;
|
||
|
||
/**
|
||
* Player-driven world actions: position sync and lifecycle verbs.
|
||
*
|
||
* Every action targets the authenticated user's own character only and
|
||
* respects city bounds and inventory/money rules from the design spec.
|
||
*/
|
||
class CityActionController extends Controller
|
||
{
|
||
public const BUILD_MATERIALS = 50;
|
||
|
||
/**
|
||
* Persist the caller's character position (throttled from the viewer).
|
||
*/
|
||
public function position(Request $request): JsonResponse
|
||
{
|
||
$character = $this->ownCharacter($request);
|
||
$character->releaseIfHospitalStayOver();
|
||
$data = $request->validate([
|
||
'pos_x' => ['required', 'numeric'],
|
||
'pos_z' => ['required', 'numeric'],
|
||
]);
|
||
|
||
if ($character->isHospitalized()) {
|
||
return response()->json([
|
||
'message' => __('city.hospital_bed'),
|
||
'pos_x' => $character->pos_x,
|
||
'pos_z' => $character->pos_z,
|
||
'state' => $character->state,
|
||
'hospitalized_until' => $character->hospitalized_until?->toIso8601String(),
|
||
]);
|
||
}
|
||
|
||
$city = $character->city;
|
||
// Players may persist onto the east beach sand (past the road grid)
|
||
// but never beyond the shoreline.
|
||
$maxX = Beach::sandTo($city) + 2;
|
||
$x = max((float) $city->bounds_min_x, min($maxX, (float) $data['pos_x']));
|
||
// Airport grounds (north/south strips) are walkable beyond the
|
||
// city bounds inside their X band.
|
||
$minZ = (float) $city->bounds_min_z;
|
||
$maxZ = (float) $city->bounds_max_z;
|
||
if (abs($x) <= Airports::VISIT_HALF_WIDTH) {
|
||
$minZ = Airports::VISIT_MIN_Z;
|
||
$maxZ = Airports::VISIT_MAX_Z;
|
||
}
|
||
// Port strip (north-east coast): walking east of the bounds opens
|
||
// the Z range down to the port grounds/quay.
|
||
if ($x >= Port::VISIT_MIN_X) {
|
||
$minZ = Port::VISIT_MIN_Z;
|
||
}
|
||
$z = max($minZ, min($maxZ, (float) $data['pos_z']));
|
||
|
||
$character->forceFill([
|
||
'pos_x' => $x,
|
||
'pos_z' => $z,
|
||
'pos_y' => 0,
|
||
'is_online' => true,
|
||
])->save();
|
||
|
||
return response()->json([
|
||
'pos_x' => $character->pos_x,
|
||
'pos_z' => $character->pos_z,
|
||
'state' => $character->state,
|
||
'hospitalized_until' => null,
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* Dispatch a named world action for the caller's character.
|
||
*/
|
||
public function handle(Request $request, string $action): JsonResponse
|
||
{
|
||
$character = $this->ownCharacter($request);
|
||
$character->releaseIfHospitalStayOver();
|
||
|
||
// Traffic accidents are always accepted, even from the hospital bed.
|
||
if ($action === 'accident') {
|
||
return $this->accident($character);
|
||
}
|
||
|
||
if ($character->isHospitalized()) {
|
||
return response()->json([
|
||
'message' => __('city.hospital_blocked'),
|
||
'hospitalized_until' => $character->hospitalized_until?->toIso8601String(),
|
||
'character' => $this->characterData($character),
|
||
], 422);
|
||
}
|
||
|
||
return match ($action) {
|
||
'eat' => $this->eat($character),
|
||
'drink' => $this->drink($character),
|
||
'sleep' => $this->sleep($character),
|
||
'wake' => $this->wake($character),
|
||
'work' => $this->work($character),
|
||
'stop-work' => $this->stopWork($character),
|
||
'study' => $this->study($character),
|
||
'buy' => $this->buy($request, $character),
|
||
'sell' => $this->sell($request, $character),
|
||
'build' => $this->build($request, $character),
|
||
'apply-job' => $this->applyJob($request, $character),
|
||
'exercise' => $this->exercise($request, $character),
|
||
'beach' => $this->beach($request, $character),
|
||
'stop-exercise' => $this->stopExercise($character),
|
||
'ride' => $this->ride($request, $character),
|
||
default => response()->json(['message' => __('city.action_unknown')], 422),
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Relax on the east beach while standing on the sand.
|
||
*
|
||
* The client posts this on a slow timer during a relax session (the
|
||
* same pattern as exercise): each tick recovers a bit of energy and
|
||
* health, capped at 100, and only inside the beach zone.
|
||
*/
|
||
private function beach(Request $request, Character $character): JsonResponse
|
||
{
|
||
if ($character->state === Character::STATE_SLEEPING) {
|
||
return response()->json(['message' => __('city.exercise_need_awake')], 422);
|
||
}
|
||
|
||
if ($character->state === Character::STATE_EXERCISING) {
|
||
return response()->json(['message' => __('city.beach_busy')], 422);
|
||
}
|
||
|
||
$data = $request->validate([
|
||
'pos_x' => ['nullable', 'numeric'],
|
||
'pos_z' => ['nullable', 'numeric'],
|
||
]);
|
||
|
||
$x = array_key_exists('pos_x', $data) ? (float) $data['pos_x'] : (float) $character->pos_x;
|
||
$z = array_key_exists('pos_z', $data) ? (float) $data['pos_z'] : (float) $character->pos_z;
|
||
|
||
if (! Beach::isOnBeach($character->city, $x, $z)) {
|
||
return response()->json(['message' => __('city.beach_need')], 422);
|
||
}
|
||
|
||
if ((int) $character->energy >= 100 && (int) $character->health >= 100) {
|
||
return response()->json([
|
||
'message' => __('city.beach_full'),
|
||
'full' => true,
|
||
'character' => $this->characterData($character),
|
||
]);
|
||
}
|
||
|
||
$character->forceFill([
|
||
'health' => min(100, (int) $character->health + 1),
|
||
'energy' => min(100, (int) $character->energy + 1),
|
||
'pos_x' => $x,
|
||
'pos_z' => $z,
|
||
])->save();
|
||
|
||
return response()->json([
|
||
'message' => __('city.beach_ok'),
|
||
'character' => $this->characterData($character),
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* Board a nearby bus stop or train for a short ride.
|
||
*/
|
||
private function ride(Request $request, Character $character): JsonResponse
|
||
{
|
||
if ($character->isHospitalized()) {
|
||
return response()->json(['message' => __('city.hospital_blocked')], 422);
|
||
}
|
||
|
||
$data = $request->validate([
|
||
'kind' => ['required', 'in:bus,train'],
|
||
'pos_x' => ['nullable', 'numeric'],
|
||
'pos_z' => ['nullable', 'numeric'],
|
||
]);
|
||
|
||
$x = array_key_exists('pos_x', $data) ? (float) $data['pos_x'] : (float) $character->pos_x;
|
||
$z = array_key_exists('pos_z', $data) ? (float) $data['pos_z'] : (float) $character->pos_z;
|
||
|
||
// Must be near a road (bus) or the train corridor (train).
|
||
$city = $character->city;
|
||
if ($data['kind'] === 'bus') {
|
||
$nearRoad = false;
|
||
for ($vx = ceil(((float) $city->bounds_min_x) / 40) * 40; $vx <= $city->bounds_max_x; $vx += 40) {
|
||
if (abs($x - $vx) < 8) {
|
||
$nearRoad = true;
|
||
break;
|
||
}
|
||
}
|
||
if (! $nearRoad) {
|
||
for ($vz = ceil(((float) $city->bounds_min_z) / 40) * 40; $vz <= $city->bounds_max_z; $vz += 40) {
|
||
if (abs($z - $vz) < 8) {
|
||
$nearRoad = true;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
if (! $nearRoad) {
|
||
return response()->json(['message' => __('city.ride_need_stop')], 422);
|
||
}
|
||
} elseif (abs($z - ((float) $city->bounds_max_z + 14)) > 20) {
|
||
return response()->json(['message' => __('city.ride_need_stop')], 422);
|
||
}
|
||
|
||
$character->forceFill([
|
||
'ride_kind' => $data['kind'],
|
||
'ride_until' => now()->addSeconds(45),
|
||
'state' => Character::STATE_AWAKE,
|
||
'pos_x' => $x,
|
||
'pos_z' => $z,
|
||
'is_online' => true,
|
||
])->save();
|
||
|
||
return response()->json([
|
||
'message' => $data['kind'] === 'train' ? __('city.ride_train_ok') : __('city.ride_bus_ok'),
|
||
'character' => $this->characterData($character),
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* One exercise rep on nearby plaza gym equipment: +health, −energy.
|
||
*
|
||
* Optional pos_x/pos_z let the client report its live position so the
|
||
* proximity check does not wait for the throttled position sync.
|
||
*/
|
||
private function exercise(Request $request, Character $character): JsonResponse
|
||
{
|
||
if ($character->state === Character::STATE_SLEEPING) {
|
||
return response()->json(['message' => __('city.exercise_need_awake')], 422);
|
||
}
|
||
|
||
$data = $request->validate([
|
||
'pos_x' => ['nullable', 'numeric'],
|
||
'pos_z' => ['nullable', 'numeric'],
|
||
]);
|
||
|
||
$x = array_key_exists('pos_x', $data) ? (float) $data['pos_x'] : (float) $character->pos_x;
|
||
$z = array_key_exists('pos_z', $data) ? (float) $data['pos_z'] : (float) $character->pos_z;
|
||
|
||
$nearby = CityPlazas::nearestEquipment(
|
||
$character->city,
|
||
$x,
|
||
$z,
|
||
4.0,
|
||
);
|
||
|
||
if ($nearby === null) {
|
||
return response()->json(['message' => __('city.exercise_need_equipment')], 422);
|
||
}
|
||
|
||
if ((int) $character->energy < 5) {
|
||
return response()->json(['message' => __('city.exercise_tired')], 422);
|
||
}
|
||
|
||
if ((int) $character->health >= 100) {
|
||
return response()->json([
|
||
'message' => __('city.exercise_max_health'),
|
||
'character' => $this->characterData($character),
|
||
]);
|
||
}
|
||
|
||
$character->forceFill([
|
||
'state' => Character::STATE_EXERCISING,
|
||
'health' => min(100, (int) $character->health + 2),
|
||
'energy' => max(0, (int) $character->energy - 2),
|
||
'exercise_count' => (int) $character->exercise_count + 1,
|
||
'pos_x' => $x,
|
||
'pos_z' => $z,
|
||
])->save();
|
||
|
||
$missionDones = MissionTracker::sync($character, 'exercise_count');
|
||
|
||
return response()->json([
|
||
'message' => __('city.exercise_ok'),
|
||
'equipment' => $nearby,
|
||
'missions_completed' => $missionDones,
|
||
'character' => $this->characterData($character),
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* Leave the exercising state (back to awake).
|
||
*/
|
||
private function stopExercise(Character $character): JsonResponse
|
||
{
|
||
if ($character->state === Character::STATE_EXERCISING) {
|
||
$character->state = Character::STATE_AWAKE;
|
||
$character->save();
|
||
}
|
||
|
||
return response()->json([
|
||
'message' => __('city.action_ok'),
|
||
'character' => $this->characterData($character),
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* Admit the character to the hospital after a vehicle collision.
|
||
*
|
||
* Stays one game hour (Character::HOSPITAL_STAY_SECONDS).
|
||
*/
|
||
private function accident(Character $character): JsonResponse
|
||
{
|
||
if ($character->isHospitalized()) {
|
||
return response()->json([
|
||
'message' => __('city.hospital_already'),
|
||
'hospitalized_until' => $character->hospitalized_until?->toIso8601String(),
|
||
'character' => $this->characterData($character),
|
||
]);
|
||
}
|
||
|
||
[$x, $z] = $character->admitToHospital();
|
||
|
||
return response()->json([
|
||
'message' => __('city.hospital_hit'),
|
||
'hospitalized_until' => $character->hospitalized_until?->toIso8601String(),
|
||
'character' => $this->characterData($character),
|
||
'bed' => ['pos_x' => $x, 'pos_z' => $z],
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* Resolve the character owned by the authenticated user.
|
||
*/
|
||
private function ownCharacter(Request $request): Character
|
||
{
|
||
$user = $request->user();
|
||
abort_unless($user !== null, 401);
|
||
|
||
return Character::ensureForUser($user);
|
||
}
|
||
|
||
/**
|
||
* Consume one food ration and restore hunger.
|
||
*/
|
||
private function eat(Character $character): JsonResponse
|
||
{
|
||
if (! $this->consume($character, InventoryItem::ITEM_FOOD, 1)) {
|
||
return response()->json(['message' => __('city.no_food')], 422);
|
||
}
|
||
|
||
$character->hunger = min(100, (int) $character->hunger + 40);
|
||
$character->save();
|
||
|
||
return $this->characterPayload($character);
|
||
}
|
||
|
||
/**
|
||
* Consume one water ration and restore thirst.
|
||
*/
|
||
private function drink(Character $character): JsonResponse
|
||
{
|
||
if (! $this->consume($character, InventoryItem::ITEM_WATER, 1)) {
|
||
return response()->json(['message' => __('city.no_water')], 422);
|
||
}
|
||
|
||
$character->thirst = min(100, (int) $character->thirst + 40);
|
||
$character->save();
|
||
|
||
return $this->characterPayload($character);
|
||
}
|
||
|
||
/**
|
||
* Lie down to sleep (requires a completed house).
|
||
*/
|
||
private function sleep(Character $character): JsonResponse
|
||
{
|
||
if ($character->state === Character::STATE_SLEEPING) {
|
||
return $this->characterPayload($character);
|
||
}
|
||
|
||
if ($character->house_id === null || ! $character->house?->is_complete) {
|
||
return response()->json(['message' => __('city.no_house')], 422);
|
||
}
|
||
|
||
$character->state = Character::STATE_SLEEPING;
|
||
$character->save();
|
||
|
||
return $this->characterPayload($character);
|
||
}
|
||
|
||
/**
|
||
* Leave the sleeping state.
|
||
*/
|
||
private function wake(Character $character): JsonResponse
|
||
{
|
||
if ($character->state === Character::STATE_SLEEPING) {
|
||
$character->state = Character::STATE_AWAKE;
|
||
$character->save();
|
||
}
|
||
|
||
return $this->characterPayload($character);
|
||
}
|
||
|
||
/**
|
||
* Start working (requires employment).
|
||
*/
|
||
private function work(Character $character): JsonResponse
|
||
{
|
||
if ($character->job_id === null) {
|
||
return response()->json(['message' => __('city.no_job')], 422);
|
||
}
|
||
|
||
$character->state = Character::STATE_WORKING;
|
||
$character->save();
|
||
|
||
return $this->characterPayload($character);
|
||
}
|
||
|
||
/**
|
||
* Stop working or studying.
|
||
*/
|
||
private function stopWork(Character $character): JsonResponse
|
||
{
|
||
if (in_array($character->state, [Character::STATE_WORKING, Character::STATE_STUDYING], true)) {
|
||
$character->state = Character::STATE_AWAKE;
|
||
$character->save();
|
||
}
|
||
|
||
return $this->characterPayload($character);
|
||
}
|
||
|
||
/**
|
||
* Study at the city school (requires a school building type presence or job city school).
|
||
*/
|
||
private function study(Character $character): JsonResponse
|
||
{
|
||
$hasSchool = Building::query()
|
||
->where('city_id', $character->city_id)
|
||
->whereIn('type', ['school', Building::TYPE_HOUSE])
|
||
->exists();
|
||
|
||
$hasCivicSchool = Building::query()
|
||
->where('city_id', $character->city_id)
|
||
->where('type', 'school')
|
||
->exists();
|
||
|
||
// City10 ships with a public school implied by the "Professor" job city.
|
||
if (! $hasCivicSchool && ! $hasSchool) {
|
||
$hasSchool = true;
|
||
}
|
||
|
||
if (! $hasSchool) {
|
||
return response()->json(['message' => __('city.no_school')], 422);
|
||
}
|
||
|
||
$character->state = Character::STATE_STUDYING;
|
||
$character->save();
|
||
|
||
return $this->characterPayload($character);
|
||
}
|
||
|
||
/**
|
||
* Accept an open job position in the character's city.
|
||
*/
|
||
private function applyJob(Request $request, Character $character): JsonResponse
|
||
{
|
||
$data = $request->validate([
|
||
'job_id' => ['required', 'integer', 'exists:job_positions,id'],
|
||
]);
|
||
|
||
$job = Job::query()->findOrFail($data['job_id']);
|
||
if ((int) $job->city_id !== (int) $character->city_id) {
|
||
return response()->json(['message' => __('city.job_other_city')], 422);
|
||
}
|
||
|
||
if ((int) $character->education < (int) $job->requires_education) {
|
||
return response()->json(['message' => __('city.job_education')], 422);
|
||
}
|
||
|
||
$character->job_id = $job->id;
|
||
$character->state = Character::STATE_AWAKE;
|
||
$character->save();
|
||
|
||
return $this->characterPayload($character);
|
||
}
|
||
|
||
/**
|
||
* Buy items from the city market using money.
|
||
*/
|
||
private function buy(Request $request, Character $character): JsonResponse
|
||
{
|
||
$data = $request->validate([
|
||
'item' => ['required', 'in:food,water,materials,product'],
|
||
'qty' => ['required', 'integer', 'min:1', 'max:50'],
|
||
'price' => ['required', 'numeric', 'min:0.01', 'max:1000'],
|
||
]);
|
||
|
||
$total = round($data['price'] * $data['qty'], 2);
|
||
if ($character->money < $total) {
|
||
return response()->json(['message' => __('city.no_money')], 422);
|
||
}
|
||
|
||
DB::transaction(function () use ($character, $data, $total): void {
|
||
$character->money = $character->money - $total;
|
||
$character->save();
|
||
$this->grant($character, $data['item'], (int) $data['qty']);
|
||
Transaction::create([
|
||
'from_character_id' => $character->id,
|
||
'item' => $data['item'],
|
||
'qty' => (int) $data['qty'],
|
||
'amount' => $total,
|
||
]);
|
||
});
|
||
|
||
return $this->characterPayload($character);
|
||
}
|
||
|
||
/**
|
||
* Sell inventory items for money.
|
||
*/
|
||
private function sell(Request $request, Character $character): JsonResponse
|
||
{
|
||
$data = $request->validate([
|
||
'item' => ['required', 'in:food,water,materials,product'],
|
||
'qty' => ['required', 'integer', 'min:1', 'max:50'],
|
||
'price' => ['required', 'numeric', 'min:0.01', 'max:1000'],
|
||
]);
|
||
|
||
if (! $this->consume($character, $data['item'], (int) $data['qty'])) {
|
||
return response()->json(['message' => __('city.no_inventory')], 422);
|
||
}
|
||
|
||
$total = round($data['price'] * $data['qty'], 2);
|
||
DB::transaction(function () use ($character, $data, $total): void {
|
||
$character->money = $character->money + $total;
|
||
$character->save();
|
||
Transaction::create([
|
||
'to_character_id' => $character->id,
|
||
'item' => $data['item'],
|
||
'qty' => (int) $data['qty'],
|
||
'amount' => $total,
|
||
]);
|
||
});
|
||
|
||
return $this->characterPayload($character);
|
||
}
|
||
|
||
/**
|
||
* Start constructing a house on free terrain.
|
||
*/
|
||
private function build(Request $request, Character $character): JsonResponse
|
||
{
|
||
$data = $request->validate([
|
||
'pos_x' => ['required', 'numeric'],
|
||
'pos_z' => ['required', 'numeric'],
|
||
'name' => ['nullable', 'string', 'max:120'],
|
||
]);
|
||
|
||
$city = $character->city;
|
||
$x = (float) $data['pos_x'];
|
||
$z = (float) $data['pos_z'];
|
||
|
||
if (! $city->contains($x, $z)) {
|
||
return response()->json(['message' => __('city.out_of_bounds')], 422);
|
||
}
|
||
|
||
if (! $this->consume($character, InventoryItem::ITEM_MATERIALS, self::BUILD_MATERIALS)) {
|
||
return response()->json(['message' => __('city.no_materials')], 422);
|
||
}
|
||
|
||
$width = 10;
|
||
$depth = 10;
|
||
$overlaps = Building::query()
|
||
->where('city_id', $city->id)
|
||
->get()
|
||
->contains(function (Building $b) use ($x, $z, $width, $depth): bool {
|
||
$minX = (float) $b->pos_x - (float) $b->width / 2;
|
||
$maxX = (float) $b->pos_x + (float) $b->width / 2;
|
||
$minZ = (float) $b->pos_z - (float) $b->depth / 2;
|
||
$maxZ = (float) $b->pos_z + (float) $b->depth / 2;
|
||
|
||
return ($x + $width / 2) > $minX && ($x - $width / 2) < $maxX
|
||
&& ($z + $depth / 2) > $minZ && ($z - $depth / 2) < $maxZ;
|
||
});
|
||
|
||
if ($overlaps) {
|
||
return response()->json(['message' => __('city.site_taken')], 422);
|
||
}
|
||
|
||
$building = Building::create([
|
||
'city_id' => $city->id,
|
||
'owner_character_id' => $character->id,
|
||
'type' => Building::TYPE_HOUSE,
|
||
'name' => $data['name'] ?? ($character->name."'s house"),
|
||
'pos_x' => $x,
|
||
'pos_z' => $z,
|
||
'width' => $width,
|
||
'depth' => $depth,
|
||
'height' => 5,
|
||
'is_complete' => false,
|
||
'build_progress' => 0,
|
||
]);
|
||
|
||
MissionTracker::sync($character->fresh(), 'buildings');
|
||
|
||
return response()->json([
|
||
'message' => __('city.build_started'),
|
||
'building' => [
|
||
'id' => $building->id,
|
||
'pos_x' => $building->pos_x,
|
||
'pos_z' => $building->pos_z,
|
||
'progress' => $building->build_progress,
|
||
],
|
||
'character' => $this->characterData($character->fresh()),
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* Grant inventory quantity (upsert).
|
||
*/
|
||
private function grant(Character $character, string $item, int $qty): void
|
||
{
|
||
$row = InventoryItem::query()
|
||
->where('character_id', $character->id)
|
||
->where('item', $item)
|
||
->first();
|
||
|
||
if ($row) {
|
||
$row->qty = (int) $row->qty + $qty;
|
||
$row->save();
|
||
|
||
return;
|
||
}
|
||
|
||
InventoryItem::create([
|
||
'character_id' => $character->id,
|
||
'item' => $item,
|
||
'qty' => $qty,
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* Consume inventory quantity when available.
|
||
*/
|
||
private function consume(Character $character, string $item, int $qty): bool
|
||
{
|
||
$row = InventoryItem::query()
|
||
->where('character_id', $character->id)
|
||
->where('item', $item)
|
||
->first();
|
||
|
||
if ($row === null || (int) $row->qty < $qty) {
|
||
return false;
|
||
}
|
||
|
||
$row->qty = (int) $row->qty - $qty;
|
||
$row->save();
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* Standard character JSON payload for action responses.
|
||
*/
|
||
private function characterPayload(Character $character): JsonResponse
|
||
{
|
||
return response()->json([
|
||
'message' => __('city.action_ok'),
|
||
'character' => $this->characterData($character),
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* Character attributes exposed to the client.
|
||
*
|
||
* @return array<string, mixed>
|
||
*/
|
||
private function characterData(Character $character): array
|
||
{
|
||
$character->loadMissing('inventory', 'job');
|
||
|
||
return [
|
||
'id' => $character->id,
|
||
'name' => $character->name,
|
||
'state' => $character->state,
|
||
'money' => (float) $character->money,
|
||
'energy' => (int) $character->energy,
|
||
'hunger' => (int) $character->hunger,
|
||
'thirst' => (int) $character->thirst,
|
||
'health' => (int) $character->health,
|
||
'education' => (int) $character->education,
|
||
'exercise_count' => (int) $character->exercise_count,
|
||
'job_id' => $character->job_id,
|
||
'job_title' => $character->job?->title,
|
||
'house_id' => $character->house_id,
|
||
'pos_x' => (float) $character->pos_x,
|
||
'pos_z' => (float) $character->pos_z,
|
||
'hospitalized_until' => $character->hospitalized_until?->toIso8601String(),
|
||
'face' => (int) $character->face,
|
||
'beard' => (int) $character->beard,
|
||
'accessory' => (int) $character->accessory,
|
||
'skin' => (int) $character->skin,
|
||
'hair' => (int) $character->hair,
|
||
'shirt' => (int) $character->shirt,
|
||
'ride_kind' => $character->ride_kind,
|
||
'ride_until' => $character->ride_until?->toIso8601String(),
|
||
'inventory' => $character->inventory->mapWithKeys(fn ($i) => [$i->item => (int) $i->qty])->all(),
|
||
];
|
||
}
|
||
}
|