559 lines
20 KiB
Dart

import 'dart:math';
import 'dart:async';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_widget_from_html/flutter_widget_from_html.dart';
import 'package:manager_api_new/api.dart';
import 'package:mymuseum_visitapp/Components/loading_common.dart';
import 'package:mymuseum_visitapp/Helpers/translationHelper.dart';
import 'package:mymuseum_visitapp/Models/visitContext.dart';
import 'package:mymuseum_visitapp/Screens/Sections/Game/message_dialog.dart';
import 'package:mymuseum_visitapp/Screens/Sections/GuidedPath/guided_path_content_progression_page.dart';
import 'package:mymuseum_visitapp/app_context.dart';
import 'package:mymuseum_visitapp/constants.dart';
import 'package:provider/provider.dart';
import 'package:mymuseum_visitapp/Screens/Sections/Game/sliding_puzzle_piece.dart';
import 'puzzle_piece.dart';
const IMAGE_PATH = 'image_path';
class GamePage extends StatefulWidget {
final GameDTO section;
GamePage({required this.section});
@override
_GamePage createState() => _GamePage();
}
class _GamePage extends State<GamePage> {
GameDTO gameDTO = GameDTO();
int allInPlaceCount = 0;
bool isFinished = false;
DateTime? _gameStartTime;
GlobalKey _widgetKey = GlobalKey();
Size? realWidgetSize;
List<Widget> pieces = [];
bool isSplittingImage = true;
bool showHint = false;
List<int> slidingTileIndices = []; // Maps current slot index to original tile index. -1 for empty.
int emptySlotIndex = -1;
@override
void initState() {
//puzzleDTO = PuzzleDTO.fromJson(jsonDecode(widget.section!.data!))!;
gameDTO = widget.section;
gameDTO.rows = gameDTO.rows ?? 3;
gameDTO.cols = gameDTO.cols ?? 3;
_gameStartTime = DateTime.now();
WidgetsBinding.instance.addPostFrameCallback((_) async {
Size size = MediaQuery.of(context).size;
final appContext = Provider.of<AppContext>(context, listen: false);
VisitAppContext visitAppContext = appContext.getContext();
print(gameDTO.messageDebut);
TranslationAndResourceDTO? messageDebut = gameDTO.messageDebut != null && gameDTO.messageDebut!.isNotEmpty ? gameDTO.messageDebut!.where((message) => message.language!.toUpperCase() == visitAppContext.language!.toUpperCase()).firstOrNull : null;
//await Future.delayed(const Duration(milliseconds: 50));
await WidgetsBinding.instance.endOfFrame;
getRealWidgetSize();
if(gameDTO.puzzleImage != null && gameDTO.puzzleImage!.url != null) {
//splitImage(Image.network(puzzleDTO.image!.resourceUrl!));
splitImage(CachedNetworkImage(
imageUrl: gameDTO.puzzleImage!.url!,
fit: BoxFit.fill,
errorWidget: (context, url, error) => Icon(Icons.error),
));
} else {
setState(() {
isSplittingImage = false;
});
}
if(messageDebut != null) {
showMessage(messageDebut, appContext, context, size);
}
});
super.initState();
}
Future<void> getRealWidgetSize() async {
RenderBox renderBox = _widgetKey.currentContext?.findRenderObject() as RenderBox;
Size size = renderBox.size;
setState(() {
realWidgetSize = size;
});
print("Taille réelle du widget : $size");
}
// here we will split the image into small pieces
// using the rows and columns defined above; each piece will be added to a stack
void splitImage(CachedNetworkImage image) async {
final Completer<Size> completer = Completer<Size>();
final ImageProvider provider = CachedNetworkImageProvider(gameDTO.puzzleImage!.url!);
provider.resolve(const ImageConfiguration()).addListener(
ImageStreamListener((ImageInfo info, bool _) {
if (!completer.isCompleted) {
completer.complete(Size(info.image.width.toDouble(), info.image.height.toDouble()));
}
}),
);
Size imageOriginalSize = await completer.future;
double imageAspectRatio = imageOriginalSize.width / imageOriginalSize.height;
// Calculate best fit for the puzzle inside the available area
double containerWidth = realWidgetSize!.width * 0.9; // 90% of available width
double containerHeight = realWidgetSize!.height * 0.8; // 80% of available height
double containerAspectRatio = containerWidth / containerHeight;
double puzzleWidth, puzzleHeight;
if (imageAspectRatio > containerAspectRatio) {
puzzleWidth = containerWidth;
puzzleHeight = containerWidth / imageAspectRatio;
} else {
puzzleHeight = containerHeight;
puzzleWidth = containerHeight * imageAspectRatio;
}
final appContext = Provider.of<AppContext>(context, listen: false);
VisitAppContext visitAppContext = appContext.getContext();
setState(() {
visitAppContext.puzzleSize = Size(puzzleWidth, puzzleHeight);
appContext.setContext(visitAppContext);
});
final pieceWidth = puzzleWidth / gameDTO.cols!;
final pieceHeight = puzzleHeight / gameDTO.rows!;
if (gameDTO.gameType == GameTypes.SlidingPuzzle) {
// Initialize sliding puzzle slots: 0 to N-2 are tiles, last one is empty (-1)
int totalTiles = gameDTO.rows! * gameDTO.cols!;
slidingTileIndices = List.generate(totalTiles, (i) => i);
// Shuffle by making random valid moves to ensure solvability
emptySlotIndex = totalTiles - 1;
slidingTileIndices[emptySlotIndex] = -1; // -1 represents the empty slot
// Perform enough random moves to shuffle decently
int shuffleMoves = 100;
Random random = Random();
for (int i = 0; i < shuffleMoves; i++) {
List<int> adjacent = _getAdjacentIndices(emptySlotIndex, gameDTO.rows!, gameDTO.cols!);
int moveIndex = adjacent[random.nextInt(adjacent.length)];
// Swap empty slot with the adjacent tile
slidingTileIndices[emptySlotIndex] = slidingTileIndices[moveIndex];
slidingTileIndices[moveIndex] = -1;
emptySlotIndex = moveIndex;
}
} else {
for (int x = 0; x < gameDTO.rows!; x++) {
for (int y = 0; y < gameDTO.cols!; y++) {
// Target position in the puzzle grid
double targetLeft = y * pieceWidth;
double targetTop = x * pieceHeight;
// Scatter logic: Randomly place within the container.
double initialLeft = Random().nextDouble() * (containerWidth - pieceWidth);
double initialTop = Random().nextDouble() * (containerHeight - pieceHeight);
setState(() {
pieces.add(
PuzzlePiece(
key: GlobalKey(),
image: image,
imageSize: Size(puzzleWidth, puzzleHeight),
row: x,
col: y,
maxRow: gameDTO.rows!,
maxCol: gameDTO.cols!,
bringToTop: bringToTop,
sendToBack: sendToBack,
initialLeft: initialLeft - targetLeft,
initialTop: initialTop - targetTop,
),
);
});
}
}
}
setState(() {
isSplittingImage = false;
});
}
List<int> _getAdjacentIndices(int index, int rows, int cols) {
List<int> adjacent = [];
int r = index ~/ cols;
int c = index % cols;
if (r > 0) adjacent.add(index - cols); // Top
if (r < rows - 1) adjacent.add(index + cols); // Bottom
if (c > 0) adjacent.add(index - 1); // Left
if (c < cols - 1) adjacent.add(index + 1); // Right
return adjacent;
}
void _onSlidingTileTapped(int currentSlot) {
if (isFinished) return;
// Check if empty slot is adjacent
List<int> adjacent = _getAdjacentIndices(currentSlot, gameDTO.rows!, gameDTO.cols!);
if (adjacent.contains(emptySlotIndex)) {
setState(() {
// Swap
slidingTileIndices[emptySlotIndex] = slidingTileIndices[currentSlot];
slidingTileIndices[currentSlot] = -1;
emptySlotIndex = currentSlot;
// Check win condition
bool won = true;
for (int i = 0; i < slidingTileIndices.length; i++) {
// In a solved puzzle, index i should contain tile i (or -1 at the very last slot)
if (i == slidingTileIndices.length - 1) {
if (slidingTileIndices[i] != -1) won = false;
} else {
if (slidingTileIndices[i] != i) won = false;
}
}
if (won) {
isFinished = true;
_onGameFinished('SlidingPuzzle');
}
});
}
}
// when the pan of a piece starts, we need to bring it to the front of the stack
void bringToTop(Widget widget) {
setState(() {
pieces.remove(widget);
pieces.add(widget);
});
}
// when a piece reaches its final position,
// it will be sent to the back of the stack to not get in the way of other, still movable, pieces
void sendToBack(Widget widget) {
setState(() {
allInPlaceCount++;
isFinished = allInPlaceCount == gameDTO.rows! * gameDTO.cols!;
pieces.remove(widget);
pieces.insert(0, widget);
if (isFinished) {
_onGameFinished('Puzzle');
}
});
}
void _onGameFinished(String gameType) {
Size size = MediaQuery.of(context).size;
final appContext = Provider.of<AppContext>(context, listen: false);
VisitAppContext visitAppContext = appContext.getContext();
final duration = _gameStartTime != null ? DateTime.now().difference(_gameStartTime!).inSeconds : 0;
visitAppContext.statisticsService?.track(
VisitEventType.gameComplete,
metadata: {'gameType': gameType, 'durationSeconds': duration},
);
TranslationAndResourceDTO? messageFin = gameDTO.messageFin != null && gameDTO.messageFin!.isNotEmpty ? gameDTO.messageFin!.where((message) => message.language!.toUpperCase() == visitAppContext.language!.toUpperCase()).firstOrNull : null;
if(messageFin != null) {
showMessage(messageFin, appContext, context, size);
}
}
Widget _buildContent(VisitAppContext visitAppContext) {
if (gameDTO.gameType == GameTypes.Escape) {
final paths = gameDTO.guidedPaths ?? [];
if (paths.isEmpty) {
return Center(
child: Text('Aucun parcours disponible', style: TextStyle(color: Colors.grey[500], fontSize: 15)),
);
}
return ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: paths.length,
itemBuilder: (_, i) {
final path = paths[i];
final title = TranslationHelper.get(path.title, visitAppContext);
final desc = TranslationHelper.get(path.description, visitAppContext);
final stepCount = path.steps?.length ?? 0;
return Card(
margin: const EdgeInsets.only(bottom: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: ListTile(
contentPadding: const EdgeInsets.all(12),
leading: Container(
width: 42,
height: 42,
decoration: BoxDecoration(
color: kMainColor.withOpacity(0.12),
shape: BoxShape.circle,
),
child: const Icon(Icons.explore, color: kMainColor, size: 22),
),
title: Text(title, style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 14)),
subtitle: Text(
desc.isNotEmpty ? desc : '$stepCount étape${stepCount > 1 ? 's' : ''}',
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(color: Colors.grey[600], fontSize: 12),
),
trailing: const Icon(Icons.arrow_forward_ios, size: 14),
onTap: () => Navigator.of(context).push(MaterialPageRoute(
builder: (_) => GuidedPathContentProgressionPage(
path: path,
visitAppContext: visitAppContext,
),
)),
),
);
},
);
}
if (gameDTO.gameType == GameTypes.SlidingPuzzle) {
if (slidingTileIndices.isEmpty) return Center(child: LoadingCommon());
final puzzleSize = visitAppContext.puzzleSize ?? Size(realWidgetSize!.width * 0.8, realWidgetSize!.height * 0.6);
final tileWidth = puzzleSize.width / gameDTO.cols!;
final tileHeight = puzzleSize.height / gameDTO.rows!;
return Center(
child: Container(
width: puzzleSize.width,
height: puzzleSize.height,
child: Stack(
children: [
// Hint Background
if (showHint)
Opacity(
opacity: 0.25,
child: CachedNetworkImage(
imageUrl: gameDTO.puzzleImage!.url!,
fit: BoxFit.fill,
),
),
// Tiles
for (int i = 0; i < slidingTileIndices.length; i++)
if (slidingTileIndices[i] != -1) // Don't draw the empty slot
AnimatedPositioned(
key: ValueKey('tile_${slidingTileIndices[i]}'),
duration: const Duration(milliseconds: 250),
curve: Curves.easeInOut,
left: (i % gameDTO.cols!) * tileWidth,
top: (i ~/ gameDTO.cols!) * tileHeight,
child: SlidingPuzzlePiece(
image: CachedNetworkImage(imageUrl: gameDTO.puzzleImage!.url!, fit: BoxFit.fill),
imageSize: puzzleSize,
originalRow: slidingTileIndices[i] ~/ gameDTO.cols!,
originalCol: slidingTileIndices[i] % gameDTO.cols!,
maxRows: gameDTO.rows!,
maxCols: gameDTO.cols!,
width: tileWidth,
height: tileHeight,
showNumberHint: showHint,
onTap: () => _onSlidingTileTapped(i),
),
),
],
),
),
);
}
// Default: Puzzle
if (gameDTO.puzzleImage == null || gameDTO.puzzleImage!.url == null || realWidgetSize == null) {
return Center(child: Text("Aucune image à afficher", style: TextStyle(fontSize: kNoneInfoOrIncorrect)));
}
final puzzleSize = visitAppContext.puzzleSize ?? Size(realWidgetSize!.width * 0.8, realWidgetSize!.height * 0.6);
return Center(
child: Container(
width: puzzleSize.width,
height: puzzleSize.height,
child: Stack(
clipBehavior: Clip.none,
children: [
// Hint Background
if (showHint)
Opacity(
opacity: 0.2, // Unified opacity for both
child: CachedNetworkImage(
imageUrl: gameDTO.puzzleImage!.url!,
fit: BoxFit.fill,
),
),
...pieces,
],
),
),
);
}
@override
Widget build(BuildContext context) {
final appContext = Provider.of<AppContext>(context);
VisitAppContext visitAppContext = appContext.getContext();
Size size = MediaQuery.of(context).size;
var title = TranslationHelper.get(widget.section.title, appContext.getContext());
String cleanedTitle = title.replaceAll('\n', ' ').replaceAll('<br>', ' ');
return Stack(
children: [
Container(
height: size.height * 0.28,
decoration: BoxDecoration(
boxShadow: const [
BoxShadow(
color: kMainGrey,
spreadRadius: 0.5,
blurRadius: 5,
offset: Offset(0, 1), // changes position of shadow
),
],
gradient: const LinearGradient(
begin: Alignment.centerRight,
end: Alignment.centerLeft,
colors: [
/*Color(0xFFDD79C2),
Color(0xFFB65FBE),
Color(0xFF9146BA),
Color(0xFF7633B8),
Color(0xFF6528B6),
Color(0xFF6025B6)*/
kMainColor0, //Color(0xFFf6b3c4)
kMainColor1,
kMainColor2,
],
),
image: widget.section.imageSource != null ? DecorationImage(
fit: BoxFit.cover,
opacity: 0.65,
image: NetworkImage(
widget.section.imageSource!,
),
): null,
),
),
Column(
children: <Widget>[
SizedBox(
height: size.height * 0.11,
width: size.width,
child: Stack(
fit: StackFit.expand,
children: [
Center(
child: Padding(
padding: const EdgeInsets.only(top: 22.0),
child: SizedBox(
width: size.width *0.7,
child: HtmlWidget(
cleanedTitle,
textStyle: const TextStyle(color: Colors.white, fontFamily: 'Roboto', fontSize: 20),
customStylesBuilder: (element)
{
return {'text-align': 'center', 'font-family': "Roboto", '-webkit-line-clamp': "2"};
},
),
),
),
),
Positioned(
top: 35,
left: 10,
child: SizedBox(
width: 50,
height: 50,
child: InkWell(
onTap: () {
Navigator.of(context).pop();
},
child: Container(
decoration: const BoxDecoration(
color: kMainColor,
shape: BoxShape.circle,
),
child: const Icon(Icons.arrow_back, size: 23, color: Colors.white)
),
)
),
),
],
),
),
Expanded(
child: Container(
margin: const EdgeInsets.only(top: 0),
decoration: const BoxDecoration(
boxShadow: [
BoxShadow(
color: kMainGrey,
spreadRadius: 0.5,
blurRadius: 2,
offset: Offset(0, 1), // changes position of shadow
),
],
color: kBackgroundColor,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(30),
topRight: Radius.circular(30),
),
),
child: ClipRRect(
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(30),
topRight: Radius.circular(30),
),
child: Center(
//color: Colors.green,
child: Container(
//color: Colors.green,
child: Padding(
key: _widgetKey,
padding: const EdgeInsets.all(0.0),
child: isSplittingImage ? Center(child: LoadingCommon()) : _buildContent(visitAppContext),
),
),
)
)
),
),
],
),
if (gameDTO.gameType == null || gameDTO.gameType == GameTypes.Puzzle || gameDTO.gameType == GameTypes.SlidingPuzzle)
Positioned(
bottom: 25,
right: 20,
child: FloatingActionButton(
heroTag: 'hint_button',
onPressed: () {
setState(() {
showHint = !showHint;
});
},
backgroundColor: showHint ? kMainColor : Colors.grey[400],
child: const Icon(Icons.help_outline, color: Colors.white, size: 28),
),
),
],
);
}
}