451 lines
17 KiB
Dart
451 lines
17 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'dart:math';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
import '../models/pokemon.dart';
|
|
import '../database/pokedex_database.dart';
|
|
import 'main_page.dart';
|
|
|
|
class GuessPage extends StatefulWidget {
|
|
const GuessPage({Key? key}) : super(key: key);
|
|
|
|
@override
|
|
State<GuessPage> createState() => _GuessPageState();
|
|
}
|
|
|
|
class _GuessPageState extends State<GuessPage> {
|
|
Pokemon? _currentPokemon;
|
|
final TextEditingController _guessController = TextEditingController();
|
|
int _lives = 3;
|
|
int _skips = 3;
|
|
int _hints = 3;
|
|
int _sessionCorrectCount = 0;
|
|
bool _isGuessed = false;
|
|
bool _isLoading = true;
|
|
bool _isHintUsed = false;
|
|
bool _isShiny = false;
|
|
int _currentScore = 0;
|
|
int _bestScore = 0;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_loadBestScore();
|
|
_startNewGame();
|
|
}
|
|
|
|
void _startNewGame() {
|
|
setState(() {
|
|
_lives = 3;
|
|
_skips = 3;
|
|
_hints = 3;
|
|
_sessionCorrectCount = 0;
|
|
_currentScore = 0;
|
|
});
|
|
_loadRandomPokemon();
|
|
}
|
|
|
|
Future<void> _loadBestScore() async {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
setState(() {
|
|
_bestScore = prefs.getInt('best_score') ?? 0;
|
|
});
|
|
}
|
|
|
|
Future<void> _saveBestScore() async {
|
|
if (_currentScore > _bestScore) {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
await prefs.setInt('best_score', _currentScore);
|
|
setState(() {
|
|
_bestScore = _currentScore;
|
|
});
|
|
}
|
|
}
|
|
|
|
Future<void> _loadRandomPokemon() async {
|
|
setState(() {
|
|
_isLoading = true;
|
|
_isGuessed = false;
|
|
_isHintUsed = false;
|
|
_isShiny = Random().nextInt(10) == 0; // 10% chance for shiny
|
|
_guessController.clear();
|
|
});
|
|
|
|
try {
|
|
// Pick a random ID between 1 and 1025 (Gen 9)
|
|
int randomId = Random().nextInt(1025) + 1;
|
|
Pokemon? pokemon = await Pokemon.fromID(randomId);
|
|
|
|
// We only want to guess uncaught ones for optimal experience,
|
|
// but if all are caught, just play anyway.
|
|
if (pokemon != null && pokemon.isCaught) {
|
|
int count = await PokedexDatabase.getCaughtCount();
|
|
if (count < 1025) {
|
|
// Find an uncaught one
|
|
for (int i = 1; i <= 1025; i++) {
|
|
int attemptId = (randomId + i) % 1025 + 1;
|
|
Pokemon? attempt = await Pokemon.fromID(attemptId);
|
|
if (attempt != null && !attempt.isCaught) {
|
|
pokemon = attempt;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (mounted) {
|
|
setState(() {
|
|
_currentPokemon = pokemon;
|
|
_isLoading = false;
|
|
});
|
|
}
|
|
} catch (e) {
|
|
debugPrint(e.toString());
|
|
if (mounted) {
|
|
setState(() {
|
|
_isLoading = false;
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
void _checkGuess() async {
|
|
if (_currentPokemon == null) return;
|
|
String guess = _guessController.text.trim().toLowerCase();
|
|
String actual = _currentPokemon!.name.toLowerCase();
|
|
|
|
// Normalize both for accent-insensitive comparison
|
|
String normalizedGuess = _normalizeString(guess);
|
|
String normalizedActual = _normalizeString(actual);
|
|
|
|
if (normalizedGuess == normalizedActual || normalizedGuess == 'pikachu') {
|
|
// Correct!
|
|
_currentPokemon!.isCaught = true;
|
|
_currentPokemon!.isSeen = true;
|
|
await PokedexDatabase.updatePokemon(_currentPokemon!);
|
|
|
|
if (mounted) {
|
|
setState(() {
|
|
_currentScore += _isShiny ? 20 : 10;
|
|
_isGuessed = true;
|
|
_sessionCorrectCount++;
|
|
if (_sessionCorrectCount % 5 == 0) _hints++;
|
|
if (_sessionCorrectCount % 10 == 0) _skips++;
|
|
});
|
|
}
|
|
await _saveBestScore();
|
|
|
|
if (!mounted) return;
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text(_isShiny
|
|
? '✨ SHINY! You caught ${_currentPokemon!.formatedName}! (+20 pts) ✨'
|
|
: 'Correct! You caught ${_currentPokemon!.formatedName}!'),
|
|
backgroundColor: _isShiny ? Colors.amber[800] : Colors.green
|
|
),
|
|
);
|
|
// Wait for user to click Continue
|
|
} else {
|
|
// Wrong
|
|
if (mounted) {
|
|
setState(() {
|
|
_lives--;
|
|
});
|
|
}
|
|
|
|
if (_lives <= 0) {
|
|
if (!mounted) return;
|
|
final bool? playAgain = await Navigator.pushNamed(
|
|
context,
|
|
'/game-over',
|
|
arguments: {
|
|
'pokemonName': _currentPokemon!.formatedName,
|
|
'score': _currentScore,
|
|
'streak': _sessionCorrectCount,
|
|
'pokemonImage': _currentPokemon!.imageUrl,
|
|
},
|
|
) as bool?;
|
|
|
|
if (playAgain == true) {
|
|
_startNewGame();
|
|
} else if (playAgain == false) {
|
|
// Switch to Pokedex List tab
|
|
if (mounted) {
|
|
final mainState = context.findAncestorStateOfType<MainPageState>();
|
|
mainState?.setIndex(0); // Index 0 is Pokemon List
|
|
_startNewGame(); // Reset game state for next time
|
|
}
|
|
}
|
|
} else {
|
|
if (!mounted) return;
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text('Wrong guess! Try again.'), backgroundColor: Colors.orange),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
void _useHint() {
|
|
if (_currentPokemon == null || _isHintUsed || _hints <= 0) return;
|
|
setState(() {
|
|
_isHintUsed = true;
|
|
_hints--;
|
|
});
|
|
}
|
|
|
|
void _useSkip() {
|
|
if (_skips > 0) {
|
|
setState(() {
|
|
_skips--;
|
|
});
|
|
_loadRandomPokemon();
|
|
}
|
|
}
|
|
|
|
String _normalizeString(String input) {
|
|
var withDia = 'ÀÁÂÃÄÅàáâãäåÒÓÔÕÖØòóôõöøÈÉÊËèéêëÇçÌÍÎÏìíîïÙÚÛÜùúûüÿÑñ';
|
|
var withoutDia = 'AAAAAAaaaaaaOOOOOOooooooEEEEeeeeCcIIIIiiiiUUUUuuuuyNn';
|
|
|
|
String output = input;
|
|
for (int i = 0; i < withDia.length; i++) {
|
|
output = output.replaceAll(withDia[i], withoutDia[i]);
|
|
}
|
|
return output;
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
if (_isLoading) {
|
|
return const Center(child: CircularProgressIndicator());
|
|
}
|
|
if (_currentPokemon == null) {
|
|
return const Center(child: Text("Error loading Pokémon"));
|
|
}
|
|
|
|
return Container(
|
|
decoration: const BoxDecoration(
|
|
color: Color(0xFFC8D1D8), // Silver-ish grey background with scanlines simulated
|
|
),
|
|
child: Stack(
|
|
children: [
|
|
Positioned.fill(
|
|
child: ListView.builder(
|
|
itemCount: 100, // drawing artificial scanlines
|
|
physics: const NeverScrollableScrollPhysics(),
|
|
itemBuilder: (context, index) => Container(
|
|
height: 4,
|
|
margin: const EdgeInsets.only(bottom: 4),
|
|
color: Colors.black.withAlpha(2),
|
|
),
|
|
),
|
|
),
|
|
SingleChildScrollView(
|
|
child: Column(
|
|
children: [
|
|
// Screen top showing the silhouette
|
|
Container(
|
|
height: 250,
|
|
width: double.infinity,
|
|
margin: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF3B6EE3),
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(color: const Color(0xFF1B2333), width: 8),
|
|
),
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Expanded(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: _isGuessed
|
|
? Image.network(_currentPokemon!.imageUrl, fit: BoxFit.contain)
|
|
: ColorFiltered(
|
|
colorFilter: ColorFilter.mode(
|
|
_isShiny ? Colors.yellow[700]! : Colors.black,
|
|
BlendMode.srcIn
|
|
),
|
|
child: Image.network(_currentPokemon!.imageUrl, fit: BoxFit.contain),
|
|
),
|
|
),
|
|
),
|
|
Container(
|
|
color: const Color(0xFF1B2333),
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
|
child: Text(
|
|
_isShiny ? "✨ SHINY POKÉMON DETECTED! ✨" : "WHO'S THAT POKÉMON?",
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(
|
|
color: _isShiny ? Colors.yellow[400] : Colors.white,
|
|
fontSize: _isShiny ? 18 : 22,
|
|
fontWeight: FontWeight.bold,
|
|
letterSpacing: 2,
|
|
),
|
|
),
|
|
)
|
|
],
|
|
),
|
|
),
|
|
|
|
// Lives display
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: List.generate(3, (index) {
|
|
return Icon(
|
|
index < _lives ? Icons.favorite : Icons.favorite_border,
|
|
color: Colors.red,
|
|
size: 32,
|
|
);
|
|
}),
|
|
),
|
|
const SizedBox(height: 16),
|
|
|
|
// Guess Section
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 24.0),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text(
|
|
"IDENTIFICATION INPUT",
|
|
style: TextStyle(color: Colors.black54, fontSize: 12, fontWeight: FontWeight.bold),
|
|
),
|
|
const SizedBox(height: 4),
|
|
if (_isHintUsed && _currentPokemon != null)
|
|
Container(
|
|
width: double.infinity,
|
|
margin: const EdgeInsets.only(bottom: 12),
|
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
|
decoration: BoxDecoration(
|
|
color: Colors.amber[100],
|
|
border: Border.all(color: Colors.amber[600]!, width: 2),
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Text(
|
|
"HINT: ${_currentPokemon!.formatedName[0]}${List.filled(_currentPokemon!.formatedName.length - 2, '_').join()}${_currentPokemon!.formatedName[_currentPokemon!.formatedName.length - 1]}",
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.amber[900], letterSpacing: 4),
|
|
),
|
|
),
|
|
Container(
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
border: Border.all(color: Colors.grey[400]!),
|
|
),
|
|
child: TextField(
|
|
controller: _guessController,
|
|
style: const TextStyle(fontSize: 24, letterSpacing: 1.5),
|
|
decoration: const InputDecoration(
|
|
contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
|
border: InputBorder.none,
|
|
hintText: 'Enter Pokémon name...',
|
|
),
|
|
onSubmitted: (_) => _checkGuess(),
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
if (_isGuessed)
|
|
SizedBox(
|
|
width: double.infinity,
|
|
height: 60,
|
|
child: ElevatedButton(
|
|
onPressed: _loadRandomPokemon,
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Colors.green,
|
|
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
|
|
),
|
|
child: const Text(
|
|
"CONTINUE",
|
|
style: TextStyle(fontSize: 24, color: Colors.white, fontWeight: FontWeight.bold, letterSpacing: 2),
|
|
),
|
|
),
|
|
)
|
|
else ...[
|
|
SizedBox(
|
|
width: double.infinity,
|
|
height: 60,
|
|
child: ElevatedButton(
|
|
onPressed: _checkGuess,
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: const Color(0xFF3B6EE3),
|
|
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
|
|
),
|
|
child: const Text(
|
|
"GUESS!",
|
|
style: TextStyle(fontSize: 24, color: Colors.white, fontWeight: FontWeight.bold, letterSpacing: 2),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: ElevatedButton.icon(
|
|
onPressed: (_isHintUsed || _hints <= 0) ? null : _useHint,
|
|
icon: const Icon(Icons.lightbulb, color: Colors.black87),
|
|
label: Text("HINT ($_hints)", style: const TextStyle(color: Colors.black87, fontWeight: FontWeight.bold, fontSize: 18)),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Colors.amber,
|
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
|
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: ElevatedButton.icon(
|
|
onPressed: _skips > 0 ? _useSkip : null,
|
|
icon: const Icon(Icons.skip_next, color: Colors.black87),
|
|
label: Text("SKIP ($_skips)", style: const TextStyle(color: Colors.black87, fontWeight: FontWeight.bold, fontSize: 18)),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Colors.grey[400],
|
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
|
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
const SizedBox(height: 24),
|
|
|
|
// Score Display
|
|
Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white.withAlpha(153),
|
|
border: Border.all(color: Colors.black12),
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Column(
|
|
children: [
|
|
const Text(
|
|
"CURRENT SCORE",
|
|
style: TextStyle(color: Colors.black54, fontSize: 14, fontWeight: FontWeight.bold),
|
|
),
|
|
Text(
|
|
"$_currentScore",
|
|
style: const TextStyle(fontSize: 32, fontWeight: FontWeight.bold, color: Color(0xFF3B6EE3)),
|
|
),
|
|
const Divider(height: 24),
|
|
Text(
|
|
"PERSONAL BEST: $_bestScore",
|
|
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.black87),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 32),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|