...
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
// packages/feature_game_spider/lib/widgets/bottom_bar_widget.dart
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../models/spider_card.dart';
|
||||
import '../controllers/spider_game_controller.dart';
|
||||
import 'card_widget.dart';
|
||||
|
||||
class BottomBarWidget extends StatelessWidget {
|
||||
final double cardWidth;
|
||||
final double cardHeight;
|
||||
|
||||
const BottomBarWidget({
|
||||
super.key, // 👈 GameScreen에서 _stockKey가 전달됨
|
||||
required this.cardWidth,
|
||||
required this.cardHeight,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final controller = Provider.of<SpiderGameController>(context);
|
||||
final state = controller.currentState;
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Container(
|
||||
height: cardHeight + 20,
|
||||
color: theme.bottomAppBarTheme.color,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
// 1. 파운데이션 (왼쪽)
|
||||
_buildFoundationPiles(context, state.foundation),
|
||||
|
||||
// 2. 이동 횟수
|
||||
Text(
|
||||
"이동: ${state.moves}",
|
||||
style: TextStyle(
|
||||
color: theme.colorScheme.onSurface,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold
|
||||
),
|
||||
),
|
||||
|
||||
// 3. 스톡 (오른쪽)
|
||||
// 🔽 [수정] _buildStockPile에 key 전달
|
||||
_buildStockPile(context, controller, state.stock, key),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 🔽 [수정] Key? key 파라미터 추가
|
||||
Widget _buildStockPile(BuildContext context, SpiderGameController controller, List<SpiderCard> stock, Key? key) {
|
||||
return GestureDetector(
|
||||
key: key, // 👈 [수정] GameScreen에서 전달받은 _stockKey를 여기에 할당
|
||||
onTap: (){
|
||||
debugPrint("[LOG] BottomBarWidget: Stock pile tapped!");
|
||||
controller.dealFromStock();
|
||||
},
|
||||
child: Container(
|
||||
width: cardWidth,
|
||||
height: cardHeight,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).primaryColor,
|
||||
border: Border.all(color: Colors.white54, width: 0.5),
|
||||
borderRadius: BorderRadius.circular(cardWidth * 0.08),
|
||||
),
|
||||
child: (stock.isEmpty)
|
||||
? Center(child: Icon(Icons.block, color: Colors.white.withOpacity(0.5), size: cardWidth * 0.5))
|
||||
: Center(
|
||||
child: Text(
|
||||
"${(stock.length / 10).ceil()}",
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: cardWidth * 0.5,
|
||||
fontWeight: FontWeight.bold,
|
||||
shadows: [Shadow(blurRadius: 2, color: Colors.black.withOpacity(0.5))]
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 🔽 [수정] key 파라미터 제거 (Foundation은 위치 계산이 필요 없음)
|
||||
Widget _buildFoundationPiles(BuildContext context, List<List<SpiderCard>> foundation) {
|
||||
return SizedBox(
|
||||
// key: foundationKey, (제거)
|
||||
width: (cardWidth * 0.7) * 4 + cardWidth,
|
||||
height: cardHeight,
|
||||
child: Stack(
|
||||
children: List.generate(8, (index) {
|
||||
return Positioned(
|
||||
left: index * (cardWidth * 0.15),
|
||||
child: Container(
|
||||
width: cardWidth,
|
||||
height: cardHeight,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.white54, width: 0.5),
|
||||
borderRadius: BorderRadius.circular(cardWidth * 0.08),
|
||||
color: Colors.black.withOpacity(0.2),
|
||||
),
|
||||
child: (foundation.length > index && foundation[index].isNotEmpty)
|
||||
? CardWidget(
|
||||
card: foundation[index].last..isFaceUp=true,
|
||||
width: cardWidth,
|
||||
height: cardHeight,
|
||||
isDraggable: false, // 👈 [추가]
|
||||
)
|
||||
: Center(child: Icon(Icons.diamond_outlined, color: Colors.white.withOpacity(0.3), size: cardWidth * 0.3)),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,298 @@
|
||||
// packages/feature_game_spider/lib/widgets/card_widget.dart
|
||||
import 'dart:math' as math;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../models/spider_card.dart';
|
||||
import '../controllers/spider_game_controller.dart';
|
||||
|
||||
class CardWidget extends StatelessWidget {
|
||||
final SpiderCard card;
|
||||
final double width;
|
||||
final double height;
|
||||
final bool isDraggable; // 👈 [추가]
|
||||
|
||||
const CardWidget({
|
||||
super.key,
|
||||
required this.card,
|
||||
required this.width,
|
||||
required this.height,
|
||||
this.isDraggable = true, // 👈 [추가] 기본값은 true
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// 🔽 [수정] isDraggable이 false이면, 드래그 기능 없이 카드 앞면만 즉시 반환
|
||||
if (!isDraggable) {
|
||||
return _buildCardFace(context, card);
|
||||
}
|
||||
|
||||
// --- (isDraggable이 true일 때만 아래 로직 실행) ---
|
||||
final controller = Provider.of<SpiderGameController>(context, listen: false);
|
||||
|
||||
final List<SpiderCard> draggableStack = controller.getDraggableStack(card);
|
||||
final bool canDrag = draggableStack.isNotEmpty;
|
||||
|
||||
return Draggable<List<SpiderCard>>(
|
||||
data: draggableStack,
|
||||
|
||||
// 🔽 [수정] 겹침 높이 계산을 0.4로 수정
|
||||
feedback: Opacity(
|
||||
opacity: 0.8,
|
||||
child: SizedBox(
|
||||
width: width,
|
||||
height: height + (draggableStack.length - 1) * (height * 0.4), // 👈 0.22 -> 0.4
|
||||
child: Stack(
|
||||
children: List.generate(draggableStack.length, (index) {
|
||||
return Positioned(
|
||||
top: index * (height * 0.4), // 👈 0.22 -> 0.4
|
||||
left: 0,
|
||||
// 🔽 [수정] 여기는 CardWidget이 아닌 _buildCardFace를 직접 호출 (중첩 Draggable 방지)
|
||||
child: _buildCardFace(context, draggableStack[index]),
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
childWhenDragging: _buildCardPlaceholder(context),
|
||||
child: (card.isBeingDragged)
|
||||
? _buildCardPlaceholder(context)
|
||||
: _buildCardFace(context, card),
|
||||
|
||||
onDragStarted: () {
|
||||
if (canDrag) {
|
||||
controller.onDragStarted(draggableStack);
|
||||
}
|
||||
},
|
||||
onDraggableCanceled: (velocity, offset) {
|
||||
controller.onDragCancelled();
|
||||
},
|
||||
onDragEnd: (details) {
|
||||
if (controller.draggedCards.isNotEmpty) {
|
||||
controller.onDragCancelled();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// ( _buildCardFace, _buildRankText, _buildCenterSymbols 는 이전과 동일 )
|
||||
Widget _buildCardFace(BuildContext context, SpiderCard card) {
|
||||
return Container(
|
||||
width: width,
|
||||
height: height,
|
||||
decoration: BoxDecoration(
|
||||
color: card.isFaceUp ? Colors.white : Theme.of(context).primaryColor,
|
||||
border: Border.all(color: Colors.black54, width: 0.5),
|
||||
borderRadius: BorderRadius.circular(width * 0.08),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.2),
|
||||
blurRadius: 2,
|
||||
offset: const Offset(1, 1),
|
||||
)
|
||||
],
|
||||
),
|
||||
child: card.isFaceUp
|
||||
? Stack(
|
||||
children: [
|
||||
_buildRankText(card, Alignment.topLeft),
|
||||
_buildRankText(card, Alignment.bottomRight),
|
||||
_buildCenterSymbols(card),
|
||||
],
|
||||
)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
Widget _buildRankText(SpiderCard card, Alignment alignment) {
|
||||
final bool isTopLeft = alignment == Alignment.topLeft;
|
||||
final double fontSize = width * 0.4;
|
||||
final double padding = width * 0.05;
|
||||
Widget content = Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
card.rankText,
|
||||
style: TextStyle(
|
||||
color: card.isRed ? Colors.red : Colors.black,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: fontSize,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
card.suitSymbol,
|
||||
style: TextStyle(
|
||||
color: card.isRed ? Colors.red : Colors.black,
|
||||
fontSize: fontSize * 0.5,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
if (!isTopLeft) {
|
||||
content = Transform.rotate(
|
||||
angle: math.pi,
|
||||
child: content,
|
||||
);
|
||||
}
|
||||
return Positioned(
|
||||
top: isTopLeft ? padding : null,
|
||||
left: isTopLeft ? padding : null,
|
||||
bottom: isTopLeft ? null : padding,
|
||||
right: isTopLeft ? null : padding,
|
||||
child: content,
|
||||
);
|
||||
}
|
||||
Widget _buildCenterSymbols(SpiderCard card) {
|
||||
final double symbolSize = width * 0.2;
|
||||
final double bigSymbolSize = width * 0.7;
|
||||
if (card.rank > 10) {
|
||||
return Center(
|
||||
child: Text(
|
||||
card.suitSymbol,
|
||||
style: TextStyle(
|
||||
color: card.isRed ? Colors.red : Colors.black,
|
||||
fontSize: bigSymbolSize,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (card.rank == 1) {
|
||||
return Center(
|
||||
child: Text(
|
||||
card.suitSymbol,
|
||||
style: TextStyle(
|
||||
color: card.isRed ? Colors.red : Colors.black,
|
||||
fontSize: bigSymbolSize * 0.8,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
List<Widget> symbols = [];
|
||||
Widget symbol(Alignment align) {
|
||||
return Align(
|
||||
alignment: align,
|
||||
child: Text(
|
||||
card.suitSymbol,
|
||||
style: TextStyle(
|
||||
color: card.isRed ? Colors.red : Colors.black,
|
||||
fontSize: symbolSize,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
Widget flippedSymbol(Alignment align) {
|
||||
return Align(
|
||||
alignment: align,
|
||||
child: Transform.rotate(
|
||||
angle: math.pi,
|
||||
child: Text(
|
||||
card.suitSymbol,
|
||||
style: TextStyle(
|
||||
color: card.isRed ? Colors.red : Colors.black,
|
||||
fontSize: symbolSize,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
switch (card.rank) {
|
||||
case 2:
|
||||
symbols.add(symbol(Alignment.topCenter));
|
||||
symbols.add(flippedSymbol(Alignment.bottomCenter));
|
||||
break;
|
||||
case 3:
|
||||
symbols.add(symbol(Alignment.topCenter));
|
||||
symbols.add(symbol(Alignment.center));
|
||||
symbols.add(flippedSymbol(Alignment.bottomCenter));
|
||||
break;
|
||||
case 4:
|
||||
symbols.add(symbol(Alignment.topLeft));
|
||||
symbols.add(symbol(Alignment.topRight));
|
||||
symbols.add(flippedSymbol(Alignment.bottomLeft));
|
||||
symbols.add(flippedSymbol(Alignment.bottomRight));
|
||||
break;
|
||||
case 5:
|
||||
symbols.addAll([
|
||||
symbol(Alignment.topLeft),
|
||||
symbol(Alignment.topRight),
|
||||
symbol(Alignment.center),
|
||||
flippedSymbol(Alignment.bottomLeft),
|
||||
flippedSymbol(Alignment.bottomRight),
|
||||
]);
|
||||
break;
|
||||
case 6:
|
||||
symbols.addAll([
|
||||
symbol(Alignment.topLeft),
|
||||
symbol(Alignment.topRight),
|
||||
symbol(Alignment.centerLeft),
|
||||
symbol(Alignment.centerRight),
|
||||
flippedSymbol(Alignment.bottomLeft),
|
||||
flippedSymbol(Alignment.bottomRight),
|
||||
]);
|
||||
break;
|
||||
case 7:
|
||||
symbols.addAll([
|
||||
symbol(Alignment.topLeft),
|
||||
symbol(Alignment.topRight),
|
||||
symbol(const Alignment(0.0, -0.25)),
|
||||
symbol(Alignment.centerLeft),
|
||||
symbol(Alignment.centerRight),
|
||||
flippedSymbol(Alignment.bottomLeft),
|
||||
flippedSymbol(Alignment.bottomRight),
|
||||
]);
|
||||
break;
|
||||
case 8:
|
||||
symbols.addAll([
|
||||
symbol(Alignment.topLeft),
|
||||
symbol(Alignment.topRight),
|
||||
symbol(const Alignment(0.0, -0.25)),
|
||||
symbol(Alignment.centerLeft),
|
||||
symbol(Alignment.centerRight),
|
||||
flippedSymbol(Alignment.bottomLeft),
|
||||
flippedSymbol(Alignment.bottomRight),
|
||||
flippedSymbol(const Alignment(0.0, 0.25)),
|
||||
]);
|
||||
break;
|
||||
case 9:
|
||||
symbols.addAll([
|
||||
symbol(const Alignment(-0.8, -0.6)),
|
||||
symbol(const Alignment(0.8, -0.6)),
|
||||
symbol(const Alignment(-0.8, 0.0)),
|
||||
symbol(const Alignment(0.8, 0.0)),
|
||||
symbol(Alignment.center),
|
||||
flippedSymbol(const Alignment(-0.8, 0.6)),
|
||||
flippedSymbol(const Alignment(0.8, 0.6)),
|
||||
symbol(const Alignment(0.0, -0.8)),
|
||||
flippedSymbol(const Alignment(0.0, 0.8)),
|
||||
]);
|
||||
break;
|
||||
case 10:
|
||||
symbols.addAll([
|
||||
symbol(const Alignment(-0.8, -0.7)),
|
||||
symbol(const Alignment(0.8, -0.7)),
|
||||
symbol(const Alignment(-0.8, -0.1)),
|
||||
symbol(const Alignment(0.8, -0.1)),
|
||||
symbol(const Alignment(0.0, -0.9)),
|
||||
symbol(const Alignment(0.0, -0.4)),
|
||||
flippedSymbol(const Alignment(-0.8, 0.7)),
|
||||
flippedSymbol(const Alignment(0.8, 0.7)),
|
||||
flippedSymbol(const Alignment(0.0, 0.9)),
|
||||
flippedSymbol(const Alignment(0.0, 0.4)),
|
||||
]);
|
||||
break;
|
||||
}
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: width * 0.2, vertical: height * 0.15),
|
||||
child: Stack(children: symbols),
|
||||
);
|
||||
}
|
||||
Widget _buildCardPlaceholder(BuildContext context) {
|
||||
return Container(
|
||||
width: width,
|
||||
height: height,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).scaffoldBackgroundColor.withOpacity(0.5),
|
||||
borderRadius: BorderRadius.circular(width * 0.08),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
// packages/feature_game_spider/lib/widgets/tableau_pile_widget.dart
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../models/spider_card.dart';
|
||||
import '../controllers/spider_game_controller.dart';
|
||||
import 'card_widget.dart';
|
||||
|
||||
class TableauPileWidget extends StatelessWidget {
|
||||
final int pileIndex;
|
||||
final List<SpiderCard> cards;
|
||||
final double cardWidth;
|
||||
final double cardHeight;
|
||||
final double cardOverlap;
|
||||
|
||||
const TableauPileWidget({
|
||||
super.key,
|
||||
required this.pileIndex,
|
||||
required this.cards,
|
||||
required this.cardWidth,
|
||||
required this.cardHeight,
|
||||
required this.cardOverlap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// 🔽 [수정] 'context.watch'를 사용하여 컨트롤러의 애니메이션 상태를 실시간으로 감지
|
||||
final controller = context.watch<SpiderGameController>();
|
||||
|
||||
return DragTarget<List<SpiderCard>>(
|
||||
onWillAccept: (draggedCards) {
|
||||
if (draggedCards == null) return false;
|
||||
|
||||
if (controller.cardsToDealAnimate.isNotEmpty ||
|
||||
controller.cardsToAnimateStack.isNotEmpty) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return controller.isValidMove(draggedCards, pileIndex);
|
||||
},
|
||||
onAccept: (draggedCards) {
|
||||
controller.onCardsDropped(draggedCards, pileIndex);
|
||||
},
|
||||
builder: (context, candidateData, rejectedData) {
|
||||
|
||||
final bool isAnimating = controller.cardsToDealAnimate.isNotEmpty ||
|
||||
controller.cardsToAnimateStack.isNotEmpty;
|
||||
final bool isHighlighted = candidateData.isNotEmpty && !isAnimating;
|
||||
|
||||
// 🔽 [로그 추가] "초록 선"의 원인을 추적합니다.
|
||||
// (하이라이트되거나, 애니메이션 중이거나, 드래그가 감지되면 로그 출력)
|
||||
if (candidateData.isNotEmpty || isAnimating || isHighlighted) {
|
||||
debugPrint("[LOG] TableauPileWidget (Pile $pileIndex): "
|
||||
"candidateData.isNotEmpty = ${candidateData.isNotEmpty}, "
|
||||
"isAnimating = $isAnimating, "
|
||||
"==> isHighlighted = $isHighlighted");
|
||||
}
|
||||
|
||||
return Container(
|
||||
width: cardWidth,
|
||||
constraints: BoxConstraints(
|
||||
minHeight: cardHeight,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: isHighlighted
|
||||
? Colors.green.withOpacity(0.3)
|
||||
: (cards.isEmpty ? Colors.black.withOpacity(0.1) : null),
|
||||
borderRadius: BorderRadius.circular(cardWidth * 0.08),
|
||||
),
|
||||
child: Stack(
|
||||
children: List.generate(cards.length, (index) {
|
||||
final card = cards[index];
|
||||
return Positioned(
|
||||
top: index * cardOverlap,
|
||||
left: 0,
|
||||
child: CardWidget(
|
||||
card: card,
|
||||
width: cardWidth,
|
||||
height: cardHeight,
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user