| // Copyright 2015 The Vanadium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| library game_component; |
| |
| import 'dart:math' as math; |
| import 'dart:async'; |
| |
| import 'package:flutter/animation.dart'; |
| import 'package:flutter/material.dart'; |
| import 'package:flutter/rendering.dart'; |
| |
| import '../logic/card.dart' as logic_card; |
| import '../logic/croupier.dart' show Croupier; |
| import '../logic/game/game.dart' show Game, GameType; |
| import '../logic/hearts/hearts.dart' show HeartsGame, HeartsPhase, HeartsType; |
| import '../logic/solitaire/solitaire.dart' show SolitaireGame, SolitairePhase; |
| import '../styles/common.dart' as style; |
| import 'board.dart' show HeartsBoard; |
| import 'card.dart' as component_card; |
| import 'card_collection.dart' |
| show CardCollectionComponent, DropType, CardCollectionOrientation, AcceptCb; |
| import 'croupier_profile.dart' show CroupierProfileComponent; |
| |
| part 'hearts/hearts.part.dart'; |
| part 'proto/proto.part.dart'; |
| part 'solitaire/solitaire.part.dart'; |
| |
| typedef void NoArgCb(); |
| |
| abstract class GameComponent extends StatefulComponent { |
| final Croupier croupier; |
| Game get game => croupier.game; |
| final NoArgCb gameEndCallback; |
| final double width; |
| final double height; |
| |
| GameComponent(this.croupier, this.gameEndCallback, |
| {Key key, this.width, this.height}) |
| : super(key: key); |
| } |
| |
| abstract class GameComponentState<T extends GameComponent> extends State<T> { |
| Map<logic_card.Card, CardAnimationData> cardLevelMap; |
| |
| void initState() { |
| super.initState(); |
| |
| cardLevelMap = new Map<logic_card.Card, CardAnimationData>(); |
| config.game.updateCallback = update; |
| } |
| |
| void _reset() { |
| cardLevelMap.clear(); |
| } |
| |
| // This callback is used to force the UI to draw when state changes occur |
| // outside of the UIs control (e.g., synced data). |
| void update() { |
| if (this.mounted) { |
| setState(() {}); |
| } |
| } |
| |
| // A helper that most subclasses use in order to quit their respective games. |
| void _quitGameCallback() { |
| setState(() { |
| config.gameEndCallback(); |
| }); |
| } |
| |
| // A helper that subclasses might override to create buttons. |
| Widget _makeButton(String text, NoArgCb callback) { |
| return new FlatButton( |
| child: new Text(text, style: style.Text.liveNow), onPressed: callback); |
| } |
| |
| @override |
| Widget build(BuildContext context); // still UNIMPLEMENTED |
| |
| void _cardLevelMapProcessAllVisible(List<int> visibleCardCollectionIndexes) { |
| Game game = config.game; |
| |
| for (int i = 0; i < visibleCardCollectionIndexes.length; i++) { |
| int index = visibleCardCollectionIndexes[i]; |
| for (int j = 0; j < game.cardCollections[index].length; j++) { |
| _cardLevelMapProcess(game.cardCollections[index][j]); |
| } |
| } |
| } |
| |
| void _cardLevelMapProcess(logic_card.Card logicCard) { |
| component_card.GlobalCardKey key = new component_card.GlobalCardKey( |
| logicCard, component_card.CardUIType.CARD); |
| component_card.CardState cardState = key.currentState; |
| if (cardState == null) { |
| return; // There's nothing we can really do about this card since it hasn't drawn yet. |
| } |
| Point p = cardState.getGlobalPosition(); |
| double z = cardState.config.z; |
| component_card.Card c = key.currentWidget; |
| |
| assert(c == cardState.config); |
| |
| CardAnimationData cad = cardLevelMap[logicCard]; |
| if (cad == null || cad.newPoint != p || cad.z != z) { |
| setState(() { |
| cardLevelMap[logicCard] = new CardAnimationData(c, cad?.newPoint, p, z); |
| }); |
| } else if (!cad.comp_card.isMatchWith(c)) { |
| // Even if the position or z index didn't change, we can still update the |
| // card itself. This can help during screen rotations, since the top-left |
| // card likely not change positions or z-index. |
| setState(() { |
| cad.comp_card = c; |
| }); |
| } |
| } |
| |
| bool _isMoving(logic_card.Card c) { |
| CardAnimationData data = cardLevelMap[c]; |
| RenderBox box = context.findRenderObject(); |
| Point localOld = |
| data.oldPoint != null ? box.globalToLocal(data.oldPoint) : null; |
| Point localNew = box.globalToLocal(data.newPoint); |
| |
| // We also need confirmation from the ZCard that we are moving. |
| component_card.GlobalCardKey zCardKey = |
| new component_card.GlobalCardKey(c, component_card.CardUIType.ZCARD); |
| component_card.ZCardState zCardKeyState = zCardKey.currentState; |
| |
| // It is moving if there is an old position, the new one isn't equal to the |
| // old one, and the ZCard hasn't arrived at the new position yet. |
| return localOld != null && |
| localOld != localNew && |
| localNew != zCardKeyState?.localPosition; |
| } |
| |
| // Helper to build the card animation layer. |
| // Note: This isn't a component because of its dependence on Widgets. |
| Widget buildCardAnimationLayer(List<int> visibleCardCollectionIndexes) { |
| // It's possible that some cards need to be moved after this build. |
| // If so, we can catch it in the next frame. |
| scheduler.requestPostFrameCallback((Duration d) { |
| _cardLevelMapProcessAllVisible(visibleCardCollectionIndexes); |
| }); |
| |
| List<Widget> positionedCards = new List<Widget>(); |
| |
| // Sort the cards by z-index. If a card is animating, it gets a z bonus. |
| List<logic_card.Card> orderedKeys = cardLevelMap.keys.toList() |
| ..sort((logic_card.Card a, logic_card.Card b) { |
| bool isMovingA = _isMoving(a); |
| bool isMovingB = _isMoving(b); |
| |
| // Moving cards take much higher priority. |
| if (isMovingA && !isMovingB) { |
| return 1; |
| } else if (!isMovingA && isMovingB) { |
| return -1; |
| } |
| |
| // If both are moving/non-moving, we break ties with the z-index. |
| double diff = cardLevelMap[a].z - cardLevelMap[b].z; |
| return diff.sign.toInt(); |
| }); |
| |
| orderedKeys.forEach((logic_card.Card c) { |
| // Don't show a card if it isn't part of a visible collection. |
| if (!visibleCardCollectionIndexes.contains(config.game.findCard(c))) { |
| cardLevelMap.remove(c); // It is an old card, which we can clean up. |
| assert(!cardLevelMap.containsKey(c)); |
| return; |
| } |
| |
| CardAnimationData data = cardLevelMap[c]; |
| RenderBox box = context.findRenderObject(); |
| Point localOld = |
| data.oldPoint != null ? box.globalToLocal(data.oldPoint) : null; |
| Point localNew = box.globalToLocal(data.newPoint); |
| |
| positionedCards.add(new Positioned( |
| key: new GlobalObjectKey(c |
| .toString()), //needed, or else the Positioned wrapper may be traded out and animations fail. |
| top: |
| 0.0, // must pass x and y or else it expands to the maximum Stack size. |
| left: |
| 0.0, // must pass x and y or else it expands to the maximum Stack size. |
| child: new component_card.ZCard(data.comp_card, localOld, localNew))); |
| }); |
| |
| return new IgnorePointer( |
| ignoring: true, |
| child: new Container( |
| width: config.width, |
| height: config.height, |
| child: new Stack(positionedCards))); |
| } |
| } |
| |
| GameComponent createGameComponent(Croupier croupier, NoArgCb gameEndCallback, |
| {Key key, double width, double height}) { |
| switch (croupier.game.gameType) { |
| case GameType.Proto: |
| return new ProtoGameComponent(croupier, gameEndCallback, |
| key: key, width: width, height: height); |
| case GameType.Hearts: |
| return new HeartsGameComponent(croupier, gameEndCallback, |
| key: key, width: width, height: height); |
| case GameType.Solitaire: |
| return new SolitaireGameComponent(croupier, gameEndCallback, |
| key: key, width: width, height: height); |
| default: |
| // We're probably not ready to serve the other games yet. |
| assert(false); |
| return null; |
| } |
| } |
| |
| abstract class GameArrangeComponent extends StatelessComponent { |
| final Croupier croupier; |
| final double width; |
| final double height; |
| GameArrangeComponent(this.croupier, {this.width, this.height}); |
| } |
| |
| GameArrangeComponent createGameArrangeComponent(Croupier croupier, |
| {double width, double height}) { |
| switch (croupier.game.gameType) { |
| case GameType.Hearts: |
| return new HeartsArrangeComponent(croupier, width: width, height: height); |
| default: |
| // We can't arrange this game. |
| throw new UnimplementedError( |
| "We cannot arrange the game type: ${croupier.game.gameType}"); |
| } |
| } |
| |
| /// CardAnimationData contains the relevant information for a ZCard to be built. |
| /// It uses the comp_card's properties, the oldPoint, newPoint, and z-index to |
| /// determine how it needs to animate. |
| class CardAnimationData { |
| component_card.Card comp_card; |
| Point oldPoint; |
| Point newPoint; |
| double z; |
| |
| CardAnimationData(this.comp_card, this.oldPoint, this.newPoint, this.z); |
| } |