This commit is contained in:
2025-11-14 18:03:50 +09:00
parent 1f5cea9a96
commit 13ed537b23
342 changed files with 18293 additions and 0 deletions
@@ -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,
),
);
}),
),
);
},
);
}
}