diff --git a/server.ts b/server.ts index 4a552ed..ecbc3fb 100644 --- a/server.ts +++ b/server.ts @@ -106,15 +106,15 @@ app.post('/api/score', async (req, res) => { } }); -// GET /api/score - 获取排行榜(前6名) +// GET /api/score - 获取排行榜(前8名) app.get('/api/score', async (req, res) => { try { const scores = await readScores(); - // 按时间排序(时间短的在前)并取前6名 + // 按时间排序(时间短的在前)并取前8名 const leaderBoard = scores .sort((a, b) => a.time - b.time) - .slice(0, 6) + .slice(0, 8) .map(score => ({ name: score.name, time: score.time, diff --git a/src/App.vue b/src/App.vue index d96267a..4446bdd 100644 --- a/src/App.vue +++ b/src/App.vue @@ -5,7 +5,7 @@ import Page1 from './pages/Page1.vue'; import assets from './assets'; import Loader from './pages/Loader.vue'; -const stage = ref(1); +const stage = ref(-1); const userData = ref({ region: '奥莱', diff --git a/src/assets/ani/crop_script.py b/src/assets/ani/crop_script.py deleted file mode 100644 index 40c0d67..0000000 --- a/src/assets/ani/crop_script.py +++ /dev/null @@ -1,78 +0,0 @@ -import os -from PIL import Image -import re - -# --- 配置 --- -# 目标尺寸 -TARGET_WIDTH = 670 -TARGET_HEIGHT = 939 - -# 裁剪区域 (left, upper, right, lower) -# 我们从左上角 (0, 0) 开始,裁剪出一个 670x939 的区域 -CROP_BOX = (0, 0, TARGET_WIDTH, TARGET_HEIGHT) - -# 输出文件夹名称 -OUTPUT_DIR = "cropped" -# --- 结束配置 --- - -def batch_crop_images(): - """ - 批量裁剪当前目录下的 WebP 图片 - """ - # 获取当前脚本所在的目录 - current_dir = os.getcwd() - output_path = os.path.join(current_dir, OUTPUT_DIR) - - # 如果输出目录不存在,则创建它 - if not os.path.exists(output_path): - os.makedirs(output_path) - print(f"已创建输出目录: {output_path}") - - # 获取目录下所有文件 - files = os.listdir(current_dir) - - # 定义一个正则表达式来匹配 '数字.webp' 格式的文件名 - file_pattern = re.compile(r"^\d+\.webp$") - - image_files_to_process = [f for f in files if file_pattern.match(f)] - - if not image_files_to_process: - print("未在当前目录找到符合 '数字.webp' 格式的图片文件。") - return - - print(f"找到了 {len(image_files_to_process)} 个待处理的图片文件。") - - # 遍历所有符合条件的图片文件 - for filename in image_files_to_process: - try: - # 构建完整的文件路径 - file_path = os.path.join(current_dir, filename) - - # 打开图片 - with Image.open(file_path) as img: - print(f"正在处理: {filename} (原始尺寸: {img.size[0]}x{img.size[1]})") - - # 检查原始尺寸是否符合预期 - if img.size != (671, 940): - print(f" -> 警告: {filename} 的尺寸 ({img.size[0]}x{img.size[1]}) 与预期的 671x940 不符,已跳过。") - continue - - # 进行裁剪 - cropped_img = img.crop(CROP_BOX) - - # 构建输出文件路径 - save_path = os.path.join(output_path, filename) - - # 保存裁剪后的图片 - # quality=100 可以尽量保持高质量,你可以根据需要调整 - cropped_img.save(save_path, "WEBP", quality=100) - - print(f" -> 已裁剪并保存至: {save_path} (新尺寸: {cropped_img.size[0]}x{cropped_img.size[1]})") - - except Exception as e: - print(f"处理 {filename} 时发生错误: {e}") - - print("\n所有图片处理完成!") - -if __name__ == "__main__": - batch_crop_images() \ No newline at end of file diff --git a/src/assets/ani/后段效果.zip b/src/assets/ani/后段效果.zip index 0d1fb20..b43718d 100644 Binary files a/src/assets/ani/后段效果.zip and b/src/assets/ani/后段效果.zip differ diff --git a/src/assets/game/share_mask.png b/src/assets/game/share_mask.png deleted file mode 100644 index 8e52884..0000000 Binary files a/src/assets/game/share_mask.png and /dev/null differ diff --git a/src/assets/game/share_mask.webp b/src/assets/game/share_mask.webp new file mode 100644 index 0000000..6254a4c Binary files /dev/null and b/src/assets/game/share_mask.webp differ diff --git a/src/assets/game/对.png b/src/assets/game/对.png new file mode 100644 index 0000000..c2dd3ce Binary files /dev/null and b/src/assets/game/对.png differ diff --git a/src/assets/game/对.webp b/src/assets/game/对.webp new file mode 100644 index 0000000..072af92 Binary files /dev/null and b/src/assets/game/对.webp differ diff --git a/src/assets/game/确认按钮.webp b/src/assets/game/确认按钮.webp new file mode 100644 index 0000000..acf381a Binary files /dev/null and b/src/assets/game/确认按钮.webp differ diff --git a/src/assets/game/答题卡片背景.webp b/src/assets/game/答题卡片背景.webp new file mode 100644 index 0000000..a6ab444 Binary files /dev/null and b/src/assets/game/答题卡片背景.webp differ diff --git a/src/assets/game/罚时.png b/src/assets/game/罚时.png new file mode 100644 index 0000000..1b5ff74 Binary files /dev/null and b/src/assets/game/罚时.png differ diff --git a/src/assets/game/罚时.webp b/src/assets/game/罚时.webp new file mode 100644 index 0000000..82f7b57 Binary files /dev/null and b/src/assets/game/罚时.webp differ diff --git a/src/assets/game/请选择你的答案.webp b/src/assets/game/请选择你的答案.webp new file mode 100644 index 0000000..a2cd9d2 Binary files /dev/null and b/src/assets/game/请选择你的答案.webp differ diff --git a/src/assets/game/错.png b/src/assets/game/错.png new file mode 100644 index 0000000..c3bb90f Binary files /dev/null and b/src/assets/game/错.png differ diff --git a/src/assets/game/错.webp b/src/assets/game/错.webp new file mode 100644 index 0000000..8635aae Binary files /dev/null and b/src/assets/game/错.webp differ diff --git a/src/assets/game/闹钟.webp b/src/assets/game/闹钟.webp new file mode 100644 index 0000000..ad1adf2 Binary files /dev/null and b/src/assets/game/闹钟.webp differ diff --git a/src/assets/index.ts b/src/assets/index.ts index e544f64..63ba015 100644 --- a/src/assets/index.ts +++ b/src/assets/index.ts @@ -20,7 +20,7 @@ export default { ani:{ 下拉蓄力提示: new URL('./ani/下拉蓄力提示.zip', import.meta.url).href, 小太阳总: new URL('./ani/小太阳总.zip', import.meta.url).href, - 后端效果: new URL('./ani/后段效果.zip', import.meta.url).href, + 后段效果: new URL('./ani/后段效果.zip', import.meta.url).href, 主标出现: new URL('./ani/主标出现.zip', import.meta.url).href, 线: new URL('./ani/线.zip', import.meta.url).href, P1太阳总: new URL('./ani/P1太阳总.zip', import.meta.url).href, diff --git a/src/components/AniEle.vue b/src/components/AniEle.vue index d9c0451..0db6805 100644 --- a/src/components/AniEle.vue +++ b/src/components/AniEle.vue @@ -53,7 +53,6 @@ const currentLoopCount = ref(0) const animationId = ref() const lastFrameTime = ref(0) const pendingJumpTo = ref() -// 【新增】: 用于存储 soft jump 的 Promise resolve 函数 const pendingJumpResolver = ref<((success: boolean) => void) | null>(null) @@ -118,30 +117,45 @@ const loadImageSequence = async (zipUrl: string) => { } } -// loadAllImages 函数 (不变) +// loadAllImages 函数 (已修改) const loadAllImages = async () => { const imageElements: HTMLImageElement[] = [] + let dimensionsSet = false; // 用于确保仅从第一个成功加载的图像设置尺寸 + for (let i = 0; i < images.value.length; i++) { const img = new Image() img.src = images.value[i] - await new Promise((resolve, reject) => { + await new Promise((resolve) => { img.onload = () => { - if (i === 0) { + // 如果尺寸尚未设置,则使用第一个成功加载的图像的尺寸 + if (!dimensionsSet) { canvasWidth.value = props.width || img.width canvasHeight.value = props.height || img.height + dimensionsSet = true } resolve() } - img.onerror = reject + img.onerror = () => { + // 当图像加载失败时,发出警告而不是中断整个过程 + console.warn(`[Animation Component] Image at index ${i} failed to load. It will be rendered as a transparent frame.`); + // 即使出错也 resolve,以允许加载序列继续 + resolve(); + } }) imageElements.push(img) progress.value = 80 + (i + 1) / images.value.length * 20 } + + if (!dimensionsSet) { + console.warn("[Animation Component] All images failed to load. Using specified or default canvas dimensions."); + } + loadedImages.value = imageElements } + // drawFrame 函数 (不变) const drawFrame = (frameIndex: number) => { if (!canvasRef.value || !loadedImages.value.length) return @@ -153,21 +167,19 @@ const drawFrame = (frameIndex: number) => { ctx.clearRect(0, 0, canvas.width, canvas.height) const img = loadedImages.value[safeIndex] + // 如果img加载失败,它仍然是一个Image对象,但drawImage不会绘制任何内容, + // 从而使该帧保持透明,这正是我们想要的效果。 if (img) { ctx.drawImage(img, 0, 0, canvas.width, canvas.height) } } -// ================================================================= -// 核心修改区域 1: jumpToRule 函数 -// ================================================================= const jumpToRule = (ruleName: string): boolean => { const ruleIndex = findRuleIndex(ruleName) if (ruleIndex === -1) { console.warn(`未找到名为 "${ruleName}" 的规则`) - // 如果有待处理的Promise,则拒绝它 if (pendingJumpResolver.value) { - pendingJumpResolver.value(false); // resolve(false) 表示失败 + pendingJumpResolver.value(false); pendingJumpResolver.value = null; } return false @@ -204,7 +216,6 @@ const jumpToRule = (ruleName: string): boolean => { lastFrameTime.value = performance.now() animate() - // 【修改】: 跳转成功,兑现Promise if (pendingJumpResolver.value) { pendingJumpResolver.value(true); pendingJumpResolver.value = null; @@ -319,7 +330,6 @@ const animate = () => { } }; -// stopAnimation 函数 (增加清理逻辑) const stopAnimation = () => { isPlaying.value = false isPaused.value = false @@ -327,21 +337,18 @@ const stopAnimation = () => { cancelAnimationFrame(animationId.value) animationId.value = undefined } - // 【修改】: 清理待处理的跳转 if (pendingJumpResolver.value) { - pendingJumpResolver.value(false); // 动画停止,视为跳转失败 + pendingJumpResolver.value(false); pendingJumpResolver.value = null; } pendingJumpTo.value = undefined; } -// resetAnimation 函数 (增加清理逻辑) const resetAnimation = () => { stopAnimation() currentRuleIndex.value = 0 currentFrame.value = 0 currentLoopCount.value = 0 - // pendingJumpTo 和 pendingJumpResolver 已在 stopAnimation 中清理 if (loadedImages.value.length > 0) { drawFrame(0) } @@ -365,9 +372,6 @@ const resumeFromPause = () => { } } -// ================================================================= -// 核心修改区域 2: 跳转方法 -// ================================================================= /** * 立即跳转到指定规则,会打断当前动画。 * @param {string} ruleName 要跳转到的规则名。 @@ -391,16 +395,13 @@ const jumpToSoftly = (ruleName: string): Promise => { return; } - // 如果之前有待处理的跳转,先将其标记为失败 if (pendingJumpResolver.value) { pendingJumpResolver.value(false); } - // 设置新的 Promise resolver pendingJumpResolver.value = resolve; if (isPaused.value || !isPlaying.value) { if(props.log) console.log(`动画已暂停或停止。立即执行跳转到 "${ruleName}"。`); - // jumpToRule 将会调用并清理 pendingJumpResolver jumpToRule(ruleName); } else if (isPlaying.value) { if(props.log) console.log(`已计划软跳转到 "${ruleName}"。将在当前循环结束后触发。`); @@ -413,7 +414,7 @@ const jumpToSoftly = (ruleName: string): Promise => { // watch 和生命周期钩子 (不变) watch(() => props.url, (newUrl) => { if (newUrl) { - resetAnimation() // 使用 reset 来确保清理 + resetAnimation() images.value.forEach(url => URL.revokeObjectURL(url)); images.value = []; loadedImages.value = []; @@ -431,7 +432,7 @@ watch(() => props.rules, () => { }, { deep: true }) onBeforeUnmount(() => { - resetAnimation(); // 使用 reset 来确保清理 + resetAnimation(); images.value.forEach(url => URL.revokeObjectURL(url)); }); @@ -440,7 +441,7 @@ const findRuleIndex = (ruleName: string): number => { return props.rules.findIndex(rule => rule.name === ruleName) } -// defineExpose (修改) +// defineExpose (不变) defineExpose({ images, loadedImages, diff --git a/src/components/Button.vue b/src/components/Button.vue new file mode 100644 index 0000000..03d7da3 --- /dev/null +++ b/src/components/Button.vue @@ -0,0 +1,28 @@ + + + + + \ No newline at end of file diff --git a/src/data/index.ts b/src/data/index.ts index 8480a70..2799502 100644 --- a/src/data/index.ts +++ b/src/data/index.ts @@ -629,4 +629,9 @@ export const arcRetailQuiz: QuizQuestion[] = [ '网站(https://global.helpwhereyouare.com)', ], }, -]; \ No newline at end of file +]; + +export function getRandomQuizQuestions(count: number): QuizQuestion[] { + const shuffled = [...arcRetailQuiz].sort(() => 0.5 - Math.random()); + return shuffled.slice(0, count); +} \ No newline at end of file diff --git a/src/pages/Game.vue b/src/pages/Game.vue index 172aabd..6258d82 100644 --- a/src/pages/Game.vue +++ b/src/pages/Game.vue @@ -1,5 +1,5 @@ \ No newline at end of file diff --git a/src/pages/Game/ScoreBoard.vue b/src/pages/Game/ScoreBoard.vue index 8d3cfb4..06a4e50 100644 --- a/src/pages/Game/ScoreBoard.vue +++ b/src/pages/Game/ScoreBoard.vue @@ -25,7 +25,7 @@ defineProps<{
-
{{ index + 4 }}
{{ item.name }}
@@ -40,7 +40,7 @@ defineProps<{ .scoreboard { position: absolute; - top: 44%; + top: 16.4%; width: 100%; .bar-container { @@ -63,8 +63,8 @@ defineProps<{ align-items: center; justify-content: center; gap: 1.5vw; - animation: bar-in 0.7s ease-out forwards; - transform: translateY(150%); + animation: bar-in 0.5s ease-out forwards; + opacity: 0; .name { font-size: 4.5vw; @@ -90,7 +90,7 @@ defineProps<{ } &.first { - top: 3vw; + top: -5vw; background-image: url('../../assets/game/first.webp'); animation-delay: 0ms; } @@ -104,17 +104,17 @@ defineProps<{ &.third { background-image: url('../../assets/game/third.webp'); animation-delay: 500ms; - top: 9vw; + top: 23vw; } } .lines { display: flex; position: relative; - top: -5vw; + top: 16vw; flex-direction: column; align-items: center; - gap: 1vw; + gap: 2vw; .line { display: flex; @@ -127,7 +127,8 @@ defineProps<{ height: 8vw; border-radius: 8vw; animation: line-in 0.5s ease-out forwards; - transform: translateX(-150%); + box-shadow: 0 0 2vw rgba(0, 0, 0, 0.1); + opacity: 0; .rank { // italic