Commit 83c25094 authored by 董政锦's avatar 董政锦

feat: 1-6 游戏音频,游戏中跑道和马运动频率需求调整加大;

parent 2ce3787b
...@@ -7,7 +7,7 @@ import Rank2 from './views/Rank2View.vue' ...@@ -7,7 +7,7 @@ import Rank2 from './views/Rank2View.vue'
import { useAdminGameSocket } from '@/composables/useAdminGameSocket' import { useAdminGameSocket } from '@/composables/useAdminGameSocket'
import type Player from '@/commons/player.ts' import type Player from '@/commons/player.ts'
import { playApplauseMusic, playGame2Music, stopAllMusic } from '@/commons/music' import { playApplauseMusic, playGame2Music, stopAllMusic } from '@/commons/music'
import { $confirm, $remove_socket_storage, $notify, $toast } from '@/commons/utils.ts' import { $confirm, $remove_socket_storage, $notify, $toast, $is_local_mode } from '@/commons/utils.ts'
const router = useRouter() const router = useRouter()
...@@ -33,7 +33,25 @@ const emptyRankPlayer = (index: number): Player => ({ ...@@ -33,7 +33,25 @@ const emptyRankPlayer = (index: number): Player => ({
}) })
const screen = ref<GameScreen>('loading') const screen = ref<GameScreen>('loading')
const loadingPlayers = ref<any[]>(Array.from({ length: PLAYER_LIMIT }, emptyLoadingPlayer))
const mockPlayers = [
{ userid: 'local-1', nickname: '疾风骑士', avatar: 'https://api.dicebear.com/9.x/fun-emoji/svg?seed=Felix', score: 45 },
{ userid: 'local-2', nickname: '闪电侠', avatar: 'https://api.dicebear.com/9.x/fun-emoji/svg?seed=Aneka', score: 38 },
{ userid: 'local-3', nickname: '追风者', avatar: 'https://api.dicebear.com/9.x/fun-emoji/svg?seed=Salem', score: 30 },
{ userid: 'local-4', nickname: '千里马', avatar: 'https://api.dicebear.com/9.x/fun-emoji/svg?seed=Midnight', score: 22 },
{ userid: 'local-5', nickname: '飞毛腿', avatar: 'https://api.dicebear.com/9.x/fun-emoji/svg?seed=Lucky', score: 15 },
{ userid: 'local-6', nickname: '烈焰驹', avatar: 'https://api.dicebear.com/9.x/fun-emoji/svg?seed=Bandit', score: 52 },
{ userid: 'local-7', nickname: '暴风战士', avatar: 'https://api.dicebear.com/9.x/fun-emoji/svg?seed=Misty', score: 41 },
{ userid: 'local-8', nickname: '神行者', avatar: 'https://api.dicebear.com/9.x/fun-emoji/svg?seed=Pepper', score: 35 },
{ userid: 'local-9', nickname: '银鞍客', avatar: 'https://api.dicebear.com/9.x/fun-emoji/svg?seed=Oreo', score: 28 },
{ userid: 'local-10', nickname: '踏雪无痕', avatar: 'https://api.dicebear.com/9.x/fun-emoji/svg?seed=Tiger', score: 18 },
];
const loadingPlayers = ref<any[]>(
$is_local_mode()
? mockPlayers
: Array.from({ length: PLAYER_LIMIT }, emptyLoadingPlayer)
)
const playerCount = ref(0) const playerCount = ref(0)
const rankPlayers = ref<Player[]>(Array.from({ length: RANK_LIMIT }, (_, index) => emptyRankPlayer(index))) const rankPlayers = ref<Player[]>(Array.from({ length: RANK_LIMIT }, (_, index) => emptyRankPlayer(index)))
const rank1Ref = ref<InstanceType<typeof Rank1> | null>(null) const rank1Ref = ref<InstanceType<typeof Rank1> | null>(null)
...@@ -156,8 +174,11 @@ const { startGame, createRoom, backRoom, closeRoom } = useAdminGameSocket({ ...@@ -156,8 +174,11 @@ const { startGame, createRoom, backRoom, closeRoom } = useAdminGameSocket({
}) })
const startGameWithMusic = () => { const startGameWithMusic = () => {
if ($is_local_mode()) {
gotoRank1WithIntro()
return
}
if (playerCount.value == 0) { if (playerCount.value == 0) {
// alert('房间中没有玩家')
$toast('房间中没有玩家'); $toast('房间中没有玩家');
return; return;
} }
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
import { onBeforeUnmount, onMounted, ref, watch } from 'vue' import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
import DesignStage from '../../../components/DesignStage.vue' import DesignStage from '../../../components/DesignStage.vue'
import type Player from '../../../commons/player.ts' import type Player from '../../../commons/player.ts'
import { $is_run_local } from '@/commons/utils.ts'; import { $is_run_local, $is_local_mode } from '@/commons/utils.ts';
import { cssAssetUrl } from '@/commons/assets.ts'; import { cssAssetUrl } from '@/commons/assets.ts';
const props = defineProps<{ const props = defineProps<{
...@@ -17,7 +17,7 @@ const HORSE_COUNT = 6; ...@@ -17,7 +17,7 @@ const HORSE_COUNT = 6;
const HORSE_FRAME_COUNT = 91; const HORSE_FRAME_COUNT = 91;
const HORSE_FRAME_SIZE = 150; const HORSE_FRAME_SIZE = 150;
const HORSE_SPRITE_COLUMNS = 10; const HORSE_SPRITE_COLUMNS = 10;
const HORSE_FRAME_DURATION = 50; const HORSE_FRAME_DURATION = 15;
const RACE_TICK_DURATION = 50; const RACE_TICK_DURATION = 50;
const RACE_DURATION = 60_000; const RACE_DURATION = 60_000;
const RANK_RUNNING_GAP = 135; const RANK_RUNNING_GAP = 135;
...@@ -28,8 +28,7 @@ const TRACK_MAX_X = 1595; ...@@ -28,8 +28,7 @@ const TRACK_MAX_X = 1595;
const BASE_SPEED_PER_TICK = (TRACK_MAX_X - RACE_START_X) / (RACE_DURATION / RACE_TICK_DURATION); const BASE_SPEED_PER_TICK = (TRACK_MAX_X - RACE_START_X) / (RACE_DURATION / RACE_TICK_DURATION);
const MAX_SPEED_PER_TICK = BASE_SPEED_PER_TICK * 1.25; const MAX_SPEED_PER_TICK = BASE_SPEED_PER_TICK * 1.25;
const MAX_ACCELERATION_PER_TICK = 0.035; const MAX_ACCELERATION_PER_TICK = 0.035;
const BACKGROUND_WIDTH = 2358;
const STAGE_WIDTH = 1920;
const designStage = ref<InstanceType<typeof DesignStage> | null>(null); const designStage = ref<InstanceType<typeof DesignStage> | null>(null);
const horseFrameIndex = ref(0); const horseFrameIndex = ref(0);
const raceElapsed = ref(0); const raceElapsed = ref(0);
...@@ -120,15 +119,6 @@ const isRealPlayer = (player: Player) => ( ...@@ -120,15 +119,6 @@ const isRealPlayer = (player: Player) => (
&& player.nickname !== '-' && player.nickname !== '-'
); );
const renderRaceBackground = () => {
const progress = Math.min(raceElapsed.value / RACE_DURATION, 1);
const backgroundX = -(BACKGROUND_WIDTH - STAGE_WIDTH) * progress;
return {
backgroundImage: imageUrls.bg,
backgroundPosition: `${backgroundX}px center`,
};
};
const targetXForRacer = (racer: Racer, rankIndex: number) => { const targetXForRacer = (racer: Racer, rankIndex: number) => {
const safeRankIndex = Math.max(0, Math.min(rankIndex, HORSE_COUNT - 1)); const safeRankIndex = Math.max(0, Math.min(rankIndex, HORSE_COUNT - 1));
...@@ -336,6 +326,47 @@ const updateTestRank = () => { ...@@ -336,6 +326,47 @@ const updateTestRank = () => {
onMounted(() => { onMounted(() => {
preloadHorseSprites(); preloadHorseSprites();
raceAnimationFrame = window.requestAnimationFrame(animateRace); raceAnimationFrame = window.requestAnimationFrame(animateRace);
if ($is_local_mode()) {
const mockAvatars = [
'https://api.dicebear.com/9.x/fun-emoji/svg?seed=Felix',
'https://api.dicebear.com/9.x/fun-emoji/svg?seed=Aneka',
'https://api.dicebear.com/9.x/fun-emoji/svg?seed=Salem',
'https://api.dicebear.com/9.x/fun-emoji/svg?seed=Midnight',
'https://api.dicebear.com/9.x/fun-emoji/svg?seed=Lucky',
'https://api.dicebear.com/9.x/fun-emoji/svg?seed=Bandit',
];
const mockData: Player[] = [
{ userid: 'local-1', nickname: '疾风骑士', avatar: mockAvatars[0], score: 45 },
{ userid: 'local-2', nickname: '闪电侠', avatar: mockAvatars[1], score: 38 },
{ userid: 'local-3', nickname: '追风者', avatar: mockAvatars[2], score: 30 },
{ userid: 'local-4', nickname: '千里马', avatar: mockAvatars[3], score: 22 },
{ userid: 'local-5', nickname: '飞毛腿', avatar: mockAvatars[4], score: 15 },
{ userid: 'local-6', nickname: '烈焰驹', avatar: mockAvatars[5], score: 52 },
];
syncRacers(mockData);
const growthRates: Record<string, number> = {};
mockData.forEach((p, i) => {
growthRates[p.userid!] = 0.4 + (i / (mockData.length - 1)) * 1.4;
});
let simInterval: ReturnType<typeof setInterval> | undefined;
watch(raceStarted, (running) => {
if (simInterval) { clearInterval(simInterval); simInterval = undefined; }
if (running) {
simInterval = setInterval(() => {
if (!raceStarted.value) { clearInterval(simInterval); simInterval = undefined; return; }
const updatedPlayers = racers.value.map(r => ({
...r.player,
score: Number(r.player.score || 0) + Math.floor(Math.random() * 10 + 5),
}));
updatedPlayers.sort((a, b) => Number(b.score) - Number(a.score));
syncRacers(updatedPlayers);
}, 1500);
}
}, { immediate: true });
}
}); });
onBeforeUnmount(() => { onBeforeUnmount(() => {
...@@ -380,7 +411,11 @@ watch( ...@@ -380,7 +411,11 @@ watch(
<template> <template>
<DesignStage ref="designStage"> <DesignStage ref="designStage">
<div class="bg" :style="renderRaceBackground()"> <div class="screen-container" :class="{ paused: !raceStarted }">
<div class="bg-track">
<div class="bg-inner"></div>
<div class="bg-inner"></div>
</div>
<div v-if="$is_run_local()" class="test-click-layer"></div> <div v-if="$is_run_local()" class="test-click-layer"></div>
<div class="img-logo"></div> <div class="img-logo"></div>
<div class="img-title"></div> <div class="img-title"></div>
...@@ -400,15 +435,42 @@ watch( ...@@ -400,15 +435,42 @@ watch(
</template> </template>
<style scoped> <style scoped>
.bg { .screen-container {
width: 1920px; width: 1920px;
height: 1080px; height: 1080px;
background-repeat: no-repeat;
background-size: 2358px 1080px;
display: flex;
flex-direction: column;
overflow: hidden;
position: relative; position: relative;
overflow: hidden;
}
.screen-container.paused .bg-track {
animation-play-state: paused;
}
.bg-track {
width: 4716px;
height: 1080px;
position: absolute;
top: 0;
left: 0;
display: flex;
animation: bg-scroll-left 20s linear infinite;
z-index: 0;
}
.bg-inner {
width: 2358px;
height: 1080px;
background: v-bind('imageUrls.bg') center / cover no-repeat;
flex-shrink: 0;
}
@keyframes bg-scroll-left {
0% {
transform: translateX(0);
}
100% {
transform: translateX(-2358px);
}
} }
.test-click-layer { .test-click-layer {
......
...@@ -66,7 +66,7 @@ const renderAvatar = (item: any | null) => { ...@@ -66,7 +66,7 @@ const renderAvatar = (item: any | null) => {
<DesignStage> <DesignStage>
<div class="bg"> <div class="bg">
<div class="img-logo"></div> <div class="img-logo"></div>
<div class="img-title1"></div> <!-- <div class="img-title1"></div> -->
<!-- <div class="img-title2"></div> --> <!-- <div class="img-title2"></div> -->
<div class="img-qrcode-container"> <div class="img-qrcode-container">
<div class="img-qrcode" @click="isQrcodeExpanded = true"><img :src="imageUrls.qrcode" /></div> <div class="img-qrcode" @click="isQrcodeExpanded = true"><img :src="imageUrls.qrcode" /></div>
......
...@@ -199,18 +199,18 @@ watch( ...@@ -199,18 +199,18 @@ watch(
// ========== 速度基准(对齐 game4 时间驱动模型) ========== // ========== 速度基准(对齐 game4 时间驱动模型) ==========
// 60s 游戏内,即使满加速也最多跑 ~75% 路程,确保马匹始终可见 // 60s 游戏内,即使满加速也最多跑 ~75% 路程,确保马匹始终可见
const TOTAL_DISTANCE_PX = HORSE_END_X - HORSE_START_X; // 2200px const TOTAL_DISTANCE_PX = HORSE_END_X - HORSE_START_X; // 2200px
const BASE_DURATION_SEC = 200; // 基准时长 200s,即无加速时跑完全程需 200s const BASE_DURATION_SEC = 90; // 基准时长 90s(速度提升约2.2倍,确保60s内跑约67%路程)
const BASE_SPEED_PX_PER_SEC = TOTAL_DISTANCE_PX / BASE_DURATION_SEC; // 11.0px/s const BASE_SPEED_PX_PER_SEC = TOTAL_DISTANCE_PX / BASE_DURATION_SEC; // 24.4px/s
// 瞬时加速脉冲机制(对齐 game4) // 瞬时加速脉冲机制(对齐 game4)
const BOOST_PULSE = 0.15; // 每次 score 增长对应的额外速度倍率(从 0.3 下调,减缓加速累积) const BOOST_PULSE = 0.08; // 每次 score 增长对应的额外速度倍率(下调以控制加速累积)
const MAX_BOOST = 1.5; // 最大加速倍率(从 3.0 下调,确保满速时 80s 才能跑完 2200px const MAX_BOOST = 1.0; // 最大加速倍率(确保60s内最多跑约90%路程
const BOOST_DECAY = 0.990; // 每帧衰减系数(微微加快衰减,避免 boost 持续高位) const BOOST_DECAY = 0.985; // 每帧衰减系数(加快衰减,避免 boost 持续高位)
// ========== 马匹动画循环 (RAF 驱动:时间驱动位移 + 帧动画) ========== // ========== 马匹动画循环 (RAF 驱动:时间驱动位移 + 帧动画) ==========
let horseRafId: number | undefined; let horseRafId: number | undefined;
let horseLastTs = 0; let horseLastTs = 0;
const HORSE_FRAME_TICK_MS = 48; const HORSE_FRAME_TICK_MS = 24;
const startHorseLoop = () => { const startHorseLoop = () => {
stopHorseLoop(); stopHorseLoop();
...@@ -584,7 +584,7 @@ onUnmounted(() => { ...@@ -584,7 +584,7 @@ onUnmounted(() => {
top: 0; top: 0;
left: 0; left: 0;
display: flex; display: flex;
animation: bg-scroll-left 30s linear infinite; animation: bg-scroll-left 15s linear infinite;
z-index: 0; z-index: 0;
} }
...@@ -657,12 +657,12 @@ onUnmounted(() => { ...@@ -657,12 +657,12 @@ onUnmounted(() => {
.horse-shadow { .horse-shadow {
position: absolute; position: absolute;
bottom: 20px; bottom: 40px;
left: 50%; left: 50%;
transform: translateX(-50%); transform: translateX(-50%);
width: 180px; width: 180px;
height: 24px; height: 24px;
background: radial-gradient(ellipse, rgba(0, 0, 0, 0.4) 0%, transparent 70%); background: radial-gradient(ellipse, rgba(0, 0, 0, 0.2) 0%, transparent 70%);
border-radius: 50%; border-radius: 50%;
pointer-events: none; pointer-events: none;
z-index: 0; z-index: 0;
......
...@@ -52,12 +52,26 @@ const TOTAL_DISTANCE_PX = 2160; // -120px ~ 2040px ...@@ -52,12 +52,26 @@ const TOTAL_DISTANCE_PX = 2160; // -120px ~ 2040px
const BASE_DURATION_SEC = 220; const BASE_DURATION_SEC = 220;
const BASE_SPEED_PERCENT_PER_SEC = 100 / BASE_DURATION_SEC; // ≈ 0.455%/s 基准速度 const BASE_SPEED_PERCENT_PER_SEC = 100 / BASE_DURATION_SEC; // ≈ 0.455%/s 基准速度
// 马匹精灵图帧动画(horse.webp 共 12 帧,改用 JS 切帧,避免 CSS steps 在部分浏览器不生效) // 马匹帧配置(使用 game3 的马匹)
const HORSE_FRAME_COUNT = 12; const HORSE_SIZE = 280;
const HORSE_FRAME_TICK_MS = 48; // ~20fps const horseFrameConfigs = [
const HORSE_SPRITE_OFFSET_X = -17; { type: 1, dir: 'game3/ma1', prefix: '1ma_', startIndex: 24, count: 12 },
const HORSE_SPRITE_FRAME_WIDTH = 120; { type: 2, dir: 'game3/ma2', prefix: '2ma_', startIndex: 24, count: 12 },
const horseSpriteUrl = assetUrl("game4/horse.webp"); { type: 3, dir: 'game3/ma3', prefix: '3ma_', startIndex: 12, count: 14 },
];
const HORSE_TYPES = [1, 2, 3] as const;
const horseFrames: Record<number, string[]> = {};
const horseFrameCount: Record<number, number> = {};
for (const config of horseFrameConfigs) {
horseFrames[config.type] = Array.from({ length: config.count }, (_, i) =>
assetUrl(`${config.dir}/${config.prefix}${String(config.startIndex + i).padStart(5, '0')}.png`),
);
horseFrameCount[config.type] = config.count;
}
const HORSE_FRAME_TICK_MS = 24; // ~40fps
// ========== 瞬时加速脉冲机制 ========== // ========== 瞬时加速脉冲机制 ==========
// H5 每次摇一摇发送累计积分,PC 端检测 score 增长 → 触发短暂加速脉冲 → 迅速衰减 // H5 每次摇一摇发送累计积分,PC 端检测 score 增长 → 触发短暂加速脉冲 → 迅速衰减
...@@ -105,6 +119,7 @@ const detectScoreChanges = (players: Player[]) => { ...@@ -105,6 +119,7 @@ const detectScoreChanges = (players: Player[]) => {
interface Horse { interface Horse {
id: string; id: string;
horseType: number;
lane: number; lane: number;
startY: number; startY: number;
xPercent: number; xPercent: number;
...@@ -146,7 +161,8 @@ const fullRankList = computed<RankItem[]>(() => { ...@@ -146,7 +161,8 @@ const fullRankList = computed<RankItem[]>(() => {
/** 倒计时结束后才为 true,控制背景动画和马匹渲染 */ /** 倒计时结束后才为 true,控制背景动画和马匹渲染 */
const gameStarted = ref(false); const gameStarted = ref(false);
const LANE_POSITIONS = [42, 160, 270, 380, 490]; // const LANE_POSITIONS = [7, 115, 235, 345, 455];
const LANE_POSITIONS = [0, 108, 228, 338, 448];
const updateHorses = (players: Player[]) => { const updateHorses = (players: Player[]) => {
const validPlayers = players.filter( const validPlayers = players.filter(
...@@ -162,15 +178,17 @@ const updateHorses = (players: Player[]) => { ...@@ -162,15 +178,17 @@ const updateHorses = (players: Player[]) => {
// WebSocket 数据无 userid/wechat_id,用 avatar 作为稳定标识 // WebSocket 数据无 userid/wechat_id,用 avatar 作为稳定标识
const id = player.userid || player.wechat_id || player.avatar || `horse-${index}`; const id = player.userid || player.wechat_id || player.avatar || `horse-${index}`;
const existing = existingMap.get(id); const existing = existingMap.get(id);
const horseType = HORSE_TYPES[index % HORSE_TYPES.length]!;
return { return {
id, id,
horseType,
lane: index % LANE_POSITIONS.length, lane: index % LANE_POSITIONS.length,
startY: LANE_POSITIONS[index % LANE_POSITIONS.length]!, startY: LANE_POSITIONS[index % LANE_POSITIONS.length]!,
xPercent: existing?.xPercent ?? 0, xPercent: existing?.xPercent ?? 0,
boost: existing?.boost ?? 0, boost: existing?.boost ?? 0,
nickname: player.nickname, nickname: player.nickname,
avatar: player.avatar || "", avatar: player.avatar || "",
frameIndex: existing?.frameIndex ?? Math.floor(Math.random() * HORSE_FRAME_COUNT), frameIndex: existing?.frameIndex ?? Math.floor(Math.random() * (horseFrameCount[horseType] ?? 12)),
frameTimer: existing?.frameTimer ?? 0, frameTimer: existing?.frameTimer ?? 0,
}; };
}); });
...@@ -279,9 +297,10 @@ const startGameLoop = () => { ...@@ -279,9 +297,10 @@ const startGameLoop = () => {
); );
horse.frameTimer += dt; horse.frameTimer += dt;
const fc = horseFrameCount[horse.horseType] ?? 12;
while (horse.frameTimer >= HORSE_FRAME_TICK_MS) { while (horse.frameTimer >= HORSE_FRAME_TICK_MS) {
horse.frameTimer -= HORSE_FRAME_TICK_MS; horse.frameTimer -= HORSE_FRAME_TICK_MS;
horse.frameIndex = (horse.frameIndex + 1) % HORSE_FRAME_COUNT; horse.frameIndex = (horse.frameIndex + 1) % fc;
} }
const decayed = boost * Math.pow(BOOST_DECAY, dt / (1000 / 60)); const decayed = boost * Math.pow(BOOST_DECAY, dt / (1000 / 60));
...@@ -433,15 +452,13 @@ const horseAvatarStyle = (horse: Horse) => { ...@@ -433,15 +452,13 @@ const horseAvatarStyle = (horse: Horse) => {
return { backgroundImage: `url(${horse.avatar})`, backgroundSize: "cover" }; return { backgroundImage: `url(${horse.avatar})`, backgroundSize: "cover" };
}; };
const horseSpriteStyle = (horse: Horse) => ({ const preloadHorseFrames = () => {
backgroundImage: `url(${horseSpriteUrl})`, for (const frames of Object.values(horseFrames)) {
backgroundPosition: `${HORSE_SPRITE_OFFSET_X - horse.frameIndex * HORSE_SPRITE_FRAME_WIDTH}px 0`, for (const url of frames) {
backgroundSize: "1472px 109px", const img = new Image();
}); img.src = url;
}
const preloadHorseSprite = () => { }
const img = new Image();
img.src = horseSpriteUrl;
}; };
// 结果弹窗头像渲染(参考 game1 Rank2View) // 结果弹窗头像渲染(参考 game1 Rank2View)
...@@ -461,7 +478,7 @@ const podiumPlayers = (): RankItem[] => { ...@@ -461,7 +478,7 @@ const podiumPlayers = (): RankItem[] => {
}; };
onMounted(() => { onMounted(() => {
preloadHorseSprite(); preloadHorseFrames();
// 本地模式:Game4.vue onMounted 会调用 startIntro() 来控制完整流程 // 本地模式:Game4.vue onMounted 会调用 startIntro() 来控制完整流程
// 此处不自动开始,避免与 startIntro 冲突 // 此处不自动开始,避免与 startIntro 冲突
}); });
...@@ -505,9 +522,17 @@ defineExpose({ ...@@ -505,9 +522,17 @@ defineExpose({
transform: `translateX(${horseTranslateX(horse.xPercent)}px)`, transform: `translateX(${horseTranslateX(horse.xPercent)}px)`,
}" }"
> >
<div class="horse" :style="horseSpriteStyle(horse)"> <div class="horse-sprite">
<div class="horse-shadow"></div> <img
v-for="(url, frameIdx) in horseFrames[horse.horseType]"
:key="frameIdx"
:src="url"
class="horse-frame"
:class="{ active: frameIdx === horse.frameIndex }"
alt=""
/>
</div> </div>
<div class="horse-shadow"></div>
<div class="horse-username"> <div class="horse-username">
<span class="username-text">{{ horse.nickname }}</span> <span class="username-text">{{ horse.nickname }}</span>
</div> </div>
...@@ -610,7 +635,7 @@ defineExpose({ ...@@ -610,7 +635,7 @@ defineExpose({
top: 0; top: 0;
left: 0; left: 0;
display: flex; display: flex;
animation: bg-scroll-left 40s linear infinite; animation: bg-scroll-left 20s linear infinite;
z-index: 1; z-index: 1;
} }
...@@ -629,7 +654,7 @@ defineExpose({ ...@@ -629,7 +654,7 @@ defineExpose({
bottom: 0; bottom: 0;
left: 0; left: 0;
display: flex; display: flex;
animation: bg-scroll-left 40s linear infinite; animation: bg-scroll-left 20s linear infinite;
z-index: 2; z-index: 2;
} }
...@@ -644,25 +669,41 @@ defineExpose({ ...@@ -644,25 +669,41 @@ defineExpose({
.horse-wrapper { .horse-wrapper {
position: absolute; position: absolute;
left: 0; left: 0;
width: 280px;
height: 280px;
transition: transform 0.2s linear; transition: transform 0.2s linear;
will-change: transform; will-change: transform;
z-index: 10; z-index: 10;
} }
.horse { .horse-sprite {
width: 120px;
height: 109px;
background-repeat: no-repeat;
position: relative; position: relative;
width: 280px;
height: 280px;
}
.horse-frame {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: contain;
opacity: 0;
pointer-events: none;
z-index: 1;
}
.horse-frame.active {
opacity: 1;
} }
.horse-shadow { .horse-shadow {
position: absolute; position: absolute;
bottom: -2px; bottom: 40px;
left: 50%; left: 50%;
transform: translateX(-50%); transform: translateX(-50%);
width: 120px; width: 180px;
height: 18px; height: 24px;
background: radial-gradient(ellipse, rgba(0, 0, 0, 0.45) 0%, transparent 70%); background: radial-gradient(ellipse, rgba(0, 0, 0, 0.45) 0%, transparent 70%);
border-radius: 50%; border-radius: 50%;
pointer-events: none; pointer-events: none;
...@@ -673,39 +714,42 @@ defineExpose({ ...@@ -673,39 +714,42 @@ defineExpose({
background: v-bind("imageUrls.username"); background: v-bind("imageUrls.username");
background-size: contain; background-size: contain;
background-repeat: no-repeat; background-repeat: no-repeat;
width: 100px; width: 120px;
height: 40px; height: 44px;
position: absolute; position: absolute;
top: 10px; top: 80px;
left: -22px; left: 60px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
z-index: 2;
} }
.username-text { .username-text {
position: absolute; position: absolute;
top: 1px; top: 2px;
color: #000; color: #000;
font-size: 14px; font-size: 16px;
font-weight: bold; font-weight: bold;
max-width: 80px; max-width: 100px;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
padding-left: 4px; padding-left: 6px;
box-sizing: border-box; box-sizing: border-box;
} }
.horse-avatar { .horse-avatar {
background: v-bind("imageUrls.avatar"); background: v-bind("imageUrls.avatar");
background-size: cover; background-size: cover;
width: 30px; width: 50px;
height: 30px; height: 50px;
position: absolute; position: absolute;
top: -4px; top: 80px;
left: -22px; left: 60px;
border-radius: 50%; border-radius: 50%;
border: 2px solid #FFCF44;
z-index: 3;
} }
@keyframes bg-scroll-left { @keyframes bg-scroll-left {
......
...@@ -55,7 +55,7 @@ const renderAvatar = (item: any | null) => { ...@@ -55,7 +55,7 @@ const renderAvatar = (item: any | null) => {
<DesignStage> <DesignStage>
<div class="bg"> <div class="bg">
<div class="img-logo"></div> <div class="img-logo"></div>
<div class="img-title1"></div> <!-- <div class="img-title1"></div> -->
<!-- <div class="img-title2"></div> --> <!-- <div class="img-title2"></div> -->
<div class="img-qrcode-container"> <div class="img-qrcode-container">
<div class="img-qrcode" @click="isQrcodeExpanded = true"><img :src="imageUrls.qrcode" /></div> <div class="img-qrcode" @click="isQrcodeExpanded = true"><img :src="imageUrls.qrcode" /></div>
...@@ -64,8 +64,8 @@ const renderAvatar = (item: any | null) => { ...@@ -64,8 +64,8 @@ const renderAvatar = (item: any | null) => {
<div v-if="isQrcodeExpanded" class="qrcode-preview-mask" @click="isQrcodeExpanded = false"> <div v-if="isQrcodeExpanded" class="qrcode-preview-mask" @click="isQrcodeExpanded = false">
<img class="qrcode-preview" :src="imageUrls.qrcode" @click.stop="isQrcodeExpanded = false" /> <img class="qrcode-preview" :src="imageUrls.qrcode" @click.stop="isQrcodeExpanded = false" />
</div> </div>
<div class="img-left"></div> <!-- <div class="img-left"></div>
<div class="img-right"></div> <div class="img-right"></div> -->
<div class="list-container"> <div class="list-container">
<div class="txt-count">当前在线人数 <span>{{ playerCount }}</span></div> <div class="txt-count">当前在线人数 <span>{{ playerCount }}</span></div>
<div class="list-container-left"> <div class="list-container-left">
......
...@@ -22,7 +22,7 @@ const showResult = ref(false); ...@@ -22,7 +22,7 @@ const showResult = ref(false);
const imageUrls = { const imageUrls = {
runway: cssAssetUrl("game5/paodao.webp"), runway: cssAssetUrl("game5/paodao.webp"),
bg: cssAssetUrl("game5/bg.webp"), bg: cssAssetUrl("game5/bg-backup.webp"),
logo: cssAssetUrl("game1/logo.png"), logo: cssAssetUrl("game1/logo.png"),
title: cssAssetUrl("game5/title3.webp"), title: cssAssetUrl("game5/title3.webp"),
rankNo4: cssAssetUrl("game6/no4.png"), rankNo4: cssAssetUrl("game6/no4.png"),
...@@ -156,7 +156,7 @@ const stopGame = () => { ...@@ -156,7 +156,7 @@ const stopGame = () => {
// ========== 马匹动画循环 (RAF 仅驱动脚步切帧,位移由 CSS animation 负责) ========== // ========== 马匹动画循环 (RAF 仅驱动脚步切帧,位移由 CSS animation 负责) ==========
let horseRafId: number | undefined; let horseRafId: number | undefined;
let horseLastTs = 0; let horseLastTs = 0;
const HORSE_FRAME_TICK_MS = 48; // 每帧间隔 ~20fps const HORSE_FRAME_TICK_MS = 14; // 每帧间隔 ~40fps(频率提升一倍)
const startHorseLoop = () => { const startHorseLoop = () => {
......
...@@ -15,7 +15,7 @@ const emit = defineEmits<{ ...@@ -15,7 +15,7 @@ const emit = defineEmits<{
const FINAL_RANK_LIMIT = 10; const FINAL_RANK_LIMIT = 10;
const imageUrls = { const imageUrls = {
bg: cssAssetUrl('game5/bg.webp'), bg: cssAssetUrl('game5/bg-backup.webp'),
logo: cssAssetUrl('game1/logo.png'), logo: cssAssetUrl('game1/logo.png'),
title: cssAssetUrl('game5/title.webp'), title: cssAssetUrl('game5/title.webp'),
title2: cssAssetUrl('game5/title2.webp'), title2: cssAssetUrl('game5/title2.webp'),
......
...@@ -15,14 +15,14 @@ const emit = defineEmits<{ ...@@ -15,14 +15,14 @@ const emit = defineEmits<{
}>(); }>();
const imageUrls = { const imageUrls = {
bg: cssAssetUrl('game6/bg.png'), bg: cssAssetUrl('game6/bg.webp'),
logo: cssAssetUrl('game1/logo.png'), logo: cssAssetUrl('game1/logo.png'),
title1: cssAssetUrl('game6/title.png'), title1: cssAssetUrl('game6/title.png'),
title2: cssAssetUrl('game1/title2.png'), title2: cssAssetUrl('game1/title2.png'),
qrcode: assetUrl('game6/qrcode.png'), qrcode: assetUrl('game6/qrcode.png'),
// gu: cssAssetUrl('game1/gu.png'), // gu: cssAssetUrl('game1/gu.png'),
// bg: cssAssetUrl('game1/bg.png'), // bg: cssAssetUrl('game1/bg.webp'),
// logo: cssAssetUrl('game1/logo.png'), // logo: cssAssetUrl('game1/logo.png'),
// title1: cssAssetUrl('game1/title1.png'), // title1: cssAssetUrl('game1/title1.png'),
// title2: cssAssetUrl('game1/title2.png'), // title2: cssAssetUrl('game1/title2.png'),
...@@ -66,7 +66,7 @@ const renderAvatar = (item: any | null) => { ...@@ -66,7 +66,7 @@ const renderAvatar = (item: any | null) => {
<DesignStage> <DesignStage>
<div class="bg"> <div class="bg">
<div class="img-logo"></div> <div class="img-logo"></div>
<div class="img-title1"></div> <!-- <div class="img-title1"></div> -->
<!-- <div class="img-title2"></div> --> <!-- <div class="img-title2"></div> -->
<div class="img-qrcode-container"> <div class="img-qrcode-container">
<div class="img-qrcode" @click="isQrcodeExpanded = true"><img :src="imageUrls.qrcode" /></div> <div class="img-qrcode" @click="isQrcodeExpanded = true"><img :src="imageUrls.qrcode" /></div>
...@@ -75,8 +75,8 @@ const renderAvatar = (item: any | null) => { ...@@ -75,8 +75,8 @@ const renderAvatar = (item: any | null) => {
<div v-if="isQrcodeExpanded" class="qrcode-preview-mask" @click="isQrcodeExpanded = false"> <div v-if="isQrcodeExpanded" class="qrcode-preview-mask" @click="isQrcodeExpanded = false">
<img class="qrcode-preview" :src="imageUrls.qrcode" @click.stop="isQrcodeExpanded = false" /> <img class="qrcode-preview" :src="imageUrls.qrcode" @click.stop="isQrcodeExpanded = false" />
</div> </div>
<div class="img-mole"></div> <!-- <div class="img-mole"></div>
<div class="img-rabbit"></div> <div class="img-rabbit"></div> -->
<div class="list-container"> <div class="list-container">
<div class="txt-count">当前在线人数 <span>{{ playerCount }}</span></div> <div class="txt-count">当前在线人数 <span>{{ playerCount }}</span></div>
<div class="list-container-left"> <div class="list-container-left">
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment