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:
2026-10-17 21:46:23 -03:00
parent 79d12fa2f4
commit 79ce2a1825
132 changed files with 11809 additions and 1036 deletions
+27
View File
@@ -1,8 +1,14 @@
<?php
use App\Http\Controllers\Web\AccountController;
use App\Http\Controllers\Web\Admin\CitizenAdminController;
use App\Http\Controllers\Web\Admin\CityAdminController;
use App\Http\Controllers\Web\Admin\StoreAdminController;
use App\Http\Controllers\Web\AuthController;
use App\Http\Controllers\Web\CharacterChatController;
use App\Http\Controllers\Web\ChatController;
use App\Http\Controllers\Web\CityActionController;
use App\Http\Controllers\Web\CitySocialController;
use App\Http\Controllers\Web\CityViewerController;
use App\Http\Controllers\Web\DashboardController;
use App\Http\Controllers\Web\LanguageController;
@@ -55,6 +61,9 @@ Route::middleware('guest')->group(function (): void {
Route::middleware('auth')->group(function (): void {
Route::post('/logout', [AuthController::class, 'logout'])->name('logout');
// Pending registration wait screen (reachable before full approval).
Route::get('/account/pending', [AccountController::class, 'pending'])->name('account.pending');
// Consent screen is reachable even before terms are accepted (by design).
Route::get('/terms/accept', [LegalController::class, 'acceptForm'])->name('terms.accept');
Route::post('/terms/accept', [LegalController::class, 'accept'])->name('terms.accept.store');
@@ -68,6 +77,16 @@ Route::middleware('auth')->group(function (): void {
Route::get('/city', CityViewerController::class)->name('city.viewer');
Route::get('/city/data', [CityViewerController::class, 'data'])->name('city.data');
// Citizen lifecycle actions inside the world.
Route::post('/city/actions/position', [CityActionController::class, 'position'])->name('city.actions.position');
Route::post('/city/actions/{action}', [CityActionController::class, 'handle'])->name('city.actions.handle');
// Character social graph and chat.
Route::post('/city/social/{verb}', [CitySocialController::class, 'handle'])->name('city.social.handle');
Route::get('/city/social/inbox', [CitySocialController::class, 'inbox'])->name('city.social.inbox');
Route::post('/city/messages/{character}', [CharacterChatController::class, 'send'])->name('city.messages.send');
Route::get('/city/messages/{character}', [CharacterChatController::class, 'index'])->name('city.messages.index');
// NPC chat (SSE stream + message POST).
Route::post('/city/chat/{agent}', [ChatController::class, 'send'])->name('city.chat.send');
Route::get('/city/chat/{agent}/stream', [ChatController::class, 'stream'])->name('city.chat.stream');
@@ -88,4 +107,12 @@ Route::middleware(['auth', EnsureTermsAccepted::class, EnsureUserIsAdmin::class]
Route::get('/stores/{store}/edit', [StoreAdminController::class, 'edit'])->name('stores.edit');
Route::put('/stores/{store}', [StoreAdminController::class, 'update'])->name('stores.update');
Route::delete('/stores/{store}', [StoreAdminController::class, 'destroy'])->name('stores.destroy');
Route::get('/citizens', [CitizenAdminController::class, 'index'])->name('citizens.index');
Route::post('/citizens/{user}/approve', [CitizenAdminController::class, 'approve'])->name('citizens.approve');
Route::post('/citizens/{user}/ban', [CitizenAdminController::class, 'ban'])->name('citizens.ban');
Route::get('/cities', [CityAdminController::class, 'index'])->name('cities.index');
Route::post('/cities', [CityAdminController::class, 'store'])->name('cities.store');
Route::post('/cities/{city}/expand', [CityAdminController::class, 'expand'])->name('cities.expand');
});