From 8fc05c678db0b5ec724eefb661ea1be0e38c7469 Mon Sep 17 00:00:00 2001 From: Dan Lopez Date: Fri, 13 Oct 2023 23:09:43 -0500 Subject: [PATCH] feat: finish flow of down game with blockchain services integrated and show alert messages --- lib/main.dart | 2 +- lib/pages/home.dart | 1 + lib/pages/mini-games/up/down.dart | 244 ++++++++++++++++++++++- lib/pages/mini-games/up/objects/box.dart | 2 +- lib/pages/mini-games/up/up.dart | 2 +- lib/pages/my-dinogrow/my_dinogrow.dart | 128 ++++-------- 6 files changed, 278 insertions(+), 101 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index f56bcca..3cb34ed 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -64,7 +64,7 @@ final GoRouter _router = GoRouter(routes: [ GoRoute( path: '/mini_games/down', builder: (context, state) { - return GameWidgetDown(game: DownGame()); + return const GameWidgetDown(); }), GoRoute( path: '/mini_games/comming_soon', diff --git a/lib/pages/home.dart b/lib/pages/home.dart index bf902ae..fc4456a 100644 --- a/lib/pages/home.dart +++ b/lib/pages/home.dart @@ -20,6 +20,7 @@ class _HomeScreenState extends State { String? _balance; SolanaClient? client; final storage = const FlutterSecureStorage(); + @override void initState() { super.initState(); diff --git a/lib/pages/mini-games/up/down.dart b/lib/pages/mini-games/up/down.dart index 3722afe..9698e4a 100644 --- a/lib/pages/mini-games/up/down.dart +++ b/lib/pages/mini-games/up/down.dart @@ -10,6 +10,17 @@ import 'package:go_router/go_router.dart'; import 'objects/floor.dart'; import 'objects/box.dart'; import 'package:flame/timer.dart' as timer_flame; +import 'package:url_launcher/url_launcher.dart'; + +import '../../../anchor_types/score_parameters.dart' as anchor_types_parameters; + +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:solana/solana.dart'; +import 'package:solana/solana.dart' as solana; +import 'package:solana/anchor.dart' as solana_anchor; +import 'package:solana/encoder.dart' as solana_encoder; +import 'package:solana_common/utils/buffer.dart' as solana_buffer; final screenSize = Vector2(720, 1280); @@ -17,8 +28,9 @@ final screenSize = Vector2(720, 1280); final worldSize = Vector2(7.2, 12.8); class DownGame extends Forge2DGame with TapDetector { + final void Function(String) endGameCallback; // setup the game camera to match the device size - DownGame() : super(zoom: 100, gravity: Vector2(0, 15)); + DownGame(this.endGameCallback) : super(zoom: 100, gravity: Vector2(0, 15)); //get the dino object and sprite animation final dino = Dino(); @@ -69,14 +81,20 @@ class DownGame extends Forge2DGame with TapDetector { if (isLeftPressed) { btnLeft.paint.color = const Color.fromARGB(255, 91, 92, 91); + isRightPressed = false; + btnRight.paint.color = BasicPalette.yellow.color; + dino.walkLeft(); timer = timer_flame.Timer(0.25, onTick: () => dino.walkLeft(), repeat: true); } if (isRightPressed) { - dino.walkRight(); btnRight.paint.color = const Color.fromARGB(255, 91, 92, 91); + isLeftPressed = false; + btnLeft.paint.color = BasicPalette.yellow.color; + + dino.walkRight(); timer = timer_flame.Timer(0.25, onTick: () => dino.walkRight(), repeat: true); @@ -146,11 +164,11 @@ class DownGame extends Forge2DGame with TapDetector { // Add instance of Box finishGame() { - print('End game with $score points in score'); + endGameCallback('$score'); } newBoxAndScore() { - score += 10; + score += 1; scoreText.text = 'Score: ${score.toString().padLeft(3, '0')}'; add(Box(newBoxAndScore, finishGame)); } @@ -205,10 +223,27 @@ class _Background extends PositionComponent { } } -class GameWidgetDown extends StatelessWidget { - final DownGame game; +class GameWidgetDown extends StatefulWidget { + const GameWidgetDown({super.key}); + + @override + State createState() => _GameWidgetDownState(); +} + +class _GameWidgetDownState extends State { + DownGame game = DownGame((String data) {}); + bool loading = true; - const GameWidgetDown({super.key, required this.game}); + @override + void initState() { + super.initState(); + + setState(() { + game = DownGame(showEndMessage); + }); + + Future.delayed(const Duration(milliseconds: 500), showWelcome); + } @override Widget build(BuildContext context) { @@ -232,8 +267,201 @@ class GameWidgetDown extends StatelessWidget { fit: BoxFit.cover, ), ), - child: GameWidget(game: game), + child: loading + ? const Center( + child: CircularProgressIndicator( + color: Colors.black, + ), + ) + : GameWidget(game: game), + ), + ); + } + + showWelcome() { + setState(() { + loading = true; + }); + showDialog( + context: context, + builder: (BuildContext context) => AlertDialog( + title: const Text('Welcome to Down'), + content: const Text( + "The aim of this minigame is avoid all boxes that come from up, while you avoid more boxes you will have great score, good luck and ... \n\n¡Watch out with the boxes! \n\nREMEMBER: you could save your score on the blockchain but you'll need at least 0.5 SOL in your wallet balance"), + actions: [ + TextButton( + onPressed: () { + Navigator.pop(context); + Navigator.pop(context); + }, + child: const Text('Go back'), + ), + TextButton( + onPressed: () { + Navigator.pop(context); + setState(() { + loading = false; + }); + }, + child: const Text("Let's go!"), + ), + ], + ), + ); + } + + showEndMessage(String score) { + showDialog( + context: context, + builder: (BuildContext context) => AlertDialog( + title: const Text('Ups ... game ended :('), + content: Text( + "Thanks for play! Congrats you score was: $score \n\nDo you want to save and share it in Ranking? Remember this has a cost so you must have at least 0.5 SOL in your wallet balance."), + actions: [ + TextButton( + onPressed: () { + Navigator.pop(context); + showWelcome(); + }, + child: const Text('Try again'), + ), + TextButton( + onPressed: () { + Navigator.pop(context); + saveScore(int.parse(score)); + }, + child: const Text("Send my score"), + ), + ], + ), + ); + } + + showrResultMessage(String transaction) { + showDialog( + context: context, + builder: (BuildContext context) => AlertDialog( + title: const Text('Score saved'), + content: Text( + "Perfect! You score is now in Ranking, good luck! \n\nIf you want you can review information on blockchain with this transaction reference: \n\n $transaction"), + actions: [ + TextButton( + onPressed: () { + _launchUrl(); + }, + child: const Text('View transaction'), + ), + TextButton( + onPressed: () { + Navigator.pop(context); + Navigator.pop(context); + }, + child: const Text("Go to minigames"), + ), + ], ), ); } + + saveScore(int score) async { + try { + setState(() { + loading = true; + }); + + await dotenv.load(fileName: ".env"); + + SolanaClient? client; + client = SolanaClient( + rpcUrl: Uri.parse(dotenv.env['QUICKNODE_RPC_URL'].toString()), + websocketUrl: Uri.parse(dotenv.env['QUICKNODE_RPC_WSS'].toString()), + ); + const storage = FlutterSecureStorage(); + + final mainWalletKey = await storage.read(key: 'mnemonic'); + + final mainWalletSolana = await solana.Ed25519HDKeyPair.fromMnemonic( + mainWalletKey!, + ); + + const programId = '9V9ttZw7WTYW78Dx3hi2hV7V76PxAs5ZwbCkGi7qq8FW'; + final systemProgramId = + solana.Ed25519HDPublicKey.fromBase58(solana.SystemProgram.programId); + + //direccion mint del DINO + String? dinoSelected = await storage.read(key: 'dinoSelected'); + + if (dinoSelected == null) { + throw "You don't have dino selected"; + } + + final dinoTest = solana.Ed25519HDPublicKey.fromBase58(dinoSelected); + + final programIdPublicKey = + solana.Ed25519HDPublicKey.fromBase58(programId); + + final gscorePda = await solana.Ed25519HDPublicKey.findProgramAddress( + programId: programIdPublicKey, + seeds: [ + solana_buffer.Buffer.fromString("score"), + mainWalletSolana.publicKey.bytes, + dinoTest.bytes, + solana_buffer.Buffer.fromInt32(1), + ]); + // print(gscorePda.toBase58()); + + final instructions = [ + await solana_anchor.AnchorInstruction.forMethod( + programId: programIdPublicKey, + method: 'savescore', + arguments: + solana_encoder.ByteArray(anchor_types_parameters.ScoreArguments( + game: 1, + score: score, + ).toBorsh().toList()), + accounts: [ + solana_encoder.AccountMeta.writeable( + pubKey: gscorePda, isSigner: false), + solana_encoder.AccountMeta.writeable( + pubKey: mainWalletSolana.publicKey, isSigner: true), + solana_encoder.AccountMeta.writeable( + pubKey: dinoTest, isSigner: false), + solana_encoder.AccountMeta.readonly( + pubKey: systemProgramId, isSigner: false), + ], + namespace: 'global', + ), + ]; + final message = solana.Message(instructions: instructions); + final signature = await client.sendAndConfirmTransaction( + message: message, + signers: [mainWalletSolana], + commitment: solana.Commitment.confirmed, + ); + + print('Tx successful with hash: $signature'); + showrResultMessage(signature); + } catch (e) { + final snackBar = SnackBar( + content: Text( + 'Error: $e', + style: const TextStyle(color: Colors.white), + ), + backgroundColor: Colors.red, + ); + + ScaffoldMessenger.of(context).showSnackBar(snackBar); + } finally { + setState(() { + loading = false; + }); + } + } +} + +Future _launchUrl() async { + Uri url = Uri(scheme: 'https', host: 'x.com', path: '/din0gr0w'); + if (!await launchUrl(url)) { + throw Exception('Could not launch $url'); + } } diff --git a/lib/pages/mini-games/up/objects/box.dart b/lib/pages/mini-games/up/objects/box.dart index e4dd9fe..7a8633b 100644 --- a/lib/pages/mini-games/up/objects/box.dart +++ b/lib/pages/mini-games/up/objects/box.dart @@ -42,7 +42,7 @@ class Box extends BodyComponent with ContactCallbacks { final bodyDef = BodyDef( userData: this, //position: Vector2(worldSize.x / 2, worldSize.y - 3), change dino position later - position: Vector2(rnd.nextDouble() * 3, 0), + position: Vector2(rnd.nextDouble() * 2.5, -1), type: BodyType.dynamic, gravityOverride: Vector2(0, rnd.nextDouble() * 3 + 1), ); diff --git a/lib/pages/mini-games/up/up.dart b/lib/pages/mini-games/up/up.dart index 0be2363..45e7c95 100644 --- a/lib/pages/mini-games/up/up.dart +++ b/lib/pages/mini-games/up/up.dart @@ -7,7 +7,7 @@ import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'objects/floor.dart'; -import 'objects/box.dart'; +// import 'objects/box.dart'; final screenSize = Vector2(720, 1280); diff --git a/lib/pages/my-dinogrow/my_dinogrow.dart b/lib/pages/my-dinogrow/my_dinogrow.dart index 432106e..b1a3efe 100644 --- a/lib/pages/my-dinogrow/my_dinogrow.dart +++ b/lib/pages/my-dinogrow/my_dinogrow.dart @@ -6,8 +6,6 @@ import 'package:http/http.dart' as http; import 'package:flutter/material.dart'; import 'package:solana/solana.dart'; -import '../../anchor_types/score_parameters.dart' as anchor_types_parameters; - import '../../ui/widgets/widgets.dart'; import 'dart:math'; @@ -49,20 +47,12 @@ class _MydinogrowScreenState extends State { text: 'Claim your Dino', onPressed: createNft, ), - // IntroButtonWidget( - // text: 'Save Score', - // onPressed: saveScore, - // ), - // IntroButtonWidget( - // text: 'Get Ranking', - // onPressed: getRanking, - // ), const SizedBox(height: 30), Container( color: Colors.orange[700], padding: const EdgeInsets.all(8), child: const Text( - 'Wait ... you must to have a Dino to start play our games, so "Claim your Dino" is our last step to auto-generate your first NFT!', + 'Wait ... you must to have a Dino to start play our games, so "Claim your Dino" is our last step to auto-generate your first NFT! Remember you must have at least 0.5 SOL in you wallet balance', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16), textAlign: TextAlign.center, ), @@ -78,11 +68,7 @@ class _MydinogrowScreenState extends State { itemCount: userNfts.length, itemBuilder: (context, index) { return GestureDetector( - onTap: () { - setState(() { - nftSelected = index; - }); - }, + onTap: () => selectNewDino(index), child: Container( margin: const EdgeInsets.only(right: 12), decoration: BoxDecoration( @@ -225,6 +211,15 @@ class _MydinogrowScreenState extends State { ); } + selectNewDino(int index) async { + setState(() { + nftSelected = index; + }); + + await storage.write( + key: 'dinoSelected', value: userNfts[index]['tokenAddress']); + } + Future fetchNfts() async { try { print('widget.address: ${widget.address}'); @@ -234,6 +229,8 @@ class _MydinogrowScreenState extends State { showDinos = false; }); + String? dinoSelected = await storage.read(key: 'dinoSelected'); + await dotenv.load(fileName: ".env"); final response = await http.post( @@ -249,13 +246,31 @@ class _MydinogrowScreenState extends State { final dataResponse = jsonDecode(response.body); final arrayAssets = dataResponse['result']['assets']; + final filteredData = arrayAssets + .where((nft) => + nft['imageUrl'] != '' && nft['collectionName'] == 'DINOGROW') + .toList(); + + if (filteredData.length == 1 || + (filteredData.length > 0 && + dinoSelected != null && + dinoSelected.isEmpty)) { + await storage.write( + key: 'dinoSelected', value: filteredData[0]['tokenAddress']); + setState(() { + nftSelected = 0; + }); + } else if (dinoSelected != null && dinoSelected.isNotEmpty) { + int index = filteredData + .indexWhere((item) => item["tokenAddress"] == dinoSelected); + setState(() { + nftSelected = index; + }); + } if (mounted) { setState(() { - userNfts = arrayAssets - .where((nft) => - nft['imageUrl'] != '' && nft['collectionName'] == 'DINOGROW') - .toList(); + userNfts = filteredData; showDinos = true; }); } @@ -332,7 +347,7 @@ class _MydinogrowScreenState extends State { int idrnd = Random().nextInt(999); String id = "Dino$idrnd"; - print(id); + // print(id); final nftMintPda = await solana.Ed25519HDPublicKey.findProgramAddress( programId: programIdPublicKey, @@ -340,7 +355,7 @@ class _MydinogrowScreenState extends State { solana_buffer.Buffer.fromString("mint"), solana_buffer.Buffer.fromString(id), ]); - print(nftMintPda.toBase58()); + // print(nftMintPda.toBase58()); final ataProgramId = solana.Ed25519HDPublicKey.fromBase58( solana.AssociatedTokenAccountProgram.programId); @@ -365,7 +380,7 @@ class _MydinogrowScreenState extends State { ], programId: ataProgramId, ); - print(aTokenAccount.toBase58()); + // print(aTokenAccount.toBase58()); final masterEditionAccountPda = await solana.Ed25519HDPublicKey.findProgramAddress( @@ -457,71 +472,4 @@ class _MydinogrowScreenState extends State { } } } - - saveScore() async { - await dotenv.load(fileName: ".env"); - - SolanaClient? client; - client = SolanaClient( - rpcUrl: Uri.parse(dotenv.env['QUICKNODE_RPC_URL'].toString()), - websocketUrl: Uri.parse(dotenv.env['QUICKNODE_RPC_WSS'].toString()), - ); - const storage = FlutterSecureStorage(); - - final mainWalletKey = await storage.read(key: 'mnemonic'); - - final mainWalletSolana = await solana.Ed25519HDKeyPair.fromMnemonic( - mainWalletKey!, - ); - - const programId = '9V9ttZw7WTYW78Dx3hi2hV7V76PxAs5ZwbCkGi7qq8FW'; - final systemProgramId = - solana.Ed25519HDPublicKey.fromBase58(solana.SystemProgram.programId); - - //direccion mint del DINO - final dinoTest = solana.Ed25519HDPublicKey.fromBase58( - "GM3EGmMCYjZs7UstuJ1fvF1Pkocn9GV34BnTGabB8Maf"); - - final programIdPublicKey = solana.Ed25519HDPublicKey.fromBase58(programId); - - final gscorePda = await solana.Ed25519HDPublicKey.findProgramAddress( - programId: programIdPublicKey, - seeds: [ - solana_buffer.Buffer.fromString("score"), - mainWalletSolana.publicKey.bytes, - dinoTest.bytes, - solana_buffer.Buffer.fromInt32(1), - ]); - print(gscorePda.toBase58()); - - final instructions = [ - await solana_anchor.AnchorInstruction.forMethod( - programId: programIdPublicKey, - method: 'savescore', - arguments: - solana_encoder.ByteArray(anchor_types_parameters.ScoreArguments( - game: 1, - score: 1120, - ).toBorsh().toList()), - accounts: [ - solana_encoder.AccountMeta.writeable( - pubKey: gscorePda, isSigner: false), - solana_encoder.AccountMeta.writeable( - pubKey: mainWalletSolana.publicKey, isSigner: true), - solana_encoder.AccountMeta.writeable( - pubKey: dinoTest, isSigner: false), - solana_encoder.AccountMeta.readonly( - pubKey: systemProgramId, isSigner: false), - ], - namespace: 'global', - ), - ]; - final message = solana.Message(instructions: instructions); - final signature = await client.sendAndConfirmTransaction( - message: message, - signers: [mainWalletSolana], - commitment: solana.Commitment.confirmed, - ); - print('Tx successful with hash: $signature'); - } }