'Grass', 2 => 'Dirt', 3 => 'Stone', 4 => 'Wood', 5 => 'Leaves', 6 => 'Sand', 7 => 'Planks']; #[State] public string $name = ''; /** This player: id, name, color and the token the server knows them by. */ public ?array $me = null; /** From the server: the changed blocks ([x, y, z, kind] each) and who's in the world. */ public array $world = ['edits' => [], 'players' => []]; /** Who's in the world, by id: name, color, and when we last heard from them (ms). */ public array $players = []; public int $selected = 1; public ?string $notice = null; /** The world the game was last given. */ private ?array $given = null; // ---- On the server ------------------------------------------------------------------- // Loaded on mount, and again if the socket was down (moves and blocks sent then are lost). #[Server, Watch, Live('voxel')] public static function world(): \Unscripted\Ui\ServerCall { return new \Unscripted\Ui\ServerCall('voxel', 'world', func_get_args()); } #[Server] public static function enter(#[Validate('required|max:16')] string $name): \Unscripted\Ui\ServerCall { return new \Unscripted\Ui\ServerCall('voxel', 'enter', func_get_args()); } /** Sets block x, y, z to `$kind` (0 for air) for everyone. The bottom layer stays. */ #[Server] public static function place( string $token, #[Validate('integer|between:0,63')] int $x, #[Validate('integer|between:1,31')] int $y, #[Validate('integer|between:0,63')] int $z, #[Validate('integer|between:0,7')] int $kind, ): \Unscripted\Ui\ServerCall { return new \Unscripted\Ui\ServerCall('voxel', 'place', func_get_args()); } /** Tells everyone where this player is. Kept for 10 seconds, for pages that load meanwhile. */ #[Server] public static function move(string $token, float $x, float $y, float $z, float $yaw): \Unscripted\Ui\ServerCall { return new \Unscripted\Ui\ServerCall('voxel', 'move', func_get_args()); } private static function player(string $token): object { return DB::table('voxel_players')->where('token', $token)->first(['id', 'name', 'color']) ?? abort(403, 'Join first'); } // ---- In the browser ------------------------------------------------------------------- protected function mounted(): void { // Players go quiet when they leave: anyone not heard from in 6 seconds has. U::setInterval(function () { foreach ($this->players as $id => $player) { if (self::now() - $player['seen'] > 6000) { unset($this->players[$id]); U::window()->voxel->removePlayer($id); } } }, 1000); } /** The game gets the world when it's loaded (again). */ protected function rendered(): void { if ($this->world === $this->given) { return; } $this->given = $this->world; U::window()->voxel->load(json_encode(array_merge(...$this->world['edits'] ?: [[]]))); foreach ($this->world['players'] as $player) { $this->show($player); } } public function join(): Generator { $this->me = yield static::enter($this->name); $this->notice = null; } /** A block you broke or placed: shown already; put back if the server says no. */ #[On('voxel-edit')] public function edited(array $edit): Generator { [$x, $y, $z, $kind, $was] = $edit; if ($this->me === null) { U::window()->voxel->setBlock($x, $y, $z, $was); $this->notice = 'Join to build.'; return; } try { yield static::place($this->me['token'], $x, $y, $z, $kind); } catch (ServerError $e) { U::window()->voxel->setBlock($x, $y, $z, $was); $this->notice = $e->getMessage(); } } #[On('voxel-move')] public function walked(array $at): Generator { if ($this->me !== null) { yield static::move($this->me['token'], ...$at); } } #[On('voxel-select')] public function picked(array $detail): void { [$this->selected] = $detail; } /** Someone else's move or block. */ #[Live('voxel')] public function heard(array $message): void { if (isset($message['block'])) { U::window()->voxel->setBlock(...$message['block']); } if (isset($message['player'])) { $this->show($message['player']); } } private function show(array $player): void { [$id, $name, $color, $x, $y, $z, $yaw] = $player; if ($id === ($this->me['id'] ?? null)) { return; } $this->players[$id] = ['name' => $name, 'color' => $color, 'seen' => self::now()]; U::window()->voxel->setPlayer($id, $x, $y, $z, $yaw, $color); } /** Now, in ms: a float, as the browser's PHP is 32-bit. */ private static function now(): float { return microtime(true) * 1000; } }; ?>
{{ $message }}
@enderrorYou can look around first: click the world.
@elseYou're {{ $me['name'] }}
@endif @if ($notice){{ $notice }}
@endif