feat(world): add persistent citizen simulation with needs, jobs, and social features
- Adds Character model with survival needs (hunger, thirst, energy) and state machine - Implements city:tick world ticker for offline progression every minute - Adds city territory expansion and building construction system - Introduces job economy, inventory, and transaction audit trail - Adds social graph (friends, marriage, children) and character chat - Implements admin approval workflow for new registrations - Adds hospital admission system for traffic accidents - Includes comprehensive feature tests and world seeders
This commit is contained in:
+210
@@ -0,0 +1,210 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Web;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Character;
|
||||
use App\Models\Relationship;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* Social graph actions: friendships, marriage and children.
|
||||
*/
|
||||
class CitySocialController extends Controller
|
||||
{
|
||||
/**
|
||||
* Handle a social verb for the caller's character.
|
||||
*/
|
||||
public function handle(Request $request, string $verb): JsonResponse
|
||||
{
|
||||
$me = Character::ensureForUser($request->user());
|
||||
|
||||
return match ($verb) {
|
||||
'friend' => $this->friend($request, $me),
|
||||
'accept-friend' => $this->acceptFriend($request, $me),
|
||||
'marry' => $this->marry($request, $me),
|
||||
'child' => $this->child($request, $me),
|
||||
default => response()->json(['message' => __('city.social_unknown')], 422),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Pending incoming social requests for the caller.
|
||||
*/
|
||||
public function inbox(Request $request): JsonResponse
|
||||
{
|
||||
$me = Character::ensureForUser($request->user());
|
||||
|
||||
$pending = Relationship::query()
|
||||
->where('character_b_id', $me->id)
|
||||
->where('status', Relationship::STATUS_PENDING)
|
||||
->with('characterA:id,name')
|
||||
->get()
|
||||
->map(fn (Relationship $r) => [
|
||||
'id' => $r->id,
|
||||
'type' => $r->type,
|
||||
'from' => $r->characterA?->name,
|
||||
]);
|
||||
|
||||
$friends = Relationship::query()
|
||||
->where('status', Relationship::STATUS_ACCEPTED)
|
||||
->where('type', Relationship::TYPE_FRIEND)
|
||||
->where(fn ($q) => $q->where('character_a_id', $me->id)->orWhere('character_b_id', $me->id))
|
||||
->get()
|
||||
->map(function (Relationship $r) use ($me) {
|
||||
$otherId = (int) $r->character_a_id === (int) $me->id ? $r->character_b_id : $r->character_a_id;
|
||||
|
||||
return Character::query()->whereKey($otherId)->first(['id', 'name']);
|
||||
})
|
||||
->filter();
|
||||
|
||||
return response()->json([
|
||||
'pending' => $pending,
|
||||
'friends' => $friends,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a friendship request to another character.
|
||||
*/
|
||||
private function friend(Request $request, Character $me): JsonResponse
|
||||
{
|
||||
$data = $request->validate(['character_id' => ['required', 'integer', 'exists:characters,id']]);
|
||||
$other = Character::query()->findOrFail($data['character_id']);
|
||||
|
||||
if ((int) $other->id === (int) $me->id) {
|
||||
return response()->json(['message' => __('city.social_self')], 422);
|
||||
}
|
||||
|
||||
$existing = Relationship::query()
|
||||
->where('type', Relationship::TYPE_FRIEND)
|
||||
->where(fn ($q) => $q->where('character_a_id', $me->id)->where('character_b_id', $other->id))
|
||||
->orWhere(fn ($q) => $q->where('character_a_id', $other->id)->where('character_b_id', $me->id))
|
||||
->first();
|
||||
|
||||
if ($existing) {
|
||||
return response()->json(['message' => __('city.social_exists')], 422);
|
||||
}
|
||||
|
||||
Relationship::create([
|
||||
'character_a_id' => $me->id,
|
||||
'character_b_id' => $other->id,
|
||||
'type' => Relationship::TYPE_FRIEND,
|
||||
'status' => Relationship::STATUS_PENDING,
|
||||
]);
|
||||
|
||||
return response()->json(['message' => __('city.social_friend_sent')]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Accept an incoming friendship request.
|
||||
*/
|
||||
private function acceptFriend(Request $request, Character $me): JsonResponse
|
||||
{
|
||||
$data = $request->validate(['relationship_id' => ['required', 'integer', 'exists:relationships,id']]);
|
||||
$rel = Relationship::query()->findOrFail($data['relationship_id']);
|
||||
|
||||
if ((int) $rel->character_b_id !== (int) $me->id || $rel->type !== Relationship::TYPE_FRIEND) {
|
||||
return response()->json(['message' => __('auth.forbidden')], 403);
|
||||
}
|
||||
|
||||
$rel->status = Relationship::STATUS_ACCEPTED;
|
||||
$rel->save();
|
||||
|
||||
return response()->json(['message' => __('city.social_friend_ok')]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Marry an accepted friend (one spouse max).
|
||||
*/
|
||||
private function marry(Request $request, Character $me): JsonResponse
|
||||
{
|
||||
$data = $request->validate(['character_id' => ['required', 'integer', 'exists:characters,id']]);
|
||||
$other = Character::query()->findOrFail($data['character_id']);
|
||||
|
||||
$friendship = Relationship::query()
|
||||
->where('type', Relationship::TYPE_FRIEND)
|
||||
->where('status', Relationship::STATUS_ACCEPTED)
|
||||
->where(fn ($q) => $q->where('character_a_id', $me->id)->where('character_b_id', $other->id)
|
||||
->orWhere(fn ($q2) => $q2->where('character_a_id', $other->id)->where('character_b_id', $me->id)))
|
||||
->exists();
|
||||
|
||||
if (! $friendship) {
|
||||
return response()->json(['message' => __('city.social_need_friend')], 422);
|
||||
}
|
||||
|
||||
$existingSpouse = Relationship::query()
|
||||
->where('type', Relationship::TYPE_SPOUSE)
|
||||
->where('status', Relationship::STATUS_ACCEPTED)
|
||||
->where(fn ($q) => $q->where('character_a_id', $me->id)->orWhere('character_b_id', $me->id))
|
||||
->exists();
|
||||
|
||||
if ($existingSpouse) {
|
||||
return response()->json(['message' => __('city.social_married')], 422);
|
||||
}
|
||||
|
||||
Relationship::create([
|
||||
'character_a_id' => $me->id,
|
||||
'character_b_id' => $other->id,
|
||||
'type' => Relationship::TYPE_SPOUSE,
|
||||
'status' => Relationship::STATUS_ACCEPTED,
|
||||
]);
|
||||
|
||||
return response()->json(['message' => __('city.social_married_ok')]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Have a child with the current spouse (creates an NPC character).
|
||||
*/
|
||||
private function child(Request $request, Character $me): JsonResponse
|
||||
{
|
||||
$data = $request->validate(['character_id' => ['required', 'integer', 'exists:characters,id']]);
|
||||
$spouse = Character::query()->findOrFail($data['character_id']);
|
||||
|
||||
$married = Relationship::query()
|
||||
->where('type', Relationship::TYPE_SPOUSE)
|
||||
->where('status', Relationship::STATUS_ACCEPTED)
|
||||
->where(fn ($q) => $q->where('character_a_id', $me->id)->where('character_b_id', $spouse->id)
|
||||
->orWhere(fn ($q2) => $q2->where('character_a_id', $spouse->id)->where('character_b_id', $me->id)))
|
||||
->exists();
|
||||
|
||||
if (! $married) {
|
||||
return response()->json(['message' => __('city.social_need_spouse')], 422);
|
||||
}
|
||||
|
||||
$childName = Str::limit($me->name.' '.$spouse->name, 40, '');
|
||||
$child = Character::create([
|
||||
'user_id' => null,
|
||||
'city_id' => $me->city_id,
|
||||
'name' => trim($childName) ?: 'Child',
|
||||
'skin' => $me->skin,
|
||||
'hair' => $me->hair,
|
||||
'shirt' => random_int(1, 5),
|
||||
'pos_x' => $me->pos_x + 1,
|
||||
'pos_z' => $me->pos_z + 1,
|
||||
'money' => 0,
|
||||
'energy' => 100,
|
||||
'hunger' => 100,
|
||||
'thirst' => 100,
|
||||
'state' => Character::STATE_AWAKE,
|
||||
'education' => 0,
|
||||
'is_online' => false,
|
||||
]);
|
||||
|
||||
foreach ([$me, $spouse] as $parent) {
|
||||
Relationship::create([
|
||||
'character_a_id' => $child->id,
|
||||
'character_b_id' => $parent->id,
|
||||
'type' => Relationship::TYPE_PARENT_CHILD,
|
||||
'status' => Relationship::STATUS_ACCEPTED,
|
||||
]);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'message' => __('city.social_child_ok'),
|
||||
'child' => ['id' => $child->id, 'name' => $child->name],
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user