Files
uno-game/ui/GamePage.slint
Kieran Kihn df6bbee5d7 feat(ui): add GamePage with player actions and game state display
- Integrated `GamePage` into `MainWindow` for active game state.
- Added player actions: play card, draw card, and call UNO.
- Implemented discard pile, game direction, and current color indicators.
- Designed player hand and opponent details with interactive components.
2025-12-10 21:47:01 +08:00

764 lines
22 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { VerticalBox, HorizontalBox, ScrollView } from "std-widgets.slint";
import { Button } from "Components.slint";
// 卡牌颜色枚举
export enum CardColor {
Red,
Blue,
Green,
Yellow
}
// 游戏方向枚举
export enum GameDirection {
Clockwise,
CounterClockwise
}
// 其他玩家信息结构
export struct OtherPlayer {
name: string,
card-count: int,
has-uno: bool,
is-current-turn: bool,
}
// 手牌信息结构
export struct HandCard {
image-path: image,
is-selected: bool,
card-id: int,
is-wild: bool,
can-be-played: bool,
}
// 玩家头像组件
component PlayerAvatar inherits Rectangle {
in property <string> player-name;
in property <int> card-count;
in property <bool> has-uno;
in property <bool> is-current-turn;
width: 100px;
height: 130px;
background: transparent;
VerticalLayout {
spacing: 4px;
alignment: center;
// UNO 标志占位区域 (固定高度)
HorizontalLayout {
alignment: center;
Rectangle {
width: 50px;
height: 20px;
background: has-uno ? #FF5722 : transparent;
border-radius: 4px;
Text {
text: "UNO!";
font-size: 11px;
font-weight: 700;
color: has-uno ? #FFFFFF : transparent;
horizontal-alignment: center;
vertical-alignment: center;
}
}
}
// 头像
HorizontalLayout {
alignment: center;
Rectangle {
width: 60px;
height: 60px;
background: is-current-turn ? #FFD54F : #E0E0E0;
border-radius: 30px;
border-width: is-current-turn ? 3px : 0px;
border-color: #FF9800;
Text {
text: "👤";
font-size: 28px;
horizontal-alignment: center;
vertical-alignment: center;
}
}
}
// 玩家名称
Text {
text: player-name;
font-size: 12px;
font-weight: 500;
color: #333333;
horizontal-alignment: center;
}
// 剩余手牌数
Text {
text: "剩余: " + card-count + " 张";
font-size: 10px;
color: #666666;
horizontal-alignment: center;
}
}
}
// 卡牌背面组件 (起牌堆)
component CardBack inherits Rectangle {
in property <int> card-width: 100;
in property <int> card-height: 150;
width: card-width * 1px;
height: card-height * 1px;
background: #2C2C2C;
border-radius: 10px;
border-width: 2px;
border-color: #444444;
Rectangle {
width: parent.width * 0.75;
height: parent.height * 0.83;
background: #1a1a1a;
border-radius: 8px;
Text {
text: "UNO";
font-size: 20px;
font-weight: 700;
color: #888888;
horizontal-alignment: center;
vertical-alignment: center;
}
}
}
// 弃牌堆卡牌组件
component DiscardCard inherits Rectangle {
in property <image> card-image;
in property <int> card-width: 100;
in property <int> card-height: 150;
width: card-width * 1px;
height: card-height * 1px;
background: transparent;
border-radius: 10px;
drop-shadow-blur: 12px;
drop-shadow-color: #00000040;
drop-shadow-offset-y: 4px;
Image {
width: 100%;
height: 100%;
source: card-image;
image-fit: contain;
}
}
// 方向环背景组件 - 只绘制环线
component DirectionRingBackground inherits Rectangle {
in property <CardColor> current-color: CardColor.Red;
in property <int> ring-width: 500;
in property <int> ring-height: 280;
property <color> ring-color: current-color == CardColor.Red ? #F44336 : current-color == CardColor.Blue ? #2196F3 : current-color == CardColor.Green ? #4CAF50 : current-color == CardColor.Yellow ? #FFEB3B : #9E9E9E;
property <color> ring-color-dim: current-color == CardColor.Red ? #E57373 : current-color == CardColor.Blue ? #64B5F6 : current-color == CardColor.Green ? #81C784 : current-color == CardColor.Yellow ? #FFF176 : #BDBDBD;
property <float> a: ring-width / 2;
property <float> b: ring-height / 2;
width: ring-width * 1px;
height: ring-height * 1px;
background: transparent;
// 底层光晕环
Path {
width: 100%;
height: 100%;
stroke: ring-color-dim;
stroke-width: 8px;
fill: transparent;
opacity: 0.3;
MoveTo {
x: ring-width / 2 + a;
y: ring-height / 2;
}
ArcTo {
x: ring-width / 2 - a;
y: ring-height / 2;
radius-x: a;
radius-y: b;
sweep: true;
large-arc: true;
}
ArcTo {
x: ring-width / 2 + a;
y: ring-height / 2;
radius-x: a;
radius-y: b;
sweep: true;
large-arc: true;
}
}
// 主环
Path {
width: 100%;
height: 100%;
stroke: ring-color;
stroke-width: 3px;
fill: transparent;
opacity: 0.9;
MoveTo {
x: ring-width / 2 + a;
y: ring-height / 2;
}
ArcTo {
x: ring-width / 2 - a;
y: ring-height / 2;
radius-x: a;
radius-y: b;
sweep: true;
large-arc: true;
}
ArcTo {
x: ring-width / 2 + a;
y: ring-height / 2;
radius-x: a;
radius-y: b;
sweep: true;
large-arc: true;
}
}
}
// 方向环光点组件 - 使用内置动画实现平滑移动
component DirectionRingOrbs inherits Rectangle {
in property <GameDirection> direction: GameDirection.Clockwise;
in property <CardColor> current-color: CardColor.Red;
in property <int> ring-width: 500;
in property <int> ring-height: 280;
// 单一的动画计数器,持续递增
in-out property <int> cycle: 0;
property <color> ring-color: current-color == CardColor.Red ? #F44336 : current-color == CardColor.Blue ? #2196F3 : current-color == CardColor.Green ? #4CAF50 : current-color == CardColor.Yellow ? #FFEB3B : #9E9E9E;
property <float> a: ring-width / 2;
property <float> b: ring-height / 2;
// 基础角度(持续增加)
property <float> base-angle: cycle * 360;
animate base-angle {
duration: 8s;
easing: linear;
}
// 方向系数顺时针为1逆时针为-1
property <float> dir-sign: direction == GameDirection.Clockwise ? 1 : -1;
// 实际角度 = 基础角度 * 方向系数
property <float> actual-angle: base-angle * dir-sign;
width: ring-width * 1px;
height: ring-height * 1px;
background: transparent;
// 光点1
Rectangle {
x: parent.width / 2 - 9px + cos(actual-angle * 1deg) * a * 1px;
y: parent.height / 2 - 9px + sin(actual-angle * 1deg) * b * 1px;
width: 18px;
height: 18px;
background: ring-color;
border-radius: 9px;
drop-shadow-blur: 8px;
drop-shadow-color: ring-color;
}
// 光点2
Rectangle {
x: parent.width / 2 - 9px + cos((actual-angle + 90) * 1deg) * a * 1px;
y: parent.height / 2 - 9px + sin((actual-angle + 90) * 1deg) * b * 1px;
width: 18px;
height: 18px;
background: ring-color;
border-radius: 9px;
drop-shadow-blur: 8px;
drop-shadow-color: ring-color;
}
// 光点3
Rectangle {
x: parent.width / 2 - 9px + cos((actual-angle + 180) * 1deg) * a * 1px;
y: parent.height / 2 - 9px + sin((actual-angle + 180) * 1deg) * b * 1px;
width: 18px;
height: 18px;
background: ring-color;
border-radius: 9px;
drop-shadow-blur: 8px;
drop-shadow-color: ring-color;
}
// 光点4
Rectangle {
x: parent.width / 2 - 9px + cos((actual-angle + 270) * 1deg) * a * 1px;
y: parent.height / 2 - 9px + sin((actual-angle + 270) * 1deg) * b * 1px;
width: 18px;
height: 18px;
background: ring-color;
border-radius: 9px;
drop-shadow-blur: 8px;
drop-shadow-color: ring-color;
}
// 递增计数器
Timer {
interval: 8s;
running: true;
triggered => {
cycle = cycle + 1;
}
}
// 初始化时启动动画
init => {
cycle = 1;
}
}
// 手牌卡牌组件
component HandCardItem inherits Rectangle {
in property <image> card-image;
in property <bool> is-selected: false;
callback clicked;
width: 120px;
height: 180px;
y: is-selected ? -25px : 0px;
background: transparent;
border-radius: 10px;
drop-shadow-blur: is-selected ? 16px : 6px;
drop-shadow-color: is-selected ? #00000040 : #00000025;
drop-shadow-offset-y: is-selected ? 6px : 3px;
animate y {
duration: 150ms;
easing: ease-out;
}
animate drop-shadow-blur { duration: 150ms; }
touch := TouchArea {
clicked => {
root.clicked();
}
}
Image {
width: 100%;
height: 100%;
source: card-image;
image-fit: contain;
}
}
// UNO 圆形按钮组件
component UnoButton inherits Rectangle {
callback clicked;
width: 90px;
height: 90px;
background: touch.pressed ? #D32F2F : #F44336;
border-radius: 45px;
drop-shadow-blur: 12px;
drop-shadow-color: #F4433660;
drop-shadow-offset-y: 4px;
touch := TouchArea {
clicked => {
root.clicked();
}
}
Text {
text: "UNO!";
font-size: 20px;
font-weight: 700;
color: #FFFFFF;
horizontal-alignment: center;
vertical-alignment: center;
}
}
// 颜色选择按钮组件
component ColorButton inherits Rectangle {
in property <color> btn-color;
in property <string> color-name;
callback clicked;
width: 80px;
height: 80px;
background: touch.pressed ? btn-color.darker(20%) : btn-color;
border-radius: 12px;
border-width: 3px;
border-color: btn-color.darker(30%);
drop-shadow-blur: 8px;
drop-shadow-color: btn-color.with-alpha(0.4);
drop-shadow-offset-y: 3px;
touch := TouchArea {
clicked => {
root.clicked();
}
}
Text {
text: color-name;
font-size: 14px;
font-weight: 600;
color: #FFFFFF;
horizontal-alignment: center;
vertical-alignment: center;
}
}
// 颜色选择弹窗组件
component ColorPickerDialog inherits Rectangle {
in property <bool> show: false;
callback color-selected(CardColor);
callback cancel;
width: 100%;
height: 100%;
background: show ? #00000080 : transparent;
opacity: show ? 1.0 : 0.0;
visible: show;
animate opacity {
duration: 200ms;
easing: ease-out;
}
// 阻止点击穿透
TouchArea {
enabled: show;
clicked => {
root.cancel();
}
}
// 弹窗内容
Rectangle {
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
width: 600px;
height: 280px;
background: #FFFFFF;
border-radius: 20px;
drop-shadow-blur: 30px;
drop-shadow-color: #00000040;
drop-shadow-offset-y: 10px;
// 阻止点击弹窗内容时关闭
TouchArea {
enabled: show;
}
VerticalLayout {
padding: 30px;
spacing: 25px;
alignment: center;
// 标题
Text {
text: "选择颜色";
font-size: 24px;
font-weight: 700;
color: #333333;
horizontal-alignment: center;
}
// 颜色按钮网格
HorizontalLayout {
spacing: 20px;
alignment: center;
ColorButton {
btn-color: #F44336;
color-name: "红色";
clicked => {
root.color-selected(CardColor.Red);
}
}
ColorButton {
btn-color: #2196F3;
color-name: "蓝色";
clicked => {
root.color-selected(CardColor.Blue);
}
}
ColorButton {
btn-color: #4CAF50;
color-name: "绿色";
clicked => {
root.color-selected(CardColor.Green);
}
}
ColorButton {
btn-color: #FFEB3B;
color-name: "黄色";
clicked => {
root.color-selected(CardColor.Yellow);
}
}
}
// 取消按钮
HorizontalLayout {
alignment: center;
Button {
text: "取消";
clicked => {
root.cancel();
}
}
}
}
}
}
// 主游戏页面
export component GamePage inherits Window {
// 其他玩家列表
in property <[OtherPlayer]> other-players;
// 当前玩家信息
in property <string> current-player-name;
in property <int> current-player-card-count;
in property <bool> current-player-has-uno;
in property <bool> is-current-player-turn;
// 手牌列表
in property <[HandCard]> hand-cards;
// 当前选中的卡牌索引 (-1 表示未选中)
property <int> selected-card-index: -1;
// 弃牌堆顶牌
in property <image> discard-top-card;
// 游戏状态
in property <GameDirection> game-direction;
in property <CardColor> current-color;
// 颜色选择弹窗状态
in-out property <bool> show-color-picker;
// 回调
callback request-play-card(int, CardColor);
callback request-draw-card;
callback request-uno;
width: 1920px;
height: 1080px;
// 背景渐变
Rectangle {
background: @linear-gradient(135deg, #fdfbf7 0%, #f3e7e9 100%);
}
// 主布局
VerticalLayout {
padding: 30px;
spacing: 20px;
// ===== 顶部:其他玩家区域 =====
Rectangle {
height: 150px;
background: #FDFBF8;
border-radius: 16px;
drop-shadow-blur: 10px;
drop-shadow-color: #00000010;
drop-shadow-offset-y: 2px;
HorizontalLayout {
padding-top: 10px;
padding-bottom: 15px;
padding-left: 15px;
padding-right: 15px;
spacing: 40px;
alignment: center;
for player[index] in other-players: PlayerAvatar {
player-name: player.name;
card-count: player.card-count;
has-uno: player.has-uno;
is-current-turn: player.is-current-turn;
}
}
}
// ===== 中间:牌堆区域 =====
Rectangle {
vertical-stretch: 1;
background: transparent;
// 中心容器:包含环和两个牌堆
Rectangle {
width: 560px;
height: 320px;
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
background: transparent;
// 方向环背景 - 在牌堆下面
DirectionRingBackground {
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
ring-width: 560;
ring-height: 320;
current-color: root.current-color;
}
// 起牌堆 - 左侧
Rectangle {
x: 80px;
y: (parent.height - 200px) / 2;
width: 130px;
height: 200px;
CardBack {
card-width: 130;
card-height: 200;
}
TouchArea {
width: parent.width;
height: parent.height;
clicked => {
root.request-draw-card();
}
}
}
// 弃牌堆 - 右侧
DiscardCard {
x: parent.width - 130px - 80px;
y: (parent.height - 200px) / 2;
card-width: 130;
card-height: 200;
card-image: root.discard-top-card;
}
// 方向环光点 - 在牌堆上面
DirectionRingOrbs {
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
ring-width: 560;
ring-height: 320;
direction: root.game-direction;
current-color: root.current-color;
}
}
}
// ===== 底部:当前玩家区域 =====
Rectangle {
height: 320px;
background: #FDFBF8;
border-radius: 16px;
drop-shadow-blur: 10px;
drop-shadow-color: #00000010;
drop-shadow-offset-y: -2px;
HorizontalLayout {
padding: 20px;
padding-left: 40px;
padding-right: 40px;
spacing: 30px;
alignment: space-between;
// 当前玩家头像 - 靠左垂直居中与UNO按钮对齐
VerticalLayout {
alignment: center;
PlayerAvatar {
player-name: current-player-name;
card-count: current-player-card-count;
has-uno: current-player-has-uno;
is-current-turn: true;
}
}
// 手牌区域
VerticalLayout {
horizontal-stretch: 1;
spacing: 10px;
alignment: center;
// 出牌按钮 (放在手牌上方)
Rectangle {
height: 50px;
background: transparent;
HorizontalLayout {
alignment: center;
padding-bottom: 10px;
Button {
text: "出牌";
enabled: is-current-player-turn && selected-card-index >= 0 && hand-cards[selected-card-index].can-be-played;
clicked => {
if (selected-card-index >= 0) {
// 检查是否是万能牌
if (hand-cards[selected-card-index].is-wild) {
// 显示颜色选择弹窗
root.show-color-picker = true;
} else {
// 非万能牌直接出牌,颜色传 Red
root.request-play-card(selected-card-index, CardColor.Red);
selected-card-index = -1;
}
}
}
}
}
}
// 手牌列表 - 上方留出空间给选中效果
Rectangle {
height: 210px;
background: transparent;
// 手牌容器,向下偏移以给选中卡牌留空间,居中显示
Rectangle {
x: (parent.width - self.width) / 2;
y: 25px;
width: hand-cards-layout.preferred-width;
height: 180px;
hand-cards-layout := HorizontalLayout {
alignment: center;
spacing: -35px; // 卡牌重叠效果
for card[index] in hand-cards: HandCardItem {
card-image: card.image-path;
is-selected: index == selected-card-index;
clicked => {
if (is-current-player-turn && card.can-be-played) {
// 切换选中状态
if (selected-card-index == index) {
selected-card-index = -1;
} else {
selected-card-index = index;
}
}
}
}
}
}
}
}
// UNO 按钮 - 靠右
VerticalLayout {
alignment: center;
UnoButton {
clicked => {
root.request-uno();
}
}
}
}
}
}
// 颜色选择弹窗 (覆盖在最上层)
ColorPickerDialog {
show: root.show-color-picker;
color-selected(color) => {
root.show-color-picker = false;
root.request-play-card(selected-card-index, color);
selected-card-index = -1;
}
cancel => {
root.show-color-picker = false;
}
}
}