127 lines
6.1 KiB
Dart
127 lines
6.1 KiB
Dart
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<CabinetPage> createState() => _CabinetPageState();
|
|
}
|
|
class _CabinetPageState extends State<CabinetPage> {
|
|
late final http.Client client;
|
|
Timer? timer;
|
|
Map<String, dynamic>? 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<void> 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<String, dynamic>;
|
|
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<void> 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<String, dynamic>;
|
|
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'),
|
|
]))),
|
|
);
|
|
}
|
|
}
|