feat(city): add plaza exercise system with health tracking
- Adds CityPlazas support class for deterministic public plaza generation with outdoor gym equipment - Implements exercise and stop-exercise actions with proximity validation to equipment - Introduces health (0-100) and exercise_count attributes to Character model - Reduces hospital stay duration from two game days to one game hour - Adds 3D gym equipment rendering (bench, bars, bike, treadmill) in city viewer - Includes exercise state machine handling in world ticker with energy drain - Adds comprehensive PlazaExerciseTest and updates HospitalAdmissionTest - Provides localized UI strings for health HUD and exercise prompts
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\CityPlazas;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
@@ -97,14 +98,90 @@ class CityActionController extends Controller
|
||||
'sell' => $this->sell($request, $character),
|
||||
'build' => $this->build($request, $character),
|
||||
'apply-job' => $this->applyJob($request, $character),
|
||||
'exercise' => $this->exercise($request, $character),
|
||||
'stop-exercise' => $this->stopExercise($character),
|
||||
default => response()->json(['message' => __('city.action_unknown')], 422),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
|
||||
return response()->json([
|
||||
'message' => __('city.exercise_ok'),
|
||||
'equipment' => $nearby,
|
||||
'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 two game days (Character::HOSPITAL_STAY_SECONDS).
|
||||
* Stays one game hour (Character::HOSPITAL_STAY_SECONDS).
|
||||
*/
|
||||
private function accident(Character $character): JsonResponse
|
||||
{
|
||||
@@ -483,7 +560,9 @@ class CityActionController extends Controller
|
||||
'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,
|
||||
|
||||
Reference in New Issue
Block a user