feat: Random.org dice and daily word matrix, interpreter voices, curated lexicon
Wire cleromancy rolls and the once-per-UTC-day 000-999 word shuffle to RANDOM.ORG JSON-RPC, with a local CSPRNG fallback. Add settings for interpreter personas and a cleaner default oracle prompt. Replace proper names and bureaucratic filler in the 1000-word lexicon with sacred symbols.
This commit is contained in:
@@ -2,6 +2,7 @@ import 'dart:async';
|
||||
import 'dart:math';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import '../services/true_random_service.dart';
|
||||
import 'gradient_star_badge.dart';
|
||||
|
||||
class D10DiceRollerWidget extends StatefulWidget {
|
||||
@@ -23,63 +24,108 @@ class D10DiceRollerWidget extends StatefulWidget {
|
||||
}
|
||||
|
||||
class D10DiceRollerWidgetState extends State<D10DiceRollerWidget> {
|
||||
final _random = Random();
|
||||
final _flicker = Random.secure();
|
||||
Timer? _rollTimer;
|
||||
|
||||
int? _animatedDigit0;
|
||||
int? _animatedDigit1;
|
||||
int? _animatedDigit2;
|
||||
bool _internalRolling = false;
|
||||
RandomSource? _lastSource;
|
||||
|
||||
void triggerRollAnimation() {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
TrueRandomService.instance.prefetch();
|
||||
}
|
||||
|
||||
Future<void> triggerRollAnimation() async {
|
||||
if (_internalRolling) return;
|
||||
setState(() {
|
||||
_internalRolling = true;
|
||||
_animatedDigit0 = _random.nextInt(10);
|
||||
_animatedDigit1 = _random.nextInt(10);
|
||||
_animatedDigit2 = _random.nextInt(10);
|
||||
_animatedDigit0 = _flicker.nextInt(10);
|
||||
_animatedDigit1 = _flicker.nextInt(10);
|
||||
_animatedDigit2 = _flicker.nextInt(10);
|
||||
});
|
||||
|
||||
final target0 = _random.nextInt(10);
|
||||
final target1 = _random.nextInt(10);
|
||||
final target2 = _random.nextInt(10);
|
||||
// Local CSPRNG is the fallback. RANDOM.ORG replaces all three faces
|
||||
// together if it arrives before the first slot locks (up to ~960ms).
|
||||
var target0 = _flicker.nextInt(10);
|
||||
var target1 = _flicker.nextInt(10);
|
||||
var target2 = _flicker.nextInt(10);
|
||||
var source = RandomSource.local;
|
||||
DiceRoll? remoteRoll;
|
||||
|
||||
int ticks = 0;
|
||||
unawaited(
|
||||
TrueRandomService.instance.take(3).then((roll) {
|
||||
remoteRoll = roll;
|
||||
}, onError: (Object _, StackTrace _) {}),
|
||||
);
|
||||
|
||||
var locked0 = false;
|
||||
var locked1 = false;
|
||||
var locked2 = false;
|
||||
var lock0At = 10;
|
||||
var lock1At = 18;
|
||||
var lock2At = 26;
|
||||
var ticks = 0;
|
||||
const interval = Duration(milliseconds: 60);
|
||||
|
||||
_rollTimer?.cancel();
|
||||
_rollTimer = Timer.periodic(interval, (timer) {
|
||||
if (!mounted) {
|
||||
timer.cancel();
|
||||
return;
|
||||
}
|
||||
ticks++;
|
||||
|
||||
// Slot 0 stops after 10 ticks (~600ms)
|
||||
if (ticks < 10) {
|
||||
setState(() => _animatedDigit0 = _random.nextInt(10));
|
||||
} else if (ticks == 10) {
|
||||
setState(() => _animatedDigit0 = target0);
|
||||
HapticFeedback.mediumImpact();
|
||||
if (!locked0) {
|
||||
final ready = remoteRoll != null && remoteRoll!.digits.length == 3;
|
||||
if (ticks >= 10 && (ready || ticks >= 16)) {
|
||||
if (ready) {
|
||||
target0 = remoteRoll!.digits[0];
|
||||
target1 = remoteRoll!.digits[1];
|
||||
target2 = remoteRoll!.digits[2];
|
||||
source = remoteRoll!.source;
|
||||
}
|
||||
locked0 = true;
|
||||
lock0At = ticks;
|
||||
lock1At = lock0At + 8;
|
||||
lock2At = lock1At + 8;
|
||||
setState(() => _animatedDigit0 = target0);
|
||||
HapticFeedback.mediumImpact();
|
||||
} else {
|
||||
setState(() => _animatedDigit0 = _flicker.nextInt(10));
|
||||
}
|
||||
}
|
||||
|
||||
// Slot 1 stops after 18 ticks (~1080ms)
|
||||
if (ticks < 18) {
|
||||
setState(() => _animatedDigit1 = _random.nextInt(10));
|
||||
} else if (ticks == 18) {
|
||||
setState(() => _animatedDigit1 = target1);
|
||||
HapticFeedback.mediumImpact();
|
||||
if (!locked1) {
|
||||
if (locked0 && ticks >= lock1At) {
|
||||
locked1 = true;
|
||||
setState(() => _animatedDigit1 = target1);
|
||||
HapticFeedback.mediumImpact();
|
||||
} else {
|
||||
setState(() => _animatedDigit1 = _flicker.nextInt(10));
|
||||
}
|
||||
}
|
||||
|
||||
// Slot 2 stops after 26 ticks (~1560ms)
|
||||
if (ticks < 26) {
|
||||
setState(() => _animatedDigit2 = _random.nextInt(10));
|
||||
} else if (ticks == 26) {
|
||||
setState(() => _animatedDigit2 = target2);
|
||||
HapticFeedback.heavyImpact();
|
||||
if (!locked2) {
|
||||
if (locked1 && ticks >= lock2At) {
|
||||
locked2 = true;
|
||||
setState(() => _animatedDigit2 = target2);
|
||||
HapticFeedback.heavyImpact();
|
||||
} else {
|
||||
setState(() => _animatedDigit2 = _flicker.nextInt(10));
|
||||
}
|
||||
}
|
||||
|
||||
if (ticks >= 28) {
|
||||
if (locked2 && ticks >= lock2At + 2) {
|
||||
timer.cancel();
|
||||
setState(() => _internalRolling = false);
|
||||
final finalNumber = target0 * 100 + target1 * 10 + target2;
|
||||
widget.onRollCompleted(finalNumber);
|
||||
setState(() {
|
||||
_internalRolling = false;
|
||||
_lastSource = source;
|
||||
});
|
||||
widget.onRollCompleted(target0 * 100 + target1 * 10 + target2);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -141,7 +187,11 @@ class D10DiceRollerWidgetState extends State<D10DiceRollerWidget> {
|
||||
Text(
|
||||
_internalRolling
|
||||
? 'Бросок...'
|
||||
: (widget.digits.length == 3 ? 'Зафиксировано' : '3 цифры'),
|
||||
: (widget.digits.length == 3
|
||||
? (_lastSource == RandomSource.randomOrg
|
||||
? 'RANDOM.ORG'
|
||||
: 'Зафиксировано')
|
||||
: '3 цифры'),
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w600,
|
||||
|
||||
Reference in New Issue
Block a user