mirror of
https://github.com/kierankihn/uno-game.git
synced 2025-12-27 02:13:18 +08:00
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.
This commit is contained in:
763
ui/GamePage.slint
Normal file
763
ui/GamePage.slint
Normal file
@@ -0,0 +1,763 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { ConnectPage } from "ConnectPage.slint";
|
import { ConnectPage } from "ConnectPage.slint";
|
||||||
import { StartPage } from "StartPage.slint";
|
import { StartPage } from "StartPage.slint";
|
||||||
|
import { GamePage, OtherPlayer, HandCard, CardColor, GameDirection } from "GamePage.slint";
|
||||||
|
|
||||||
enum PageType {
|
enum PageType {
|
||||||
ConnectPage,
|
ConnectPage,
|
||||||
@@ -9,14 +10,33 @@ enum PageType {
|
|||||||
|
|
||||||
export component MainWindow inherits Window {
|
export component MainWindow inherits Window {
|
||||||
in property <PageType> active-page: PageType.ConnectPage;
|
in property <PageType> active-page: PageType.ConnectPage;
|
||||||
|
|
||||||
|
// ConnectPage
|
||||||
in property <bool> is-connecting;
|
in property <bool> is-connecting;
|
||||||
|
|
||||||
|
// StartPage
|
||||||
in property <bool> is-ready;
|
in property <bool> is-ready;
|
||||||
|
|
||||||
|
// GamePage
|
||||||
|
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;
|
||||||
|
in property <image> discard-top-card;
|
||||||
|
in property <GameDirection> game-direction;
|
||||||
|
in property <CardColor> current-color;
|
||||||
|
|
||||||
callback request-connect(string, string, string);
|
callback request-connect(string, string, string);
|
||||||
callback request-start;
|
callback request-start;
|
||||||
|
callback request-play-card(int, CardColor);
|
||||||
|
callback request-draw-card;
|
||||||
|
callback request-uno;
|
||||||
|
|
||||||
width: 1920px;
|
width: 1920px;
|
||||||
height: 1080px;
|
height: 1080px;
|
||||||
|
title: "UNO!";
|
||||||
|
|
||||||
if root.active-page == PageType.ConnectPage: connect-page := ConnectPage {
|
if root.active-page == PageType.ConnectPage: connect-page := ConnectPage {
|
||||||
is-connecting: root.is-connecting;
|
is-connecting: root.is-connecting;
|
||||||
@@ -30,4 +50,24 @@ export component MainWindow inherits Window {
|
|||||||
root.request-start();
|
root.request-start();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if root.active-page == PageType.GamePage: game-page := GamePage {
|
||||||
|
other-players: root.other-players;
|
||||||
|
current-player-name: root.current-player-name;
|
||||||
|
current-player-card-count: root.current-player-card-count;
|
||||||
|
current-player-has-uno: root.current-player-has-uno;
|
||||||
|
is-current-player-turn: root.is-current-player-turn;
|
||||||
|
hand-cards: root.hand-cards;
|
||||||
|
discard-top-card: root.discard-top-card;
|
||||||
|
game-direction: root.game-direction;
|
||||||
|
current-color: root.current-color;
|
||||||
|
request-play-card(index, card-color) => {
|
||||||
|
root.request-play-card(index, card-color);
|
||||||
|
}
|
||||||
|
request-draw-card => {
|
||||||
|
root.request-draw-card();
|
||||||
|
}
|
||||||
|
request-uno => {
|
||||||
|
root.request-uno();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user