feat: add City10 platform with Laravel 13, AI agents, 3D viewer, and LGPD compliance
Initial commit of the complete City10 virtual city simulation platform. Backend (Laravel 13): - REST API with Sanctum tokens and session-based web authentication - Role-based access control (admin/user) with middleware protection - AI agent CRUD with OpenRouter.ai integration restricted to free models - Store management with 3D coordinates and AI shopkeeper assignment - Versioned legal documents (Terms, Privacy, Cookies) with LGPD audit trail - Math CAPTCHA on public forms and multi-language support (pt_BR, en, es) Frontend: - Responsive Blade templates with Tailwind CSS v4 - Three.js 3D city viewer with SSE chat streaming to AI agents - Admin panel for store and user administration Infrastructure: - MariaDB/MySQL database with comprehensive migrations - Database seeders for demo content and test accounts - PHPUnit test suite and Laravel Pint code styling - Vite build configuration with hot reload
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Web;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\AiAgent;
|
||||
use App\Models\Store;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\View\View;
|
||||
|
||||
/**
|
||||
* 3D city viewer.
|
||||
*
|
||||
* Serves the Three.js scene page plus a lightweight JSON payload of stores
|
||||
* and agents. The JSON endpoint reuses the web session so the browser can
|
||||
* fetch world data without a separate API token.
|
||||
*/
|
||||
class CityViewerController extends Controller
|
||||
{
|
||||
/**
|
||||
* Render the city viewer page.
|
||||
*/
|
||||
public function __invoke(): View
|
||||
{
|
||||
return view('city.viewer');
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the world snapshot consumed by the viewer scene.
|
||||
*/
|
||||
public function data(): JsonResponse
|
||||
{
|
||||
$stores = Store::query()
|
||||
->get(['id', 'name', 'type', 'pos_x', 'pos_y', 'pos_z', 'width', 'height', 'depth', 'is_open'])
|
||||
->map(fn (Store $store): array => [
|
||||
'id' => $store->id,
|
||||
'name' => $store->name,
|
||||
'type' => $store->type,
|
||||
'position' => [(float) $store->pos_x, (float) $store->pos_y, (float) $store->pos_z],
|
||||
'size' => [(float) $store->width, (float) $store->height, (float) $store->depth],
|
||||
'is_open' => $store->is_open,
|
||||
]);
|
||||
|
||||
$agents = AiAgent::query()
|
||||
->where('status', '!=', 'paused')
|
||||
->get(['id', 'name', 'type', 'status', 'pos_x', 'pos_y', 'pos_z', 'rotation_y', 'last_decision'])
|
||||
->map(fn (AiAgent $agent): array => [
|
||||
'id' => $agent->id,
|
||||
'name' => $agent->name,
|
||||
'type' => $agent->type,
|
||||
'status' => $agent->status,
|
||||
'position' => [(float) $agent->pos_x, (float) $agent->pos_y, (float) $agent->pos_z],
|
||||
'rotation_y' => (float) $agent->rotation_y,
|
||||
'last_decision' => $agent->last_decision,
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'generated_at' => now()->toIso8601String(),
|
||||
'stores' => $stores,
|
||||
'agents' => $agents,
|
||||
'layout' => self::cityLayout($stores->all()),
|
||||
])->header('Cache-Control', 'no-store');
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive a deterministic street layout from store positions.
|
||||
*
|
||||
* Returns road segments and block rectangles so the frontend can build
|
||||
* a consistent grid-based city without additional database tables.
|
||||
*
|
||||
* @param array<int, array<string, mixed>> $stores
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private static function cityLayout(array $stores): array
|
||||
{
|
||||
if ($stores === []) {
|
||||
return ['roads' => [], 'blocks' => []];
|
||||
}
|
||||
|
||||
$xs = array_map(fn ($s) => $s['position'][0], $stores);
|
||||
$zs = array_map(fn ($s) => $s['position'][2], $stores);
|
||||
|
||||
$minX = floor(min($xs) / 40) * 40 - 40;
|
||||
$maxX = ceil(max($xs) / 40) * 40 + 40;
|
||||
$minZ = floor(min($zs) / 40) * 40 - 40;
|
||||
$maxZ = ceil(max($zs) / 40) * 40 + 40;
|
||||
|
||||
$roads = [];
|
||||
|
||||
// Vertical roads every 40 units.
|
||||
for ($x = $minX; $x <= $maxX; $x += 40) {
|
||||
$roads[] = [
|
||||
'from' => [(float) $x, 0.0, (float) $minZ],
|
||||
'to' => [(float) $x, 0.0, (float) $maxZ],
|
||||
'width' => 8.0,
|
||||
'axis' => 'z',
|
||||
];
|
||||
}
|
||||
|
||||
// Horizontal roads every 40 units.
|
||||
for ($z = $minZ; $z <= $maxZ; $z += 40) {
|
||||
$roads[] = [
|
||||
'from' => [(float) $minX, 0.0, (float) $z],
|
||||
'to' => [(float) $maxX, 0.0, (float) $z],
|
||||
'width' => 8.0,
|
||||
'axis' => 'x',
|
||||
];
|
||||
}
|
||||
|
||||
// Blocks = cells between roads.
|
||||
$blocks = [];
|
||||
for ($x = $minX; $x < $maxX; $x += 40) {
|
||||
for ($z = $minZ; $z < $maxZ; $z += 40) {
|
||||
$blocks[] = [
|
||||
'min' => [(float) $x + 5.0, 0.0, (float) $z + 5.0],
|
||||
'max' => [(float) $x + 35.0, 0.0, (float) $z + 35.0],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return ['roads' => $roads, 'blocks' => $blocks];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user