blob: 7a858adc6e8773839318e21eb19d60eaafcf1eb9 [file] [log] [blame]
// 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/scheduler.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:vector_math/vector_math_64.dart' as vector_math;
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.addPostFrameCallback((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);
}