- Add two airports (north/south) with full geometry: runways, taxiways,
aprons, terminals, hangars, stands, and helipads; walkable airport
grounds extend beyond city bounds.
- Introduce ATC AI agent ("Torre de Controle City10") using AI_ATC_MODEL
to generate takeoff clearances; includes dispatch endpoint with
30-second cooldown and deterministic fallback on failure.
- Implement AI model fallback chain: preferred model → AI_FALLBACK_MODEL
before failing, applied in ChatController and PersonTalkController.
- Add luminous sign labels (letreiros) for plazas, beach, food, hospital,
school, city hall, and airports with day/night glow.
- Fix city presence cache: compute is_me per viewer instead of baking
into shared per-second cache to prevent cross-user flag leakage.
- Add tests: AirportTest, CityPresenceTest, SignLabelsTest, and
PersonTalkTest fallback model retry scenario.
168 lines
8.5 KiB
PHP
Executable File
168 lines
8.5 KiB
PHP
Executable File
<?php
|
|
|
|
use App\Http\Controllers\Web\AccountController;
|
|
use App\Http\Controllers\Web\Admin\AdminWorldController;
|
|
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\CharacterCustomizeController;
|
|
use App\Http\Controllers\Web\ChatController;
|
|
use App\Http\Controllers\Web\CityActionController;
|
|
use App\Http\Controllers\Web\CityMarketController;
|
|
use App\Http\Controllers\Web\CityPresenceController;
|
|
use App\Http\Controllers\Web\CitySocialController;
|
|
use App\Http\Controllers\Web\CityViewerController;
|
|
use App\Http\Controllers\Web\DashboardController;
|
|
use App\Http\Controllers\Web\LanguageController;
|
|
use App\Http\Controllers\Web\LegalController;
|
|
use App\Http\Controllers\Web\PersonTalkController;
|
|
use App\Http\Middleware\EnsureTermsAccepted;
|
|
use App\Http\Middleware\EnsureUserIsAdmin;
|
|
use Illuminate\Support\Facades\Route;
|
|
|
|
/*
|
|
|--------------------------------------------------------------------------
|
|
| City10 Web Routes
|
|
|--------------------------------------------------------------------------
|
|
|
|
|
| Public pages (welcome, legal documents, language switch) are open to
|
|
| guests. The dashboard and consent screen require an authenticated web
|
|
| session and are blocked until the current legal documents are accepted.
|
|
|
|
|
*/
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Public pages
|
|
// ---------------------------------------------------------------------------
|
|
|
|
Route::get('/', function () {
|
|
return view('welcome');
|
|
})->name('welcome');
|
|
|
|
Route::get('/lang', [LanguageController::class, 'switch'])->name('lang.switch');
|
|
|
|
// Legal documents (public, readable without an account).
|
|
Route::get('/legal/{type}', [LegalController::class, 'show'])
|
|
->whereIn('type', ['terms', 'privacy', 'cookies'])
|
|
->name('legal.show');
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Guest authentication
|
|
// ---------------------------------------------------------------------------
|
|
|
|
Route::middleware('guest')->group(function (): void {
|
|
Route::get('/login', [AuthController::class, 'showLogin'])->name('login');
|
|
Route::post('/login', [AuthController::class, 'login'])->middleware('throttle:10,1');
|
|
Route::get('/register', [AuthController::class, 'showRegister'])->name('register');
|
|
Route::post('/register', [AuthController::class, 'register'])->middleware('throttle:10,1');
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Authenticated area
|
|
// ---------------------------------------------------------------------------
|
|
|
|
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');
|
|
Route::get('/terms/history', [LegalController::class, 'history'])->name('terms.history');
|
|
|
|
// Main platform features (blocked until terms are accepted).
|
|
Route::middleware(EnsureTermsAccepted::class)->group(function (): void {
|
|
Route::get('/dashboard', DashboardController::class)->name('dashboard');
|
|
|
|
// 3D city viewer (Phase 3).
|
|
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'])
|
|
->middleware('throttle:60,1')
|
|
->name('city.actions.position');
|
|
Route::post('/city/actions/{action}', [CityActionController::class, 'handle'])
|
|
->middleware('throttle:90,1')
|
|
->name('city.actions.handle');
|
|
|
|
// Character social graph and chat.
|
|
Route::post('/city/social/{verb}', [CitySocialController::class, 'handle'])
|
|
->middleware('throttle:30,1')
|
|
->name('city.social.handle');
|
|
Route::get('/city/social/inbox', [CitySocialController::class, 'inbox'])->name('city.social.inbox');
|
|
Route::post('/city/messages/{character}', [CharacterChatController::class, 'send'])
|
|
->middleware('throttle:60,1')
|
|
->name('city.messages.send');
|
|
Route::get('/city/messages/{character}', [CharacterChatController::class, 'index'])->name('city.messages.index');
|
|
|
|
// Multiplayer presence + marketplace + journal + cosmetics.
|
|
Route::get('/city/presence', CityPresenceController::class)
|
|
->middleware('throttle:60,1')
|
|
->name('city.presence');
|
|
Route::get('/city/market', [CityMarketController::class, 'index'])->name('city.market.index');
|
|
Route::post('/city/market/sell', [CityMarketController::class, 'sell'])
|
|
->middleware('throttle:30,1')
|
|
->name('city.market.sell');
|
|
Route::post('/city/market/buy', [CityMarketController::class, 'buy'])
|
|
->middleware('throttle:30,1')
|
|
->name('city.market.buy');
|
|
Route::get('/city/customize', [CharacterCustomizeController::class, 'show'])->name('city.customize.show');
|
|
Route::put('/city/customize', [CharacterCustomizeController::class, 'update'])
|
|
->middleware('throttle:20,1')
|
|
->name('city.customize.update');
|
|
|
|
// NPC chat (SSE stream + message POST).
|
|
Route::post('/city/chat/{agent}', [ChatController::class, 'send'])
|
|
->middleware('throttle:30,1')
|
|
->name('city.chat.send');
|
|
Route::get('/city/chat/{agent}/stream', [ChatController::class, 'stream'])->name('city.chat.stream');
|
|
|
|
// Daily street conversations (bump-to-talk) with persistent context.
|
|
Route::get('/talk/person', [PersonTalkController::class, 'index'])
|
|
->middleware('throttle:120,1')
|
|
->name('talk.person.index');
|
|
Route::post('/talk/person', [PersonTalkController::class, 'send'])
|
|
->middleware('throttle:30,1')
|
|
->name('talk.person.send');
|
|
|
|
// Airport ops: site info + ATC clearances (AI_ATC_MODEL).
|
|
Route::get('/city/airport', [CityViewerController::class, 'airport'])
|
|
->middleware('throttle:60,1')
|
|
->name('city.airport');
|
|
Route::post('/city/airport/dispatch', [CityViewerController::class, 'dispatch'])
|
|
->middleware('throttle:60,1')
|
|
->name('city.airport.dispatch');
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Admin area (administrator role required, on top of auth + terms)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
Route::middleware(['auth', EnsureTermsAccepted::class, EnsureUserIsAdmin::class])
|
|
->prefix('admin')
|
|
->name('admin.')
|
|
->group(function (): void {
|
|
Route::get('/stores', [StoreAdminController::class, 'index'])->name('stores.index');
|
|
Route::get('/stores/create', [StoreAdminController::class, 'create'])->name('stores.create');
|
|
Route::post('/stores', [StoreAdminController::class, 'store'])->name('stores.store');
|
|
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');
|
|
|
|
Route::get('/world', [AdminWorldController::class, 'index'])->name('world.index');
|
|
});
|