feat(city): add east shore beach leisure activity
Characters can now relax on the beach sand to recover energy and health. The server clamps position to the shoreline, exposes beach geometry in the city payload, and the client renders animated waves, sand, stalls, chairs, and ambient NPCs. Includes Beach support class, new beach action endpoint, localization strings, and feature tests.
This commit is contained in:
@@ -9,6 +9,7 @@ use App\Models\City;
|
||||
use App\Models\InventoryItem;
|
||||
use App\Models\Job;
|
||||
use App\Models\Transaction;
|
||||
use App\Support\Beach;
|
||||
use App\Support\CityPlazas;
|
||||
use App\Support\MissionTracker;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
@@ -48,7 +49,10 @@ class CityActionController extends Controller
|
||||
}
|
||||
|
||||
$city = $character->city;
|
||||
$x = max((float) $city->bounds_min_x, min((float) $city->bounds_max_x, (float) $data['pos_x']));
|
||||
// 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']));
|
||||
$z = max((float) $city->bounds_min_z, min((float) $city->bounds_max_z, (float) $data['pos_z']));
|
||||
|
||||
$character->forceFill([
|
||||
@@ -100,12 +104,63 @@ class CityActionController extends Controller
|
||||
'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.
|
||||
*/
|
||||
|
||||
@@ -9,6 +9,7 @@ use App\Models\Character;
|
||||
use App\Models\City;
|
||||
use App\Models\Job;
|
||||
use App\Models\Store;
|
||||
use App\Support\Beach;
|
||||
use App\Support\CityPlazas;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
@@ -141,6 +142,7 @@ class CityViewerController extends Controller
|
||||
] : null,
|
||||
'layout' => $this->cityLayout($city, $stores->all()),
|
||||
'plazas' => CityPlazas::forCity($city),
|
||||
'beach' => Beach::geometry($city),
|
||||
'agents' => AiAgent::orderBy('id')->get()->map(fn ($a): array => [
|
||||
'id' => $a->id,
|
||||
'name' => $a->name,
|
||||
|
||||
Reference in New Issue
Block a user