import 'room_page.dart'; import 'dart:async'; import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; void main() => runApp(const GlitchLab()); class GlitchLab extends StatelessWidget { const GlitchLab({super.key}); @override Widget build(BuildContext context) => MaterialApp( title: 'Glitch GM Lab', theme: ThemeData.dark(useMaterial3: true).copyWith( colorScheme: ColorScheme.fromSeed(seedColor: Colors.cyan, brightness: Brightness.dark)), home: Uri.base.queryParameters['room'] == '1' ? const RoomPage() : const CabinetPage(), ); } class CabinetPage extends StatefulWidget { const CabinetPage({super.key, this.client}); final http.Client? client; @override State createState() => _CabinetPageState(); } class _CabinetPageState extends State { late final http.Client client; Timer? timer; Map? cabinet; String? error; String notice = 'Connecting to the local simulation...'; bool pending = false; bool polling = false; int sequence = 0; int revision = 0; static const base = 'http://127.0.0.1:18766'; @override void initState() { super.initState(); client = widget.client ?? http.Client(); refresh(); timer = Timer.periodic(const Duration(seconds: 1), (_) => refresh()); } Future refresh() async { if (polling || pending) return; polling = true; final requestRevision = revision; try { final response = await client.get(Uri.parse('$base/state')).timeout(const Duration(seconds: 3)); if (response.statusCode != 200) throw Exception('Cabinet or controller unavailable'); final data = jsonDecode(response.body) as Map; if (mounted && !pending && requestRevision == revision) setState(() { cabinet = data['state']; error = null; }); } catch (_) { if (mounted && requestRevision == revision) setState(() => error = 'Disconnected - controls disabled. Check the lab terminals.'); } finally { polling = false; } } Future command(String action) async { if (pending || error != null || cabinet == null) return; revision++; setState(() { pending = true; notice = 'Waiting for cabinet acknowledgement...'; }); try { final response = await client.post(Uri.parse('$base/command'), headers: {'Content-Type': 'application/json'}, body: jsonEncode({ 'id': 'gm-${DateTime.now().microsecondsSinceEpoch}-${sequence++}', 'session': cabinet!['session'], 'expires_ms': DateTime.now().millisecondsSinceEpoch + 10000, 'action': action, })).timeout(const Duration(seconds: 4)); if (response.statusCode != 200) throw Exception('No acknowledgement'); final data = jsonDecode(response.body) as Map; if (mounted) setState(() { cabinet = data['state']; notice = '${data['accepted'] == true ? 'Accepted' : 'Rejected'}: ${data['message']}'; error = null; }); } catch (_) { if (mounted) setState(() { error = 'Delivery unconfirmed. Refresh state before trying again.'; notice = 'No success acknowledgement received.'; }); } finally { if (mounted) setState(() => pending = false); } } @override void dispose() { timer?.cancel(); client.close(); super.dispose(); } @override Widget build(BuildContext context) { final phase = cabinet?['phase'] ?? 'unknown'; final available = cabinet != null && error == null && !pending; Widget control(String label, String action, bool enabled) => FilledButton( onPressed: available && enabled ? () => command(action) : null, child: Text(label)); return Scaffold( appBar: AppBar(title: const Text('Escape Gallery / Glitch: the Dark Machine')), body: Center(child: ConstrainedBox(constraints: const BoxConstraints(maxWidth: 900), child: ListView(padding: const EdgeInsets.all(24), children: [ Text(cabinet?['simulated'] == false ? 'INTEGRATION LAB - REAL GODOT CABINET' : 'INTEGRATION LAB - SIMULATED CABINET', style: const TextStyle(color: Colors.amber)), const SizedBox(height: 16), const Text('Cabinet 1 / Pac-Man', style: TextStyle(fontSize: 28)), Text(cabinet?['simulated'] == false ? 'Real Pac-Man. Target: 500 points. No reward outputs. RFID injection: ${cabinet?['rfid_injection'] == true ? 'enabled' : 'disabled'}.' : 'No physical RFID, LEDs, GPIO, or arcade game is connected.'), const SizedBox(height: 24), Card(child: Padding(padding: const EdgeInsets.all(24), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(phase.replaceAll('_', ' ').toUpperCase(), style: const TextStyle(fontSize: 32)), Text('Credits: ${cabinet?['credits'] ?? '-'} Accepted scans: ${cabinet?['scans'] ?? '-'}'), const SizedBox(height: 12), Text(phase == 'powered_off' ? 'Waiting for operator' : phase == 'booting' ? 'Four-second boot sequence...' : phase == 'ready' ? 'Scan a card, then press Start' : phase == 'playing' ? 'Game in progress' : phase == 'won' ? 'Objective completed' : phase == 'lost' ? 'Game lost - reset the test to retry' : 'Awaiting state'), ]))), const SizedBox(height: 16), Wrap(spacing: 12, runSpacing: 12, children: [ control('Power on', 'power_on', phase == 'powered_off'), control('Simulate RFID scan', 'scan', phase == 'ready' && (cabinet?['simulated'] == true || cabinet?['rfid_injection'] == true)), control('Start game', 'start', phase == 'ready' && cabinet?['credits'] == 1), control('Simulate win', 'win', phase == 'playing' && cabinet?['simulated'] == true), control('Reset test', 'reset', true), ]), const SizedBox(height: 24), Text(error ?? notice, style: TextStyle(color: error == null ? Colors.white70 : Colors.orange)), const SizedBox(height: 16), const Text('Flutter → local JSON gateway → NATS / MessagePack → simulated cabinet'), ]))), ); } }