Files
uno-game/ui/GamePage.slint
2025-12-18 23:34:24 +08:00

788 lines
24 KiB
Plaintext
Raw Permalink 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 <float> scale;
in property <string> player-name;
in property <int> card-count;
in property <bool> has-uno;
in property <bool> is-current-turn;
width: 100px * scale;
height: 130px * scale;
background: transparent;
VerticalLayout {
spacing: 4px * scale;
alignment: center;
// UNO 标志占位区域 (固定高度)
HorizontalLayout {
alignment: center;
Rectangle {
width: 50px * scale;
height: 20px * scale;
background: has-uno ? #FF5722 : transparent;
border-radius: 4px * scale;
Text {
text: "UNO!";
font-size: 11px * scale;
font-weight: 700;
color: has-uno ? #FFFFFF : transparent;
horizontal-alignment: center;
vertical-alignment: center;
}
}
}
// 头像
HorizontalLayout {
alignment: center;
Rectangle {
width: 60px * scale;
height: 60px * scale;
background: is-current-turn ? #FFD54F : #E0E0E0;
border-radius: 30px * scale;
border-width: is-current-turn ? 3px * scale : 0px;
border-color: #FF9800;
Text {
text: "👤";
font-size: 28px * scale;
horizontal-alignment: center;
vertical-alignment: center;
}
}
}
// 玩家名称
Text {
text: player-name;
font-size: 12px * scale;
font-weight: 500;
color: #333333;
horizontal-alignment: center;
}
// 剩余手牌数
Text {
text: "剩余: " + card-count + " 张";
font-size: 10px * scale;
color: #666666;
horizontal-alignment: center;
}
}
}
// 卡牌背面组件 (起牌堆)
component CardBack inherits Rectangle {
in property <float> scale;
in property <int> card-width: 100;
in property <int> card-height: 150;
width: card-width * 1px * scale;
height: card-height * 1px * scale;
background: #2C2C2C;
border-radius: 10px * scale;
border-width: 2px * scale;
border-color: #444444;
Rectangle {
width: parent.width * 0.75;
height: parent.height * 0.83;
background: #1a1a1a;
border-radius: 8px * scale;
Text {
text: "UNO";
font-size: 20px * scale;
font-weight: 700;
color: #888888;
horizontal-alignment: center;
vertical-alignment: center;
}
}
}
// 弃牌堆卡牌组件
component DiscardCard inherits Rectangle {
in property <float> scale;
in property <image> card-image;
in property <int> card-width: 100;
in property <int> card-height: 150;
width: card-width * 1px * scale;
height: card-height * 1px * scale;
background: transparent;
border-radius: 10px * scale;
drop-shadow-blur: 12px * scale;
drop-shadow-color: #00000040;
drop-shadow-offset-y: 4px * scale;
Image {
width: 100%;
height: 100%;
source: card-image;
image-fit: contain;
}
}
// 方向环背景组件 - 只绘制环线
component DirectionRingBackground inherits Rectangle {
in property <float> scale;
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 * scale;
height: ring-height * 1px * scale;
background: transparent;
// 底层光晕环
Path {
width: 100%;
height: 100%;
stroke: ring-color-dim;
stroke-width: 8px * scale;
fill: transparent;
opacity: 0.3;
MoveTo {
x: (ring-width / 2 + a) * scale;
y: (ring-height / 2) * scale;
}
ArcTo {
x: (ring-width / 2 - a) * scale;
y: (ring-height / 2) * scale;
radius-x: a * scale;
radius-y: b * scale;
sweep: true;
large-arc: true;
}
ArcTo {
x: (ring-width / 2 + a) * scale;
y: (ring-height / 2) * scale;
radius-x: a * scale;
radius-y: b * scale;
sweep: true;
large-arc: true;
}
}
// 主环
Path {
width: 100%;
height: 100%;
stroke: ring-color;
stroke-width: 3px * scale;
fill: transparent;
opacity: 0.9;
MoveTo {
x: (ring-width / 2 + a) * scale;
y: (ring-height / 2) * scale;
}
ArcTo {
x: (ring-width / 2 - a) * scale;
y: (ring-height / 2) * scale;
radius-x: a * scale;
radius-y: b * scale;
sweep: true;
large-arc: true;
}
ArcTo {
x: (ring-width / 2 + a) * scale;
y: (ring-height / 2) * scale;
radius-x: a * scale;
radius-y: b * scale;
sweep: true;
large-arc: true;
}
}
}
// 方向环光点组件 - 使用内置动画实现平滑移动
component DirectionRingOrbs inherits Rectangle {
in property <float> scale;
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;
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: 0;
animate base-angle {
duration: 8s;
easing: linear;
iteration-count: -1;
}
// 方向系数顺时针为1逆时针为-1
property <float> dir-sign: direction == GameDirection.Clockwise ? 1 : -1;
// 实际角度 = 基础角度 * 方向系数
property <float> actual-angle: base-angle * dir-sign;
width: ring-width * 1px * scale;
height: ring-height * 1px * scale;
background: transparent;
// 光点1
Rectangle {
x: (parent.width / 2 - 9px * scale) + cos(actual-angle * 1deg) * a * 1px * scale;
y: (parent.height / 2 - 9px * scale) + sin(actual-angle * 1deg) * b * 1px * scale;
width: 18px * scale;
height: 18px * scale;
background: ring-color;
border-radius: 9px * scale;
drop-shadow-blur: 8px * scale;
drop-shadow-color: ring-color;
}
// 光点2
Rectangle {
x: (parent.width / 2 - 9px * scale) + cos((actual-angle + 90) * 1deg) * a * 1px * scale;
y: (parent.height / 2 - 9px * scale) + sin((actual-angle + 90) * 1deg) * b * 1px * scale;
width: 18px * scale;
height: 18px * scale;
background: ring-color;
border-radius: 9px * scale;
drop-shadow-blur: 8px * scale;
drop-shadow-color: ring-color;
}
// 光点3
Rectangle {
x: (parent.width / 2 - 9px * scale) + cos((actual-angle + 180) * 1deg) * a * 1px * scale;
y: (parent.height / 2 - 9px * scale) + sin((actual-angle + 180) * 1deg) * b * 1px * scale;
width: 18px * scale;
height: 18px * scale;
background: ring-color;
border-radius: 9px * scale;
drop-shadow-blur: 8px * scale;
drop-shadow-color: ring-color;
}
// 光点4
Rectangle {
x: (parent.width / 2 - 9px * scale) + cos((actual-angle + 270) * 1deg) * a * 1px * scale;
y: (parent.height / 2 - 9px * scale) + sin((actual-angle + 270) * 1deg) * b * 1px * scale;
width: 18px * scale;
height: 18px * scale;
background: ring-color;
border-radius: 9px * scale;
drop-shadow-blur: 8px * scale;
drop-shadow-color: ring-color;
}
// 初始化时启动动画
init => {
base-angle = 360;
}
}
// 手牌卡牌组件
component HandCardItem inherits Rectangle {
in property <float> scale;
in property <image> card-image;
in property <bool> is-selected: false;
callback clicked;
width: 120px * scale;
height: 180px * scale;
y: is-selected ? -25px * scale : 0px;
background: transparent;
border-radius: 10px * scale;
drop-shadow-blur: is-selected ? 16px * scale : 6px * scale;
drop-shadow-color: is-selected ? #00000040 : #00000025;
drop-shadow-offset-y: is-selected ? 6px * scale : 3px * scale;
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 {
in property <float> scale;
callback clicked;
width: 90px * scale;
height: 90px * scale;
background: touch.pressed ? #D32F2F : #F44336;
border-radius: 45px * scale;
drop-shadow-blur: 12px * scale;
drop-shadow-color: #F4433660;
drop-shadow-offset-y: 4px * scale;
touch := TouchArea {
clicked => {
root.clicked();
}
}
Text {
text: "UNO!";
font-size: 20px * scale;
font-weight: 700;
color: #FFFFFF;
horizontal-alignment: center;
vertical-alignment: center;
}
}
// 颜色选择按钮组件
component ColorButton inherits Rectangle {
in property <float> scale;
in property <color> btn-color;
in property <string> color-name;
callback clicked;
width: 80px * scale;
height: 80px * scale;
background: touch.pressed ? btn-color.darker(20%) : btn-color;
border-radius: 12px * scale;
border-width: 3px * scale;
border-color: btn-color.darker(30%);
drop-shadow-blur: 8px * scale;
drop-shadow-color: btn-color.with-alpha(0.4);
drop-shadow-offset-y: 3px * scale;
touch := TouchArea {
clicked => {
root.clicked();
}
}
Text {
text: color-name;
font-size: 14px * scale;
font-weight: 600;
color: #FFFFFF;
horizontal-alignment: center;
vertical-alignment: center;
}
}
// 颜色选择弹窗组件
component ColorPickerDialog inherits Rectangle {
in property <float> scale;
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 * scale;
height: 280px * scale;
background: #FFFFFF;
border-radius: 20px * scale;
drop-shadow-blur: 30px * scale;
drop-shadow-color: #00000040;
drop-shadow-offset-y: 10px * scale;
// 阻止点击弹窗内容时关闭
TouchArea {
enabled: show;
}
VerticalLayout {
padding: 30px * scale;
spacing: 25px * scale;
alignment: center;
// 标题
Text {
text: "选择颜色";
font-size: 24px * scale;
font-weight: 700;
color: #333333;
horizontal-alignment: center;
}
// 颜色按钮网格
HorizontalLayout {
spacing: 20px * scale;
alignment: center;
ColorButton {
scale: root.scale;
btn-color: #F44336;
color-name: "红色";
clicked => {
root.color-selected(CardColor.Red);
}
}
ColorButton {
scale: root.scale;
btn-color: #2196F3;
color-name: "蓝色";
clicked => {
root.color-selected(CardColor.Blue);
}
}
ColorButton {
scale: root.scale;
btn-color: #4CAF50;
color-name: "绿色";
clicked => {
root.color-selected(CardColor.Green);
}
}
ColorButton {
scale: root.scale;
btn-color: #FFEB3B;
color-name: "黄色";
clicked => {
root.color-selected(CardColor.Yellow);
}
}
}
// 取消按钮
HorizontalLayout {
alignment: center;
Button {
scale: root.scale;
text: "取消";
clicked => {
root.cancel();
}
}
}
}
}
}
// 主游戏页面
export component GamePage inherits Rectangle {
in property <float> scale;
// 其他玩家列表
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: 100%;
height: 100%;
// 背景渐变
Rectangle {
background: @linear-gradient(135deg, #fdfbf7 0%, #f3e7e9 100%);
}
// 主布局
VerticalLayout {
padding: 30px * scale;
spacing: 20px * scale;
// ===== 顶部:其他玩家区域 =====
Rectangle {
height: 150px * scale;
background: #FDFBF8;
border-radius: 16px * scale;
drop-shadow-blur: 10px * scale;
drop-shadow-color: #00000010;
drop-shadow-offset-y: 2px * scale;
HorizontalLayout {
padding-top: 10px * scale;
padding-bottom: 15px * scale;
padding-left: 15px * scale;
padding-right: 15px * scale;
spacing: 40px * scale;
alignment: center;
for player[index] in other-players: PlayerAvatar {
scale: root.scale;
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 * scale;
height: 320px * scale;
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
background: transparent;
// 方向环背景 - 在牌堆下面
DirectionRingBackground {
scale: root.scale;
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 * scale;
y: (parent.height - 200px * scale) / 2;
width: 130px * scale;
height: 200px * scale;
CardBack {
scale: root.scale;
card-width: 130;
card-height: 200;
}
TouchArea {
enabled: root.is-current-player-turn;
width: parent.width;
height: parent.height;
clicked => {
root.selected-card-index = -1;
root.request-draw-card();
}
}
}
// 弃牌堆 - 右侧
DiscardCard {
scale: root.scale;
x: parent.width - 130px * scale - 80px * scale;
y: (parent.height - 200px * scale) / 2;
card-width: 130;
card-height: 200;
card-image: root.discard-top-card;
}
// 方向环光点 - 在牌堆上面
DirectionRingOrbs {
scale: root.scale;
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: 350px * scale;
background: #FDFBF8;
border-radius: 16px * scale;
drop-shadow-blur: 10px * scale;
drop-shadow-color: #00000010;
drop-shadow-offset-y: -2px * scale;
HorizontalLayout {
padding: 20px * scale;
padding-left: 40px * scale;
padding-right: 40px * scale;
spacing: 30px * scale;
alignment: space-between;
// 当前玩家头像 - 靠左垂直居中与UNO按钮对齐
VerticalLayout {
alignment: center;
PlayerAvatar {
scale: root.scale;
player-name: current-player-name;
card-count: current-player-card-count;
has-uno: current-player-has-uno;
is-current-turn: is-current-player-turn;
}
}
// 手牌区域
VerticalLayout {
horizontal-stretch: 1;
spacing: 10px * scale;
alignment: center;
// 出牌按钮 (放在手牌上方)
Rectangle {
height: 50px * scale;
background: transparent;
HorizontalLayout {
alignment: center;
padding-bottom: 10px * scale;
Button {
scale: root.scale;
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 {
width: 1200px * scale;
height: 230px * scale;
horizontal-stretch: 1;
background: transparent;
// 手牌滚动容器
ScrollView {
width: 100%;
height: 100%;
// 手牌容器,向下偏移以给选中卡牌留空间
hand-cards-layout := HorizontalLayout {
y: 25px * scale;
alignment: center;
spacing: -35px * scale; // 卡牌重叠效果
for card[index] in hand-cards: HandCardItem {
scale: root.scale;
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 {
scale: root.scale;
clicked => {
root.request-uno();
}
}
}
}
}
}
// 颜色选择弹窗 (覆盖在最上层)
ColorPickerDialog {
scale: root.scale;
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;
}
}
}