MapGame.vue 58 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050
  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. <!-- 游戏编号 -->
  10. <div class="game-badge">{{ gameSort }}</div>
  11. </div>
  12. <div class="content">
  13. <!-- 地图显示区域 -->
  14. <div class="map-section">
  15. <!-- 内容简介提示 -->
  16. <div v-if="currentGameData?.info" class="info-message-container">
  17. <div class="message-item">
  18. <div class="avatar">
  19. <img src="@/assets/images/xiaozhi2.png" alt="头像" class="avatar-image" />
  20. </div>
  21. <p v-if="currentGameData?.info" v-html="currentGameData?.info"></p>
  22. </div>
  23. </div>
  24. <div class="map-container">
  25. <!-- 地图背景 -->
  26. <div class="map-background">
  27. <img :src="mapBackground" alt="地图背景" class="map-image" @load="onMapImageLoad" />
  28. <!-- 可行走区域标记 -->
  29. <div
  30. v-for="(point, index) in walkablePoints"
  31. :key="index"
  32. class="walkable-point"
  33. :style="getPointStyle(point)"
  34. >
  35. </div>
  36. <!-- 玩家角色 -->
  37. <div
  38. class="player"
  39. :style="playerStyle"
  40. :class="{ 'collision': isColliding, 'success': hasReachedEnd }"
  41. >
  42. </div>
  43. <!-- 携带物品容器 -->
  44. <div class="carried-items-container" v-show="gameState.player.carriedItems.length > 0">
  45. <div
  46. v-for="(item, index) in gameState.player.carriedItems"
  47. :key="index"
  48. class="carried-item"
  49. :style="getCarriedItemStyle(index, item)"
  50. ></div>
  51. </div>
  52. </div>
  53. <!-- 游戏状态提示 -->
  54. <div v-if="gameMessage" :class="['game-message', messageType]">
  55. {{ gameMessage }}
  56. </div>
  57. </div>
  58. </div>
  59. <!-- Blockly工作区 -->
  60. <div class="blockly-section">
  61. <div class="toolbox-section" style="display: none;">
  62. <h2>工具箱</h2>
  63. <div id="toolbox">
  64. <!-- 移动控制积木 -->
  65. <category name="移动控制" colour="%{BKY_MOTION_HUE}">
  66. <block type="move_forward"></block>
  67. <block type="turn_left"></block>
  68. <block type="turn_right"></block>
  69. <block type="turn_around"></block>
  70. <block type="pickup_item"></block>
  71. <block type="use_item"></block>
  72. </category>
  73. <!-- 逻辑控制积木 -->
  74. <category name="逻辑" colour="%{BKY_LOGIC_HUE}">
  75. <block type="controls_if"></block>
  76. <block type="logic_compare"></block>
  77. <block type="logic_operation"></block>
  78. <block type="logic_boolean"></block>
  79. </category>
  80. <!-- 循环控制积木 -->
  81. <category name="循环" colour="%{BKY_LOOPS_HUE}">
  82. <block type="controls_repeat_ext"></block>
  83. <!-- <block type="controls_whileUntil"></block>-->
  84. </category>
  85. <!-- 数学运算积木 -->
  86. <category name="数学" colour="%{BKY_MATH_HUE}">
  87. <block type="math_number"></block>
  88. <block type="math_arithmetic"></block>
  89. </category>
  90. </div>
  91. </div>
  92. <div class="workspace-section">
  93. <div class="controls">
  94. <button id="runCode" @click="runCode">运行代码</button>
  95. <button @click="clearWorkspace">清空工作区</button>
  96. <button @click="resetPlayer">重置玩家</button>
  97. </div>
  98. <div id="blocklyDiv"></div>
  99. </div>
  100. </div>
  101. </div>
  102. </div>
  103. </template>
  104. <script setup>
  105. import {ref, onMounted, onUnmounted, reactive, computed, nextTick} from 'vue';
  106. import { useRouter, useRoute } from 'vue-router';
  107. import { ArrowLeftBold } from '@element-plus/icons-vue';
  108. import * as Blockly from "blockly";
  109. import 'blockly/msg/zh-hans';
  110. import { javascriptGenerator } from "blockly/javascript";
  111. import playerImage from '@/assets/images/blockly/user.png';
  112. // 游戏接口数据
  113. import { getMapGameById } from '@/api/blockly/game.js';
  114. import { BLOCKLY_MAP_TYPE_DICT } from '@/api/blockly/blockly.js';
  115. // 配置常量
  116. const CONFIG = {
  117. // 动画时长配置(毫秒)
  118. ANIMATION: {
  119. MOVE_DURATION: 500, // 移动动画持续时间
  120. ROTATE_DURATION: 500, // 旋转动画持续时间(左转/右转)
  121. TURN_AROUND_DURATION: 750, // 向后转动画持续时间
  122. },
  123. // 延迟配置(毫秒)
  124. DELAY: {
  125. ACTION_DELAY: 200, // 每次动作后的延迟时间
  126. COLLISION_DELAY: 300, // 碰撞后的延迟时间
  127. RESET_DELAY: 300, // 重置后的延迟时间
  128. MESSAGE_DISPLAY: 2000, // 消息显示时间
  129. COLLISION_RESET: 1000, // 碰撞状态重置时间
  130. LOOP_PREVENTION: 10, // 循环防止UI阻塞的延迟
  131. ITEMS_APPEAR: 300, // 物品出现延迟时间
  132. ITEMS_FLIGHT_ANIMATION_DELAY: 800, // 飞行动画延迟时间
  133. },
  134. // 游戏配置
  135. GAME: {
  136. MAX_LOOP_COUNT: 100, // 最大循环次数
  137. DIRECTIONS: {
  138. UP: 0,
  139. RIGHT: 1,
  140. DOWN: 2,
  141. LEFT: 3
  142. }
  143. },
  144. // 样式配置
  145. STYLES: {
  146. DEFAULT_TILE_SIZE: 143, // 默认瓦片大小
  147. PLAYER_SIZE_RATIO: 0.8, // 玩家大小占瓦片的比例
  148. PLAYER_SIZE_MARGIN: 0.1 // 玩家大小占瓦片的比例
  149. },
  150. //提示语
  151. TIPS: {
  152. NO_ENTRY: '当前位置无通路,无法移动',
  153. UNFINISHED: '任务未完成!',
  154. FINISH: '恭喜你到达终点!',
  155. PICKUP_ITEM: '拾取物品成功!',
  156. NULL_PICKUP_ITEM: '当前位置没有可拾取的物品',
  157. USE_ITEM_SUCCESS: '使用物品成功',
  158. USE_SPECIAL_ITEM: '需要特殊物品才能使用',
  159. NO_USE_ITEM: '当前位置不需要使用物品',
  160. }
  161. };
  162. // 路由和游戏状态
  163. const router = useRouter();
  164. const route = useRoute();
  165. const gameTitle = ref('地图游戏编程'); // 默认标题
  166. const currentGameData = ref(null);
  167. const playerInitialDirection = ref(0); // 人物初始朝向
  168. const gameSort = ref('Game1'); // 默认排序
  169. // 运行控制标志
  170. let shouldStopExecution = false;
  171. let currentExecutionPromise = null;
  172. let executionAbortController = null;
  173. // 添加响应式的容器尺寸
  174. const mapContainerDimensions = ref({ width: 0, height: 0 });
  175. // Blockly相关状态
  176. let workspace = null;
  177. // 使用 Map 存储可行走点及其类型,提高查询效率
  178. let walkablePointsMap = new Map();
  179. // 创建游戏状态的响应式对象
  180. const gameState = reactive({
  181. // 地图配置信息
  182. mapConfig: {
  183. // 地图背景图片路径
  184. background: '',
  185. // 每个瓦片的尺寸(像素)
  186. tileSize: CONFIG.STYLES.DEFAULT_TILE_SIZE,
  187. },
  188. // 玩家相关状态
  189. player: {
  190. // 玩家当前位置坐标
  191. position: {},
  192. // 玩家当前朝向:0=上, 1=右, 2=下, 3=左
  193. direction: 1,
  194. // 是否正在发生碰撞
  195. isColliding: false,
  196. // 是否已到达终点
  197. hasReachedEnd: false,
  198. // 是否正在冰块上滑行
  199. isSliding: false,
  200. // 携带的物品数组
  201. carriedItems: [],
  202. },
  203. // 游戏状态信息
  204. status: {
  205. // 当前显示的游戏消息
  206. message: '',
  207. // 消息类型(如success、error、info等)
  208. messageType: ''
  209. },
  210. // 地图数据信息
  211. mapData: {
  212. // 游戏起点位置
  213. startPoint: {},
  214. // 游戏终点位置
  215. endPoint: {},
  216. // 地图上所有可行走的点坐标集合,添加type属性区分普通点和冰块
  217. walkablePoints: [],
  218. // 保存原始的可行走点数据,用于重置
  219. originalWalkablePoints: [],
  220. }
  221. });
  222. // BLOCKLY_MAP_TYPE_DICT和BLOCKLY_CUSTOMIZE_DICT已从blockly.js导入
  223. // 计算属性 - 提高性能和可读性
  224. const mapBackground = computed(() => gameState.mapConfig.background);
  225. // const tileSize = computed(() => gameState.mapConfig.tileSize);
  226. const walkablePoints = computed(() => gameState.mapData.walkablePoints);
  227. const startPoint = computed(() => gameState.mapData.startPoint);
  228. const endPoint = computed(() => gameState.mapData.endPoint);
  229. const playerPosition = computed(() => gameState.player.position);
  230. const playerDirection = computed(() => gameState.player.direction);
  231. const isColliding = computed(() => gameState.player.isColliding);
  232. const hasReachedEnd = computed(() => gameState.player.hasReachedEnd);
  233. const gameMessage = computed(() => gameState.status.message);
  234. const messageType = computed(() => gameState.status.messageType);
  235. const isSliding = computed(() => gameState.player.isSliding);
  236. // 计算玩家图片路径,优先使用接口数据
  237. const playerImageSrc = computed(() => {
  238. if (currentGameData.value && currentGameData.value.userImage) {
  239. return currentGameData.value.userImage.trim();
  240. }
  241. return playerImage;
  242. });
  243. // 地图图片加载完成后更新容器尺寸
  244. const onMapImageLoad = () => {
  245. updateMapContainerDimensions();
  246. };
  247. // 计算实际瓦片大小(基于容器尺寸和地图数据)
  248. const tileSize = computed(() => {
  249. if (mapContainerDimensions.value.width === 0 || mapContainerDimensions.value.height === 0) {
  250. return gameState.mapConfig.tileSize;
  251. }
  252. // 获取地图数据中的最大坐标
  253. let size = JSON.parse(gameState.mapConfig.tileSize);
  254. // 计算基于容器的瓦片大小,确保地图完全可见
  255. const tileWidth = mapContainerDimensions.value.width / size.x;
  256. const tileHeight = mapContainerDimensions.value.height / size.y;
  257. // 返回较小的值以确保地图完全可见
  258. return Math.min(tileWidth, tileHeight);
  259. });
  260. // 生命周期钩子
  261. onMounted(async () => {
  262. // 获取游戏数据
  263. await fetchGameData();
  264. // 初始化可行走点集合
  265. initWalkablePointsSet();
  266. // 注册自定义积木
  267. registerCustomBlocks();
  268. // 注册JavaScript生成器
  269. registerJavaScriptGenerators();
  270. // 初始化Blockly工作区
  271. initBlockly();
  272. // 重置玩家位置
  273. resetPlayer();
  274. await nextTick();
  275. // 添加对窗口大小变化的监听
  276. updateMapContainerDimensions();
  277. window.addEventListener('resize', updateMapContainerDimensions);
  278. });
  279. // 获取游戏数据
  280. const fetchGameData = async () => {
  281. try {
  282. const gameId = route.query.gameId;
  283. const nameFromRoute = route.query.gameName;
  284. const sortFromRoute = route.query.gameSort;
  285. if (nameFromRoute) {
  286. gameTitle.value = nameFromRoute;
  287. }
  288. if (sortFromRoute) {
  289. gameSort.value = sortFromRoute;
  290. }
  291. let mapGameData = await getMapGameById(gameId);
  292. if (mapGameData.code === 0) {
  293. currentGameData.value = mapGameData?.data;
  294. // 使用接口数据
  295. if (currentGameData.value && currentGameData.value.sort) {
  296. let sortNum = currentGameData.value.sort;
  297. gameSort.value = sortNum > 9 ? `Game${sortNum}` : `Game${sortNum}`;
  298. }
  299. await updateGameStateFromData(currentGameData.value);
  300. // 数据更新后强制刷新容器尺寸(等待DOM更新)
  301. await nextTick();
  302. updateMapContainerDimensions();
  303. }
  304. } catch (error) {
  305. console.error('获取游戏数据失败:', error);
  306. }
  307. };
  308. // 根据获取到的数据更新游戏状态
  309. const updateGameStateFromData = (gameData) => {
  310. try {
  311. // 更新地图配置
  312. gameState.mapConfig.background = gameData.mapBackground ? gameData.mapBackground.trim() : '';
  313. gameState.mapConfig.tileSize = gameData.mapTileSize || CONFIG.STYLES.DEFAULT_TILE_SIZE;
  314. // 更新玩家方向
  315. if (gameData.userDirection) {
  316. playerInitialDirection.value = gameData.userDirection;
  317. }
  318. // 更新地图数据
  319. // 地图起点
  320. if (gameData.mapStartPoint) {
  321. const startPoint = JSON.parse(gameData.mapStartPoint);
  322. gameState.mapData.startPoint = { x: startPoint.x, y: startPoint.y };
  323. gameState.player.position = { x: startPoint.x, y: startPoint.y };
  324. }
  325. // 地图终点
  326. if (gameData.mapEndPoint) {
  327. const endPoint = JSON.parse(gameData.mapEndPoint);
  328. gameState.mapData.endPoint = { x: endPoint.x, y: endPoint.y };
  329. }
  330. if (gameData.mapWalkablePoints) {
  331. gameState.mapData.walkablePoints = JSON.parse(gameData.mapWalkablePoints);
  332. }
  333. // 重新初始化可行走点集合
  334. initWalkablePointsSet();
  335. } catch (error) {
  336. console.error('更新游戏状态失败:', error);
  337. }
  338. };
  339. // 初始化可行走点映射
  340. function initWalkablePointsSet() {
  341. walkablePointsMap.clear();
  342. gameState.mapData.walkablePoints.forEach(point => {
  343. walkablePointsMap.set(`${point.x},${point.y}`, point);
  344. });
  345. // 保存原始的可行走点数据,用于重置
  346. gameState.mapData.originalWalkablePoints = JSON.parse(JSON.stringify(gameState.mapData.walkablePoints));
  347. }
  348. // 可行走检查函数
  349. function isWalkable(x, y) {
  350. return walkablePointsMap.has(`${x},${y}`);
  351. }
  352. // 计算点的样式
  353. function getPointStyle(point) {
  354. const style = {
  355. left: point.x * tileSize.value - tileSize.value + 'px',
  356. top: point.y * tileSize.value - tileSize.value + 'px',
  357. width: tileSize.value + 'px',
  358. height: tileSize.value + 'px',
  359. };
  360. // 如果point有img属性,则添加图标
  361. if (point.img) {
  362. const iconSize = tileSize.value * CONFIG.STYLES.PLAYER_SIZE_RATIO;
  363. const marginSize = tileSize.value * CONFIG.STYLES.PLAYER_SIZE_MARGIN;
  364. // 重置可能影响背景图显示的样式
  365. style.backgroundColor = 'transparent';
  366. style.backgroundImage = `url(${point.img})`;
  367. style.backgroundSize = 'contain';
  368. style.backgroundPosition = 'center';
  369. style.backgroundRepeat = 'no-repeat';
  370. // 设置与玩家相同的宽高和边距
  371. style.width = iconSize + 'px';
  372. style.height = iconSize + 'px';
  373. style.margin = marginSize + 'px';
  374. }
  375. return style;
  376. }
  377. // 计算玩家样式
  378. const playerStyle = computed(() => ({
  379. left: playerPosition.value.x * tileSize.value - tileSize.value + 'px',
  380. top: playerPosition.value.y * tileSize.value - tileSize.value + 'px',
  381. transform: `rotate(${playerDirection.value * 90}deg)`,
  382. '--player-rotation': `${playerDirection.value * 90}deg`,
  383. '--player-image': `url(${playerImageSrc.value})`,
  384. width: (tileSize.value * CONFIG.STYLES.PLAYER_SIZE_RATIO) + 'px',
  385. height: (tileSize.value * CONFIG.STYLES.PLAYER_SIZE_RATIO) + 'px',
  386. margin: (tileSize.value * CONFIG.STYLES.PLAYER_SIZE_MARGIN) + 'px',
  387. }));
  388. // 计算携带物品样式
  389. function getCarriedItemStyle(index, item) {
  390. const baseSize = tileSize.value * CONFIG.STYLES.PLAYER_SIZE_RATIO * 0.3;
  391. return {
  392. position: 'relative',
  393. width: baseSize + 'px',
  394. height: baseSize + 'px',
  395. backgroundSize: 'contain',
  396. backgroundPosition: 'center',
  397. backgroundRepeat: 'no-repeat',
  398. backgroundImage: `url(${item.img})`,
  399. animationDelay: index * 0.1 + 's'
  400. };
  401. }
  402. // 导航返回
  403. function navigateBack() {
  404. router.back();
  405. }
  406. // 注册自定义积木
  407. function registerCustomBlocks() {
  408. // 向前移动积木
  409. Blockly.Blocks['move_forward'] = {
  410. init: function() {
  411. this.jsonInit({
  412. "type": "move_forward",
  413. "message0": "向前移动",
  414. "previousStatement": null,
  415. "nextStatement": null,
  416. "colour": 230,
  417. "tooltip": "控制角色向前移动一格",
  418. "helpUrl": ""
  419. });
  420. }
  421. };
  422. // 向左转积木
  423. Blockly.Blocks['turn_left'] = {
  424. init: function() {
  425. this.jsonInit({
  426. "type": "turn_left",
  427. "message0": "向左转",
  428. "previousStatement": null,
  429. "nextStatement": null,
  430. "colour": 230,
  431. "tooltip": "控制角色向左转",
  432. "helpUrl": ""
  433. });
  434. }
  435. };
  436. // 向右转积木
  437. Blockly.Blocks['turn_right'] = {
  438. init: function() {
  439. this.jsonInit({
  440. "type": "turn_right",
  441. "message0": "向右转",
  442. "previousStatement": null,
  443. "nextStatement": null,
  444. "colour": 230,
  445. "tooltip": "控制角色向右转",
  446. "helpUrl": ""
  447. });
  448. }
  449. };
  450. // 向后转积木
  451. Blockly.Blocks['turn_around'] = {
  452. init: function() {
  453. this.jsonInit({
  454. "type": "turn_around",
  455. "message0": "向后转",
  456. "previousStatement": null,
  457. "nextStatement": null,
  458. "colour": 230,
  459. "tooltip": "控制角色向后转",
  460. "helpUrl": ""
  461. });
  462. }
  463. };
  464. // 拾取物品积木
  465. Blockly.Blocks['pickup_item'] = {
  466. init: function() {
  467. this.jsonInit({
  468. "type": "pickup_item",
  469. "message0": "拾取物品",
  470. "previousStatement": null,
  471. "nextStatement": null,
  472. "colour": 30,
  473. "tooltip": "尝试拾取当前位置的物品",
  474. "helpUrl": ""
  475. });
  476. }
  477. };
  478. // 使用物品积木
  479. Blockly.Blocks['use_item'] = {
  480. init: function() {
  481. this.jsonInit({
  482. "type": "use_item",
  483. "message0": "使用物品",
  484. "previousStatement": null,
  485. "nextStatement": null,
  486. "colour": 30,
  487. "tooltip": "在当前位置使用物品",
  488. "helpUrl": ""
  489. });
  490. }
  491. };
  492. }
  493. // 注册JavaScript生成器
  494. function registerJavaScriptGenerators() {
  495. // 向前移动生成器
  496. javascriptGenerator.forBlock['move_forward'] = function(block) {
  497. return 'await moveForward();\n';
  498. };
  499. // 向左转生成器
  500. javascriptGenerator.forBlock['turn_left'] = function(block) {
  501. return 'await turnLeft();\n';
  502. };
  503. // 向右转生成器
  504. javascriptGenerator.forBlock['turn_right'] = function(block) {
  505. return 'await turnRight();\n';
  506. };
  507. // 向后转生成器
  508. javascriptGenerator.forBlock['turn_around'] = function(block) {
  509. return 'await turnAround();\n';
  510. };
  511. // 拾取物品生成器
  512. javascriptGenerator.forBlock['pickup_item'] = function(block) {
  513. return 'await pickupItem();\n';
  514. };
  515. // 使用物品生成器
  516. javascriptGenerator.forBlock['use_item'] = function(block) {
  517. return 'await useItem();\n';
  518. };
  519. // 为重复循环块注册自定义生成器,确保支持异步操作
  520. javascriptGenerator.forBlock['controls_repeat_ext'] = function(block) {
  521. const repeats = javascriptGenerator.valueToCode(block, 'TIMES', javascriptGenerator.ORDER_ATOMIC) || '0';
  522. // 确保获取到的是数字类型,如果是字符串需要转换
  523. const safeRepeats = `(function() {
  524. const num = Number(${repeats});
  525. return isNaN(num) ? 0 : Math.max(0, Math.floor(num));
  526. })()`;
  527. // 获取循环体代码
  528. const branch = javascriptGenerator.statementToCode(block, 'DO');
  529. // 生成支持异步的循环代码
  530. let code = `for (let i = 0; i < ${safeRepeats}; i++) {\n`;
  531. code += javascriptGenerator.prefixLines(branch, javascriptGenerator.INDENT);
  532. code += '}\n';
  533. return code;
  534. };
  535. // 为while/until循环块注册自定义生成器
  536. javascriptGenerator.forBlock['controls_whileUntil'] = function(block) {
  537. const until = block.getFieldValue('MODE') === 'UNTIL';
  538. const condition = javascriptGenerator.valueToCode(block, 'CONDITION',
  539. javascriptGenerator.ORDER_NONE) || 'false';
  540. const branch = javascriptGenerator.statementToCode(block, 'DO');
  541. // 修复变量作用域问题,使用IIFE包装循环
  542. let code = '(async function() {\n';
  543. code += ' let loopCount = 0;\n';
  544. code += until ? ' while (!((' + condition + ')) && loopCount < ' + CONFIG.GAME.MAX_LOOP_COUNT + ') {\n' :
  545. ' while (((' + condition + ')) && loopCount < ' + CONFIG.GAME.MAX_LOOP_COUNT + ') {\n';
  546. code += javascriptGenerator.prefixLines(branch, javascriptGenerator.INDENT + ' ');
  547. code += ' loopCount++';
  548. code += ' await new Promise(resolve => setTimeout(resolve, ' + CONFIG.DELAY.LOOP_PREVENTION + '));\n'; // 防止UI阻塞
  549. code += ' }\n';
  550. code += '})();\n';
  551. return code;
  552. };
  553. // 为text_print块添加生成器,用于调试
  554. javascriptGenerator.forBlock['text_print'] = function(block) {
  555. const msg = javascriptGenerator.valueToCode(block, 'TEXT', javascriptGenerator.ORDER_NONE) || '';
  556. return msg;
  557. };
  558. }
  559. const setupBlocklyChineseLocale = () => {
  560. // 使用扩展方式覆盖默认的英文文本为中文
  561. const locale = {
  562. // 逻辑积木中文配置
  563. CONTROLS_IF_MSG_IF: "如果",
  564. CONTROLS_IF_MSG_ELSEIF: "否则如果",
  565. CONTROLS_IF_MSG_ELSE: "否则",
  566. CONTROLS_IF_MSG_THEN: "执行",
  567. CONTROLS_IF_MSG_DO: "执行",
  568. CONTROLS_IF_TOOLTIP_1: "如果条件为真,则执行相应的代码。",
  569. CONTROLS_IF_TOOLTIP_2: "如果条件为真,则执行相应的代码;否则执行其他代码。",
  570. CONTROLS_IF_TOOLTIP_3: "如果条件为真,则执行相应的代码;否则检查其他条件。",
  571. CONTROLS_IF_TOOLTIP_4: "如果条件为真,则执行相应的代码;否则检查其他条件,如果都不满足则执行最后代码。",
  572. // 控制块的弹出菜单文本
  573. CONTROLS_IF_IF: "如果",
  574. CONTROLS_IF_ELSEIF: "否则如果",
  575. CONTROLS_IF_ELSE: "否则",
  576. CONTROLS_IF_IF_TITLE: "如果",
  577. CONTROLS_IF_IF_TITLE_IF: "如果",
  578. CONTROLS_IF_ELSEIF_TITLE_ELSEIF: "否则如果",
  579. CONTROLS_IF_ELSE_TITLE: "否则",
  580. CONTROLS_IF_ELSE_TITLE_ELSE: "否则",
  581. // 条件块的提示文本和帮助URL
  582. CONTROLS_IF_HELPURL: "条件语句帮助",
  583. CONTROLS_IF_TOOLTIP_IF: "添加或删除条件。",
  584. CONTROLS_IF_TOOLTIP_ELSE: "添加或删除否则块。",
  585. // 添加设置按钮相关的中文配置
  586. CONTROLS_IF_ELSEIF_TOOLTIP: "否则如果条件为真,则执行相应的代码。",
  587. CONTROLS_IF_ELSE_TOOLTIP: "否则执行相应的代码。",
  588. // 比较运算符中文配置
  589. LOGIC_COMPARE_EQ: "等于",
  590. LOGIC_COMPARE_NEQ: "不等于",
  591. LOGIC_COMPARE_LT: "小于",
  592. LOGIC_COMPARE_LTE: "小于等于",
  593. LOGIC_COMPARE_GT: "大于",
  594. LOGIC_COMPARE_GTE: "大于等于",
  595. LOGIC_COMPARE_TOOLTIP_EQ: "比较两个值是否相等。",
  596. LOGIC_COMPARE_TOOLTIP_NEQ: "比较两个值是否不相等。",
  597. LOGIC_COMPARE_TOOLTIP_LT: "比较第一个值是否小于第二个值。",
  598. LOGIC_COMPARE_TOOLTIP_LTE: "比较第一个值是否小于或等于第二个值。",
  599. LOGIC_COMPARE_TOOLTIP_GT: "比较第一个值是否大于第二个值。",
  600. LOGIC_COMPARE_TOOLTIP_GTE: "比较第一个值是否大于或等于第二个值。",
  601. // 逻辑运算中文配置
  602. LOGIC_OPERATION_AND: "且",
  603. LOGIC_OPERATION_OR: "或",
  604. LOGIC_OPERATION_TOOLTIP_AND: "如果两个条件都为真,则结果为真。",
  605. LOGIC_OPERATION_TOOLTIP_OR: "如果任一条件为真,则结果为真。",
  606. // 布尔值配置
  607. LOGIC_BOOLEAN_TRUE: "真",
  608. LOGIC_BOOLEAN_FALSE: "假",
  609. LOGIC_BOOLEAN_TOOLTIP: "返回一个布尔值:真或假。",
  610. // 循环积木中文配置
  611. CONTROLS_REPEAT_TITLE: "重复%1次",
  612. CONTROLS_REPEAT_TOOLTIP: "重复执行内部代码指定次数。",
  613. CONTROLS_REPEAT_MSG_DO: "执行",
  614. CONTROLS_REPEAT_INPUT_DO: "执行",
  615. CONTROLS_WHILEUNTIL_MSG_DO: "执行",
  616. CONTROLS_WHILEUNTIL_INPUT_DO: "执行",
  617. CONTROLS_FOR_MSG_DO: "执行",
  618. CONTROLS_FOR_INPUT_DO: "执行",
  619. CONTROLS_FOR_EACH_MSG_DO: "执行",
  620. CONTROLS_FOR_EACH_INPUT_DO: "执行",
  621. // 数学积木中文配置
  622. MATH_NUMBER_TOOLTIP: "一个数字。在编辑器中双击以更改。",
  623. MATH_ADDITION_SYMBOL: "加",
  624. MATH_SUBTRACTION_SYMBOL: "减",
  625. MATH_MULTIPLICATION_SYMBOL: "乘",
  626. MATH_DIVISION_SYMBOL: "除",
  627. MATH_ARITHMETIC_TOOLTIP_ADD: "返回两个数的和。",
  628. MATH_ARITHMETIC_TOOLTIP_SUBTRACT: "返回第一个数减去第二个数的差。",
  629. MATH_ARITHMETIC_TOOLTIP_MULTIPLY: "返回两个数的积。",
  630. MATH_ARITHMETIC_TOOLTIP_DIVIDE: "返回第一个数除以第二个数的商。"
  631. };
  632. // 使用Object.assign来合并配置,避免直接修改导入对象
  633. Object.assign(Blockly.Msg, locale);
  634. }
  635. // 初始化Blockly工作区
  636. function initBlockly() {
  637. // 应用中文配置
  638. setupBlocklyChineseLocale();
  639. const toolbox = document.getElementById('toolbox');
  640. workspace = Blockly.inject('blocklyDiv', {
  641. toolbox: toolbox,
  642. collapse: false,
  643. comments: true,
  644. disable: false, // 设为false以允许编辑
  645. maxBlocks: Infinity,
  646. trashcan: true,
  647. horizontalLayout: false,
  648. toolboxPosition: 'start',
  649. toolboxCollapse: false, // 设置工具箱默认展开
  650. toolboxAlwaysExpanded: true, // 确保点击类别后保持展开状态
  651. css: true,
  652. media: 'https://unpkg.com/blockly/media/',
  653. rtl: false,
  654. scrollbars: true,
  655. sounds: false, // 禁用声音以提高性能
  656. oneBasedIndex: true,
  657. grid: {
  658. spacing: 20,
  659. length: 3,
  660. colour: "#ccc",
  661. snap: true
  662. },
  663. zoom: {
  664. controls: true,
  665. wheel: true,
  666. startScale: 1.0,
  667. maxScale: 3,
  668. minScale: 0.3,
  669. scaleSpeed: 1.2
  670. }
  671. });
  672. }
  673. // 平滑移动函数
  674. async function smoothMoveTo(targetX, targetY) {
  675. const startX = playerPosition.value.x;
  676. const startY = playerPosition.value.y;
  677. const startTime = performance.now();
  678. // 使用Promise包装动画过程,使其可以await
  679. return new Promise(resolve => {
  680. function animate(currentTime) {
  681. // 检查是否应该停止执行
  682. if (shouldStopExecution) {
  683. resolve();
  684. return;
  685. }
  686. const elapsed = currentTime - startTime;
  687. const progress = Math.min(elapsed / CONFIG.ANIMATION.MOVE_DURATION, 1); // 计算进度,最大为1
  688. // 线性插值计算当前位置
  689. const currentX = startX + (targetX - startX) * progress;
  690. const currentY = startY + (targetY - startY) * progress;
  691. gameState.player = {
  692. ...gameState.player,
  693. position: { x: currentX, y: currentY },
  694. };
  695. // 检查是否到达终点
  696. if (progress < 1) {
  697. // 继续动画
  698. requestAnimationFrame(animate);
  699. } else {
  700. resolve(); // 动画完成
  701. }
  702. }
  703. // 启动动画
  704. requestAnimationFrame(animate);
  705. });
  706. }
  707. // 处理地图类型逻辑
  708. async function switchMapType(type, isMapType = null) {
  709. //取人物当前位置
  710. let x = playerPosition.value.x;
  711. let y = playerPosition.value.y;
  712. let tileMap = walkablePointsMap.get(`${x},${y}`);
  713. //判断是否是指定地图类型
  714. if (isMapType) {
  715. return isMapType === tileMap.type;
  716. }
  717. //移动前置
  718. if (type === 0) {
  719. //判断方块类型并处理逻辑
  720. switch (tileMap.type) {
  721. case BLOCKLY_MAP_TYPE_DICT.TASK:
  722. await taskLogic(tileMap);
  723. break;
  724. }
  725. }else {//移动后置
  726. //判断方块类型并处理逻辑
  727. switch (tileMap.type) {
  728. case BLOCKLY_MAP_TYPE_DICT.ICE:
  729. do {
  730. showGameMessage(tileMap.tip, 'warning',300)
  731. // 处理方块类型逻辑
  732. await switchMapType(0);
  733. console.log("滑行前位置:" + playerPosition.value.x + "," + playerPosition.value.y);
  734. await slidingLogic();
  735. tileMap = walkablePointsMap.get(`${playerPosition.value.x},${playerPosition.value.y}`);
  736. }while (tileMap.type === BLOCKLY_MAP_TYPE_DICT.ICE && !isColliding.value)
  737. break;
  738. case BLOCKLY_MAP_TYPE_DICT.TRAP:
  739. showGameMessage(tileMap.tip, 'error')
  740. await handleWallCollision(tileMap.tip);
  741. break;
  742. }
  743. }
  744. }
  745. // 处理冰块滑行逻辑
  746. async function slidingLogic() {
  747. if (shouldStopExecution || isColliding.value) {
  748. return;
  749. }
  750. gameState.player.isSliding = true;
  751. try {
  752. // 计算下一个位置
  753. let nextX = playerPosition.value.x;
  754. let nextY = playerPosition.value.y;
  755. // 根据当前方向计算下一个位置
  756. switch(playerDirection.value) {
  757. case CONFIG.GAME.DIRECTIONS.UP: nextY--; break;
  758. case CONFIG.GAME.DIRECTIONS.RIGHT: nextX++; break;
  759. case CONFIG.GAME.DIRECTIONS.DOWN: nextY++; break;
  760. case CONFIG.GAME.DIRECTIONS.LEFT: nextX--; break;
  761. }
  762. // 检查下一个位置是否可行走
  763. if (isWalkable(nextX, nextY)) {
  764. // 执行平滑移动到下一个位置
  765. await smoothMoveTo(nextX, nextY);
  766. } else {
  767. // 使用统一的碰撞处理方法,但不等待延迟(因为slidingLogic不需要这个延迟)
  768. await handleWallCollision();
  769. }
  770. } catch (error) {
  771. console.error('滑行过程中发生错误:', error);
  772. } finally {
  773. // 无论如何都要确保滑行状态被重置
  774. gameState.player.isSliding = false;
  775. }
  776. }
  777. // 处理任务逻辑
  778. async function taskLogic(tileMap) {
  779. if (shouldStopExecution || isColliding.value) {
  780. return;
  781. }
  782. try {
  783. // 判断当前位置是否有特殊需求
  784. if (tileMap && tileMap.type === BLOCKLY_MAP_TYPE_DICT.TASK && tileMap.must === true && tileMap.status !== true) {
  785. await handleWallCollision(tileMap.unfinishedTip);
  786. return;
  787. }
  788. } catch (error) {
  789. console.error('处理任务逻辑发生错误:', error);
  790. }
  791. }
  792. // 物品拾取动画函数
  793. async function animateItemPickup(item, itemX, itemY) {
  794. // 创建临时动画元素
  795. const tempItem = document.createElement('div');
  796. const iconSize = tileSize.value * CONFIG.STYLES.PLAYER_SIZE_RATIO;
  797. const marginSize = tileSize.value * CONFIG.STYLES.PLAYER_SIZE_MARGIN;
  798. // 计算物品在地图上的实际位置
  799. const itemLeft = itemX * tileSize.value - tileSize.value + marginSize;
  800. const itemTop = itemY * tileSize.value - tileSize.value + marginSize;
  801. // 平行移动到容器位置,同时调整大小
  802. const finalSize = tileSize.value * CONFIG.STYLES.PLAYER_SIZE_RATIO * 0.3;
  803. // 获取携带物品容器的位置(左上角)
  804. // 注意:这里不需要考虑当前已携带物品数量,因为物品还没被添加
  805. let containerLeft = 30;
  806. let containerTop = 30;
  807. // 如果已经有物品,计算新物品应该出现的位置
  808. if (gameState.player.carriedItems.length > 0) {
  809. let containerSize = finalSize + 10;
  810. containerLeft = 30 + gameState.player.carriedItems.length * containerSize;
  811. }
  812. // 设置临时元素样式
  813. Object.assign(tempItem.style, {
  814. position: 'absolute',
  815. left: itemLeft + 'px',
  816. top: itemTop + 'px',
  817. width: iconSize + 'px',
  818. height: iconSize + 'px',
  819. backgroundImage: `url(${item.img})`,
  820. backgroundSize: 'contain',
  821. backgroundPosition: 'center',
  822. backgroundRepeat: 'no-repeat',
  823. zIndex: '100', // 确保在最上层
  824. opacity: '0', // 初始完全透明
  825. transform: 'scale(1)',
  826. });
  827. // 将临时元素添加到地图容器
  828. const mapBackground = document.querySelector('.map-background');
  829. if (!mapBackground) {
  830. console.error('地图容器未找到');
  831. return;
  832. }
  833. mapBackground.appendChild(tempItem);
  834. // 触发淡入动画
  835. tempItem.offsetHeight;
  836. tempItem.style.transition = 'opacity ' + CONFIG.DELAY.ITEMS_APPEAR + 'ms ease-out';
  837. tempItem.style.opacity = '1'; // 完全显示
  838. await new Promise(resolve => setTimeout(resolve, CONFIG.DELAY.ITEMS_APPEAR));
  839. // 移动动画
  840. tempItem.style.transition = 'all ' + CONFIG.DELAY.ITEMS_FLIGHT_ANIMATION_DELAY + 'ms ease-out'; // 减慢动画速度
  841. Object.assign(tempItem.style, {
  842. left: containerLeft + 'px',
  843. top: containerTop + 'px',
  844. width: finalSize + 'px',
  845. height: finalSize + 'px',
  846. opacity: '0.8',
  847. transform: 'scale(1)',
  848. });
  849. // 等待动画完成
  850. await new Promise(resolve => setTimeout(resolve, CONFIG.DELAY.ITEMS_FLIGHT_ANIMATION_DELAY));
  851. // 移除临时元素
  852. if (mapBackground.contains(tempItem)) {
  853. mapBackground.removeChild(tempItem);
  854. }
  855. }
  856. // 物品使用动画函数
  857. async function animateItemUse(item, itemIndex) {
  858. // 创建临时动画元素
  859. const tempItem = document.createElement('div');
  860. const finalSize = tileSize.value * CONFIG.STYLES.PLAYER_SIZE_RATIO * 0.3;
  861. const iconSize = tileSize.value * CONFIG.STYLES.PLAYER_SIZE_RATIO;
  862. const marginSize = tileSize.value * CONFIG.STYLES.PLAYER_SIZE_MARGIN;
  863. // 计算物品在物品栏中的初始位置
  864. let startLeft = 30;
  865. let startTop = 30;
  866. let containerSize = finalSize + 10;
  867. // 根据物品索引计算在物品栏中的位置
  868. if (itemIndex > 0) {
  869. startLeft = 30 + itemIndex * containerSize;
  870. }
  871. // 计算玩家当前位置(物品目标位置)
  872. const playerLeft = playerPosition.value.x * tileSize.value - tileSize.value + marginSize;
  873. const playerTop = playerPosition.value.y * tileSize.value - tileSize.value + marginSize;
  874. // 获取玩家元素
  875. const playerElement = document.querySelector('.player');
  876. const originalPlayerZIndex = playerElement ? playerElement.style.zIndex : '';
  877. // 获取当前位置的任务点元素
  878. let taskPointElement = null;
  879. const walkablePointsElements = document.querySelectorAll('.walkable-point');
  880. walkablePointsElements.forEach(el => {
  881. const rect = el.getBoundingClientRect();
  882. const targetRect = {
  883. left: playerLeft,
  884. top: playerTop,
  885. width: iconSize,
  886. height: iconSize
  887. };
  888. // 简单判断元素是否在玩家位置附近
  889. if (Math.abs(rect.left - targetRect.left) < iconSize &&
  890. Math.abs(rect.top - targetRect.top) < iconSize) {
  891. taskPointElement = el;
  892. }
  893. });
  894. // 先将任务图标置顶盖住人物
  895. if (taskPointElement) {
  896. taskPointElement.style.zIndex = '150';
  897. }
  898. // 设置临时元素初始样式
  899. Object.assign(tempItem.style, {
  900. position: 'absolute',
  901. left: startLeft + 'px',
  902. top: startTop + 'px',
  903. width: finalSize + 'px',
  904. height: finalSize + 'px',
  905. backgroundImage: `url(${item.img})`,
  906. backgroundSize: 'contain',
  907. backgroundPosition: 'center',
  908. backgroundRepeat: 'no-repeat',
  909. zIndex: '120',
  910. transition: 'transform ' + CONFIG.DELAY.ITEMS_APPEAR + 'ms ease-out',
  911. transform: 'scale(1)',
  912. });
  913. // 将临时元素添加到地图容器
  914. const mapBackground = document.querySelector('.map-background');
  915. if (!mapBackground) {
  916. console.error('地图容器未找到');
  917. return;
  918. }
  919. mapBackground.appendChild(tempItem);
  920. // 准备漂移动画
  921. await new Promise(resolve => setTimeout(resolve, CONFIG.DELAY.ITEMS_APPEAR));
  922. // 设置漂移动画样式
  923. tempItem.style.transition = 'all 1s ease-out';
  924. Object.assign(tempItem.style, {
  925. left: playerLeft + 'px',
  926. top: playerTop + 'px',
  927. width: iconSize + 'px',
  928. height: iconSize + 'px',
  929. });
  930. // 等待漂移到玩家位置
  931. await new Promise(resolve => setTimeout(resolve, CONFIG.DELAY.ITEMS_FLIGHT_ANIMATION_DELAY));
  932. // 物品使用效果动画
  933. tempItem.style.transition = 'transform 0.3s ease-in-out';
  934. tempItem.style.transform = 'scale(1.2)';
  935. await new Promise(resolve => setTimeout(resolve, CONFIG.DELAY.ITEMS_APPEAR));
  936. // 移除临时元素
  937. if (mapBackground.contains(tempItem)) {
  938. mapBackground.removeChild(tempItem);
  939. }
  940. // 延迟一段时间后将人物图标置顶,露出完整的人物图标
  941. await new Promise(resolve => setTimeout(resolve, CONFIG.DELAY.ITEMS_APPEAR));
  942. // 恢复层级关系
  943. if (taskPointElement) {
  944. taskPointElement.style.zIndex = '';
  945. }
  946. if (playerElement) {
  947. playerElement.style.zIndex = originalPlayerZIndex || '';
  948. }
  949. }
  950. // 拾取物品函数
  951. window.pickupItem = async function() {
  952. if (shouldStopExecution || isColliding.value || isSliding.value) {
  953. return;
  954. }
  955. //取人物当前位置
  956. let x = playerPosition.value.x;
  957. let y = playerPosition.value.y;
  958. let tileMap = walkablePointsMap.get(`${x},${y}`);
  959. // 判断是否是要拾取的方块类型
  960. if (tileMap && tileMap.type === BLOCKLY_MAP_TYPE_DICT.ITEM) {
  961. showGameMessage(tileMap.tip || CONFIG.TIPS.PICKUP_ITEM, 'warning')
  962. // 处理携带物品逻辑
  963. if (tileMap && tileMap.img) {
  964. // 从地图上移除图标(但保留点的可通行性)
  965. const pointIndex = gameState.mapData.walkablePoints.findIndex(
  966. p => p.x === x && p.y === y
  967. );
  968. if (pointIndex !== -1) {
  969. // 保留点但移除img属性
  970. const updatedPoint = { ...gameState.mapData.walkablePoints[pointIndex] };
  971. delete updatedPoint.img;
  972. gameState.mapData.walkablePoints.splice(pointIndex, 1, updatedPoint);
  973. // 更新映射
  974. walkablePointsMap.set(`${x},${y}`, updatedPoint);
  975. // 执行物品拾取动画:放大晃动两下然后移动到左上角物品容器
  976. await animateItemPickup(tileMap, x, y);
  977. // 将物品添加到玩家携带物品中
  978. gameState.player.carriedItems.push({
  979. ...tileMap,
  980. originalX: x,
  981. originalY: y
  982. });
  983. }
  984. }
  985. } else {
  986. showGameMessage(CONFIG.TIPS.NULL_PICKUP_ITEM, 'info')
  987. }
  988. await new Promise(resolve => setTimeout(resolve, CONFIG.DELAY.ACTION_DELAY));
  989. }
  990. // 使用物品函数
  991. window.useItem = async function() {
  992. if (shouldStopExecution || isColliding.value || isSliding.value) {
  993. return;
  994. }
  995. //取人物当前位置
  996. let x = playerPosition.value.x;
  997. let y = playerPosition.value.y;
  998. let tileMap = walkablePointsMap.get(`${x},${y}`);
  999. // 判断当前位置是否有特殊需求
  1000. if (tileMap && tileMap.type === BLOCKLY_MAP_TYPE_DICT.TASK) {
  1001. // 检查玩家是否携带了需要的物品
  1002. const requiredItems = tileMap.type;
  1003. let hasRequiredItem = false;
  1004. let itemIndex = -1;
  1005. if (gameState.player.carriedItems.length > 0) {
  1006. hasRequiredItem = true;
  1007. itemIndex = 0;
  1008. }
  1009. if (hasRequiredItem) {
  1010. // 获取要使用的物品
  1011. const itemToUse = gameState.player.carriedItems[itemIndex];
  1012. // 执行物品使用动画
  1013. await animateItemUse(itemToUse, itemIndex);
  1014. // 从携带物品中移除已使用的物品
  1015. gameState.player.carriedItems.splice(itemIndex, 1);
  1016. // 从地图上移除图标(但保留点的可通行性)
  1017. const pointIndex = gameState.mapData.walkablePoints.findIndex(
  1018. p => p.x === x && p.y === y
  1019. );
  1020. if (pointIndex !== -1) {
  1021. // 保留点但移除img属性并设置完成状态图标
  1022. const updatedPoint = { ...gameState.mapData.walkablePoints[pointIndex] };
  1023. updatedPoint.img = updatedPoint.endImg; // 设置完成状态图标
  1024. updatedPoint.status = true;
  1025. gameState.mapData.walkablePoints.splice(pointIndex, 1, updatedPoint);
  1026. // 更新映射
  1027. walkablePointsMap.set(`${x},${y}`, updatedPoint);
  1028. }
  1029. // 使用物品成功
  1030. showGameMessage(tileMap.finishedTip || CONFIG.TIPS.USE_ITEM_SUCCESS, 'success');
  1031. } else {
  1032. // 提示缺少所需物品
  1033. showGameMessage(tileMap.unfinishedTip || CONFIG.TIPS.USE_SPECIAL_ITEM, 'warning');
  1034. }
  1035. } else {
  1036. showGameMessage(CONFIG.TIPS.NO_USE_ITEM, 'info');
  1037. }
  1038. await new Promise(resolve => setTimeout(resolve, CONFIG.DELAY.ACTION_DELAY));
  1039. }
  1040. // 向前移动
  1041. window.moveForward = async function() {
  1042. if (shouldStopExecution || isColliding.value || isSliding.value) {
  1043. return;
  1044. }
  1045. let newX = playerPosition.value.x;
  1046. let newY = playerPosition.value.y;
  1047. // 向前移动
  1048. switch(playerDirection.value) {
  1049. case CONFIG.GAME.DIRECTIONS.UP: newY--; break;
  1050. case CONFIG.GAME.DIRECTIONS.RIGHT: newX++; break;
  1051. case CONFIG.GAME.DIRECTIONS.DOWN: newY++; break;
  1052. case CONFIG.GAME.DIRECTIONS.LEFT: newX--; break;
  1053. }
  1054. // 检查是否可以移动
  1055. if (isWalkable(newX, newY)) {
  1056. // 处理方块类型逻辑
  1057. await switchMapType(0);
  1058. // 使用平滑移动动画
  1059. await smoothMoveTo(newX, newY);
  1060. // 处理方块类型逻辑
  1061. await switchMapType(1);
  1062. await new Promise(resolve => setTimeout(resolve, CONFIG.DELAY.ACTION_DELAY));
  1063. } else {
  1064. // 发生碰撞 使用统一的碰撞处理方法
  1065. await handleWallCollision();
  1066. }
  1067. };
  1068. //向左转(逆时针旋转90度)
  1069. window.turnLeft = async function() {
  1070. // 如果已经发生过碰撞,不再执行任何旋转
  1071. if (shouldStopExecution || isColliding.value) {
  1072. return;
  1073. }
  1074. // 记录起始方向和目标方向
  1075. const startDirection = playerDirection.value;
  1076. const targetDirection = (playerDirection.value - 1 + 4) % 4;
  1077. // 实现平滑旋转
  1078. const startTime = performance.now();
  1079. // 使用 requestAnimationFrame 实现平滑动画
  1080. await new Promise(resolve => {
  1081. function animate(currentTime) {
  1082. // 检查是否应该停止执行
  1083. if (shouldStopExecution) {
  1084. resolve();
  1085. return;
  1086. }
  1087. const elapsedTime = currentTime - startTime;
  1088. const progress = Math.min(elapsedTime / CONFIG.ANIMATION.ROTATE_DURATION, 1);
  1089. // 在动画过程中更新方向
  1090. gameState.player.direction = startDirection - progress;
  1091. // 如果动画未完成,继续下一帧
  1092. if (progress < 1) {
  1093. requestAnimationFrame(animate);
  1094. } else {
  1095. // 动画完成后设置最终方向
  1096. gameState.player.direction = targetDirection;
  1097. resolve();
  1098. }
  1099. }
  1100. // 开始动画
  1101. requestAnimationFrame(animate);
  1102. });
  1103. await new Promise(resolve => setTimeout(resolve, CONFIG.DELAY.ACTION_DELAY));
  1104. };
  1105. //向右转(顺时针旋转90度)
  1106. window.turnRight = async function() {
  1107. // 如果已经发生过碰撞,不再执行任何旋转
  1108. if (shouldStopExecution || isColliding.value) {
  1109. return;
  1110. }
  1111. // 记录起始方向和目标方向
  1112. const startDirection = playerDirection.value;
  1113. const targetDirection = (playerDirection.value + 1) % 4;
  1114. // 实现平滑旋转
  1115. const startTime = performance.now();
  1116. // 使用 requestAnimationFrame 实现平滑动画
  1117. await new Promise(resolve => {
  1118. function animate(currentTime) {
  1119. // 检查是否应该停止执行
  1120. if (shouldStopExecution) {
  1121. resolve();
  1122. return;
  1123. }
  1124. const elapsedTime = currentTime - startTime;
  1125. const progress = Math.min(elapsedTime / CONFIG.ANIMATION.ROTATE_DURATION, 1);
  1126. // 处理从3到0的边界情况,确保顺时针旋转
  1127. let currentDirection;
  1128. if (startDirection === 3 && targetDirection === 0) {
  1129. // 对于从3到0的顺时针旋转,我们需要模拟+1的效果而不是-3
  1130. currentDirection = startDirection + progress;
  1131. // 当超过3.99时,设置为0(避免显示4)
  1132. if (currentDirection > 3.99) {
  1133. currentDirection = 0;
  1134. }
  1135. } else {
  1136. // 正常情况下的线性插值
  1137. currentDirection = startDirection + (targetDirection - startDirection) * progress;
  1138. }
  1139. // 在动画过程中更新方向
  1140. gameState.player.direction = currentDirection;
  1141. // 如果动画未完成,继续下一帧
  1142. if (progress < 1) {
  1143. requestAnimationFrame(animate);
  1144. } else {
  1145. // 动画完成后设置最终方向
  1146. gameState.player.direction = targetDirection;
  1147. resolve();
  1148. }
  1149. }
  1150. // 开始动画
  1151. requestAnimationFrame(animate);
  1152. });
  1153. await new Promise(resolve => setTimeout(resolve, CONFIG.DELAY.ACTION_DELAY));
  1154. };
  1155. // 向后转(旋转180度)
  1156. window.turnAround = async function() {
  1157. // 如果已经发生过碰撞,不再执行任何旋转
  1158. if (shouldStopExecution || isColliding.value) {
  1159. return;
  1160. }
  1161. // 记录起始方向和目标方向
  1162. const startDirection = playerDirection.value;
  1163. const targetDirection = (playerDirection.value + 2) % 4;
  1164. // 实现平滑旋转
  1165. const startTime = performance.now();
  1166. // 使用 requestAnimationFrame 实现平滑动画
  1167. await new Promise(resolve => {
  1168. function animate(currentTime) {
  1169. // 检查是否应该停止执行
  1170. if (shouldStopExecution) {
  1171. resolve();
  1172. return;
  1173. }
  1174. const elapsedTime = currentTime - startTime;
  1175. const progress = Math.min(elapsedTime / CONFIG.ANIMATION.TURN_AROUND_DURATION, 1);
  1176. // 在动画过程中更新方向
  1177. gameState.player.direction = startDirection + 2 * progress;
  1178. // 如果动画未完成,继续下一帧
  1179. if (progress < 1) {
  1180. requestAnimationFrame(animate);
  1181. } else {
  1182. // 动画完成后设置最终方向
  1183. gameState.player.direction = targetDirection;
  1184. resolve();
  1185. }
  1186. }
  1187. // 开始动画
  1188. requestAnimationFrame(animate);
  1189. });
  1190. await new Promise(resolve => setTimeout(resolve, CONFIG.DELAY.ACTION_DELAY));
  1191. };
  1192. //校验是否到达终点
  1193. window.isFinish = async function() {
  1194. // 如果已经发生过碰撞,不再执行任何检查
  1195. if (isColliding.value) {
  1196. return;
  1197. }
  1198. if (gameState.player.position.x === endPoint.value.x && gameState.player.position.y === endPoint.value.y) {
  1199. //检查是否有未完成的任务点
  1200. const pointIndex = gameState.mapData.walkablePoints.findIndex(
  1201. p => p.type === BLOCKLY_MAP_TYPE_DICT.TASK && p.status !== true
  1202. );
  1203. if (pointIndex !== -1) {
  1204. showGameMessage(CONFIG.TIPS.UNFINISHED, 'error');
  1205. return;
  1206. }
  1207. gameState.player.hasReachedEnd = true;
  1208. showGameMessage(CONFIG.TIPS.FINISH, 'success' );
  1209. }
  1210. };
  1211. // 运行代码
  1212. const runCode = async () => {
  1213. try {
  1214. await resetPlayer();
  1215. await new Promise(resolve => setTimeout(resolve, CONFIG.DELAY.RESET_DELAY));
  1216. // 重置执行标志,允许新的执行
  1217. shouldStopExecution = false;
  1218. // 创建新的AbortController用于取消执行
  1219. executionAbortController = new AbortController();
  1220. const signal = executionAbortController.signal;
  1221. // 确保生成器和工作区都存在
  1222. if (!javascriptGenerator || !workspace) {
  1223. throw new Error('生成器或工作区未正确初始化');
  1224. }
  1225. // 生成JavaScript代码
  1226. const code = javascriptGenerator.workspaceToCode(workspace) + "await isFinish();";
  1227. try {
  1228. // 增强的安全检查
  1229. const unsafePatterns = [
  1230. 'eval(', 'Function(', 'document.write', 'window.location',
  1231. 'document.createElement', 'XMLHttpRequest', 'fetch',
  1232. 'setInterval', 'setTimeout', 'window.',
  1233. 'alert(', 'confirm(', 'prompt(',
  1234. 'document.cookie', 'localStorage', 'sessionStorage'
  1235. ];
  1236. const hasUnsafeCode = unsafePatterns.some(pattern => code.includes(pattern));
  1237. if (hasUnsafeCode) {
  1238. throw new Error('代码包含不安全的操作');
  1239. }
  1240. // 包装代码为异步函数执行,并设置超时保护
  1241. currentExecutionPromise = new Promise(async (resolve, reject) => {
  1242. try {
  1243. // 检查信号是否已中止
  1244. if (signal.aborted) {
  1245. throw new Error('执行已取消');
  1246. }
  1247. // 添加信号监听
  1248. signal.addEventListener('abort', () => {
  1249. reject(new Error('执行已取消'));
  1250. });
  1251. const wrappedCode = `(async () => { ${code} })()`;
  1252. await new Function(wrappedCode)();
  1253. resolve();
  1254. } catch (error) {
  1255. reject(error);
  1256. }
  1257. });
  1258. } catch (error) {
  1259. // 捕获并显示执行错误
  1260. if (error.message !== '执行已取消') {
  1261. const errorMsg = error.message || '未知错误';
  1262. showGameMessage(`代码执行错误: ${errorMsg}`, 'error');
  1263. console.error('代码执行错误:', error);
  1264. }
  1265. } finally {
  1266. // 清除当前执行的Promise引用
  1267. currentExecutionPromise = null;
  1268. }
  1269. } catch (error) {
  1270. showGameMessage(`运行时错误: ${error.message || '未知错误'}`, 'error');
  1271. console.error('运行时错误:', error);
  1272. }
  1273. };
  1274. // 清空工作区
  1275. const clearWorkspace = () => {
  1276. workspace.clear();
  1277. showGameMessage('工作区已清空', 'info');
  1278. };
  1279. // 重置玩家位置和状态
  1280. const resetPlayer = () => {
  1281. // 设置标志强制停止所有执行
  1282. shouldStopExecution = true;
  1283. // 取消任何正在执行的代码
  1284. if (executionAbortController) {
  1285. executionAbortController.abort();
  1286. executionAbortController = null;
  1287. }
  1288. if (currentExecutionPromise) {
  1289. currentExecutionPromise = null;
  1290. }
  1291. // 重置携带的物品回地图
  1292. if (gameState.mapData.originalWalkablePoints.length > 0) {
  1293. gameState.mapData.walkablePoints = JSON.parse(JSON.stringify(gameState.mapData.originalWalkablePoints));
  1294. }
  1295. // 清空携带物品
  1296. gameState.player.carriedItems = [];
  1297. // 重新初始化可行走点集合
  1298. initWalkablePointsSet();
  1299. gameState.player.position = { ...startPoint.value };
  1300. gameState.player.direction = playerInitialDirection.value; // 重置为初始方向
  1301. gameState.player.isColliding = false; //碰撞标志
  1302. gameState.player.hasReachedEnd = false;
  1303. gameState.player.isSliding = false; // 重置滑行状态
  1304. };
  1305. // 更新地图容器尺寸的函数
  1306. function updateMapContainerDimensions() {
  1307. const mapContainer = document.querySelector('.map-container');
  1308. if (mapContainer) {
  1309. const rect = mapContainer.getBoundingClientRect();
  1310. // 确保获取到有效的尺寸值
  1311. if (rect.width > 0 && rect.height > 0) {
  1312. mapContainerDimensions.value = {
  1313. width: rect.width,
  1314. height: rect.height
  1315. };
  1316. } else {
  1317. // 若尺寸无效,使用默认容器尺寸(可根据实际情况调整)
  1318. mapContainerDimensions.value = {
  1319. width: 800,
  1320. height: 600
  1321. };
  1322. }
  1323. }
  1324. };
  1325. // 显示游戏消息
  1326. function showGameMessage(message, type = 'info', duration = CONFIG.DELAY.MESSAGE_DISPLAY) {
  1327. gameState.status.message = message;
  1328. gameState.status.messageType = type;
  1329. // 消息显示时间后自动清除消息
  1330. setTimeout(() => {
  1331. gameState.status.message = '';
  1332. }, duration);
  1333. }
  1334. // 统一处理撞到墙时的停止逻辑
  1335. async function handleWallCollision(endMsg = CONFIG.TIPS.NO_ENTRY) {
  1336. // 设置碰撞状态
  1337. gameState.player.isColliding = true;
  1338. // 显示错误消息
  1339. showGameMessage(endMsg, 'error');
  1340. // 立即中止整个代码执行
  1341. if (executionAbortController) {
  1342. executionAbortController.abort();
  1343. }
  1344. // 所有动画和移动操作立即停止
  1345. shouldStopExecution = true;
  1346. // 碰撞状态重置时间后取消碰撞状态
  1347. setTimeout(() => {
  1348. gameState.player.isColliding = false;
  1349. }, CONFIG.DELAY.COLLISION_RESET);
  1350. // 返回一个Promise,允许调用者等待碰撞延迟
  1351. return new Promise(resolve => setTimeout(resolve, CONFIG.DELAY.COLLISION_DELAY));
  1352. }
  1353. // 组件卸载时清理
  1354. onUnmounted(() => {
  1355. if (workspace) {
  1356. workspace.dispose();
  1357. }
  1358. window.removeEventListener('resize', updateMapContainerDimensions);
  1359. });
  1360. </script>
  1361. <style scoped lang="scss">
  1362. @use "sass:math";
  1363. @function rpx($px) {
  1364. @return math.div($px, 750) * 100vw;
  1365. }
  1366. //将tileSize属性绑定到CSS变量上
  1367. :root {
  1368. --tile-size: v-bind('tileSize + "px"');
  1369. }
  1370. .map-game-container {
  1371. position: fixed;
  1372. top: 0;
  1373. left: 0;
  1374. right: 0;
  1375. bottom: 0;
  1376. background: transparent;
  1377. overflow-y: auto;
  1378. }
  1379. /* 自定义滚动条样式 */
  1380. .map-game-container::-webkit-scrollbar {
  1381. width: rpx(2); /* 滚动条宽度 */
  1382. }
  1383. .map-game-container::-webkit-scrollbar-track {
  1384. background: #f1effd; /* 滚动条轨道背景色 */
  1385. border-radius: rpx(4);
  1386. }
  1387. .map-game-container::-webkit-scrollbar-thumb {
  1388. background: #e2ddfc; /* 滚动条滑块颜色 */
  1389. border-radius: rpx(4);
  1390. }
  1391. .map-game-container::-webkit-scrollbar-thumb:hover {
  1392. background: #e2ddfc; /* 滚动条滑块 hover 状态颜色 */
  1393. }
  1394. /* 游戏简介样式 */
  1395. .info-message-container {
  1396. display: flex;
  1397. flex-direction: column;
  1398. align-items: flex-start;
  1399. }
  1400. .message-item {
  1401. display: flex;
  1402. align-items: flex-start;
  1403. width: 100%;
  1404. margin-bottom: rpx(5);
  1405. }
  1406. /* 头像样式 */
  1407. .avatar {
  1408. margin-right: rpx(4);
  1409. flex-shrink: 0;
  1410. }
  1411. .avatar-image {
  1412. width: rpx(30);
  1413. height: rpx(30);
  1414. object-fit: cover;
  1415. }
  1416. /* 消息内容样式 */
  1417. .message-item {
  1418. flex: 1;
  1419. }
  1420. .message-item p {
  1421. margin: rpx(4) 0;
  1422. line-height: 1.6;
  1423. font-size: rpx(7);
  1424. text-align: left;
  1425. color: black;
  1426. background-color: #e6faff;
  1427. opacity: 0.8;
  1428. border-radius: rpx(4);
  1429. padding: rpx(6);
  1430. max-width: 100%;
  1431. }
  1432. .message-item p:first-child {
  1433. margin-top: 0;
  1434. font-weight: 500;
  1435. }
  1436. .message-item p:last-child {
  1437. margin-bottom: 0;
  1438. }
  1439. .title-box {
  1440. position: relative;
  1441. top: rpx(5);
  1442. padding-left: 15px;
  1443. z-index: 10;
  1444. display: flex;
  1445. flex-direction: column;
  1446. }
  1447. /* 右侧两个角为圆角的长方形格子样式 */
  1448. .game-badge {
  1449. width: rpx(80);
  1450. height: rpx(20);
  1451. margin-left: rpx(10);
  1452. background-color: #5fb5dc;
  1453. color: #fff;
  1454. border-radius: 0 rpx(20) rpx(20) 0;
  1455. display: flex;
  1456. align-items: center;
  1457. justify-content: center;
  1458. font-size: rpx(15);
  1459. font-weight: bold;
  1460. }
  1461. .box-icon {
  1462. display: flex;
  1463. align-items: center;
  1464. gap: 10px;
  1465. padding: 10px 20px;
  1466. background-color: rgba(255, 255, 255, 0.8);
  1467. border-radius: 30px;
  1468. backdrop-filter: blur(10px);
  1469. cursor: pointer;
  1470. transition: all 0.3s ease;
  1471. font-size: 16px;
  1472. color: #333;
  1473. font-weight: 500;
  1474. width: fit-content;
  1475. }
  1476. .box-icon:hover {
  1477. background-color: rgba(255, 255, 255, 0.9);
  1478. transform: translate(-3px);
  1479. }
  1480. .left-icon {
  1481. font-size: 18px;
  1482. }
  1483. .content {
  1484. display: flex;
  1485. flex-wrap: nowrap;
  1486. gap: 20px;
  1487. padding: 20px;
  1488. min-width: 1160px;
  1489. }
  1490. /* 地图区域样式 */
  1491. .map-section {
  1492. flex: 1;
  1493. min-width: 500px;
  1494. background: rgba(248, 249, 250, 0.82);
  1495. padding: 15px;
  1496. border-radius: 15px;
  1497. }
  1498. .map-container {
  1499. position: relative;
  1500. width: 100%;
  1501. height: 100%;
  1502. overflow: hidden; // 防止内容溢出
  1503. }
  1504. .map-background {
  1505. position: relative;
  1506. width: 100%;
  1507. height: 100%;
  1508. background-size: cover;
  1509. background-position: center;
  1510. background-repeat: no-repeat;
  1511. }
  1512. .map-image {
  1513. width: 100%;
  1514. height: 100%;
  1515. object-fit: cover;
  1516. }
  1517. /* 可行走区域样式 */
  1518. .walkable-point {
  1519. position: absolute;
  1520. //background-color: rgba(52, 152, 219, 0.2);
  1521. //border: 1px solid rgba(52, 152, 219, 0.5);
  1522. //box-sizing: border-box;
  1523. //是否显示可行路线
  1524. opacity: 1;
  1525. }
  1526. /* 玩家样式 */
  1527. .player {
  1528. position: absolute;
  1529. background-image: var(--player-image);
  1530. background-size: contain;
  1531. background-repeat: no-repeat;
  1532. background-position: center;
  1533. border-radius: 5px;
  1534. z-index: 10;
  1535. }
  1536. /* 碰撞动画 */
  1537. .player.collision {
  1538. animation: collision 0.5s ease-in-out;
  1539. }
  1540. @keyframes collision {
  1541. 0% { transform: rotate(var(--player-rotation)) translateX(0) translateY(0) scale(1); }
  1542. 25% { transform: rotate(var(--player-rotation)) translateX(-3px) translateY(-2px) scale(1.2); }
  1543. 50% { transform: rotate(var(--player-rotation)) translateX(3px) translateY(2px) scale(1.2); }
  1544. 75% { transform: rotate(var(--player-rotation)) translateX(-3px) translateY(-2px) scale(1.2); }
  1545. 100% { transform: rotate(var(--player-rotation)) translateX(0) translateY(0) scale(1); }
  1546. }
  1547. /* 滑行动画 */
  1548. @keyframes sliding {
  1549. 0% { transform: rotate(var(--player-rotation)) translateX(0) translateY(0); }
  1550. 25% { transform: rotate(var(--player-rotation)) translateX(2px) translateY(0); }
  1551. 75% { transform: rotate(var(--player-rotation)) translateX(-2px) translateY(0); }
  1552. 100% { transform: rotate(var(--player-rotation)) translateX(0) translateY(0); }
  1553. }
  1554. /* 携带物品容器 */
  1555. .carried-items-container {
  1556. position: absolute;
  1557. top: 20px;
  1558. left: 20px;
  1559. background: rgba(255, 255, 255, 0.8);
  1560. border: 2px solid #3498db;
  1561. border-radius: 10px;
  1562. padding: 10px;
  1563. display: flex;
  1564. gap: 10px;
  1565. z-index: 15;
  1566. backdrop-filter: blur(10px);
  1567. animation: fadeInScale 0.5s ease-out;
  1568. }
  1569. /* 淡入缩放动画 */
  1570. @keyframes fadeInScale {
  1571. 0% {
  1572. opacity: 0;
  1573. transform: scale(0.5) translateY(-10px);
  1574. }
  1575. 100% {
  1576. opacity: 1;
  1577. transform: scale(1) translateY(0);
  1578. }
  1579. }
  1580. /* 携带物品样式 */
  1581. .carried-item {
  1582. animation: bounceIn 0.3s ease-out forwards;
  1583. opacity: 0;
  1584. }
  1585. /* 弹入动画 */
  1586. @keyframes bounceIn {
  1587. 0% {
  1588. opacity: 0;
  1589. transform: scale(0.3) translateY(-20px);
  1590. }
  1591. 50% {
  1592. opacity: 0.7;
  1593. transform: scale(1.1) translateY(5px);
  1594. }
  1595. 80% {
  1596. opacity: 0.9;
  1597. transform: scale(0.95) translateY(-2px);
  1598. }
  1599. 100% {
  1600. opacity: 1;
  1601. transform: scale(1) translateY(0);
  1602. }
  1603. }
  1604. /* 成功到达终点动画 */
  1605. .player.success {
  1606. animation: success 1s ease-in-out;
  1607. }
  1608. @keyframes success {
  1609. 0% { transform: rotate(var(--player-rotation)) scale(1); }
  1610. 10% { transform: rotate(var(--player-rotation)) scale(1.2) translateX(-5px) translateY(-5px); }
  1611. 20% { transform: rotate(var(--player-rotation)) scale(1.3) translateX(5px) translateY(5px); }
  1612. 30% { transform: rotate(var(--player-rotation)) scale(1.2) translateX(-5px) translateY(-5px); }
  1613. 40% { transform: rotate(var(--player-rotation)) scale(1.3) translateX(5px) translateY(5px); }
  1614. 50% { transform: rotate(var(--player-rotation)) scale(1.4) translateX(0) translateY(0); }
  1615. 60% { transform: rotate(var(--player-rotation)) scale(1.3) translateX(-3px) translateY(-3px); }
  1616. 70% { transform: rotate(var(--player-rotation)) scale(1.2) translateX(3px) translateY(3px); }
  1617. 80% { transform: rotate(var(--player-rotation)) scale(1.3) translateX(-3px) translateY(-3px); }
  1618. 90% { transform: rotate(var(--player-rotation)) scale(1.2) translateX(3px) translateY(3px); }
  1619. 100% { transform: rotate(var(--player-rotation)) scale(1); }
  1620. }
  1621. /* 游戏消息样式 */
  1622. .game-message {
  1623. position: absolute;
  1624. top: 20px;
  1625. left: 50%;
  1626. transform: translateX(-50%);
  1627. padding: 10px 20px;
  1628. border-radius: 5px;
  1629. font-weight: bold;
  1630. z-index: 20;
  1631. min-width: 200px;
  1632. text-align: center;
  1633. }
  1634. .game-message.success {
  1635. background-color: #d4edda;
  1636. color: #155724;
  1637. border: 1px solid #c3e6cb;
  1638. }
  1639. .game-message.error {
  1640. background-color: #f8d7da;
  1641. color: #721c24;
  1642. border: 1px solid #f5c6cb;
  1643. }
  1644. .game-message.info {
  1645. background-color: #d1ecf1;
  1646. color: #0c5460;
  1647. border: 1px solid #bee5eb;
  1648. }
  1649. .game-message.warning {
  1650. background-color: #baeff8;
  1651. color: #035767;
  1652. border: 1px solid #9be9f6;
  1653. }
  1654. /* Blockly区域样式 */
  1655. .blockly-section {
  1656. flex: 1;
  1657. min-width: 600px;
  1658. display: flex;
  1659. flex-direction: column;
  1660. gap: 20px;
  1661. }
  1662. // 合并重复的区块样式
  1663. .map-section, .toolbox-section, .workspace-section {
  1664. background: rgba(248, 249, 250, 0.82);
  1665. padding: 15px;
  1666. border-radius: 15px;
  1667. height: 100%;
  1668. }
  1669. .map-section h2, .toolbox-section h2, .workspace-section h2 {
  1670. margin-bottom: 15px;
  1671. color: #2c3e50;
  1672. border-bottom: 2px solid #3498db;
  1673. padding-bottom: 8px;
  1674. }
  1675. // 合并重复的区块背景样式
  1676. .map-section,
  1677. .toolbox-section,
  1678. .workspace-section {
  1679. background: rgba(248, 249, 250, 0.82);
  1680. padding: 15px;
  1681. border-radius: 15px;
  1682. }
  1683. #blocklyDiv {
  1684. height: rpx(300);
  1685. // min-height: 500px;
  1686. width: 100%;
  1687. background: #fff;
  1688. border: 1px solid #ddd;
  1689. border-radius: 8px;
  1690. }
  1691. /* 优化Blockly积木样式 */
  1692. /* 增加积木高度 */
  1693. .blocklyBlockCanvas .blocklyBlock {
  1694. height: 45px; /* 增加默认高度 */
  1695. min-height: 45px;
  1696. }
  1697. /* 增加积木内部元素的行高和间距 */
  1698. .blocklyText {
  1699. font-size: 16px;
  1700. line-height: 20px;
  1701. font-weight: 500;
  1702. }
  1703. /* 增加输入字段的高度 */
  1704. .blocklyHtmlInput {
  1705. height: 30px;
  1706. font-size: 14px;
  1707. padding: 5px;
  1708. }
  1709. /* 增加下拉菜单的高度 */
  1710. .blocklyDropdownMenu {
  1711. line-height: 28px;
  1712. font-size: 14px;
  1713. }
  1714. /* 优化积木圆角和阴影效果 */
  1715. .blocklyBlock {
  1716. border-radius: 8px;
  1717. filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.1));
  1718. }
  1719. /* 增加积木之间的连接点间距 */
  1720. .blocklyConnection {
  1721. height: 20px;
  1722. width: 20px;
  1723. }
  1724. /* 增加工具箱中积木的高度 */
  1725. .blocklyTreeRow {
  1726. height: 40px;
  1727. line-height: 40px;
  1728. }
  1729. .controls {
  1730. display: flex;
  1731. gap: 10px;
  1732. margin: 15px 0px;
  1733. flex-wrap: wrap;
  1734. }
  1735. button {
  1736. padding: 10px 20px;
  1737. border: none;
  1738. border-radius: 5px;
  1739. background: #3498db;
  1740. color: #fff;
  1741. font-weight: 700;
  1742. cursor: pointer;
  1743. transition: all 0.3s ease;
  1744. }
  1745. button:hover {
  1746. background: #2980b9;
  1747. transform: translateY(-2px);
  1748. box-shadow: 0 5px 10px rgba(0, 0, 0, 0.1);
  1749. }
  1750. #runCode {
  1751. background: #e74c3c;
  1752. }
  1753. #runCode:hover {
  1754. background: #c0392b;
  1755. }
  1756. /* 响应式布局 */
  1757. @media (max-width: 1200px) {
  1758. .map-section,
  1759. .blockly-section {
  1760. flex: 1;
  1761. min-width: 45%;
  1762. }
  1763. .map-background {
  1764. width: 100%;
  1765. height: 400px;
  1766. }
  1767. }
  1768. </style>