feat: 添加游戏内动画特效,优化玩家卡片UI并调整布局。

This commit is contained in:
邹方成 2026-01-03 02:26:24 +08:00
parent 191895567c
commit 58d9edc766
3 changed files with 755 additions and 308 deletions

View File

@ -447,10 +447,172 @@
color: $accent-cyan;
}
.log-effect .log-content {
color: $brand-primary;
// =====================================
// 动画特效库
// =====================================
// 飘字上浮动画
@keyframes floatUp {
0% {
transform: translate(-50%, 0);
opacity: 0;
}
20% {
opacity: 1;
}
80% {
opacity: 1;
}
100% {
transform: translate(-50%, -60rpx);
opacity: 0;
}
}
.float-label {
position: absolute;
pointer-events: none;
font-weight: 900;
font-size: 36rpx;
z-index: 100;
animation: floatUp 1s ease-out forwards;
text-shadow: 0 0 10rpx rgba(0, 0, 0, 0.8), 2rpx 2rpx 0 #000;
white-space: nowrap;
&.text-heal {
color: $color-success;
}
&.text-damage {
color: $color-error;
}
&.text-item {
color: $color-warning;
}
&.global {
position: fixed;
font-size: 48rpx;
z-index: 9999;
}
}
// 屏幕震动
@keyframes screenShake {
0%,
100% {
transform: translate(0);
}
10% {
transform: translate(-8rpx, 6rpx);
}
20% {
transform: translate(8rpx, -6rpx);
}
30% {
transform: translate(-6rpx, -6rpx);
}
40% {
transform: translate(6rpx, 6rpx);
}
50% {
transform: translate(-6rpx, 4rpx);
}
60% {
transform: translate(6rpx, -4rpx);
}
70% {
transform: translate(-4rpx, 6rpx);
}
80% {
transform: translate(4rpx, -6rpx);
}
90% {
transform: translate(-2rpx, 2rpx);
}
}
.screen-shake {
animation: screenShake 0.4s ease-in-out;
}
// 玩家受伤红闪
@keyframes damageFlash {
0%,
100% {
box-shadow: none;
background: $bg-dark-card;
}
25%,
75% {
background: rgba($color-error, 0.2);
box-shadow: 0 0 20rpx rgba($color-error, 0.4), inset 0 0 15rpx rgba($color-error, 0.2);
}
50% {
background: rgba($color-error, 0.4);
box-shadow: 0 0 30rpx rgba($color-error, 0.6), inset 0 0 20rpx rgba($color-error, 0.3);
}
}
.player-damaged {
animation: damageFlash 0.6s ease-in-out, shake 0.4s ease-in-out;
}
// 玩家治疗绿闪
@keyframes healGlow {
0%,
100% {
box-shadow: none;
}
50% {
box-shadow: 0 0 20rpx rgba($color-success, 0.6), inset 0 0 10rpx rgba($color-success, 0.3);
}
}
.player-healed {
animation: healGlow 0.6s ease-in-out;
}
// 倒计时紧急脉冲
@keyframes urgentPulse {
0%,
100% {
opacity: 1;
transform: scale(1);
}
50% {
opacity: 0.5;
transform: scale(1.1);
}
}
.urgent {
animation: urgentPulse 0.5s ease-in-out infinite;
color: $color-error !important;
}
.log-empty {
height: 100%;
display: flex;
@ -526,7 +688,7 @@
// 对手栏
// =====================================
.opponents-bar {
height: 180rpx;
height: 150rpx;
background: rgba(0, 0, 0, 0.2);
border-bottom: 1px solid $border-dark;
}
@ -538,20 +700,79 @@
}
.player-card {
min-width: 260rpx;
height: 140rpx;
// 默认样式 (通用)
background: $bg-dark-card;
border: 1px solid $border-dark;
border-radius: $radius-lg;
padding: $spacing-md;
padding: $spacing-sm;
display: flex;
align-items: center;
position: relative;
transition: all $transition-normal;
// 对手样式 (紧凑纵向)
&.opponent {
min-width: 140rpx;
height: 120rpx;
flex-direction: column;
justify-content: center;
gap: 6rpx;
.avatar {
margin-right: 0;
width: 60rpx;
height: 60rpx;
font-size: 36rpx;
margin-bottom: 4rpx;
}
.player-info {
align-items: center;
width: 100%;
}
.username {
font-size: 20rpx; // 更小的字体
max-width: 120rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
text-align: center;
}
.status-icons {
position: absolute;
top: 6rpx;
right: 6rpx;
flex-direction: column; // 图标纵向排列以节省横向空间
gap: 2rpx;
.icon {
font-size: 18rpx;
background: rgba(0, 0, 0, 0.5);
border-radius: 50%;
padding: 2rpx;
}
}
}
// ""的样式 (保持横向但更精致)
&.me {
background: rgba($brand-primary, 0.1);
border-color: rgba($brand-primary, 0.3);
min-width: 280rpx;
height: 140rpx;
padding: $spacing-md;
.avatar {
width: 80rpx;
height: 80rpx;
font-size: 44rpx;
}
.username {
font-size: $font-sm;
}
}
&.active-turn {
@ -560,11 +781,14 @@
}
&.damaged {
animation: shake 0.5s ease-in-out;
animation: damageFlash 0.6s ease-in-out, shake 0.5s ease-in-out;
}
}
@keyframes shake {
&.healed {
animation: healGlow 0.6s ease-in-out;
}
@keyframes shake {
0%,
100% {
@ -578,9 +802,9 @@
75% {
transform: translateX(5rpx);
}
}
}
.avatar {
.avatar {
font-size: 44rpx;
margin-right: $spacing-md;
display: flex;
@ -596,36 +820,37 @@
height: 100rpx;
font-size: 64rpx;
}
}
}
.player-info {
.player-info {
display: flex;
flex-direction: column;
gap: 4rpx;
}
}
.username {
.username {
font-size: $font-sm;
color: $text-dark-main;
font-weight: 600;
}
}
.hp-bar {
.hp-bar {
display: flex;
gap: 2rpx;
}
}
.heart {
.heart {
font-size: 18rpx;
}
}
.status-icons {
.status-icons {
display: flex;
gap: 4rpx;
.icon {
font-size: 16rpx;
}
}
}
// =====================================
@ -637,7 +862,7 @@
flex-direction: column;
align-items: center;
justify-content: center;
padding: $spacing-lg;
padding: $spacing-md;
}
.turn-indicator {
@ -744,18 +969,192 @@
&.has-magnifier {
border: 2rpx dashed rgba($accent-cyan, 0.5);
}
}
.cell-icon {
// --- 新增特效样式 ---
&.explosion {
animation: explode 0.5s ease-out;
}
// 道具特效 - 移植自 Explosion.css
&.item-medkit {
animation: itemReveal 0.5s ease-out, pulseGlowGreen 2s infinite;
background-color: rgba(16, 185, 129, 0.2) !important;
}
&.item-revive {
animation: itemReveal 0.5s ease-out, pulseGlowGreen 1.5s infinite;
background-color: rgba(16, 185, 129, 0.3) !important;
border: 1px solid #10b981;
}
&.item-knife {
animation: itemReveal 0.5s ease-out, glint 1s infinite;
background-color: rgba(239, 68, 68, 0.1) !important;
}
&.item-lightning {
animation: itemReveal 0.4s ease-out, glint 0.5s infinite;
background-color: rgba(239, 68, 68, 0.2) !important;
}
&.item-poison {
animation: itemReveal 0.6s ease-out, pulseGlowPurple 3s infinite;
background-color: rgba(139, 92, 246, 0.2) !important;
}
&.item-shield {
animation: itemReveal 0.5s ease-out, pulseGlowBlue 2s infinite;
background-color: rgba(59, 130, 246, 0.2) !important;
border-radius: 50% !important;
}
&.item-skip {
animation: itemReveal 0.5s ease-out, pulseGlowGold 2s infinite;
background-color: rgba(245, 158, 11, 0.2) !important;
opacity: 0.8;
}
&.item-magnifier {
animation: itemReveal 0.5s ease-out, float 2s ease-in-out infinite;
background-color: rgba(59, 130, 246, 0.1) !important;
}
&.item-bomb_timer {
animation: itemReveal 0.5s ease-out, pulseGlowRed 0.5s infinite, shake 0.2s infinite;
background-color: rgba(239, 68, 68, 0.3) !important;
}
&.item-curse {
animation: itemReveal 0.7s ease-out, pulseGlowPurple 1s infinite;
background-color: rgba(139, 92, 246, 0.3) !important;
filter: grayscale(0.5) contrast(1.5);
}
&.item-chest {
animation: itemReveal 0.5s ease-out, pulseGlowGold 3s infinite;
background-color: rgba(245, 158, 11, 0.3) !important;
}
// 补充缺失的动画 Keyframes
@keyframes explode {
0% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(1.5);
opacity: 0.8;
box-shadow: 0 0 20rpx #ef4444, 0 0 40rpx #fb923c;
}
100% {
transform: scale(1);
opacity: 1;
box-shadow: 0 0 10rpx rgba(239, 68, 68, 0.4);
}
}
@keyframes itemReveal {
0% {
transform: scale(0.5) rotate(-20deg);
opacity: 0;
}
60% {
transform: scale(1.2) rotate(10deg);
}
100% {
transform: scale(1) rotate(0);
opacity: 1;
}
}
@keyframes pulseGlowGreen {
0%,
100% {
box-shadow: 0 0 5rpx #10b981;
}
50% {
box-shadow: 0 0 20rpx #10b981, 0 0 30rpx #34d399;
}
}
@keyframes pulseGlowBlue {
0%,
100% {
box-shadow: 0 0 5rpx #3b82f6;
}
50% {
box-shadow: 0 0 20rpx #3b82f6, 0 0 30rpx #60a5fa;
}
}
@keyframes pulseGlowPurple {
0%,
100% {
box-shadow: 0 0 5rpx #8b5cf6;
}
50% {
box-shadow: 0 0 20rpx #8b5cf6, 0 0 30rpx #a78bfa;
}
}
@keyframes pulseGlowRed {
0%,
100% {
box-shadow: 0 0 5rpx #ef4444;
}
50% {
box-shadow: 0 0 20rpx #ef4444, 0 0 30rpx #f87171;
}
}
@keyframes pulseGlowGold {
0%,
100% {
box-shadow: 0 0 5rpx #f59e0b;
}
50% {
box-shadow: 0 0 20rpx #f59e0b, 0 0 30rpx #fbbf24;
}
}
@keyframes glint {
0% {
filter: brightness(1);
}
50% {
filter: brightness(1.8) contrast(1.2);
}
100% {
filter: brightness(1);
}
}
.cell-icon {
font-size: 32rpx;
}
}
.cell-num {
.cell-num {
font-size: $font-lg;
font-weight: 900;
}
}
.magnifier-mark {
.magnifier-mark {
display: flex;
justify-content: center;
align-items: center;
@ -772,30 +1171,30 @@
right: 2rpx;
font-size: 16rpx;
}
}
}
// =====================================
// 底部面板
// =====================================
.bottom-panel {
padding: $spacing-md;
padding-bottom: calc($spacing-md + env(safe-area-inset-bottom));
// =====================================
// 底部面板
// =====================================
.bottom-panel {
padding: $spacing-sm;
padding-bottom: calc($spacing-sm + env(safe-area-inset-bottom));
background: rgba(0, 0, 0, 0.4);
display: flex;
gap: $spacing-md;
border-top: 1px solid $border-dark;
height: 240rpx; // 增加底部面板高度以容纳更大的卡片
}
height: 180rpx; // 减小高度
}
.game-logs {
.game-logs {
flex: 1;
height: 100%;
padding: $spacing-sm;
background: rgba(0, 0, 0, 0.3);
border-radius: $radius-md;
}
}
.log-line {
.log-line {
font-size: $font-xxs;
line-height: 1.6;
color: $text-dark-sub;
@ -810,12 +1209,12 @@
&.log-effect {
color: $brand-primary;
}
}
}
// =====================================
// 弹窗
// =====================================
.modal-overlay {
// =====================================
// 弹窗
// =====================================
.modal-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.8);
@ -824,53 +1223,53 @@
justify-content: center;
align-items: center;
z-index: 1000;
}
}
.modal-content {
.modal-content {
width: 520rpx;
padding: $spacing-xxl $spacing-xl;
display: flex;
flex-direction: column;
align-items: center;
gap: $spacing-lg;
}
}
.modal-emoji {
.modal-emoji {
font-size: 100rpx;
}
}
.modal-title {
.modal-title {
font-size: 40rpx;
font-weight: 900;
color: $text-dark-main;
}
}
// =====================================
// 玩法说明弹窗
// =====================================
.guide-modal {
// =====================================
// 玩法说明弹窗
// =====================================
.guide-modal {
width: 90%;
max-width: 640rpx;
max-height: 80vh;
display: flex;
flex-direction: column;
}
}
.guide-header {
.guide-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: $spacing-lg;
border-bottom: 1px solid $border-dark;
}
}
.guide-title {
.guide-title {
font-size: $font-lg;
font-weight: 800;
color: $text-dark-main;
}
}
.close-btn {
.close-btn {
width: 56rpx;
height: 56rpx;
display: flex;
@ -880,28 +1279,28 @@
color: $text-dark-sub;
background: rgba(255, 255, 255, 0.05);
border-radius: 50%;
}
}
.guide-body {
.guide-body {
padding: $spacing-lg;
max-height: 60vh;
}
}
.section-title {
.section-title {
display: block;
font-size: $font-md;
font-weight: 700;
color: $brand-primary;
margin-bottom: $spacing-md;
}
}
.guide-grid {
.guide-grid {
display: flex;
flex-direction: column;
gap: $spacing-md;
}
}
.guide-item {
.guide-item {
display: flex;
align-items: flex-start;
gap: $spacing-md;
@ -931,12 +1330,12 @@
color: $text-dark-sub;
line-height: 1.5;
}
}
}
// =====================================
// 飘字动画
// =====================================
.float-label {
// =====================================
// 飘字动画
// =====================================
.float-label {
position: absolute;
font-size: $font-md;
font-weight: 900;
@ -960,9 +1359,9 @@
&.text-effect {
color: $accent-cyan;
}
}
}
@keyframes floatUp {
@keyframes floatUp {
0% {
opacity: 1;
transform: translateY(0) scale(1);
@ -972,16 +1371,16 @@
opacity: 0;
transform: translateY(-60rpx) scale(0.8);
}
}
}
// =====================================
// 屏幕震动 & 通用动画
// =====================================
.screen-shake {
// =====================================
// 屏幕震动 & 通用动画
// =====================================
.screen-shake {
animation: screenShake 0.4s ease-in-out;
}
}
@keyframes screenShake {
@keyframes screenShake {
0%,
100% {
@ -1003,13 +1402,13 @@
80% {
transform: translateX(4rpx);
}
}
}
.fadeInUp {
.fadeInUp {
animation: fadeInUp 0.5s ease-out both;
}
}
@keyframes fadeInUp {
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(40rpx);
@ -1019,13 +1418,13 @@
opacity: 1;
transform: translateY(0);
}
}
}
.pulse {
.pulse {
animation: pulse 2s ease-in-out infinite;
}
}
@keyframes pulse {
@keyframes pulse {
0%,
100% {
@ -1037,4 +1436,5 @@
transform: scale(1.03);
opacity: 1;
}
}
}

View File

@ -156,12 +156,13 @@
v-for="(cell, i) in gameState.grid"
:key="i"
class="grid-cell"
:class="{
:class="[
cell.revealed ? (cell.type === 'bomb' ? 'type-bomb explosion' : (cell.type === 'item' ? 'type-item item-' + (cell.itemId || 'generic') : 'type-empty')) : 'bg-slate-800',
{
'revealed': cell.revealed,
'type-bomb': cell.revealed && cell.type === 'bomb',
'bg-slate-800': !cell.revealed,
'has-magnifier': myPlayer && myPlayer.revealedCells && myPlayer.revealedCells[i]
}"
}
]"
@tap="handleCellClick(i)"
>
<view v-if="cell.revealed">
@ -555,6 +556,11 @@ export default {
console.log('[我的位置]', data.turnOrder?.indexOf(this.myUserId));
console.log('[网格大小]', data.gridSize);
console.log('[游戏状态]', data);
console.log('[DEBUG] myUserId:', this.myUserId);
console.log('[DEBUG] turnOrder:', data.turnOrder);
console.log('[DEBUG] currentTurnIndex:', data.currentTurnIndex);
console.log('[DEBUG] 当前回合玩家:', data.turnOrder?.[data.currentTurnIndex]);
console.log('[DEBUG] isMyTurn?', data.turnOrder?.[data.currentTurnIndex] === this.myUserId);
console.log('================================');
} else if (opCode === 6) {
// ========== - ==========
@ -565,13 +571,42 @@ export default {
console.log('================================');
}
if (opCode === 1 || opCode === 2) {
if (opCode === 1) {
this.gameState = data;
this.addLog('system', '战局开始,准备翻格!');
} else if (opCode === 2) {
// - HP
const prevState = this.gameState;
if (prevState && prevState.players) {
Object.keys(data.players || {}).forEach(uid => {
const prevP = prevState.players[uid];
const newP = data.players[uid];
if (!prevP || !newP) return;
const hpDiff = newP.hp - prevP.hp;
if (hpDiff < 0) {
//
const dmg = Math.abs(hpDiff);
this.spawnLabel(0, 0, `-${dmg}`, 'damage', undefined, uid);
this.triggerDamageEffect(uid, dmg);
} else if (hpDiff > 0) {
//
const heal = Math.abs(hpDiff);
this.spawnLabel(0, 0, `+${heal}`, 'heal', undefined, uid);
this.healedPlayers.push(uid);
setTimeout(() => this.healedPlayers = this.healedPlayers.filter(id => id !== uid), 600);
}
});
}
this.gameState = data;
if (opCode === 1) this.addLog('system', '战局开始,准备翻格!');
} else if (opCode === 5) {
this.handleEvent(data);
} else if (opCode === 6) {
if (data.gameState) {
this.gameState = data.gameState;
} else {
this.gameState = data;
}
this.addLog('system', `战局结束:${data.winnerId === this.myUserId ? '您获得了胜利!' : '很遗憾失败了'}`);
}
},
@ -593,6 +628,7 @@ export default {
if (event.type === 'damage' || event.type === 'item') {
if (iAmTarget && event.value) this.spawnLabel(0, 0, `-${event.value}`, 'damage', undefined, this.myUserId);
}
// opCode 5 damage opCode 2
if (event.type === 'damage' && iAmTarget) this.triggerDamageEffect(this.myUserId, event.value);
}
},
@ -661,22 +697,32 @@ export default {
return this.getItemIcon(content);
},
getPlayerAvatar(player) {
// avatar使
if (player.avatar) {
return player.avatar;
// character emoji
const charAvatars = {
dog: '🐶',
elephant: '🐘',
tiger: '🐯',
monkey: '🐵',
sloth: '🦥',
hippo: '🦛',
cat: '🐱',
chicken: '🐔'
};
if (player.character && charAvatars[player.character]) {
return charAvatars[player.character];
}
// userIdusername
const avatars = ['🐶', '🐘', '🐯', '🐵', '🦥', '🦛'];
// avatar
if (player.avatar) return player.avatar;
// 使userIdusername
// userId
const avatars = ['🐶', '🐘', '🐯', '🐵', '🦥', '🦛'];
const seed = player.userId || player.username || player.id || '';
let hash = 0;
for (let i = 0; i < seed.length; i++) {
hash = ((hash << 5) - hash) + seed.charCodeAt(i);
hash = hash & hash; // 32
}
const index = Math.abs(hash) % avatars.length;
return avatars[index];
},

View File

@ -340,7 +340,8 @@ class NakamaManager {
}
this.isConnected = false;
this.session = null;
this.gameToken = null;
// 注意:不要清空 gameToken以便重连时仍然可以使用
// this.gameToken 只在 logout 或新 authenticate 时才会被更新
console.log('[Nakama] Disconnected');
}