Переглянути джерело

blockly编程课
1、处理任务完成动画视频效果

liyanbo 4 місяців тому
батько
коміт
be7c2c3ed7
2 змінених файлів з 168 додано та 75 видалено
  1. 4 0
      src/api/blockly/blockly.js
  2. 164 75
      src/components/blockly/MapGame.vue

+ 4 - 0
src/api/blockly/blockly.js

@@ -202,6 +202,8 @@ const availableGenerators = {
  * @param {Array<string>} blocklySpecialBlocks - 特殊积木方法标识集合
  */
 export function registerCustomBlocks(blocklySpecialBlocks = []) {
+  // 确保blocklySpecialBlocks始终是数组
+  blocklySpecialBlocks = Array.isArray(blocklySpecialBlocks) ? blocklySpecialBlocks : [];
   // 遍历所有可用积木
   for (const [blockName, blockConfig] of Object.entries(availableBlocks)) {
     // 如果是通用积木,或者是允许的特殊积木,则注册
@@ -290,6 +292,8 @@ export function setupBlocklyChineseLocale() {
  * @param {Array<string>} blocklySpecialBlocks - 特殊积木方法标识集合
  */
 export function registerJavaScriptGenerators(blocklySpecialBlocks = []) {
+  // 确保blocklySpecialBlocks始终是数组
+  blocklySpecialBlocks = Array.isArray(blocklySpecialBlocks) ? blocklySpecialBlocks : [];
   // 遍历所有可用生成器
   for (const [blockName, generatorFunction] of Object.entries(availableGenerators)) {
     // 检查对应的积木是否为通用积木或允许的特殊积木

+ 164 - 75
src/components/blockly/MapGame.vue

@@ -195,6 +195,11 @@ const CONFIG = {
     ITEMS_APPEAR: 300,      // 物品出现延迟时间
     ITEMS_FLIGHT_ANIMATION_DELAY: 800, // 飞行动画延迟时间
     PALY_MP3_TIMES: 1000,     // 播放MP3文件的时间间隔
+    VIDEO_TIMEOUT: 5000, // 视频播放超时处理时间
+    VIDEO_FADE_DURATION: 500, // 视频淡入淡出持续时间
+    PLAYER_FADE_DURATION: 500, // 玩家淡入淡出持续时间
+    TEMP_ITEM_FADE_DURATION: 500, // 临时物品淡入淡出持续时间
+    COMPLETION_DISPLAY: 1000, // 任务完成后显示延迟时间
   },
   // 游戏配置
   GAME: {
@@ -394,6 +399,7 @@ onMounted(async () => {
   await fetchGameData();
   // 初始化可行走点集合
   initWalkablePointsSet();
+
   // 注册自定义积木
   registerCustomBlocks(props.blocklySpecialBlocks);
   // 注册JavaScript生成器
@@ -429,11 +435,13 @@ function generateToolboxXml() {
       <block type="use_item"></block>
   `;
 
+  // 确保blocklySpecialBlocks是数组
+  const specialBlocks = Array.isArray(props.blocklySpecialBlocks) ? props.blocklySpecialBlocks : [];
   // 根据允许的特殊积木动态添加
-  if (props.blocklySpecialBlocks.includes(BLOCKLY_CUSTOMIZE_DICT.PAUSE)) {
+  if (specialBlocks.includes(BLOCKLY_CUSTOMIZE_DICT.PAUSE)) {
     toolboxXml += '<block type="pause"></block>';
   }
-  if (props.blocklySpecialBlocks.includes(BLOCKLY_CUSTOMIZE_DICT.PLAY_SOUND)) {
+  if (specialBlocks.includes(BLOCKLY_CUSTOMIZE_DICT.PLAY_SOUND)) {
     toolboxXml += '<block type="play_sound"></block>';
   }
 
@@ -697,6 +705,24 @@ const resetPlayer = async () => {
     currentExecutionPromise = null;
   }
 
+  // 清除所有视频组件
+  const mapBackground = document.querySelector('.map-background');
+  if (mapBackground) {
+    const videoElements = mapBackground.querySelectorAll('video');
+    videoElements.forEach(video => {
+      if (mapBackground.contains(video)) {
+        mapBackground.removeChild(video);
+      }
+    });
+  }
+
+  // 确保小智显形(不隐形)
+  const playerElement = document.querySelector('.player');
+  if (playerElement) {
+    playerElement.style.opacity = '1';
+    playerElement.style.transition = 'none'; // 取消过渡效果,立即显示
+  }
+
   // 重置携带的物品回地图
   if (gameState.mapData.originalWalkablePoints.length > 0) {
     gameState.mapData.walkablePoints = JSON.parse(JSON.stringify(gameState.mapData.originalWalkablePoints));
@@ -1020,26 +1046,10 @@ window.useItem = async function() {
 
       // 获取要使用的物品
       const itemToUse = gameState.player.carriedItems[itemIndex];
-      // 执行物品使用动画
-      await animateItemUse(itemToUse, itemIndex);
       // 从携带物品中移除已使用的物品
       gameState.player.carriedItems.splice(itemIndex, 1);
-
-      // 从地图上移除图标
-      const pointIndex = gameState.mapData.walkablePoints.findIndex(
-          p => p.x === x && p.y === y
-      );
-      if (pointIndex !== -1) {
-
-        // 保留点但移除img属性并设置完成状态图标
-        const updatedPoint = { ...gameState.mapData.walkablePoints[pointIndex] };
-        updatedPoint.img = updatedPoint.endImg; // 设置完成状态图标
-        updatedPoint.status = true;
-        gameState.mapData.walkablePoints.splice(pointIndex, 1, updatedPoint);
-
-        // 更新映射
-        walkablePointsMap.set(`${x},${y}`, updatedPoint);
-      }
+      // 执行物品使用动画,传递finishAnimation
+      await animateItemUse(itemToUse, itemIndex, tileMap.finishAnimation);
 
       // 使用物品成功
       showGameMessage(tileMap.finishedTip || CONFIG.TIPS.USE_ITEM_SUCCESS, 'success');
@@ -1101,21 +1111,21 @@ window.pause = async function(seconds) {
   await new Promise(resolve => setTimeout(resolve, CONFIG.DELAY.ACTION_DELAY));
 }
 
-// 函数
+// 声音函数
 window.playSound = async function() {
   if (shouldStopExecution || isColliding.value || isSliding.value) {
     return;
   }
 
   if(processingSpecialTasksDisappearing()){
-    //增加延迟,确保声音播放完成
+    //延迟,确保声音播放完成
     await playMp3(passMp3);
     await new Promise(resolve => setTimeout(resolve, CONFIG.DELAY.PALY_MP3_TIMES));
     return
   }
 
   showGameMessage(CONFIG.TIPS.ERROR_ERROR, 'info');
-  //增加延迟,确保声音播放完成
+  //延迟,确保声音播放完成
   await playMp3(failureMp3);
   await new Promise(resolve => setTimeout(resolve, CONFIG.DELAY.PALY_MP3_TIMES));
 };
@@ -1406,7 +1416,7 @@ async function animateItemPickup(item, itemX, itemY) {
 }
 
 // 物品使用动画函数
-async function animateItemUse(item, itemIndex) {
+async function animateItemUse(item, itemIndex, finishAnimation) {
   // 创建临时动画元素
   const tempItem = document.createElement('div');
   const finalSize = tileSize.value * CONFIG.STYLES.PLAYER_SIZE_RATIO * 0.5;
@@ -1430,28 +1440,13 @@ async function animateItemUse(item, itemIndex) {
   // 获取玩家元素
   const playerElement = document.querySelector('.player');
   const originalPlayerZIndex = playerElement ? playerElement.style.zIndex : '';
+  const originalPlayerOpacity = playerElement ? playerElement.style.opacity : '1';
 
-  // 获取当前位置的任务点元素
-  let taskPointElement = null;
-  const walkablePointsElements = document.querySelectorAll('.walkable-point');
-  walkablePointsElements.forEach(el => {
-    const rect = el.getBoundingClientRect();
-    const targetRect = {
-      left: playerLeft,
-      top: playerTop,
-      width: iconSize,
-      height: iconSize
-    };
-    // 简单判断元素是否在玩家位置附近
-    if (Math.abs(rect.left - targetRect.left) < iconSize &&
-        Math.abs(rect.top - targetRect.top) < iconSize) {
-      taskPointElement = el;
-    }
-  });
-
-  // 先将任务图标置顶盖住人物
-  if (taskPointElement) {
-    taskPointElement.style.zIndex = '150';
+  // 将临时元素添加到地图容器
+  const mapBackground = document.querySelector('.map-background');
+  if (!mapBackground) {
+    console.error('地图容器未找到');
+    return;
   }
 
   // 设置临时元素初始样式
@@ -1470,19 +1465,52 @@ async function animateItemUse(item, itemIndex) {
     transform: 'scale(1)',
   });
 
-  // 将临时元素添加到地图容器
-  const mapBackground = document.querySelector('.map-background');
-  if (!mapBackground) {
-    console.error('地图容器未找到');
-    return;
-  }
   mapBackground.appendChild(tempItem);
 
-  // 准备漂移动画
+  // 1. 首先让小智逐渐消失
+  if (playerElement) {
+    playerElement.style.transition = 'opacity ' + CONFIG.DELAY.PLAYER_FADE_DURATION + 'ms ease-out';
+    playerElement.style.opacity = '0';
+  }
+
+  // 2. 准备漂移动画
   await new Promise(resolve => setTimeout(resolve, CONFIG.DELAY.ITEMS_APPEAR));
 
-  // 设置漂移动画样式
-  tempItem.style.transition = 'all 1s ease-out';
+  // 3. 如果有视频,先创建视频元素但不自动播放
+  let videoElement = null;
+  if (finishAnimation) {
+    // 创建视频元素
+    videoElement = document.createElement('video');
+    videoElement.src = finishAnimation;
+    videoElement.autoplay = false; // 不自动播放
+    // videoElement.muted = true; // 视频默认静音
+    videoElement.playsinline = true;
+    videoElement.style.position = 'absolute';
+    videoElement.style.left = playerLeft + 'px';
+    videoElement.style.top = playerTop + 'px';
+    videoElement.style.width = iconSize + 'px';
+    videoElement.style.height = iconSize + 'px';
+    videoElement.style.zIndex = '2000'; // 提高z-index确保视频可见
+    videoElement.style.objectFit = 'contain';
+
+    // 添加渐变显示效果
+    videoElement.style.opacity = '0';
+    videoElement.style.transition = 'opacity ' + CONFIG.DELAY.VIDEO_FADE_DURATION + 'ms ease-in-out';
+
+    // 添加视频到地图容器
+    mapBackground.appendChild(videoElement);
+
+    // 触发重绘后设置透明度为1,实现渐变效果
+    setTimeout(() => {
+      videoElement.style.opacity = '1';
+      console.log('Video opacity set to 1');
+    }, CONFIG.DELAY.VIDEO_FADE_DURATION);
+  } else {
+    console.log('无视频');
+  }
+
+  // 4. 设置漂移动画样式
+  tempItem.style.transition = 'all ' + CONFIG.DELAY.TEMP_ITEM_FADE_DURATION + 'ms ease-out';
   Object.assign(tempItem.style, {
     left: playerLeft + 'px',
     top: playerTop + 'px',
@@ -1490,30 +1518,91 @@ async function animateItemUse(item, itemIndex) {
     height: iconSize + 'px',
   });
 
-  // 等待漂移到玩家位置
+  // 5. 等待漂移到玩家位置
   await new Promise(resolve => setTimeout(resolve, CONFIG.DELAY.ITEMS_FLIGHT_ANIMATION_DELAY));
 
-  // 物品使用效果动画
-  tempItem.style.transition = 'transform 0.3s ease-in-out';
-  tempItem.style.transform = 'scale(1.2)';
-
-  await new Promise(resolve => setTimeout(resolve, CONFIG.DELAY.ITEMS_APPEAR));
-
-  // 移除临时元素
+  // 6. 移除临时元素
   if (mapBackground.contains(tempItem)) {
     mapBackground.removeChild(tempItem);
   }
 
-  // 延迟一段时间后将人物图标置顶,露出完整的人物图标
-  await new Promise(resolve => setTimeout(resolve, CONFIG.DELAY.ITEMS_APPEAR));
+  let x = playerPosition.value.x;
+  let y = playerPosition.value.y;
+
+  // 8. 视频播放完成后替换任务图片
+  const pointIndex = gameState.mapData.walkablePoints.findIndex(
+      p => p.x === x && p.y === y
+  );
+  if (pointIndex !== -1) {
+    // 保留点但移除img属性并设置完成状态图标
+    const updatedPoint = { ...gameState.mapData.walkablePoints[pointIndex] };
+    updatedPoint.img = updatedPoint.endImg; // 设置完成状态图标
+    updatedPoint.status = true;
+    gameState.mapData.walkablePoints.splice(pointIndex, 1, updatedPoint);
+
+    // 更新映射
+    walkablePointsMap.set(`${x},${y}`, updatedPoint);
+  }
 
-  // 恢复层级关系
-  if (taskPointElement) {
-    taskPointElement.style.zIndex = '';
+  // 9. 处理视频播放或直接替换任务图片
+  if (videoElement) {
+    // 开始播放视频
+    try {
+      const playPromise = videoElement.play();
+      if (playPromise !== undefined) {
+        playPromise.then(_ => {
+          console.log('Video started playing successfully');
+        }).catch(error => {
+          console.error('Error playing video:', error);
+        });
+      }
+    } catch (error) {
+      console.error('Exception when playing video:', error);
+    }
+
+    // 等待视频播放完成,添加超时处理
+    await new Promise(resolve => {
+      videoElement.onended = () => {
+        console.log('Video ended');
+        resolve();
+      };
+
+      // 添加超时处理,防止视频卡住
+      setTimeout(() => {
+        resolve();
+      }, CONFIG.DELAY.VIDEO_TIMEOUT);
+    });
+
+
+    // 10. 移除视频元素 - 添加淡出效果
+    if (mapBackground.contains(videoElement)) {
+      // 设置淡出效果
+      videoElement.style.transition = 'opacity ' + CONFIG.DELAY.VIDEO_FADE_DURATION + 'ms ease-in-out';
+      videoElement.style.opacity = '0';
+
+      // 等待淡出动画完成后再移除元素
+    setTimeout(() => {
+      if (mapBackground.contains(videoElement)) {
+        mapBackground.removeChild(videoElement);
+      }
+    }, CONFIG.DELAY.VIDEO_FADE_DURATION); // 与动画时长一致
+    }
+  } else {
+    // 10.无视频
   }
 
+  // 11. 视频消失后稍作停留,让用户看到完成后的图片
+  await new Promise(resolve => setTimeout(resolve, CONFIG.DELAY.COMPLETION_DISPLAY));
+
+  // 逐步显示玩家并将其置顶
   if (playerElement) {
-    playerElement.style.zIndex = originalPlayerZIndex || '';
+    playerElement.style.transition = 'opacity ' + CONFIG.DELAY.PLAYER_FADE_DURATION + 'ms ease-in';
+    playerElement.style.opacity = originalPlayerOpacity;
+    playerElement.style.zIndex = originalPlayerZIndex || '100';
+    // 额外设置一个较高的z-index确保盖住其他人物图片
+    setTimeout(() => {
+      playerElement.style.zIndex = '150';
+    }, CONFIG.DELAY.PLAYER_FADE_DURATION);
   }
 }
 
@@ -2029,27 +2118,27 @@ onUnmounted(() => {
 }
 
 /* 优化Blockly积木样式 */
-/* 增加积木高度 */
+/* 积木高度 */
 .blocklyBlockCanvas .blocklyBlock {
-  height: 45px; /* 增加默认高度 */
+  height: 45px; /* 默认高度 */
   min-height: 45px;
 }
 
-/* 增加积木内部元素的行高和间距 */
+/* 积木内部元素的行高和间距 */
 .blocklyText {
   font-size: 16px;
   line-height: 20px;
   font-weight: 500;
 }
 
-/* 增加输入字段的高度 */
+/* 输入字段的高度 */
 .blocklyHtmlInput {
   height: 30px;
   font-size: 14px;
   padding: 5px;
 }
 
-/* 增加下拉菜单的高度 */
+/* 下拉菜单的高度 */
 .blocklyDropdownMenu {
   line-height: 28px;
   font-size: 14px;
@@ -2061,13 +2150,13 @@ onUnmounted(() => {
   filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.1));
 }
 
-/* 增加积木之间的连接点间距 */
+/* 积木之间的连接点间距 */
 .blocklyConnection {
   height: 20px;
   width: 20px;
 }
 
-/* 增加工具箱中积木的高度 */
+/* 工具箱中积木的高度 */
 .blocklyTreeRow {
   height: 40px;
   line-height: 40px;