MapGame.vue 31 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177
  1. <template>
  2. <div class="map-game-container">
  3. <!-- 标题栏 -->
  4. <div class="title-box">
  5. <div class="box-icon" @click="navigateBack">
  6. <el-icon class="left-icon"><ArrowLeftBold /></el-icon>
  7. {{ gameTitle }}
  8. </div>
  9. </div>
  10. <div class="content">
  11. <!-- 地图显示区域 -->
  12. <div class="map-section">
  13. <h2>游戏地图</h2>
  14. <div class="map-container">
  15. <!-- 地图背景 -->
  16. <div class="map-background">
  17. <img :src="mapBackground" alt="地图背景" class="map-image" />
  18. <!-- 可行走区域标记 -->
  19. <div
  20. v-for="(point, index) in walkablePoints"
  21. :key="index"
  22. class="walkable-point"
  23. :style="getPointStyle(point)"
  24. ></div>
  25. <!-- 玩家角色 -->
  26. <div
  27. class="player"
  28. :style="playerStyle"
  29. :class="{ 'collision': isColliding, 'success': hasReachedEnd }"
  30. ></div>
  31. </div>
  32. <!-- 游戏状态提示 -->
  33. <div v-if="gameMessage" :class="['game-message', messageType]">
  34. {{ gameMessage }}
  35. </div>
  36. </div>
  37. </div>
  38. <!-- Blockly工作区 -->
  39. <div class="blockly-section">
  40. <div class="toolbox-section" style="display: none;">
  41. <h2>工具箱</h2>
  42. <div id="toolbox">
  43. <!-- 移动控制积木 -->
  44. <category name="移动控制" colour="%{BKY_MOTION_HUE}">
  45. <block type="move_forward"></block>
  46. <block type="move_backward"></block>
  47. <block type="turn_left"></block>
  48. <block type="turn_right"></block>
  49. <block type="turn_around"></block>
  50. </category>
  51. <!-- 逻辑控制积木 -->
  52. <category name="逻辑" colour="%{BKY_LOGIC_HUE}">
  53. <block type="controls_if"></block>
  54. <block type="logic_compare"></block>
  55. <block type="logic_operation"></block>
  56. <block type="logic_negate"></block>
  57. <block type="logic_boolean"></block>
  58. </category>
  59. <!-- 循环控制积木 -->
  60. <category name="循环" colour="%{BKY_LOOPS_HUE}">
  61. <block type="controls_repeat_ext">
  62. <value name="TIMES">
  63. <shadow type="math_number">
  64. <field name="NUM"></field>
  65. </shadow>
  66. </value>
  67. </block>
  68. <block type="controls_whileUntil"></block>
  69. </category>
  70. <!-- 数学运算积木 -->
  71. <category name="数学" colour="%{BKY_MATH_HUE}">
  72. <block type="math_number"></block>
  73. <block type="math_arithmetic"></block>
  74. </category>
  75. </div>
  76. </div>
  77. <div class="workspace-section">
  78. <h2>工作区</h2>
  79. <div class="controls">
  80. <button id="runCode" @click="runCode">运行代码</button>
  81. <button @click="clearWorkspace">清空工作区</button>
  82. <button @click="resetPlayer">重置玩家</button>
  83. </div>
  84. <div id="blocklyDiv"></div>
  85. </div>
  86. </div>
  87. </div>
  88. </div>
  89. </template>
  90. <script setup>
  91. import { ref, onMounted, onUnmounted, reactive, computed } from 'vue';
  92. import { useRouter, useRoute } from 'vue-router';
  93. import { ArrowLeftBold } from '@element-plus/icons-vue';
  94. import * as Blockly from "blockly";
  95. import 'blockly/msg/zh-hans';
  96. import { javascriptGenerator } from "blockly/javascript";
  97. import mapBackgroundImage from '@/assets/images/blockly/mapGame2.png';
  98. import playerImage from '@/assets/images/blockly/user.png';
  99. // 游戏接口数据
  100. import { getMapGameById } from '@/api/blockly/game.js';
  101. const router = useRouter();
  102. const route = useRoute();
  103. const gameTitle = ref('地图游戏编程'); // 默认标题
  104. const currentGameData = ref(null);
  105. //人物初始朝向
  106. const playerInitialDirection = ref(0);
  107. onMounted(async () => {
  108. //数据
  109. await fetchGameData();
  110. // 初始化可行走点集合
  111. initWalkablePointsSet();
  112. // 注册自定义积木
  113. registerCustomBlocks();
  114. // 注册JavaScript生成器
  115. registerJavaScriptGenerators();
  116. // 初始化Blockly工作区
  117. initBlockly();
  118. // 重置玩家位置
  119. resetPlayer();
  120. // 获取路由参数中的gameName
  121. const nameFromRoute = route.query.gameName;
  122. if (nameFromRoute) {
  123. gameTitle.value = nameFromRoute;
  124. }
  125. })
  126. // 获取游戏数据
  127. const fetchGameData = async () => {
  128. try {
  129. const gameId = route.query.gameId;
  130. const nameFromRoute = route.query.gameName;
  131. if (nameFromRoute) {
  132. gameTitle.value = nameFromRoute;
  133. }
  134. let mapGameData = await getMapGameById(gameId);
  135. if (mapGameData.code === 0) {
  136. currentGameData.value = mapGameData?.data;
  137. await updateGameStateFromData(currentGameData.value);
  138. }
  139. } catch (error) {
  140. console.error('获取游戏数据失败:', error);
  141. }
  142. };
  143. // 根据获取到的数据更新游戏状态
  144. const updateGameStateFromData = (gameData) => {
  145. try {
  146. // 更新地图配置
  147. gameState.mapConfig.background = gameData.mapBackground ? gameData.mapBackground.trim() : mapBackgroundImage;
  148. gameState.mapConfig.tileSize = gameData.mapTileSize;
  149. // 更新玩家位置和方向
  150. if (gameData.userDirection) {
  151. playerInitialDirection.value = gameData.userDirection;
  152. }
  153. // 更新地图数据
  154. // 地图起点
  155. if (gameData.mapStartPoint) {
  156. const startPoint = JSON.parse(gameData.mapStartPoint);
  157. gameState.mapData.startPoint = { x: startPoint.x, y: startPoint.y };
  158. gameState.player.position = { x: startPoint.x, y: startPoint.y };
  159. }
  160. // 地图终点
  161. if (gameData.mapEndPoint) {
  162. const endPoint = JSON.parse(gameData.mapEndPoint);
  163. gameState.mapData.endPoint = { x: endPoint.x, y: endPoint.y };
  164. }
  165. if (gameData.mapWalkablePoints) {
  166. gameState.mapData.walkablePoints = JSON.parse(gameData.mapWalkablePoints);
  167. }
  168. // 重新初始化可行走点集合
  169. initWalkablePointsSet();
  170. } catch (error) {
  171. console.error('更新游戏状态失败:', error);
  172. }
  173. };
  174. // 创建游戏状态的响应式对象
  175. const gameState = reactive({
  176. // 地图配置信息
  177. mapConfig: {
  178. // 地图背景图片路径
  179. background: mapBackgroundImage,
  180. // 每个瓦片的尺寸(像素)
  181. tileSize: 110,
  182. },
  183. // 玩家相关状态
  184. player: {
  185. // 玩家当前位置坐标
  186. position: { x: 1, y: 1 },
  187. // 玩家当前朝向:0=上, 1=右, 2=下, 3=左
  188. direction: 1,
  189. // 是否正在发生碰撞
  190. isColliding: false,
  191. // 是否已到达终点
  192. hasReachedEnd: false,
  193. // 是否正在冰块上滑行
  194. isSliding: false,
  195. },
  196. // 游戏状态信息
  197. status: {
  198. // 当前显示的游戏消息
  199. message: '',
  200. // 消息类型(如success、error、info等)
  201. messageType: ''
  202. },
  203. // 地图数据信息
  204. mapData: {
  205. // 游戏起点位置
  206. startPoint: { x: 1, y: 1 },
  207. // 游戏终点位置
  208. endPoint: { x: 4, y: 3 },
  209. // 地图上所有可行走的点坐标集合,添加type属性区分普通点和冰块
  210. walkablePoints: [
  211. { x: 1, y: 1, type: 'ice' }, { x: 2, y: 1, type: 'ice' }, { x: 3, y: 1 }, { x: 4, y: 1 },
  212. { x: 1, y: 2 }, { x: 2, y: 2 }, { x: 3, y: 2, type: 'ice' }, { x: 4, y: 2 },
  213. { x: 1, y: 3 }, { x: 2, y: 3 }, { x: 3, y: 3 }, { x: 4, y: 3 },
  214. { x: 4, y: 3 },
  215. ],
  216. }
  217. });
  218. // 计算属性 - 提高性能和可读性
  219. const mapBackground = computed(() => gameState.mapConfig.background);
  220. const tileSize = computed(() => gameState.mapConfig.tileSize);
  221. const walkablePoints = computed(() => gameState.mapData.walkablePoints);
  222. const startPoint = computed(() => gameState.mapData.startPoint);
  223. const endPoint = computed(() => gameState.mapData.endPoint);
  224. const playerPosition = computed(() => gameState.player.position);
  225. const playerDirection = computed(() => gameState.player.direction);
  226. const isColliding = computed(() => gameState.player.isColliding);
  227. const hasReachedEnd = computed(() => gameState.player.hasReachedEnd);
  228. const gameMessage = computed(() => gameState.status.message);
  229. const messageType = computed(() => gameState.status.messageType);
  230. // 计算玩家图片路径,优先使用接口数据
  231. const playerImageSrc = computed(() => {
  232. if (currentGameData.value && currentGameData.value.userImage) {
  233. return currentGameData.value.userImage.trim();
  234. }
  235. return playerImage;
  236. });
  237. const isSliding = computed(() => gameState.player.isSliding);
  238. // Blockly相关状态
  239. let workspace = null;
  240. // 使用 Map 存储可行走点及其类型,提高查询效率
  241. let walkablePointsMap = new Map();
  242. // 初始化可行走点映射
  243. function initWalkablePointsSet() {
  244. walkablePointsMap.clear();
  245. gameState.mapData.walkablePoints.forEach(point => {
  246. walkablePointsMap.set(`${point.x},${point.y}`, point.type);
  247. });
  248. }
  249. // 可行走检查函数
  250. function isWalkable(x, y) {
  251. return walkablePointsMap.has(`${x},${y}`);
  252. }
  253. // 检查是否是冰块点 - 基于walkablePoints中的type属性
  254. function isIce(x, y) {
  255. return walkablePointsMap.get(`${x},${y}`) === 'ice';
  256. }
  257. // 计算点的样式
  258. function getPointStyle(point) {
  259. // 检查是否是冰块点,添加不同的样式
  260. const isIcePoint = isIce(point.x, point.y);
  261. return {
  262. left: point.x * tileSize.value - tileSize.value + 'px',
  263. top: point.y * tileSize.value - tileSize.value + 'px',
  264. width: tileSize.value + 'px',
  265. height: tileSize.value + 'px',
  266. backgroundColor: isIcePoint ? 'rgba(144, 202, 249, 0.5)' : 'rgba(52, 152, 219, 0.2)',
  267. border: isIcePoint ? '1px solid rgba(33, 150, 243, 0.8)' : '1px solid rgba(52, 152, 219, 0.5)',
  268. boxShadow: isIcePoint ? '0 0 10px rgba(33, 150, 243, 0.5)' : 'none',
  269. };
  270. }
  271. // 计算玩家样式
  272. const playerStyle = computed(() => ({
  273. left: playerPosition.value.x * tileSize.value - tileSize.value + 'px',
  274. top: playerPosition.value.y * tileSize.value - tileSize.value + 'px',
  275. transform: `rotate(${playerDirection.value * 90}deg)`,
  276. '--player-rotation': `${playerDirection.value * 90}deg`,
  277. '--player-image': `url(${playerImageSrc.value})`,
  278. width: (tileSize.value * 0.8) + 'px',
  279. height: (tileSize.value * 0.8) + 'px',
  280. margin: (tileSize.value * 0.1) + 'px',
  281. }));
  282. // 显示游戏消息
  283. function showGameMessage(message, type = 'info') {
  284. gameState.status.message = message;
  285. gameState.status.messageType = type;
  286. // 3秒后自动清除消息
  287. setTimeout(() => {
  288. gameState.status.message = '';
  289. }, 3000);
  290. }
  291. // 导航返回
  292. function navigateBack() {
  293. router.back();
  294. }
  295. // 注册自定义积木
  296. function registerCustomBlocks() {
  297. // 向前移动积木
  298. Blockly.Blocks['move_forward'] = {
  299. init: function() {
  300. this.jsonInit({
  301. "type": "move_forward",
  302. "message0": "向前移动",
  303. "previousStatement": null,
  304. "nextStatement": null,
  305. "colour": 230,
  306. "tooltip": "控制角色向前移动一格",
  307. "helpUrl": ""
  308. });
  309. }
  310. };
  311. // 向后移动积木
  312. Blockly.Blocks['move_backward'] = {
  313. init: function() {
  314. this.jsonInit({
  315. "type": "move_backward",
  316. "message0": "向后移动",
  317. "previousStatement": null,
  318. "nextStatement": null,
  319. "colour": 230,
  320. "tooltip": "控制角色向后移动一格",
  321. "helpUrl": ""
  322. });
  323. }
  324. };
  325. // 向左转积木
  326. Blockly.Blocks['turn_left'] = {
  327. init: function() {
  328. this.jsonInit({
  329. "type": "turn_left",
  330. "message0": "向左转",
  331. "previousStatement": null,
  332. "nextStatement": null,
  333. "colour": 230,
  334. "tooltip": "控制角色向左转",
  335. "helpUrl": ""
  336. });
  337. }
  338. };
  339. // 向右转积木
  340. Blockly.Blocks['turn_right'] = {
  341. init: function() {
  342. this.jsonInit({
  343. "type": "turn_right",
  344. "message0": "向右转",
  345. "previousStatement": null,
  346. "nextStatement": null,
  347. "colour": 230,
  348. "tooltip": "控制角色向右转",
  349. "helpUrl": ""
  350. });
  351. }
  352. };
  353. // 向后转积木
  354. Blockly.Blocks['turn_around'] = {
  355. init: function() {
  356. this.jsonInit({
  357. "type": "turn_around",
  358. "message0": "向后转",
  359. "previousStatement": null,
  360. "nextStatement": null,
  361. "colour": 230,
  362. "tooltip": "控制角色向后转",
  363. "helpUrl": ""
  364. });
  365. }
  366. };
  367. }
  368. // 注册JavaScript生成器
  369. function registerJavaScriptGenerators() {
  370. // 向前移动生成器
  371. javascriptGenerator.forBlock['move_forward'] = function(block) {
  372. return 'await moveForward();\n';
  373. };
  374. // 向后移动生成器
  375. javascriptGenerator.forBlock['move_backward'] = function(block) {
  376. return 'await moveBackward();\n';
  377. };
  378. // 向左转生成器
  379. javascriptGenerator.forBlock['turn_left'] = function(block) {
  380. return 'await turnLeft();\n';
  381. };
  382. // 向右转生成器
  383. javascriptGenerator.forBlock['turn_right'] = function(block) {
  384. return 'await turnRight();\n';
  385. };
  386. // 向后转生成器
  387. javascriptGenerator.forBlock['turn_around'] = function(block) {
  388. return 'await turnAround();\n';
  389. };
  390. // 为重复循环块注册自定义生成器,确保支持异步操作
  391. javascriptGenerator.forBlock['controls_repeat_ext'] = function(block) {
  392. const repeats = javascriptGenerator.valueToCode(block, 'TIMES', javascriptGenerator.ORDER_ATOMIC) || '0';
  393. // 确保获取到的是数字类型,如果是字符串需要转换
  394. const safeRepeats = `(function() {
  395. const num = Number(${repeats});
  396. return isNaN(num) ? 0 : Math.max(0, Math.floor(num));
  397. })()`;
  398. // 获取循环体代码
  399. const branch = javascriptGenerator.statementToCode(block, 'DO');
  400. // 生成支持异步的循环代码
  401. let code = `for (let i = 0; i < ${safeRepeats}; i++) {\n`;
  402. code += javascriptGenerator.prefixLines(branch, javascriptGenerator.INDENT);
  403. code += '}\n';
  404. return code;
  405. };
  406. // 为while/until循环块注册自定义生成器
  407. javascriptGenerator.forBlock['controls_whileUntil'] = function(block) {
  408. const until = block.getFieldValue('MODE') === 'UNTIL';
  409. const condition = javascriptGenerator.valueToCode(block, 'CONDITION',
  410. javascriptGenerator.ORDER_NONE) || 'false';
  411. const branch = javascriptGenerator.statementToCode(block, 'DO');
  412. // 修复变量作用域问题,使用IIFE包装循环
  413. let code = '(async function() {\n';
  414. code += ' let loopCount = 0;\n';
  415. code += until ? ' while (!((' + condition + ')) && loopCount < 100) {\n' :
  416. ' while (((' + condition + ')) && loopCount < 100) {\n';
  417. code += javascriptGenerator.prefixLines(branch, javascriptGenerator.INDENT + ' ');
  418. code += ' loopCount++';
  419. code += ' await new Promise(resolve => setTimeout(resolve, 10));\n'; // 防止UI阻塞
  420. code += ' }\n';
  421. code += '})();\n';
  422. return code;
  423. };
  424. // 为text_print块添加生成器,用于调试
  425. javascriptGenerator.forBlock['text_print'] = function(block) {
  426. const msg = javascriptGenerator.valueToCode(block, 'TEXT', javascriptGenerator.ORDER_NONE) || '';
  427. return msg;
  428. };
  429. }
  430. // 初始化Blockly工作区
  431. function initBlockly() {
  432. const toolbox = document.getElementById('toolbox');
  433. workspace = Blockly.inject('blocklyDiv', {
  434. toolbox: toolbox,
  435. collapse: true,
  436. comments: true,
  437. disable: false, // 设为false以允许编辑
  438. maxBlocks: Infinity,
  439. trashcan: true,
  440. horizontalLayout: false,
  441. toolboxPosition: 'start',
  442. css: true,
  443. media: 'https://unpkg.com/blockly/media/',
  444. rtl: false,
  445. scrollbars: true,
  446. sounds: false, // 禁用声音以提高性能
  447. oneBasedIndex: true,
  448. grid: {
  449. spacing: 20,
  450. length: 3,
  451. colour: "#ccc",
  452. snap: true
  453. },
  454. zoom: {
  455. controls: true,
  456. wheel: true,
  457. startScale: 1.0,
  458. maxScale: 3,
  459. minScale: 0.3,
  460. scaleSpeed: 1.2
  461. }
  462. });
  463. }
  464. // 平滑移动函数
  465. async function smoothMoveTo(targetX, targetY) {
  466. const startX = playerPosition.value.x;
  467. const startY = playerPosition.value.y;
  468. const duration = 500; // 移动动画持续时间(毫秒)
  469. const startTime = performance.now();
  470. // 使用 requestAnimationFrame 实现平滑动画
  471. return new Promise(resolve => {
  472. function animate(currentTime) {
  473. const elapsedTime = currentTime - startTime;
  474. // 计算进度,确保不会超过1
  475. const progress = Math.min(elapsedTime / duration, 1);
  476. // 使用缓动函数使动画更自然
  477. const easedProgress = progress * (2 - progress); // easeOutQuad 缓动
  478. // 计算当前位置
  479. const currentX = startX + (targetX - startX) * easedProgress;
  480. const currentY = startY + (targetY - startY) * easedProgress;
  481. // 更新玩家位置
  482. gameState.player.position = { x: currentX, y: currentY };
  483. // 如果动画未完成,继续下一帧
  484. if (progress < 1) {
  485. requestAnimationFrame(animate);
  486. } else {
  487. // 动画完成后解析Promise
  488. resolve();
  489. }
  490. }
  491. // 开始动画
  492. requestAnimationFrame(animate);
  493. });
  494. }
  495. // 创建通用的移动函数
  496. async function move(direction) {
  497. if (isColliding.value || isSliding.value) {
  498. return;
  499. }
  500. let newX = playerPosition.value.x;
  501. let newY = playerPosition.value.y;
  502. // 根据当前方向和移动类型计算新位置
  503. if (direction === 1) {
  504. // 向前移动
  505. switch(playerDirection.value) {
  506. case 0: newY--; break;
  507. case 1: newX++; break;
  508. case 2: newY++; break;
  509. case 3: newX--; break;
  510. }
  511. } else {
  512. // 向后移动
  513. switch(playerDirection.value) {
  514. case 0: newY++; break;
  515. case 1: newX--; break;
  516. case 2: newY--; break;
  517. case 3: newX++; break;
  518. }
  519. }
  520. // 检查是否可以移动
  521. if (isWalkable(newX, newY)) {
  522. // 使用平滑移动动画
  523. await smoothMoveTo(newX, newY);
  524. // 检查新位置是否是冰块,如果是则触发滑行
  525. if (isIce(newX, newY)) {
  526. await handleIceSliding();
  527. }
  528. } else {
  529. // 发生碰撞
  530. gameState.player.isColliding = true;
  531. showGameMessage('哎呀,撞到墙了!', 'error');
  532. // 立即中止整个代码执行
  533. if (executionAbortController) {
  534. executionAbortController.abort();
  535. }
  536. // 1秒后取消碰撞状态
  537. setTimeout(() => {
  538. gameState.player.isColliding = false;
  539. }, 1000);
  540. // 添加碰撞延迟
  541. await new Promise(resolve => setTimeout(resolve, 500));
  542. }
  543. }
  544. // 处理冰块滑行逻辑
  545. async function handleIceSliding() {
  546. gameState.player.isSliding = true;
  547. try {
  548. // 循环检查是否可以继续滑行
  549. while (true) {
  550. // 计算下一个位置
  551. let nextX = playerPosition.value.x;
  552. let nextY = playerPosition.value.y;
  553. // 根据当前方向计算下一个位置
  554. switch(playerDirection.value) {
  555. case 0: nextY--; break;
  556. case 1: nextX++; break;
  557. case 2: nextY++; break;
  558. case 3: nextX--; break;
  559. }
  560. // 检查下一个位置是否可行走
  561. if (isWalkable(nextX, nextY)) {
  562. // 执行平滑移动到下一个位置
  563. await smoothMoveTo(nextX, nextY);
  564. // 检查下一个位置是否还是冰块
  565. if (!isIce(nextX, nextY)) {
  566. // 如果不是冰块,结束滑行
  567. break;
  568. }
  569. // 添加滑行间隔时间
  570. await new Promise(resolve => setTimeout(resolve, 500));
  571. } else {
  572. // 下一个位置不可行走,检查是否是墙体
  573. gameState.player.isColliding = true;
  574. showGameMessage('哎呀,撞到墙了!', 'error');
  575. // 立即中止整个代码执行
  576. if (executionAbortController) {
  577. executionAbortController.abort();
  578. }
  579. // 1秒后取消碰撞状态
  580. setTimeout(() => {
  581. gameState.player.isColliding = false;
  582. }, 1000);
  583. // 添加碰撞延迟
  584. await new Promise(resolve => setTimeout(resolve, 500));
  585. break;
  586. }
  587. }
  588. } catch (error) {
  589. console.error('滑行过程中发生错误:', error);
  590. } finally {
  591. // 无论如何都要确保滑行状态被重置
  592. gameState.player.isSliding = false;
  593. }
  594. }
  595. // 保留原有的API接口
  596. window.moveForward = async function() {
  597. await move(1);
  598. };
  599. window.moveBackward = async function() {
  600. await move(-1);
  601. };
  602. //向左转(逆时针旋转90度)
  603. window.turnLeft = async function() {
  604. // 如果已经发生过碰撞,不再执行任何旋转
  605. if (isColliding.value) {
  606. return;
  607. }
  608. // 向左转(逆时针旋转90度)
  609. gameState.player.direction = (playerDirection.value - 1 + 4) % 4;
  610. // 添加旋转延迟
  611. await new Promise(resolve => setTimeout(resolve, 500));
  612. };
  613. //向右转(顺时针旋转90度)
  614. window.turnRight = async function() {
  615. // 如果已经发生过碰撞,不再执行任何旋转
  616. if (isColliding.value) {
  617. return;
  618. }
  619. // 向右转(顺时针旋转90度)
  620. gameState.player.direction = (playerDirection.value + 1) % 4;
  621. // 添加旋转延迟
  622. await new Promise(resolve => setTimeout(resolve, 500));
  623. };
  624. // 向后转(旋转180度)
  625. window.turnAround = async function() {
  626. // 如果已经发生过碰撞,不再执行任何旋转
  627. if (isColliding.value) {
  628. return;
  629. }
  630. // 向后转(旋转180度)
  631. gameState.player.direction = (playerDirection.value + 2) % 4;
  632. // 添加旋转延迟
  633. await new Promise(resolve => setTimeout(resolve, 500));
  634. };
  635. //校验是否到达终点
  636. window.isFinish = async function() {
  637. // 如果已经发生过碰撞,不再执行任何旋转
  638. if (isColliding.value) {
  639. return;
  640. }
  641. if (gameState.player.position.x === endPoint.value.x && gameState.player.position.y === endPoint.value.y) {
  642. gameState.player.hasReachedEnd = true;
  643. showGameMessage('恭喜你到达终点!', 'success' );
  644. }
  645. };
  646. // 添加一个变量来跟踪当前执行的代码
  647. let currentExecutionPromise = null;
  648. let executionAbortController = null;
  649. // 运行代码
  650. const runCode = async () => {
  651. try {
  652. await resetPlayer();
  653. await new Promise(resolve => setTimeout(resolve, 500));
  654. // 创建新的AbortController用于取消执行
  655. executionAbortController = new AbortController();
  656. const signal = executionAbortController.signal;
  657. // 确保生成器和工作区都存在
  658. if (!javascriptGenerator || !workspace) {
  659. throw new Error('生成器或工作区未正确初始化');
  660. }
  661. // 生成JavaScript代码
  662. const code = javascriptGenerator.workspaceToCode(workspace) + "await isFinish();";
  663. try {
  664. // 增强的安全检查
  665. const unsafePatterns = [
  666. 'eval(', 'Function(', 'document.write', 'window.location',
  667. 'document.createElement', 'XMLHttpRequest', 'fetch',
  668. 'setInterval', 'setTimeout', 'window.',
  669. 'alert(', 'confirm(', 'prompt(',
  670. 'document.cookie', 'localStorage', 'sessionStorage'
  671. ];
  672. const hasUnsafeCode = unsafePatterns.some(pattern => code.includes(pattern));
  673. if (hasUnsafeCode) {
  674. throw new Error('代码包含不安全的操作');
  675. }
  676. // 包装代码为异步函数执行,并设置超时保护
  677. currentExecutionPromise = new Promise(async (resolve, reject) => {
  678. try {
  679. // 检查信号是否已中止
  680. if (signal.aborted) {
  681. throw new Error('执行已取消');
  682. }
  683. // 添加信号监听
  684. signal.addEventListener('abort', () => {
  685. reject(new Error('执行已取消'));
  686. });
  687. const wrappedCode = `(async () => { ${code} })()`;
  688. await new Function(wrappedCode)();
  689. resolve();
  690. } catch (error) {
  691. reject(error);
  692. }
  693. });
  694. } catch (error) {
  695. // 捕获并显示执行错误
  696. if (error.message !== '执行已取消') {
  697. const errorMsg = error.message || '未知错误';
  698. showGameMessage(`代码执行错误: ${errorMsg}`, 'error');
  699. console.error('代码执行错误:', error);
  700. }
  701. } finally {
  702. // 清除当前执行的Promise引用
  703. currentExecutionPromise = null;
  704. }
  705. } catch (error) {
  706. showGameMessage(`运行时错误: ${error.message || '未知错误'}`, 'error');
  707. console.error('运行时错误:', error);
  708. }
  709. };
  710. // 清空工作区
  711. const clearWorkspace = () => {
  712. workspace.clear();
  713. showGameMessage('工作区已清空', 'info');
  714. };
  715. // 重置玩家位置和状态
  716. const resetPlayer = () => {
  717. // 取消任何正在执行的代码
  718. if (executionAbortController) {
  719. executionAbortController.abort();
  720. executionAbortController = null;
  721. }
  722. if (currentExecutionPromise) {
  723. currentExecutionPromise = null;
  724. }
  725. gameState.player.position = { ...startPoint.value };
  726. gameState.player.direction = playerInitialDirection.value; // 重置为向上方向
  727. gameState.player.isColliding = false; //碰撞标志
  728. gameState.player.hasReachedEnd = false;
  729. gameState.player.isSliding = false; // 重置滑行状态
  730. };
  731. // 组件卸载时清理
  732. onUnmounted(() => {
  733. if (workspace) {
  734. workspace.dispose();
  735. }
  736. });
  737. </script>
  738. <style scoped lang="scss">
  739. @use "sass:math";
  740. @function rpx($px) {
  741. @return math.div($px, 750) * 100vw;
  742. }
  743. //将tileSize属性绑定到CSS变量上
  744. :root {
  745. --tile-size: v-bind('tileSize + "px"');
  746. }
  747. .map-game-container {
  748. position: fixed;
  749. top: 0;
  750. left: 0;
  751. right: 0;
  752. bottom: 0;
  753. background: transparent;
  754. overflow-y: auto;
  755. }
  756. /* 自定义滚动条样式 */
  757. .map-game-container::-webkit-scrollbar {
  758. width: rpx(2); /* 滚动条宽度 */
  759. }
  760. .map-game-container::-webkit-scrollbar-track {
  761. background: #f1effd; /* 滚动条轨道背景色 */
  762. border-radius: rpx(4);
  763. }
  764. .map-game-container::-webkit-scrollbar-thumb {
  765. background: #e2ddfc; /* 滚动条滑块颜色 */
  766. border-radius: rpx(4);
  767. }
  768. .map-game-container::-webkit-scrollbar-thumb:hover {
  769. background: #e2ddfc; /* 滚动条滑块 hover 状态颜色 */
  770. }
  771. .title-box {
  772. position: relative;
  773. top: 10px;
  774. padding-left: 15px;
  775. margin-bottom: 20px;
  776. z-index: 10;
  777. }
  778. .box-icon {
  779. display: flex;
  780. align-items: center;
  781. gap: 10px;
  782. padding: 10px 20px;
  783. background-color: rgba(255, 255, 255, 0.8);
  784. border-radius: 30px;
  785. backdrop-filter: blur(10px);
  786. cursor: pointer;
  787. transition: all 0.3s ease;
  788. font-size: 16px;
  789. color: #333;
  790. font-weight: 500;
  791. width: fit-content;
  792. }
  793. .box-icon:hover {
  794. background-color: rgba(255, 255, 255, 0.9);
  795. transform: translate(-3px);
  796. }
  797. .left-icon {
  798. font-size: 18px;
  799. }
  800. .content {
  801. display: flex;
  802. flex-wrap: wrap;
  803. gap: 20px;
  804. padding: 20px;
  805. }
  806. /* 地图区域样式 */
  807. .map-section {
  808. flex: 1;
  809. min-width: 500px;
  810. background: rgba(248, 249, 250, 0.82);
  811. padding: 15px;
  812. border-radius: 15px;
  813. }
  814. .map-section h2 {
  815. margin-bottom: 15px;
  816. color: #2c3e50;
  817. border-bottom: 2px solid #3498db;
  818. padding-bottom: 8px;
  819. }
  820. .map-container {
  821. //position: relative;
  822. //width: 100%;
  823. //height: 100% ;
  824. //display: flex;
  825. //justify-content: center;
  826. //align-items: center;
  827. }
  828. .map-background {
  829. position: relative;
  830. width: 100%;
  831. height: 100%;
  832. background-size: cover;
  833. background-position: center;
  834. background-repeat: no-repeat;
  835. //margin: 0 auto;
  836. //border: 2px solid #ddd;
  837. //border-radius: 8px;
  838. //overflow: hidden;
  839. //background-color: #f0f0f0;
  840. }
  841. .map-image {
  842. width: 100%;
  843. height: 100%;
  844. object-fit: cover;
  845. }
  846. /* 可行走区域样式 */
  847. .walkable-point {
  848. position: absolute;
  849. background-color: rgba(52, 152, 219, 0.2);
  850. border: 1px solid rgba(52, 152, 219, 0.5);
  851. box-sizing: border-box;
  852. //是否显示可行路线
  853. opacity: 0;
  854. }
  855. /* 玩家样式 */
  856. .player {
  857. position: absolute;
  858. background-image: var(--player-image);
  859. background-size: contain;
  860. background-repeat: no-repeat;
  861. background-position: center;
  862. border-radius: 5px;
  863. transition: all 0.3s ease;
  864. z-index: 10;
  865. }
  866. /* 碰撞动画 */
  867. .player.collision {
  868. animation: collision 0.5s ease-in-out;
  869. }
  870. @keyframes collision {
  871. 0% { transform: rotate(var(--player-rotation)) translateX(0) translateY(0) scale(1); }
  872. 25% { transform: rotate(var(--player-rotation)) translateX(-3px) translateY(-2px) scale(1.2); }
  873. 50% { transform: rotate(var(--player-rotation)) translateX(3px) translateY(2px) scale(1.2); }
  874. 75% { transform: rotate(var(--player-rotation)) translateX(-3px) translateY(-2px) scale(1.2); }
  875. 100% { transform: rotate(var(--player-rotation)) translateX(0) translateY(0) scale(1); }
  876. }
  877. /* 滑行动画 */
  878. @keyframes sliding {
  879. 0% { transform: rotate(var(--player-rotation)) translateX(0) translateY(0); }
  880. 25% { transform: rotate(var(--player-rotation)) translateX(2px) translateY(0); }
  881. 75% { transform: rotate(var(--player-rotation)) translateX(-2px) translateY(0); }
  882. 100% { transform: rotate(var(--player-rotation)) translateX(0) translateY(0); }
  883. }
  884. /* 成功到达终点动画 */
  885. .player.success {
  886. animation: success 1s ease-in-out;
  887. }
  888. @keyframes success {
  889. 0% { transform: rotate(var(--player-rotation)) scale(1); }
  890. 10% { transform: rotate(var(--player-rotation)) scale(1.2) translateX(-5px) translateY(-5px); }
  891. 20% { transform: rotate(var(--player-rotation)) scale(1.3) translateX(5px) translateY(5px); }
  892. 30% { transform: rotate(var(--player-rotation)) scale(1.2) translateX(-5px) translateY(-5px); }
  893. 40% { transform: rotate(var(--player-rotation)) scale(1.3) translateX(5px) translateY(5px); }
  894. 50% { transform: rotate(var(--player-rotation)) scale(1.4) translateX(0) translateY(0); }
  895. 60% { transform: rotate(var(--player-rotation)) scale(1.3) translateX(-3px) translateY(-3px); }
  896. 70% { transform: rotate(var(--player-rotation)) scale(1.2) translateX(3px) translateY(3px); }
  897. 80% { transform: rotate(var(--player-rotation)) scale(1.3) translateX(-3px) translateY(-3px); }
  898. 90% { transform: rotate(var(--player-rotation)) scale(1.2) translateX(3px) translateY(3px); }
  899. 100% { transform: rotate(var(--player-rotation)) scale(1); }
  900. }
  901. /* 游戏消息样式 */
  902. .game-message {
  903. position: absolute;
  904. top: 20px;
  905. left: 50%;
  906. transform: translateX(-50%);
  907. padding: 10px 20px;
  908. border-radius: 5px;
  909. font-weight: bold;
  910. z-index: 20;
  911. min-width: 200px;
  912. text-align: center;
  913. }
  914. .game-message.success {
  915. background-color: #d4edda;
  916. color: #155724;
  917. border: 1px solid #c3e6cb;
  918. }
  919. .game-message.error {
  920. background-color: #f8d7da;
  921. color: #721c24;
  922. border: 1px solid #f5c6cb;
  923. }
  924. .game-message.info {
  925. background-color: #d1ecf1;
  926. color: #0c5460;
  927. border: 1px solid #bee5eb;
  928. }
  929. /* Blockly区域样式 */
  930. .blockly-section {
  931. flex: 1;
  932. min-width: 600px;
  933. display: flex;
  934. flex-direction: column;
  935. gap: 20px;
  936. }
  937. .toolbox-section {
  938. background: rgba(248, 249, 250, 0.82);
  939. padding: 15px;
  940. border-radius: 15px;
  941. }
  942. // 合并重复的区块标题样式
  943. .map-section h2,
  944. .toolbox-section h2,
  945. .workspace-section h2 {
  946. margin-bottom: 15px;
  947. color: #2c3e50;
  948. border-bottom: 2px solid #3498db;
  949. padding-bottom: 8px;
  950. }
  951. // 合并重复的区块背景样式
  952. .map-section,
  953. .toolbox-section,
  954. .workspace-section {
  955. background: rgba(248, 249, 250, 0.82);
  956. padding: 15px;
  957. border-radius: 15px;
  958. height: 100%;
  959. }
  960. .workspace-section h2 {
  961. margin-bottom: 15px;
  962. color: #2c3e50;
  963. border-bottom: 2px solid #3498db;
  964. padding-bottom: 8px;
  965. }
  966. #blocklyDiv {
  967. height: 80%;
  968. min-height: 500px;
  969. width: 100%;
  970. background: #fff;
  971. border: 1px solid #ddd;
  972. border-radius: 8px;
  973. }
  974. .controls {
  975. display: flex;
  976. gap: 10px;
  977. margin: 15px 0px;
  978. flex-wrap: wrap;
  979. }
  980. button {
  981. padding: 10px 20px;
  982. border: none;
  983. border-radius: 5px;
  984. background: #3498db;
  985. color: #fff;
  986. font-weight: 700;
  987. cursor: pointer;
  988. transition: all 0.3s ease;
  989. }
  990. button:hover {
  991. background: #2980b9;
  992. transform: translateY(-2px);
  993. box-shadow: 0 5px 10px rgba(0, 0, 0, 0.1);
  994. }
  995. #runCode {
  996. background: #e74c3c;
  997. }
  998. #runCode:hover {
  999. background: #c0392b;
  1000. }
  1001. /* 响应式布局 */
  1002. @media (max-width: 1200px) {
  1003. .content {
  1004. flex-direction: column;
  1005. }
  1006. .map-section,
  1007. .blockly-section {
  1008. min-width: 100%;
  1009. }
  1010. .map-background {
  1011. width: 100%;
  1012. height: 400px;
  1013. }
  1014. }
  1015. </style>