MapGame.vue 78 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803
  1. <template>
  2. <div class="title-box">
  3. <!-- 左侧标题部分 -->
  4. <div class="left-container">
  5. <!-- 线路切换按钮 -->
  6. <div class="route-buttons">
  7. <button
  8. v-for="(route, index) in parsedRouteList"
  9. :key="index"
  10. class="game-badge"
  11. :class="{ 'active': currentRouteIndex === index }"
  12. >
  13. {{ gameSort }}{{ index + 1 }}
  14. </button>
  15. </div>
  16. </div>
  17. </div>
  18. <!-- 中间地图组件部分-->
  19. <div class="content">
  20. <!-- 地图显示区域 -->
  21. <div class="map-section">
  22. <!-- 内容简介提示 -->
  23. <div v-if="currentGameData?.info" class="info-message-container">
  24. <div class="message-item">
  25. <div class="avatar">
  26. <img src="@/assets/images/xiaozhi2.png" alt="头像" class="avatar-image" />
  27. </div>
  28. <p v-if="currentGameData?.info" v-html="currentGameData?.info"></p>
  29. </div>
  30. </div>
  31. <div class="map-container">
  32. <!-- 地图背景 -->
  33. <div class="map-background">
  34. <img :src="mapBackground" alt="地图背景" class="map-image" @load="updateMapContainerDimensions" />
  35. <!-- 可行走区域标记 -->
  36. <div
  37. v-for="(point, index) in walkablePoints"
  38. :key="index"
  39. class="walkable-point"
  40. :style="getPointStyle(point)"
  41. >
  42. </div>
  43. <!-- 玩家角色 -->
  44. <div
  45. class="player"
  46. :style="playerStyle"
  47. :class="{ 'collision': isColliding, 'success': hasReachedEnd }"
  48. >
  49. </div>
  50. <!-- 怀表倒计时容器 -->
  51. <div v-if="showCountdown" class="watch-container" :style="countdownStyle">
  52. <div class="watch-face">
  53. <div class="watch-center"></div>
  54. <div class="watch-hands">
  55. <div class="watch-hand hour-hand"></div>
  56. <div class="watch-hand minute-hand"></div>
  57. </div>
  58. <div class="watch-countdown-number">{{ countdownValue }}</div>
  59. </div>
  60. </div>
  61. <!-- 携带物品容器 -->
  62. <div class="carried-items-container" v-show="gameState.player.carriedItems.length > 0">
  63. <div
  64. v-for="(item, index) in gameState.player.carriedItems"
  65. :key="index"
  66. class="carried-item"
  67. :style="getCarriedItemStyle(index, item)"
  68. ></div>
  69. </div>
  70. </div>
  71. <!-- 游戏状态提示 -->
  72. <div v-if="gameMessage" :class="['game-message', messageType]">
  73. {{ gameMessage }}
  74. </div>
  75. </div>
  76. </div>
  77. <!-- Blockly工作区 -->
  78. <div class="blockly-section">
  79. <div class="workspace-section">
  80. <div class="controls">
  81. <button id="runCode" @click="resetPlayer();runCode();" :disabled="isRunning">运行代码</button>
  82. <!-- <button @click="generatePythonCode">生成Python代码</button>-->
  83. <button @click="resetPlayer" >重置玩家</button>
  84. <button @click="clearWorkspace">清空工作区</button>
  85. </div>
  86. <!-- 工具箱-->
  87. <div id="toolbox"></div>
  88. <!-- 工作区-->
  89. <div id="blocklyDiv"></div>
  90. </div>
  91. </div>
  92. </div>
  93. <!-- 全屏遮罩盒子 通关后弹框得奖杯 -->
  94. <div class="fullscreen-overlay" v-if="showOverlay">
  95. <div
  96. class="centered-box"
  97. :style="{
  98. backgroundImage: `url(${passConfig.passBackground})`,
  99. backgroundSize: '110%',
  100. backgroundPosition: 'center',
  101. backgroundRepeat: 'no-repeat'
  102. }">
  103. <!-- 彩带飘落效果 -->
  104. <div class="confetti-container" v-if="passConfig.passStar > 0">
  105. <div v-for="n in 50" :key="n" class="confetti" :class="`confetti-${n % 12 + 1}`"></div>
  106. </div>
  107. <!-- 关闭按钮 -->
  108. <div class="close-button" @click="closeOverlay">×</div>
  109. <!-- 题目 -->
  110. <div class="top-box">
  111. <div class="title-text">{{passConfig.title}}</div>
  112. </div>
  113. <!-- 奖杯 -->
  114. <div class="middle-box">
  115. <img :src="passConfig.passTrophy" alt="奖杯" class="gold-cup-image" />
  116. </div>
  117. <!-- 星星 -->
  118. <div class="bottom-box">
  119. <img
  120. v-for="index in passConfig.starTotal"
  121. :key="index"
  122. :src="index <= passConfig.passStar ? star01 : star02"
  123. alt="星星"
  124. class="gold-star-image"
  125. :class="index % 2 === 0 ? 'star-bottom' : 'star-top'"
  126. />
  127. </div>
  128. </div>
  129. </div>
  130. </template>
  131. <script setup>
  132. import {ref, onMounted, onUnmounted, reactive, computed, nextTick, watch, defineEmits} from 'vue';
  133. import { javascriptGenerator } from "blockly/javascript";
  134. import { pythonGenerator } from "blockly/python";
  135. import playerImage from '@/assets/images/blockly/user.png';
  136. // 导入音频文件
  137. import passMp3 from '@/assets/music/blockly/pass.MP3';
  138. import failureMp3 from '@/assets/music/blockly/failure.MP3';
  139. import passRouteMp3 from '@/assets/music/blockly/pass_route.MP3';
  140. import errorMp3 from '@/assets/music/blockly/error.MP3'
  141. // 游戏接口数据
  142. import { registerCustomBlocks, registerJavaScriptGenerators, registerPythonGenerators, initBlockly, BLOCKLY_MAP_TYPE_DICT, BLOCKLY_MAP_SPECIAL_DICT } from '@/api/blockly/blockly.js';
  143. import {playMp3} from "@/api/blockly/music.js";
  144. import cupbg from '@/assets/blockly/cupbg.png' // 通关后奖杯底图
  145. import nocupbg from '@/assets/blockly/nocupbg.png' // 通关失败后奖杯底图
  146. import goldcup from '@/assets/blockly/goldcup.png' // 金杯
  147. import silvercup from '@/assets/blockly/silvercup.png' // 银杯
  148. import coppercup from '@/assets/blockly/coppercup.png' // 铜杯
  149. import nocup from '@/assets/blockly/nocup.png' // 无杯状态
  150. import star02 from '@/assets/blockly/star02.png' // 星星
  151. import star01 from '@/assets/blockly/star01.png' // 金星
  152. import passFailure from '@/assets/music/blockly/pass_failure.MP3' // 通关失败音效
  153. import passFireworks from '@/assets/music/blockly/pass_fireworks.MP3' // 通关小号音效
  154. import passTrump from '@/assets/music/blockly/pass_trumpet.MP3' // 通关庆祝礼炮奖杯
  155. // 星星数量,可根据需要调整
  156. const passConfig = ref({
  157. starTotal: 3,// 总星星数量
  158. title: 'YOU WIN!',// 通关标题
  159. passStar: 0,// 已通关星星数量
  160. passTrophy: "",// 通关奖杯
  161. passBackground: "",// 通关后奖杯底图
  162. });
  163. // 定义emits
  164. const emits = defineEmits(['saveProgress'])
  165. // 定义组件属性
  166. const props = defineProps({
  167. // 游戏ID
  168. gameId: {
  169. type: [String, Number],
  170. default: ''
  171. },
  172. // 地图背景
  173. mapBackground: {
  174. type: String,
  175. default: ''
  176. },
  177. // 地图瓦片大小
  178. mapTileSize: {
  179. type: [String, Object],
  180. default: ''
  181. },
  182. // 可行走点
  183. mapWalkablePoints: {
  184. type: [String, Array],
  185. default: ''
  186. },
  187. // 用户图片
  188. userImage: {
  189. type: String,
  190. default: ''
  191. },
  192. // 游戏信息
  193. info: {
  194. type: String,
  195. default: ''
  196. },
  197. // 路线列表
  198. routeList: {
  199. type: [String, Array],
  200. default: ''
  201. },
  202. // 课程列表
  203. courseList: {
  204. type: Array,
  205. default: () => []
  206. },
  207. // 当前课程索引
  208. currentIndex: {
  209. type: Number,
  210. default: 0
  211. },
  212. // 特殊积木方法标识集合
  213. blocklySpecialBlocks: {
  214. type: Array,
  215. default: () => []
  216. }
  217. });
  218. // 配置常量
  219. const CONFIG = {
  220. // 动画时长配置(毫秒)
  221. ANIMATION: {
  222. MOVE_DURATION: 500, // 移动动画持续时间
  223. ROTATE_DURATION: 500, // 旋转动画持续时间(左转/右转)
  224. TURN_AROUND_DURATION: 750, // 向后转动画持续时间
  225. },
  226. // 延迟配置(毫秒)
  227. DELAY: {
  228. ACTION_DELAY: 200, // 每次动作后的延迟时间
  229. COLLISION_DELAY: 300, // 碰撞后的延迟时间
  230. RESET_DELAY: 300, // 重置后的延迟时间
  231. MESSAGE_DISPLAY: 2000, // 消息显示时间
  232. ICE_MESSAGE_DISPLAY: 300, // 冰块消息显示时间
  233. COLLISION_RESET: 1000, // 碰撞状态重置时间
  234. LOOP_PREVENTION: 10, // 循环防止UI阻塞的延迟
  235. ITEMS_APPEAR: 300, // 物品出现延迟时间
  236. ITEMS_FLIGHT_ANIMATION_DELAY: 800, // 飞行动画延迟时间
  237. PALY_MP3_TIMES: 1000, // 播放MP3文件的时间间隔
  238. VIDEO_TIMEOUT: 5000, // 视频播放超时处理时间
  239. VIDEO_FADE_DURATION: 500, // 视频淡入淡出持续时间
  240. PLAYER_FADE_DURATION: 500, // 玩家淡入淡出持续时间
  241. TEMP_ITEM_FADE_DURATION: 500, // 临时物品淡入淡出持续时间
  242. COMPLETION_DISPLAY: 1000, // 任务完成后显示延迟时间
  243. },
  244. // 游戏配置
  245. GAME: {
  246. MAX_LOOP_COUNT: 100, // 最大循环次数
  247. DIRECTIONS: {
  248. UP: 0,
  249. RIGHT: 1,
  250. DOWN: 2,
  251. LEFT: 3
  252. }
  253. },
  254. // 样式配置
  255. STYLES: {
  256. DEFAULT_TILE_SIZE: 143, // 默认瓦片大小
  257. PLAYER_SIZE_RATIO: 0.8, // 玩家大小占瓦片的比例
  258. PLAYER_SIZE_MARGIN: 0.1, // 玩家大小占瓦片边距
  259. IMG_SIZE_RATIO: 0.8, // 地图素材大小占瓦片的比例
  260. IMG_SIZE_MARGIN: 0.1, // 地图素材大小占瓦片边距
  261. ITEM_CONTAINER_POSITION: { x: 30, y: 30 }, // 物品容器位置
  262. ITEM_CONTAINER_RATIO: 0.4, // 物品大小占容器的比例
  263. ITEM_CONTAINER_SPACING: 10 // 物品大小占容器的间距
  264. },
  265. //提示语
  266. TIPS: {
  267. NO_ENTRY: '当前位置无通路,无法移动',
  268. UNFINISHED: '任务未完成!',
  269. EFFORT: '再接再厉!',
  270. PASS_ROUTE: '恭喜你通过了当前路线,自动进入下一条路线!',
  271. FINISH: '恭喜你到达终点!',
  272. PICKUP_ITEM: '拾取物品成功!',
  273. NULL_PICKUP_ITEM: '当前位置没有可拾取的物品',
  274. USE_ITEM_SUCCESS: '使用物品成功',
  275. USE_SPECIAL_ITEM: '需要特殊物品才能使用',
  276. NO_USE_ITEM: '当前位置不需要使用物品',
  277. ERROR_ERROR: '错了错了',//任务消失时的错误提示
  278. }
  279. };
  280. // 路由和游戏状态
  281. const currentGameData = ref(null);
  282. const playerInitialDirection = ref(0); // 人物初始朝向
  283. const gameSort = ref('路线'); // 默认排序
  284. const currentRouteIndex = ref(0); // 当前线路索引
  285. // 路线通过状态:存储各路线的通过状态
  286. const routePassedStatus = ref([]);
  287. // 解析routeList
  288. const parsedRouteList = computed(() => {
  289. if (!props.routeList) return [];
  290. try {
  291. const routeData = typeof props.routeList === 'string' ? JSON.parse(props.routeList) : props.routeList;
  292. const routes = Array.isArray(routeData) ? routeData : [];
  293. // 初始化路线通过状态
  294. if (routePassedStatus.value.length === 0 && routes.length > 0) {
  295. routePassedStatus.value = routes.map((_, index) => index === 0); // 第一条路线默认可用
  296. }
  297. return routes;
  298. } catch (error) {
  299. console.error('解析routeList失败:', error);
  300. return [];
  301. }
  302. });
  303. // 运行控制标志
  304. let shouldStopExecution = false;
  305. let currentExecutionPromise = null;
  306. let executionAbortController = null;
  307. // 添加响应式的容器尺寸
  308. const mapContainerDimensions = ref({ width: 0, height: 0 });
  309. // 用于控制运行/重置按钮状态
  310. const isRunning = ref(false);
  311. // 存储生成的Python代码
  312. const generatedPythonCode = ref('');
  313. // 暂停模块-倒计时相关状态
  314. const showCountdown = ref(false);
  315. const countdownValue = ref(0);
  316. // Blockly相关状态
  317. let workspace = null;
  318. // 使用 Map 存储可行走点及其类型,提高查询效率
  319. let walkablePointsMap = new Map();
  320. // 创建游戏状态的响应式对象
  321. const gameState = reactive({
  322. // 地图配置信息
  323. mapConfig: {
  324. // 地图背景图片路径
  325. background: '',
  326. // 每个瓦片的尺寸(像素)
  327. tileSize: CONFIG.STYLES.DEFAULT_TILE_SIZE,
  328. },
  329. // 玩家相关状态
  330. player: {
  331. // 玩家当前位置坐标
  332. position: {},
  333. // 玩家当前朝向:0=上, 1=右, 2=下, 3=左
  334. direction: 1,
  335. // 是否正在发生碰撞
  336. isColliding: false,
  337. // 是否已到达终点
  338. hasReachedEnd: false,
  339. // 是否正在冰块上滑行
  340. isSliding: false,
  341. // 携带的物品数组
  342. carriedItems: [],
  343. },
  344. // 游戏状态信息
  345. status: {
  346. // 当前显示的游戏消息
  347. message: '',
  348. // 消息类型(如success、error、info等)
  349. messageType: ''
  350. },
  351. // 地图数据信息
  352. mapData: {
  353. // 游戏起点位置
  354. startPoint: {},
  355. // 游戏终点位置
  356. endPoint: {},
  357. // 地图上所有可行走的点坐标集合,添加type属性区分普通点和冰块
  358. walkablePoints: [],
  359. // 路线列表
  360. routeList: [],
  361. // 保存原始的可行走点数据,用于重置
  362. originalWalkablePoints: [],
  363. }
  364. });
  365. // 控制遮罩层显示的状态
  366. const showOverlay = ref(false);
  367. // 字典常量已从blockly.js中导入
  368. // 计算属性 - 提高性能和可读性
  369. const mapBackground = computed(() => gameState.mapConfig.background);
  370. // const tileSize = computed(() => gameState.mapConfig.tileSize);
  371. const walkablePoints = computed(() => gameState.mapData.walkablePoints);
  372. const startPoint = computed(() => gameState.mapData.startPoint);
  373. const endPoint = computed(() => gameState.mapData.endPoint);
  374. const playerPosition = computed(() => gameState.player.position);
  375. const playerDirection = computed(() => gameState.player.direction);
  376. const isColliding = computed(() => gameState.player.isColliding);
  377. const hasReachedEnd = computed(() => gameState.player.hasReachedEnd);
  378. const gameMessage = computed(() => gameState.status.message);
  379. const messageType = computed(() => gameState.status.messageType);
  380. const isSliding = computed(() => gameState.player.isSliding);
  381. // 计算玩家图片路径,优先使用接口数据
  382. const playerImageSrc = computed(() => {
  383. if (currentGameData.value && currentGameData.value.userImage) {
  384. return currentGameData.value.userImage.trim();
  385. }
  386. return playerImage;
  387. });
  388. // 计算实际瓦片大小(基于容器尺寸和地图数据)
  389. const tileSize = computed(() => {
  390. if (mapContainerDimensions.value.width === 0 || mapContainerDimensions.value.height === 0) {
  391. return gameState.mapConfig.tileSize;
  392. }
  393. // 获取地图数据中的最大坐标
  394. let size = JSON.parse(gameState.mapConfig.tileSize);
  395. // 计算基于容器的瓦片大小,确保地图完全可见
  396. const tileWidth = mapContainerDimensions.value.width / size.x;
  397. const tileHeight = mapContainerDimensions.value.height / size.y;
  398. // 返回较小的值以确保地图完全可见
  399. return Math.min(tileWidth, tileHeight);
  400. });
  401. // 计算玩家样式
  402. const playerStyle = computed(() => ({
  403. left: playerPosition.value.x * tileSize.value - tileSize.value + 'px',
  404. top: playerPosition.value.y * tileSize.value - tileSize.value + 'px',
  405. transform: `rotate(${playerDirection.value * 90}deg)`,
  406. '--player-rotation': `${playerDirection.value * 90}deg`,
  407. '--player-image': `url(${playerImageSrc.value})`,
  408. width: (tileSize.value * CONFIG.STYLES.PLAYER_SIZE_RATIO) + 'px',
  409. height: (tileSize.value * CONFIG.STYLES.PLAYER_SIZE_RATIO) + 'px',
  410. margin: (tileSize.value * CONFIG.STYLES.PLAYER_SIZE_MARGIN) + 'px',
  411. }));
  412. // 暂停倒计时样式计算
  413. const countdownStyle = computed(() => {
  414. return {
  415. position: 'absolute',
  416. left: playerPosition.value.x * tileSize.value - tileSize.value - (tileSize.value * 0.2) + 'px',
  417. top: playerPosition.value.y * tileSize.value - tileSize.value - (tileSize.value * 0.2) + 'px',
  418. width: tileSize.value * 0.5 + 'px',
  419. height: tileSize.value * 0.5 + 'px',
  420. display: 'flex',
  421. justifyContent: 'center',
  422. alignItems: 'center',
  423. zIndex: 100
  424. };
  425. });
  426. // 监听props变化,当地图数据变化时更新游戏数据
  427. watch(() => [props.gameId, props.mapBackground, props.mapWalkablePoints], async () => {
  428. // 获取游戏数据
  429. await fetchGameData();
  430. // 初始化可行走点集合
  431. initWalkablePointsSet();
  432. // 重置玩家位置
  433. await resetPlayer();
  434. await nextTick();
  435. // 更新容器尺寸
  436. updateMapContainerDimensions();
  437. }, { deep: true });
  438. // 生命周期钩子
  439. onMounted(async () => {
  440. // 获取游戏数据
  441. await fetchGameData();
  442. // 初始化可行走点集合
  443. initWalkablePointsSet();
  444. // 注册自定义积木
  445. registerCustomBlocks(props.blocklySpecialBlocks);
  446. // 注册JavaScript生成器
  447. registerJavaScriptGenerators(props.blocklySpecialBlocks);
  448. // 注册Python生成器
  449. registerPythonGenerators(props.blocklySpecialBlocks);
  450. // 动态生成工具箱XML
  451. const toolboxContainer = document.getElementById('toolbox');
  452. toolboxContainer.innerHTML = generateToolboxXml();
  453. // 初始化Blockly工作区
  454. workspace = initBlockly();
  455. // 重置玩家位置
  456. await resetPlayer();
  457. await nextTick();
  458. // 添加对窗口大小变化的监听
  459. updateMapContainerDimensions();
  460. window.addEventListener('resize', updateMapContainerDimensions);
  461. });
  462. //================初始化=====================
  463. // 动态生成工具箱XML
  464. function generateToolboxXml() {
  465. let toolboxXml = `
  466. <category name="移动控制" colour="230">
  467. <block type="move_forward"></block>
  468. <block type="move_backward"></block>
  469. <block type="turn_left"></block>
  470. <block type="turn_right"></block>
  471. <block type="turn_around"></block>
  472. <block type="move_forward_param"></block>
  473. <block type="move_backward_param"></block>
  474. </category>
  475. <category name="功能" colour="120">
  476. <block type="pickup_item"></block>
  477. <block type="use_item"></block>
  478. <block type="when_passed"></block>
  479. `;
  480. // 确保blocklySpecialBlocks是数组
  481. const specialBlocks = Array.isArray(props.blocklySpecialBlocks) ? props.blocklySpecialBlocks : [];
  482. // 获取所有特殊积木类型
  483. const specialBlockTypes = Object.values(BLOCKLY_MAP_SPECIAL_DICT);
  484. specialBlockTypes.forEach(blockType => {
  485. if (specialBlocks.includes(blockType)) {
  486. toolboxXml += `<block type="${blockType}"></block>`;
  487. }
  488. })
  489. // 根据允许的特殊积木动态添加
  490. // if (specialBlocks.includes(BLOCKLY_MAP_SPECIAL_DICT.PAUSE)) {
  491. // toolboxXml += '<block type="pause"></block>';
  492. // }
  493. // if (specialBlocks.includes(BLOCKLY_MAP_SPECIAL_DICT.PLAY_SOUND)) {
  494. // toolboxXml += '<block type="play_sound"></block>';
  495. // }
  496. // if (specialBlocks.includes(BLOCKLY_MAP_SPECIAL_DICT.CONSTRUCT)) {
  497. // toolboxXml += '<block type="construct"></block>';
  498. // }
  499. toolboxXml += `
  500. </category>
  501. <category name="逻辑" colour="210">
  502. <block type="controls_if"></block>
  503. <block type="logic_compare"></block>
  504. <block type="logic_operation"></block>
  505. <block type="logic_boolean"></block>
  506. </category>
  507. <category name="循环" colour="160">
  508. <block type="controls_repeat_ext"></block>
  509. </category>
  510. <category name="数学" colour="60">
  511. <block type="math_number"></block>
  512. <block type="math_arithmetic"></block>
  513. </category>
  514. `;
  515. return toolboxXml;
  516. }
  517. // 获取游戏数据
  518. const fetchGameData = async () => {
  519. try {
  520. // 优先使用props传递的数据
  521. if (props.gameId && props.mapBackground) {
  522. // 使用props数据构建游戏数据对象
  523. currentGameData.value = {
  524. mapBackground: props.mapBackground,
  525. mapTileSize: props.mapTileSize,
  526. mapWalkablePoints: props.mapWalkablePoints,
  527. routeList: props.routeList,
  528. userImage: props.userImage,
  529. info: props.info,
  530. };
  531. // 直接更新游戏状态
  532. await updateGameStateFromData(currentGameData.value);
  533. // 数据更新后强制刷新容器尺寸(等待DOM更新)
  534. await nextTick();
  535. updateMapContainerDimensions();
  536. } else {
  537. // 注释:如果没有props数据,则从API获取
  538. }
  539. } catch (error) {
  540. console.error('获取游戏数据失败:', error);
  541. }
  542. };
  543. // 切换线路
  544. const switchRoute = (index) => {
  545. // 检查路线是否可用
  546. if (index < 0 || index >= parsedRouteList.value.length || !isRouteAvailable(index)) return;
  547. currentRouteIndex.value = index;
  548. const route = parsedRouteList.value[index];
  549. // 更新人物朝向
  550. if (route.direction !== undefined) {
  551. playerInitialDirection.value = route.direction;
  552. gameState.player.direction = route.direction;
  553. }
  554. // 更新开始坐标
  555. if (route.startPoint) {
  556. const startPoint = typeof route.startPoint === 'string' ? JSON.parse(route.startPoint) : route.startPoint;
  557. gameState.mapData.startPoint = { x: startPoint.x, y: startPoint.y };
  558. gameState.player.position = { x: startPoint.x, y: startPoint.y };
  559. }
  560. // 更新结束坐标
  561. if (route.endPoint) {
  562. const endPoint = typeof route.endPoint === 'string' ? JSON.parse(route.endPoint) : route.endPoint;
  563. gameState.mapData.endPoint = { x: endPoint.x, y: endPoint.y };
  564. }
  565. // 重新初始化可行走点集合【目前没有单独配置路线的可行走集合,一张地图通用】
  566. // initWalkablePointsSet();
  567. };
  568. // 检查路线是否可用
  569. const isRouteAvailable = (index) => {
  570. // 第一条路线始终可用
  571. if (index === 0) return true;
  572. // 其他路线需要前一条路线通过
  573. return routePassedStatus.value[index - 1];
  574. };
  575. // 根据获取到的数据更新游戏状态
  576. const updateGameStateFromData = (gameData) => {
  577. try {
  578. // 更新地图配置
  579. gameState.mapConfig.background = gameData.mapBackground ? gameData.mapBackground.trim() : '';
  580. gameState.mapConfig.tileSize = gameData.mapTileSize || CONFIG.STYLES.DEFAULT_TILE_SIZE;
  581. // 更新玩家方向
  582. if (gameData.userDirection) {
  583. playerInitialDirection.value = gameData.userDirection;
  584. }
  585. // 更新地图数据
  586. // 地图起点
  587. if (gameData.mapStartPoint) {
  588. const startPoint = JSON.parse(gameData.mapStartPoint);
  589. gameState.mapData.startPoint = { x: startPoint.x, y: startPoint.y };
  590. gameState.player.position = { x: startPoint.x, y: startPoint.y };
  591. }
  592. // 地图终点
  593. if (gameData.mapEndPoint) {
  594. const endPoint = JSON.parse(gameData.mapEndPoint);
  595. gameState.mapData.endPoint = { x: endPoint.x, y: endPoint.y };
  596. }
  597. if (gameData.mapWalkablePoints) {
  598. gameState.mapData.walkablePoints = JSON.parse(gameData.mapWalkablePoints);
  599. }
  600. // 路线列表
  601. if (gameData.routeList) {
  602. gameState.mapData.routeList = JSON.parse(gameData.routeList);
  603. }
  604. // 初始加载时选中第一条线路
  605. if (parsedRouteList.value.length > 0) {
  606. switchRoute(0);
  607. }
  608. // 重新初始化可行走点集合
  609. initWalkablePointsSet();
  610. } catch (error) {
  611. console.error('更新游戏状态失败:', error);
  612. }
  613. };
  614. // 更新地图容器尺寸的函数
  615. function updateMapContainerDimensions() {
  616. const mapContainer = document.querySelector('.map-container');
  617. if (mapContainer) {
  618. const rect = mapContainer.getBoundingClientRect();
  619. // 确保获取到有效的尺寸值
  620. if (rect.width > 0 && rect.height > 0) {
  621. mapContainerDimensions.value = {
  622. width: rect.width,
  623. height: rect.height
  624. };
  625. } else {
  626. // 若尺寸无效,使用默认容器尺寸(可根据实际情况调整)
  627. mapContainerDimensions.value = {
  628. width: 800,
  629. height: 600
  630. };
  631. }
  632. }
  633. };
  634. // 初始化可行走点映射
  635. function initWalkablePointsSet() {
  636. walkablePointsMap.clear();
  637. gameState.mapData.walkablePoints.forEach(point => {
  638. walkablePointsMap.set(`${point.x},${point.y}`, point);
  639. });
  640. // 保存原始的可行走点数据,用于重置
  641. gameState.mapData.originalWalkablePoints = JSON.parse(JSON.stringify(gameState.mapData.walkablePoints));
  642. }
  643. // 计算点的样式
  644. function getPointStyle(point) {
  645. const style = {
  646. left: point.x * tileSize.value - tileSize.value + 'px',
  647. top: point.y * tileSize.value - tileSize.value + 'px',
  648. width: tileSize.value + 'px',
  649. height: tileSize.value + 'px',
  650. };
  651. // 如果point有img属性,则添加图标
  652. if (point.img) {
  653. const iconSize = tileSize.value * CONFIG.STYLES.IMG_SIZE_RATIO;
  654. const marginSize = tileSize.value * CONFIG.STYLES.IMG_SIZE_MARGIN;
  655. // 重置可能影响背景图显示的样式
  656. style.backgroundColor = 'transparent';
  657. style.backgroundImage = `url(${point.img})`;
  658. style.backgroundSize = 'contain';
  659. style.backgroundPosition = 'center';
  660. style.backgroundRepeat = 'no-repeat';
  661. // 设置与玩家相同的宽高和边距
  662. style.width = iconSize + 'px';
  663. style.height = iconSize + 'px';
  664. style.margin = marginSize + 'px';
  665. }
  666. return style;
  667. }
  668. // 计算携带物品样式
  669. function getCarriedItemStyle(index, item) {
  670. const baseSize = tileSize.value * CONFIG.STYLES.ITEM_CONTAINER_RATIO;
  671. return {
  672. position: 'relative',
  673. width: baseSize + 'px',
  674. height: baseSize + 'px',
  675. backgroundSize: 'contain',
  676. backgroundPosition: 'center',
  677. backgroundRepeat: 'no-repeat',
  678. backgroundImage: `url(${item.img})`,
  679. animationDelay: index * 0.1 + 's'
  680. };
  681. }
  682. //================操作按钮=====================
  683. // 运行代码
  684. const runCode = async () => {
  685. isRunning.value = true;
  686. try {
  687. await new Promise(resolve => setTimeout(resolve, CONFIG.DELAY.RESET_DELAY));
  688. // 重置执行标志,允许新的执行
  689. shouldStopExecution = false;
  690. // 创建新的AbortController用于取消执行
  691. executionAbortController = new AbortController();
  692. const signal = executionAbortController.signal;
  693. // 确保生成器和工作区都存在
  694. if (!javascriptGenerator || !workspace) {
  695. throw new Error('生成器或工作区未正确初始化');
  696. }
  697. // 生成JavaScript代码
  698. const code = javascriptGenerator.workspaceToCode(workspace) + "await isFinish();";
  699. try {
  700. // 增强的安全检查
  701. const unsafePatterns = [
  702. 'eval(', 'Function(', 'document.write', 'window.location',
  703. 'document.createElement', 'XMLHttpRequest', 'fetch',
  704. 'setInterval', 'setTimeout', 'window.',
  705. 'alert(', 'confirm(', 'prompt(',
  706. 'document.cookie', 'localStorage', 'sessionStorage'
  707. ];
  708. const hasUnsafeCode = unsafePatterns.some(pattern => code.includes(pattern));
  709. if (hasUnsafeCode) {
  710. throw new Error('代码包含不安全的操作');
  711. }
  712. // 包装代码为异步函数执行,并设置超时保护
  713. currentExecutionPromise = new Promise(async (resolve, reject) => {
  714. try {
  715. // 检查信号是否已中止
  716. if (signal.aborted) {
  717. throw new Error('执行已取消');
  718. }
  719. // 添加信号监听
  720. signal.addEventListener('abort', () => {
  721. reject(new Error('执行已取消'));
  722. });
  723. const wrappedCode = `(async () => { ${code} })()`;
  724. await new Function(wrappedCode)();
  725. resolve();
  726. } catch (error) {
  727. reject(error);
  728. }
  729. });
  730. } catch (error) {
  731. // 捕获并显示执行错误
  732. if (error.message !== '执行已取消') {
  733. const errorMsg = error.message || '未知错误';
  734. showGameMessage(`代码执行错误: ${errorMsg}`, 'error');
  735. console.error('代码执行错误:', error);
  736. }
  737. } finally {
  738. // 清除当前执行的Promise引用
  739. currentExecutionPromise = null;
  740. }
  741. } catch (error) {
  742. showGameMessage(`运行时错误: ${error.message || '未知错误'}`, 'error');
  743. console.error('运行时错误:', error);
  744. } finally {
  745. isRunning.value = false;
  746. }
  747. };
  748. // 重置玩家位置和状态
  749. const resetPlayer = async () => {
  750. isRunning.value = false;
  751. // 设置标志强制停止所有执行
  752. shouldStopExecution = true;
  753. // 清除倒计时显示和重置倒计时值
  754. showCountdown.value = false;
  755. countdownValue.value = 0;
  756. // 取消任何正在执行的代码
  757. if (executionAbortController) {
  758. executionAbortController.abort();
  759. executionAbortController = null;
  760. }
  761. if (currentExecutionPromise) {
  762. currentExecutionPromise = null;
  763. }
  764. // 清除所有视频组件
  765. const mapBackground = document.querySelector('.map-background');
  766. if (mapBackground) {
  767. // 清除视频元素
  768. const videoElements = mapBackground.querySelectorAll('video');
  769. videoElements.forEach(video => {
  770. if (mapBackground.contains(video)) {
  771. mapBackground.removeChild(video);
  772. }
  773. });
  774. // 清除临时动画元素
  775. const tempElements = mapBackground.querySelectorAll('div[style*="backgroundImage"]');
  776. tempElements.forEach(temp => {
  777. if (mapBackground.contains(temp)) {
  778. mapBackground.removeChild(temp);
  779. }
  780. });
  781. }
  782. // 确保小智显形(不隐形)
  783. const playerElement = document.querySelector('.player');
  784. if (playerElement) {
  785. playerElement.style.opacity = '1';
  786. playerElement.style.transition = 'none'; // 取消过渡效果,立即显示
  787. }
  788. // 重置携带的物品回地图
  789. if (gameState.mapData.originalWalkablePoints.length > 0) {
  790. gameState.mapData.walkablePoints = JSON.parse(JSON.stringify(gameState.mapData.originalWalkablePoints));
  791. }
  792. // 清空携带物品
  793. gameState.player.carriedItems = [];
  794. // 重新初始化可行走点集合
  795. initWalkablePointsSet();
  796. gameState.player.position = { ...startPoint.value };
  797. gameState.player.direction = playerInitialDirection.value; // 重置为初始方向
  798. gameState.player.isColliding = false; //碰撞标志
  799. gameState.player.hasReachedEnd = false;
  800. gameState.player.isSliding = false; // 重置滑行状态
  801. showOverlay.value = false; // 隐藏遮罩层
  802. // 处理多路线情况:重置回初始的第一条路线并清空路线过关标识
  803. if (parsedRouteList.value.length > 1) {
  804. // 重置路线通过状态:只有第一条路线可用
  805. routePassedStatus.value = parsedRouteList.value.map((_, index) => index === 0);
  806. // 切换回第一条路线
  807. switchRoute(0);
  808. }
  809. };
  810. // 清空工作区
  811. const clearWorkspace = () => {
  812. workspace.clear();
  813. showGameMessage('工作区已清空', 'info');
  814. };
  815. // 生成Python代码
  816. const generatePythonCode = () => {
  817. if (!workspace) {
  818. return;
  819. }
  820. try {
  821. // 生成Python代码
  822. const pythonCode = pythonGenerator.workspaceToCode(workspace);
  823. // 存储到常量中
  824. generatedPythonCode.value = pythonCode;
  825. console.log('生成的Python代码:', pythonCode);
  826. // 可以添加提示信息
  827. showGameMessage('Python代码生成成功!', 'success');
  828. } catch (error) {
  829. console.error('生成Python代码时出错:', error);
  830. showGameMessage('生成Python代码失败', 'error');
  831. }
  832. };
  833. // 统一处理撞到墙时的停止逻辑
  834. async function handleWallCollision(endMsg = CONFIG.TIPS.NO_ENTRY) {
  835. // 设置碰撞状态
  836. gameState.player.isColliding = true;
  837. // 显示错误消息
  838. showGameMessage(endMsg, 'error');
  839. await playMp3(errorMp3);
  840. await new Promise(resolve => setTimeout(resolve, CONFIG.DELAY.PALY_MP3_TIMES));
  841. // 立即中止整个代码执行
  842. if (executionAbortController) {
  843. executionAbortController.abort();
  844. }
  845. // 所有动画和移动操作立即停止
  846. shouldStopExecution = true;
  847. // 碰撞状态重置时间后取消碰撞状态
  848. setTimeout(() => {
  849. gameState.player.isColliding = false;
  850. }, CONFIG.DELAY.COLLISION_RESET);
  851. // 返回一个Promise,允许调用者等待碰撞延迟
  852. return new Promise(resolve => setTimeout(resolve, CONFIG.DELAY.COLLISION_DELAY));
  853. }
  854. // 显示游戏消息
  855. function showGameMessage(message, type = 'info', duration = CONFIG.DELAY.MESSAGE_DISPLAY) {
  856. gameState.status.message = message;
  857. gameState.status.messageType = type;
  858. // 消息显示时间后自动清除消息
  859. setTimeout(() => {
  860. gameState.status.message = '';
  861. }, duration);
  862. }
  863. // 关闭遮罩层
  864. const closeOverlay = () => {
  865. gameState.player.hasReachedEnd = false;
  866. showOverlay.value = false;
  867. };
  868. //================积木组件方法=====================
  869. // 向前移动
  870. window.moveForward = async function(stepCount = 1) {
  871. if (shouldStopExecution || isColliding.value || isSliding.value) {
  872. return;
  873. }
  874. for (let i = 1; i <= stepCount; i++) {
  875. let newX = playerPosition.value.x;
  876. let newY = playerPosition.value.y;
  877. // 向前移动
  878. switch(playerDirection.value) {
  879. case CONFIG.GAME.DIRECTIONS.UP: newY--; break;
  880. case CONFIG.GAME.DIRECTIONS.RIGHT: newX++; break;
  881. case CONFIG.GAME.DIRECTIONS.DOWN: newY++; break;
  882. case CONFIG.GAME.DIRECTIONS.LEFT: newX--; break;
  883. }
  884. await moveStep(newX, newY);
  885. }
  886. };
  887. // 向后移动
  888. window.moveBackward = async function(stepCount = 1) {
  889. // 如果已经发生过碰撞,不再执行任何移动
  890. if (shouldStopExecution || isColliding.value || isSliding.value) {
  891. return;
  892. }
  893. for (let i = 1; i <= stepCount; i++) {
  894. let newX = playerPosition.value.x;
  895. let newY = playerPosition.value.y;
  896. // 根据当前方向计算新位置(向后移动)
  897. switch(playerDirection.value) {
  898. case CONFIG.GAME.DIRECTIONS.UP: newY++; break;
  899. case CONFIG.GAME.DIRECTIONS.RIGHT: newX--; break;
  900. case CONFIG.GAME.DIRECTIONS.DOWN: newY--; break;
  901. case CONFIG.GAME.DIRECTIONS.LEFT: newX++; break;
  902. }
  903. await moveStep(newX, newY, 1);
  904. }
  905. };
  906. //向左转(逆时针旋转90度)
  907. window.turnLeft = async function() {
  908. // 如果已经发生过碰撞,不再执行任何旋转
  909. if (shouldStopExecution || isColliding.value) {
  910. return;
  911. }
  912. // 记录起始方向和目标方向
  913. const startDirection = playerDirection.value;
  914. const targetDirection = (playerDirection.value - 1 + 4) % 4;
  915. // 实现平滑旋转
  916. const startTime = performance.now();
  917. // 使用 requestAnimationFrame 实现平滑动画
  918. await new Promise(resolve => {
  919. function animate(currentTime) {
  920. // 检查是否应该停止执行
  921. if (shouldStopExecution) {
  922. resolve();
  923. return;
  924. }
  925. const elapsedTime = currentTime - startTime;
  926. const progress = Math.min(elapsedTime / CONFIG.ANIMATION.ROTATE_DURATION, 1);
  927. // 在动画过程中更新方向
  928. gameState.player.direction = startDirection - progress;
  929. // 如果动画未完成,继续下一帧
  930. if (progress < 1) {
  931. requestAnimationFrame(animate);
  932. } else {
  933. // 动画完成后设置最终方向
  934. gameState.player.direction = targetDirection;
  935. resolve();
  936. }
  937. }
  938. // 开始动画
  939. requestAnimationFrame(animate);
  940. });
  941. await new Promise(resolve => setTimeout(resolve, CONFIG.DELAY.ACTION_DELAY));
  942. };
  943. //向右转(顺时针旋转90度)
  944. window.turnRight = async function() {
  945. // 如果已经发生过碰撞,不再执行任何旋转
  946. if (shouldStopExecution || isColliding.value) {
  947. return;
  948. }
  949. // 记录起始方向和目标方向
  950. const startDirection = playerDirection.value;
  951. const targetDirection = (playerDirection.value + 1) % 4;
  952. // 实现平滑旋转
  953. const startTime = performance.now();
  954. // 使用 requestAnimationFrame 实现平滑动画
  955. await new Promise(resolve => {
  956. function animate(currentTime) {
  957. // 检查是否应该停止执行
  958. if (shouldStopExecution) {
  959. resolve();
  960. return;
  961. }
  962. const elapsedTime = currentTime - startTime;
  963. const progress = Math.min(elapsedTime / CONFIG.ANIMATION.ROTATE_DURATION, 1);
  964. // 处理从3到0的边界情况,确保顺时针旋转
  965. let currentDirection;
  966. if (startDirection === 3 && targetDirection === 0) {
  967. // 对于从3到0的顺时针旋转,我们需要模拟+1的效果而不是-3
  968. currentDirection = startDirection + progress;
  969. // 当超过3.99时,设置为0(避免显示4)
  970. if (currentDirection > 3.99) {
  971. currentDirection = 0;
  972. }
  973. } else {
  974. // 正常情况下的线性插值
  975. currentDirection = startDirection + (targetDirection - startDirection) * progress;
  976. }
  977. // 在动画过程中更新方向
  978. gameState.player.direction = currentDirection;
  979. // 如果动画未完成,继续下一帧
  980. if (progress < 1) {
  981. requestAnimationFrame(animate);
  982. } else {
  983. // 动画完成后设置最终方向
  984. gameState.player.direction = targetDirection;
  985. resolve();
  986. }
  987. }
  988. // 开始动画
  989. requestAnimationFrame(animate);
  990. });
  991. await new Promise(resolve => setTimeout(resolve, CONFIG.DELAY.ACTION_DELAY));
  992. };
  993. // 向后转(旋转180度)
  994. window.turnAround = async function() {
  995. // 如果已经发生过碰撞,不再执行任何旋转
  996. if (shouldStopExecution || isColliding.value) {
  997. return;
  998. }
  999. // 记录起始方向和目标方向
  1000. const startDirection = playerDirection.value;
  1001. const targetDirection = (playerDirection.value + 2) % 4;
  1002. // 实现平滑旋转
  1003. const startTime = performance.now();
  1004. // 使用 requestAnimationFrame 实现平滑动画
  1005. await new Promise(resolve => {
  1006. function animate(currentTime) {
  1007. // 检查是否应该停止执行
  1008. if (shouldStopExecution) {
  1009. resolve();
  1010. return;
  1011. }
  1012. const elapsedTime = currentTime - startTime;
  1013. const progress = Math.min(elapsedTime / CONFIG.ANIMATION.TURN_AROUND_DURATION, 1);
  1014. // 在动画过程中更新方向
  1015. gameState.player.direction = startDirection + 2 * progress;
  1016. // 如果动画未完成,继续下一帧
  1017. if (progress < 1) {
  1018. requestAnimationFrame(animate);
  1019. } else {
  1020. // 动画完成后设置最终方向
  1021. gameState.player.direction = targetDirection;
  1022. resolve();
  1023. }
  1024. }
  1025. // 开始动画
  1026. requestAnimationFrame(animate);
  1027. });
  1028. await new Promise(resolve => setTimeout(resolve, CONFIG.DELAY.ACTION_DELAY));
  1029. };
  1030. // 拾取物品函数
  1031. window.pickupItem = async function() {
  1032. if (shouldStopExecution || isColliding.value || isSliding.value) {
  1033. return;
  1034. }
  1035. //取人物当前位置
  1036. let x = playerPosition.value.x;
  1037. let y = playerPosition.value.y;
  1038. let tileMap = walkablePointsMap.get(`${x},${y}`);
  1039. // 判断是否是要拾取的方块类型
  1040. if (tileMap && tileMap.type === BLOCKLY_MAP_TYPE_DICT.ITEM) {
  1041. showGameMessage(tileMap.tip || CONFIG.TIPS.PICKUP_ITEM, 'warning');
  1042. // 处理携带物品逻辑
  1043. if (tileMap && tileMap.img) {
  1044. // 从地图上移除图标
  1045. const pointIndex = gameState.mapData.walkablePoints.findIndex(
  1046. p => p.x === x && p.y === y
  1047. );
  1048. if (pointIndex !== -1) {
  1049. // 保留点但移除img属性
  1050. const updatedPoint = { ...gameState.mapData.walkablePoints[pointIndex] };
  1051. delete updatedPoint.img;
  1052. updatedPoint.status = updatedPoint.must === true;
  1053. gameState.mapData.walkablePoints.splice(pointIndex, 1, updatedPoint);
  1054. // 更新映射
  1055. walkablePointsMap.set(`${x},${y}`, updatedPoint);
  1056. // 执行物品拾取动画:放大晃动两下然后移动到左上角物品容器
  1057. await animateItemPickup(tileMap, x, y);
  1058. // 将物品添加到玩家携带物品中
  1059. gameState.player.carriedItems.push({
  1060. ...tileMap,
  1061. originalX: x,
  1062. originalY: y
  1063. });
  1064. }
  1065. }
  1066. } else {
  1067. showGameMessage(CONFIG.TIPS.NULL_PICKUP_ITEM, 'info');
  1068. }
  1069. await new Promise(resolve => setTimeout(resolve, CONFIG.DELAY.ACTION_DELAY));
  1070. }
  1071. // 使用物品函数
  1072. window.useItem = async function() {
  1073. if (shouldStopExecution || isColliding.value || isSliding.value) {
  1074. return;
  1075. }
  1076. //取人物当前位置
  1077. let x = playerPosition.value.x;
  1078. let y = playerPosition.value.y;
  1079. let tileMap = walkablePointsMap.get(`${x},${y}`);
  1080. // 判断当前位置是否有特殊需求
  1081. if (tileMap && tileMap.type === BLOCKLY_MAP_TYPE_DICT.TASK) {
  1082. // 检查玩家是否携带了需要的物品
  1083. const requiredItems = tileMap.type;
  1084. let hasRequiredItem = false;
  1085. let itemIndex = -1;
  1086. if (gameState.player.carriedItems.length > 0) {
  1087. hasRequiredItem = true;
  1088. itemIndex = 0;
  1089. }
  1090. if (hasRequiredItem) {
  1091. // 获取要使用的物品
  1092. const itemToUse = gameState.player.carriedItems[itemIndex];
  1093. // 从携带物品中移除已使用的物品
  1094. gameState.player.carriedItems.splice(itemIndex, 1);
  1095. // 执行物品使用动画,传递finishAnimation
  1096. await animateItemUse(itemToUse, itemIndex, tileMap.finishAnimation);
  1097. // 使用物品成功
  1098. showGameMessage(tileMap.finishedTip || CONFIG.TIPS.USE_ITEM_SUCCESS, 'success');
  1099. } else {
  1100. // 提示缺少所需物品
  1101. showGameMessage(tileMap.unfinishedTip || CONFIG.TIPS.USE_SPECIAL_ITEM, 'warning');
  1102. }
  1103. } else {
  1104. showGameMessage(CONFIG.TIPS.NO_USE_ITEM, 'info');
  1105. }
  1106. await new Promise(resolve => setTimeout(resolve, CONFIG.DELAY.ACTION_DELAY));
  1107. }
  1108. // 暂停函数
  1109. window.pause = async function(seconds) {
  1110. if (shouldStopExecution || isSliding.value) {
  1111. return;
  1112. }
  1113. // 显示倒计时
  1114. showCountdown.value = true;
  1115. countdownValue.value = seconds;
  1116. // 使用更细粒度的时间间隔,确保能快速响应重置操作
  1117. const interval = 100; // 100毫秒检查一次
  1118. const totalIterations = seconds * 10; // 总迭代次数
  1119. //处理特殊任务消失
  1120. processingSpecialTasksDisappearing();
  1121. for (let i = 1; i <= totalIterations; i++) {
  1122. // 检查是否应该停止执行
  1123. if (shouldStopExecution) {
  1124. break;
  1125. }
  1126. // 每10次迭代(即1秒)减少倒计时值
  1127. if (i % 10 === 0) {
  1128. countdownValue.value--;
  1129. }
  1130. // 等待一小段时间
  1131. await new Promise(resolve => setTimeout(resolve, interval));
  1132. // 检查是否应该停止执行
  1133. if (shouldStopExecution) {
  1134. break;
  1135. }
  1136. }
  1137. // 隐藏倒计时
  1138. showCountdown.value = false;
  1139. await new Promise(resolve => setTimeout(resolve, CONFIG.DELAY.ACTION_DELAY));
  1140. }
  1141. // 声音函数
  1142. window.playSound = async function() {
  1143. if (shouldStopExecution || isColliding.value || isSliding.value) {
  1144. return;
  1145. }
  1146. if(processingSpecialTasksDisappearing()){
  1147. //延迟,确保声音播放完成
  1148. await playMp3(passMp3);
  1149. await new Promise(resolve => setTimeout(resolve, CONFIG.DELAY.PALY_MP3_TIMES));
  1150. return
  1151. }
  1152. //延迟,确保声音播放完成
  1153. await playMp3(failureMp3);
  1154. await new Promise(resolve => setTimeout(resolve, CONFIG.DELAY.PALY_MP3_TIMES));
  1155. };
  1156. // 修建函数
  1157. window.construct = async function() {
  1158. if (shouldStopExecution || isColliding.value || isSliding.value) {
  1159. return;
  1160. }
  1161. await new Promise(resolve => setTimeout(resolve, CONFIG.DELAY.TEMP_ITEM_FADE_DURATION));
  1162. //处理特殊任务消失
  1163. processingSpecialTasksDisappearing()
  1164. };
  1165. // 当经过指定形状时执行的函数(返回布尔值,可作为参数使用)
  1166. window.whenPassed = function(shape) {
  1167. if (shouldStopExecution || isColliding.value || isSliding.value) {
  1168. return false;
  1169. }
  1170. // 取人物当前位置
  1171. let x = playerPosition.value.x;
  1172. let y = playerPosition.value.y;
  1173. let tileMap = walkablePointsMap.get(`${x},${y}`);
  1174. // 检查当前位置的瓦片是否匹配指定形状
  1175. const isMatch = tileMap && tileMap.mark && tileMap.mark.toLowerCase() === shape;
  1176. return isMatch;
  1177. };
  1178. //校验是否到达终点
  1179. window.isFinish = async function() {
  1180. // 如果已经发生过碰撞,不再执行任何检查
  1181. if (isColliding.value) {
  1182. return;
  1183. }
  1184. // 校验是否到达终点
  1185. if (gameState.player.position.x === endPoint.value.x && gameState.player.position.y === endPoint.value.y) {
  1186. // 如果通过了当前路线,标记为已通过并自动切换到下一个路线(true:切换下一关,false:全部通关)
  1187. if (await markCurrentRouteAsPassed()){
  1188. //继续执行代码
  1189. await runCode();
  1190. return;
  1191. }
  1192. // 全部通关
  1193. // 统计所有类型为TASK的任务点总数||必须完成拾取物品的人物总数
  1194. const totalTasks = gameState.mapData.walkablePoints.filter(
  1195. p => p.type === BLOCKLY_MAP_TYPE_DICT.TASK ||
  1196. p.type === BLOCKLY_MAP_TYPE_DICT.ITEM && p.must === true
  1197. ).length;
  1198. // 统计其中status为true的完成任务数||完成拾取物品的人物总数
  1199. const completedTasks = gameState.mapData.walkablePoints.filter(
  1200. p => p.type === BLOCKLY_MAP_TYPE_DICT.TASK && p.status === true
  1201. || p.type === BLOCKLY_MAP_TYPE_DICT.ITEM && p.must === true && p.status === true
  1202. ).length;
  1203. //blockly总星星数量
  1204. //无任务情况下直接完成
  1205. if (totalTasks === 0 || completedTasks === totalTasks) {
  1206. gameState.player.hasReachedEnd = true;
  1207. emits('saveProgress', 'blockly', passConfig.value.starTotal * 100)
  1208. showGameMessage(CONFIG.TIPS.FINISH, 'success' );
  1209. // 展示通过动画
  1210. await showPass(passConfig.value.starTotal);
  1211. return;
  1212. }
  1213. //任务失败
  1214. // 计算完成百分比
  1215. const completionPercentage = totalTasks > 0 ? Math.round(completedTasks / totalTasks * passConfig.value.starTotal) : passConfig.value.starTotal;
  1216. if (completionPercentage > 0){
  1217. showGameMessage(CONFIG.TIPS.EFFORT, 'warning');
  1218. }else{
  1219. showGameMessage(CONFIG.TIPS.UNFINISHED, 'error');
  1220. }
  1221. emits('saveProgress', 'blockly', completionPercentage * 100)
  1222. // 展示通过动画
  1223. await showPass(completionPercentage);
  1224. }
  1225. };
  1226. //================特殊组件积木逻辑=====================
  1227. //移动逻辑处理(前后通用)
  1228. async function moveStep(newX, newY, moveDirection = 0){
  1229. // 检查是否应该停止执行
  1230. if (shouldStopExecution) {
  1231. return;
  1232. }
  1233. // 检查是否可以移动
  1234. if (walkablePointsMap.has(`${newX},${newY}`)) {
  1235. // 移动前处理方块类型逻辑
  1236. await switchMapType(0);
  1237. // 使用平滑移动动画
  1238. await smoothMoveTo(newX, newY);
  1239. // 移动后处理方块类型逻辑
  1240. await switchMapType(1, moveDirection);
  1241. await new Promise(resolve => setTimeout(resolve, CONFIG.DELAY.ACTION_DELAY));
  1242. } else {
  1243. // 发生碰撞 使用统一的碰撞处理方法
  1244. await handleWallCollision();
  1245. }
  1246. }
  1247. // 处理地图类型逻辑
  1248. async function switchMapType(type, moveDirection = 0) {
  1249. //取人物当前位置
  1250. let x = playerPosition.value.x;
  1251. let y = playerPosition.value.y;
  1252. let tileMap = walkablePointsMap.get(`${x},${y}`);
  1253. //移动前置
  1254. if (type === 0) {
  1255. //判断方块类型并处理逻辑
  1256. switch (tileMap.type) {
  1257. case BLOCKLY_MAP_TYPE_DICT.TASK:
  1258. await taskLogic(tileMap);
  1259. break;
  1260. }
  1261. }else {//移动后置
  1262. //判断方块类型并处理逻辑
  1263. switch (tileMap.type) {
  1264. case BLOCKLY_MAP_TYPE_DICT.ICE:
  1265. do {
  1266. showGameMessage(tileMap.tip, 'warning',CONFIG.DELAY.ICE_MESSAGE_DISPLAY)
  1267. // 处理方块类型逻辑
  1268. await switchMapType(0);
  1269. console.log("滑行前位置:" + playerPosition.value.x + "," + playerPosition.value.y);
  1270. await slidingLogic(moveDirection);
  1271. tileMap = walkablePointsMap.get(`${playerPosition.value.x},${playerPosition.value.y}`);
  1272. }while (tileMap.type === BLOCKLY_MAP_TYPE_DICT.ICE && !isColliding.value)
  1273. break;
  1274. case BLOCKLY_MAP_TYPE_DICT.TRAP:
  1275. showGameMessage(tileMap.tip, 'error')
  1276. await handleWallCollision(tileMap.tip);
  1277. break;
  1278. }
  1279. }
  1280. }
  1281. // 平滑移动函数
  1282. async function smoothMoveTo(targetX, targetY) {
  1283. const startX = playerPosition.value.x;
  1284. const startY = playerPosition.value.y;
  1285. const startTime = performance.now();
  1286. // 使用Promise包装动画过程,使其可以await
  1287. return new Promise(resolve => {
  1288. function animate(currentTime) {
  1289. // 检查是否应该停止执行
  1290. if (shouldStopExecution) {
  1291. resolve();
  1292. return;
  1293. }
  1294. const elapsed = currentTime - startTime;
  1295. const progress = Math.min(elapsed / CONFIG.ANIMATION.MOVE_DURATION, 1); // 计算进度,最大为1
  1296. // 线性插值计算当前位置
  1297. const currentX = startX + (targetX - startX) * progress;
  1298. const currentY = startY + (targetY - startY) * progress;
  1299. gameState.player = {
  1300. ...gameState.player,
  1301. position: { x: currentX, y: currentY },
  1302. };
  1303. // 检查是否到达终点
  1304. if (progress < 1) {
  1305. // 继续动画
  1306. requestAnimationFrame(animate);
  1307. } else {
  1308. resolve(); // 动画完成
  1309. }
  1310. }
  1311. // 启动动画
  1312. requestAnimationFrame(animate);
  1313. });
  1314. }
  1315. // 特殊任务处理消失(不需要物品)
  1316. function processingSpecialTasksDisappearing(errorTip = CONFIG.TIPS.ERROR_ERROR) {
  1317. let x = playerPosition.value.x;
  1318. let y = playerPosition.value.y;
  1319. let tileMap = walkablePointsMap.get(`${x},${y}`);
  1320. if (tileMap && tileMap.type === BLOCKLY_MAP_TYPE_DICT.TASK) {
  1321. const pointIndex = gameState.mapData.walkablePoints.findIndex(
  1322. p => p.x === x && p.y === y
  1323. );
  1324. if (pointIndex !== -1) {
  1325. const updatedPoint = { ...gameState.mapData.walkablePoints[pointIndex] };
  1326. updatedPoint.img = updatedPoint.endImg;
  1327. updatedPoint.status = true;
  1328. gameState.mapData.walkablePoints.splice(pointIndex, 1, updatedPoint);
  1329. walkablePointsMap.set(`${x},${y}`, updatedPoint);
  1330. }
  1331. showGameMessage(tileMap.finishedTip || CONFIG.TIPS.USE_ITEM_SUCCESS, 'success');
  1332. return true;
  1333. }else{
  1334. showGameMessage(errorTip, 'info');
  1335. return false;
  1336. }
  1337. }
  1338. // 处理冰块滑行逻辑
  1339. async function slidingLogic(moveDirection = 0) {
  1340. if (shouldStopExecution || isColliding.value) {
  1341. return;
  1342. }
  1343. gameState.player.isSliding = true;
  1344. try {
  1345. // 计算下一个位置
  1346. let nextX = playerPosition.value.x;
  1347. let nextY = playerPosition.value.y;
  1348. // 根据当前方向计算下一个位置
  1349. // 如果有指定方向,使用该方向滑行
  1350. if (moveDirection === 0) {
  1351. switch(playerDirection.value) {
  1352. case CONFIG.GAME.DIRECTIONS.UP: nextY--; break;
  1353. case CONFIG.GAME.DIRECTIONS.RIGHT: nextX++; break;
  1354. case CONFIG.GAME.DIRECTIONS.DOWN: nextY++; break;
  1355. case CONFIG.GAME.DIRECTIONS.LEFT: nextX--; break;
  1356. }
  1357. }else{
  1358. switch(playerDirection.value) {
  1359. case CONFIG.GAME.DIRECTIONS.UP: nextY++; break;
  1360. case CONFIG.GAME.DIRECTIONS.RIGHT: nextX--; break;
  1361. case CONFIG.GAME.DIRECTIONS.DOWN: nextY--; break;
  1362. case CONFIG.GAME.DIRECTIONS.LEFT: nextX++; break;
  1363. }
  1364. }
  1365. // 检查下一个位置是否可行走
  1366. if (walkablePointsMap.has(`${nextX},${nextY}`)) {
  1367. // 执行平滑移动到下一个位置
  1368. await smoothMoveTo(nextX, nextY);
  1369. } else {
  1370. // 使用统一的碰撞处理方法,但不等待延迟(因为slidingLogic不需要这个延迟)
  1371. await handleWallCollision();
  1372. }
  1373. } catch (error) {
  1374. console.error('滑行过程中发生错误:', error);
  1375. } finally {
  1376. // 无论如何都要确保滑行状态被重置
  1377. gameState.player.isSliding = false;
  1378. }
  1379. }
  1380. // 处理任务逻辑
  1381. async function taskLogic(tileMap) {
  1382. if (shouldStopExecution || isColliding.value) {
  1383. return;
  1384. }
  1385. try {
  1386. // 判断当前位置是否有特殊需求
  1387. if (tileMap && tileMap.type === BLOCKLY_MAP_TYPE_DICT.TASK && tileMap.noPassing === true && tileMap.status !== true) {
  1388. await handleWallCollision(tileMap.unfinishedTip);
  1389. return;
  1390. }
  1391. } catch (error) {
  1392. console.error('处理任务逻辑发生错误:', error);
  1393. }
  1394. }
  1395. // 物品拾取动画函数
  1396. async function animateItemPickup(item, itemX, itemY) {
  1397. // 创建临时动画元素
  1398. const tempItem = document.createElement('div');
  1399. const iconSize = tileSize.value * CONFIG.STYLES.IMG_SIZE_RATIO;
  1400. const marginSize = tileSize.value * CONFIG.STYLES.IMG_SIZE_MARGIN;
  1401. // 计算物品在地图上的实际位置
  1402. const itemLeft = itemX * tileSize.value - tileSize.value + marginSize;
  1403. const itemTop = itemY * tileSize.value - tileSize.value + marginSize;
  1404. // 平行移动到容器位置,同时调整大小
  1405. const finalSize = tileSize.value * CONFIG.STYLES.ITEM_CONTAINER_RATIO;
  1406. // 获取携带物品容器的位置(左上角)
  1407. // 注意:这里不需要考虑当前已携带物品数量,因为物品还没被添加
  1408. let containerLeft = CONFIG.STYLES.ITEM_CONTAINER_POSITION.x;
  1409. let containerTop = CONFIG.STYLES.ITEM_CONTAINER_POSITION.y;
  1410. // 如果已经有物品,计算新物品应该出现的位置
  1411. if (gameState.player.carriedItems.length > 0) {
  1412. let containerSize = finalSize + CONFIG.STYLES.ITEM_CONTAINER_SPACING;
  1413. containerLeft = CONFIG.STYLES.ITEM_CONTAINER_POSITION.x + gameState.player.carriedItems.length * containerSize;
  1414. }
  1415. // 设置临时元素样式
  1416. Object.assign(tempItem.style, {
  1417. position: 'absolute',
  1418. left: itemLeft + 'px',
  1419. top: itemTop + 'px',
  1420. width: iconSize + 'px',
  1421. height: iconSize + 'px',
  1422. backgroundImage: `url(${item.img})`,
  1423. backgroundSize: 'contain',
  1424. backgroundPosition: 'center',
  1425. backgroundRepeat: 'no-repeat',
  1426. zIndex: '100', // 确保在最上层
  1427. opacity: '0', // 初始完全透明
  1428. transform: 'scale(1)',
  1429. });
  1430. // 将临时元素添加到地图容器
  1431. const mapBackground = document.querySelector('.map-background');
  1432. if (!mapBackground) {
  1433. console.error('地图容器未找到');
  1434. return;
  1435. }
  1436. mapBackground.appendChild(tempItem);
  1437. // 触发淡入动画
  1438. tempItem.offsetHeight;
  1439. tempItem.style.transition = 'opacity ' + CONFIG.DELAY.ITEMS_APPEAR + 'ms ease-out';
  1440. tempItem.style.opacity = '1'; // 完全显示
  1441. await new Promise(resolve => setTimeout(resolve, CONFIG.DELAY.ITEMS_APPEAR));
  1442. // 移动动画
  1443. tempItem.style.transition = 'all ' + CONFIG.DELAY.ITEMS_FLIGHT_ANIMATION_DELAY + 'ms ease-out'; // 减慢动画速度
  1444. Object.assign(tempItem.style, {
  1445. left: containerLeft + 'px',
  1446. top: containerTop + 'px',
  1447. width: finalSize + 'px',
  1448. height: finalSize + 'px',
  1449. opacity: '0.8',
  1450. transform: 'scale(1)',
  1451. });
  1452. // 等待动画完成
  1453. await new Promise(resolve => setTimeout(resolve, CONFIG.DELAY.ITEMS_FLIGHT_ANIMATION_DELAY));
  1454. // 移除临时元素
  1455. if (mapBackground.contains(tempItem)) {
  1456. mapBackground.removeChild(tempItem);
  1457. }
  1458. }
  1459. // 物品使用动画函数
  1460. async function animateItemUse(item, itemIndex, finishAnimation) {
  1461. // 创建临时动画元素
  1462. const tempItem = document.createElement('div');
  1463. const finalSize = tileSize.value * CONFIG.STYLES.ITEM_CONTAINER_RATIO;
  1464. const iconSize = tileSize.value * CONFIG.STYLES.IMG_SIZE_RATIO;
  1465. const marginSize = tileSize.value * CONFIG.STYLES.IMG_SIZE_MARGIN;
  1466. // 计算物品在物品栏中的初始位置
  1467. let startLeft = CONFIG.STYLES.ITEM_CONTAINER_POSITION.x;
  1468. let startTop = CONFIG.STYLES.ITEM_CONTAINER_POSITION.y;
  1469. let containerSize = finalSize + CONFIG.STYLES.ITEM_CONTAINER_SPACING;
  1470. // 根据物品索引计算在物品栏中的位置
  1471. if (itemIndex > 0) {
  1472. startLeft = CONFIG.STYLES.ITEM_CONTAINER_POSITION.x + itemIndex * containerSize;
  1473. }
  1474. // 计算玩家当前位置(物品目标位置)
  1475. const playerLeft = playerPosition.value.x * tileSize.value - tileSize.value + marginSize;
  1476. const playerTop = playerPosition.value.y * tileSize.value - tileSize.value + marginSize;
  1477. // 获取玩家元素
  1478. const playerElement = document.querySelector('.player');
  1479. const originalPlayerZIndex = playerElement ? playerElement.style.zIndex : '';
  1480. const originalPlayerOpacity = playerElement ? playerElement.style.opacity : '1';
  1481. // 将临时元素添加到地图容器
  1482. const mapBackground = document.querySelector('.map-background');
  1483. if (!mapBackground) {
  1484. console.error('地图容器未找到');
  1485. return;
  1486. }
  1487. // 设置临时元素初始样式
  1488. Object.assign(tempItem.style, {
  1489. position: 'absolute',
  1490. left: startLeft + 'px',
  1491. top: startTop + 'px',
  1492. width: finalSize + 'px',
  1493. height: finalSize + 'px',
  1494. backgroundImage: `url(${item.img})`,
  1495. backgroundSize: 'contain',
  1496. backgroundPosition: 'center',
  1497. backgroundRepeat: 'no-repeat',
  1498. zIndex: '120',
  1499. transition: 'transform ' + CONFIG.DELAY.ITEMS_APPEAR + 'ms ease-out',
  1500. transform: 'scale(1)',
  1501. });
  1502. mapBackground.appendChild(tempItem);
  1503. // 1. 首先让小智逐渐消失
  1504. if (playerElement) {
  1505. playerElement.style.transition = 'opacity ' + CONFIG.DELAY.PLAYER_FADE_DURATION + 'ms ease-out';
  1506. playerElement.style.opacity = '0';
  1507. }
  1508. // 2. 准备漂移动画
  1509. await new Promise(resolve => setTimeout(resolve, CONFIG.DELAY.ITEMS_APPEAR));
  1510. // 3. 如果有视频,先创建视频元素但不自动播放
  1511. let videoElement = null;
  1512. if (finishAnimation) {
  1513. // 创建视频元素
  1514. videoElement = document.createElement('video');
  1515. videoElement.src = finishAnimation;
  1516. videoElement.autoplay = false; // 不自动播放
  1517. // videoElement.muted = true; // 视频默认静音
  1518. videoElement.playsinline = true;
  1519. videoElement.style.position = 'absolute';
  1520. videoElement.style.left = playerLeft + 'px';
  1521. videoElement.style.top = playerTop + 'px';
  1522. videoElement.style.width = iconSize + 'px';
  1523. videoElement.style.height = iconSize + 'px';
  1524. videoElement.style.zIndex = '2000'; // 提高z-index确保视频可见
  1525. videoElement.style.objectFit = 'contain';
  1526. // 添加渐变显示效果
  1527. videoElement.style.opacity = '0';
  1528. videoElement.style.transition = 'opacity ' + CONFIG.DELAY.VIDEO_FADE_DURATION + 'ms ease-in-out';
  1529. // 添加视频到地图容器
  1530. mapBackground.appendChild(videoElement);
  1531. // 触发重绘后设置透明度为1,实现渐变效果
  1532. setTimeout(() => {
  1533. videoElement.style.opacity = '1';
  1534. console.log('Video opacity set to 1');
  1535. }, CONFIG.DELAY.VIDEO_FADE_DURATION);
  1536. } else {
  1537. console.log('无视频');
  1538. }
  1539. // 4. 设置漂移动画样式
  1540. tempItem.style.transition = 'all ' + CONFIG.DELAY.TEMP_ITEM_FADE_DURATION + 'ms ease-out';
  1541. Object.assign(tempItem.style, {
  1542. left: playerLeft + 'px',
  1543. top: playerTop + 'px',
  1544. width: iconSize + 'px',
  1545. height: iconSize + 'px',
  1546. });
  1547. // 5. 等待漂移到玩家位置
  1548. await new Promise(resolve => setTimeout(resolve, CONFIG.DELAY.ITEMS_FLIGHT_ANIMATION_DELAY));
  1549. // 6. 移除临时元素
  1550. if (mapBackground.contains(tempItem)) {
  1551. mapBackground.removeChild(tempItem);
  1552. }
  1553. let x = playerPosition.value.x;
  1554. let y = playerPosition.value.y;
  1555. // 8. 视频播放完成后替换任务图片
  1556. const pointIndex = gameState.mapData.walkablePoints.findIndex(
  1557. p => p.x === x && p.y === y
  1558. );
  1559. if (pointIndex !== -1) {
  1560. // 保留点但移除img属性并设置完成状态图标
  1561. const updatedPoint = { ...gameState.mapData.walkablePoints[pointIndex] };
  1562. updatedPoint.img = updatedPoint.endImg; // 设置完成状态图标
  1563. updatedPoint.status = true;
  1564. gameState.mapData.walkablePoints.splice(pointIndex, 1, updatedPoint);
  1565. // 更新映射
  1566. walkablePointsMap.set(`${x},${y}`, updatedPoint);
  1567. }
  1568. // 9. 处理视频播放或直接替换任务图片
  1569. if (videoElement) {
  1570. // 开始播放视频
  1571. try {
  1572. const playPromise = videoElement.play();
  1573. if (playPromise !== undefined) {
  1574. playPromise.then(_ => {
  1575. console.log('Video started playing successfully');
  1576. }).catch(error => {
  1577. console.error('Error playing video:', error);
  1578. });
  1579. }
  1580. } catch (error) {
  1581. console.error('Exception when playing video:', error);
  1582. }
  1583. // 等待视频播放完成,添加超时处理
  1584. await new Promise(resolve => {
  1585. videoElement.onended = () => {
  1586. console.log('Video ended');
  1587. resolve();
  1588. };
  1589. // 添加超时处理,防止视频卡住
  1590. setTimeout(() => {
  1591. resolve();
  1592. }, CONFIG.DELAY.VIDEO_TIMEOUT);
  1593. });
  1594. // 10. 移除视频元素 - 添加淡出效果
  1595. if (mapBackground.contains(videoElement)) {
  1596. // 设置淡出效果
  1597. videoElement.style.transition = 'opacity ' + CONFIG.DELAY.VIDEO_FADE_DURATION + 'ms ease-in-out';
  1598. videoElement.style.opacity = '0';
  1599. // 等待淡出动画完成后再移除元素
  1600. setTimeout(() => {
  1601. if (mapBackground.contains(videoElement)) {
  1602. mapBackground.removeChild(videoElement);
  1603. }
  1604. }, CONFIG.DELAY.VIDEO_FADE_DURATION); // 与动画时长一致
  1605. }
  1606. } else {
  1607. // 10.无视频
  1608. }
  1609. // 11. 视频消失后稍作停留,让用户看到完成后的图片
  1610. await new Promise(resolve => setTimeout(resolve, CONFIG.DELAY.COMPLETION_DISPLAY));
  1611. // 逐步显示玩家并将其置顶
  1612. if (playerElement) {
  1613. playerElement.style.transition = 'opacity ' + CONFIG.DELAY.PLAYER_FADE_DURATION + 'ms ease-in';
  1614. playerElement.style.opacity = originalPlayerOpacity;
  1615. playerElement.style.zIndex = originalPlayerZIndex || '100';
  1616. // 额外设置一个较高的z-index确保盖住其他人物图片
  1617. setTimeout(() => {
  1618. playerElement.style.zIndex = '150';
  1619. }, CONFIG.DELAY.PLAYER_FADE_DURATION);
  1620. }
  1621. }
  1622. // 展示通过动画
  1623. async function showPass(passStar) {
  1624. // 记录通过的星星数量
  1625. passConfig.value.passStar = passStar;
  1626. passConfig.value.title = 'YOU WIN!';
  1627. passConfig.value.passBackground = cupbg;
  1628. switch (passStar) {
  1629. case 0:
  1630. passConfig.value.title = 'YOU LOSE!';
  1631. passConfig.value.passTrophy = nocup;
  1632. passConfig.value.passBackground = nocupbg;
  1633. await playMp3(passFailure);
  1634. break;
  1635. case 1:
  1636. passConfig.value.passTrophy = coppercup;
  1637. await playMp3(passTrump);
  1638. break;
  1639. case 2:
  1640. passConfig.value.passTrophy = silvercup;
  1641. await playMp3(passTrump);
  1642. break;
  1643. case 3:
  1644. passConfig.value.passTrophy = goldcup;
  1645. await playMp3(passFireworks);
  1646. break;
  1647. }
  1648. showOverlay.value = true;
  1649. }
  1650. // 标记当前路线为已通过
  1651. const markCurrentRouteAsPassed = async () => {
  1652. routePassedStatus.value[currentRouteIndex.value] = true;
  1653. // 自动切换到下一个可用路线
  1654. if (currentRouteIndex.value < parsedRouteList.value.length - 1) {
  1655. // 解锁下一个路线
  1656. routePassedStatus.value[currentRouteIndex.value + 1] = true;
  1657. // 自动切换到下一个路线
  1658. switchRoute(currentRouteIndex.value + 1);
  1659. //通关路线提示
  1660. showGameMessage(CONFIG.TIPS.PASS_ROUTE, 'success');
  1661. //播放通过路线声音
  1662. await playMp3(passRouteMp3);
  1663. await new Promise(resolve => setTimeout(resolve, CONFIG.DELAY.PALY_MP3_TIMES));
  1664. return true;
  1665. }
  1666. return false;
  1667. };
  1668. //================卸载区=====================
  1669. // 组件卸载时清理
  1670. onUnmounted(() => {
  1671. // 清理Blockly工作区
  1672. if (workspace) {
  1673. workspace.dispose();
  1674. workspace = null;
  1675. }
  1676. // 移除事件监听器
  1677. window.removeEventListener('resize', updateMapContainerDimensions);
  1678. // 停止当前正在执行的代码
  1679. shouldStopExecution = true;
  1680. // 取消任何正在进行的执行
  1681. if (executionAbortController) {
  1682. executionAbortController.abort();
  1683. executionAbortController = null;
  1684. }
  1685. // 清除当前执行的Promise引用
  1686. currentExecutionPromise = null;
  1687. // 清理所有临时DOM元素
  1688. const mapBackground = document.querySelector('.map-background');
  1689. if (mapBackground) {
  1690. // 清除视频元素
  1691. const videoElements = mapBackground.querySelectorAll('video');
  1692. videoElements.forEach(video => {
  1693. if (mapBackground.contains(video)) {
  1694. mapBackground.removeChild(video);
  1695. }
  1696. });
  1697. // 清除临时动画元素
  1698. const tempElements = mapBackground.querySelectorAll('div[style*="backgroundImage"]');
  1699. tempElements.forEach(temp => {
  1700. if (mapBackground.contains(temp)) {
  1701. mapBackground.removeChild(temp);
  1702. }
  1703. });
  1704. }
  1705. // 清理全局函数引用
  1706. window.pickupItem = null;
  1707. window.useItem = null;
  1708. // 重置游戏状态
  1709. gameState.player.carriedItems = [];
  1710. gameState.player.position = { ...startPoint.value };
  1711. });
  1712. </script>
  1713. <style scoped lang="scss">
  1714. @use "sass:math";
  1715. @function rpx($px) {
  1716. @return math.div($px, 750) * 100vw;
  1717. }
  1718. /* 全屏遮罩盒子样式 */
  1719. .fullscreen-overlay {
  1720. position: fixed;
  1721. top: 0;
  1722. left: 0;
  1723. width: 100%;
  1724. height: 100%;
  1725. background-color: rgba(0, 0, 0, 0.5); /* 半透明黑色 遮罩 */
  1726. z-index: 9999; /* 确保在最上层 */
  1727. // pointer-events: none; /* 允许点击穿透 */
  1728. display: flex;
  1729. justify-content: center;
  1730. align-items: center;
  1731. }
  1732. /* 垂直居中盒子样式 */
  1733. .centered-box {
  1734. border-radius: rpx(10);
  1735. z-index: 10000;
  1736. width: rpx(220);
  1737. height: rpx(255);
  1738. overflow: auto;
  1739. pointer-events: auto;
  1740. // display: flex;
  1741. flex-direction: column;
  1742. position: relative;
  1743. /* 初始状态:缩小、透明 */
  1744. transform: scale(0.7);
  1745. opacity: 0;
  1746. /* 动画效果 */
  1747. animation: popUp 0.5s ease-out forwards;
  1748. }
  1749. /* 弹出动画定义 */
  1750. @keyframes popUp {
  1751. 0% {
  1752. transform: scale(0.7);
  1753. opacity: 0;
  1754. }
  1755. 70% {
  1756. transform: scale(1.05);
  1757. opacity: 0.9;
  1758. }
  1759. 100% {
  1760. transform: scale(1);
  1761. opacity: 1;
  1762. }
  1763. }
  1764. /* 彩带容器样式 */
  1765. .confetti-container {
  1766. position: absolute;
  1767. width: 100%;
  1768. height: 100%;
  1769. overflow: hidden;
  1770. z-index: 9999;
  1771. pointer-events: none;
  1772. }
  1773. /* 彩带基本样式 */
  1774. .confetti {
  1775. position: absolute;
  1776. width: rpx(5);
  1777. height: rpx(15);
  1778. opacity: 0;
  1779. animation-timing-function: ease-in-out;
  1780. animation-iteration-count: infinite;
  1781. animation-fill-mode: forwards;
  1782. }
  1783. /* 彩带颜色和动画 */
  1784. .confetti-1 { background-color: #FF5252; animation: confetti-fall 2s 0.2s linear infinite; left: 5%; }
  1785. .confetti-2 { background-color: #536DFE; animation: confetti-fall 2.7s 0.9s linear infinite; left: 15%; }
  1786. .confetti-3 { background-color: #FFC107; animation: confetti-fall 2.3s 1.8s linear infinite; left: 25%; }
  1787. .confetti-4 { background-color: #4CAF50; animation: confetti-fall 2.1s 1.4s linear infinite; left: 35%; }
  1788. .confetti-5 { background-color: #9C27B0; animation: confetti-fall 2.5s 1.8s linear infinite; left: 45%; }
  1789. .confetti-6 { background-color: #2196F3; animation: confetti-fall 2.2s 0.2s linear infinite; left: 55%; }
  1790. .confetti-7 { background-color: #FF9800; animation: confetti-fall 2.4s 1.8s linear infinite; left: 65%; }
  1791. .confetti-8 { background-color: #795548; animation: confetti-fall 2s 1.4s linear infinite; left: 75%; }
  1792. .confetti-9 { background-color: #607D8B; animation: confetti-fall 2.6s 3.4s linear infinite; left: 85%; }
  1793. .confetti-10 { background-color: #E91E63; animation: confetti-fall 2.3s 1.8s linear infinite; left: 95%; }
  1794. /* 彩带飘落动画 */
  1795. @keyframes confetti-fall {
  1796. 0% {
  1797. top: -10%;
  1798. opacity: 0;
  1799. transform: rotate(0deg);
  1800. }
  1801. 10% {
  1802. opacity: 1;
  1803. }
  1804. 90% {
  1805. opacity: 1;
  1806. }
  1807. 100% {
  1808. top: 100%;
  1809. opacity: 0;
  1810. transform: rotate(360deg);
  1811. }
  1812. }
  1813. /* 关闭按钮样式 */
  1814. .close-button {
  1815. position: absolute;
  1816. top: rpx(0);
  1817. right: rpx(2);
  1818. font-size: rpx(20);
  1819. color: #2b8fdd;
  1820. font-weight: bold;
  1821. cursor: pointer;
  1822. width: rpx(25);
  1823. height: rpx(25);
  1824. display: flex;
  1825. justify-content: center;
  1826. align-items: center;
  1827. z-index: 10001;
  1828. text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
  1829. // -webkit-text-stroke: 1px white; // 描
  1830. }
  1831. .close-button:hover {
  1832. transform: scale(1.1);
  1833. }
  1834. .top-box {
  1835. width: 100%;
  1836. height: rpx(60);
  1837. border-radius: 5px;
  1838. display: flex;
  1839. justify-content: center;
  1840. align-items: center;
  1841. }
  1842. .title-text {
  1843. font-size: rpx(20);
  1844. font-weight: bold;
  1845. color: white;
  1846. font-family: 'SourceHanSansCN-Bold_0';
  1847. text-align: center;
  1848. width: 100%;
  1849. height: 100%;
  1850. display: flex;
  1851. justify-content: center;
  1852. align-items: center;
  1853. text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
  1854. }
  1855. .middle-box {
  1856. width: 100%;
  1857. height: rpx(130);
  1858. border-radius: 5px;
  1859. display: flex;
  1860. justify-content: center;
  1861. align-items: center;
  1862. }
  1863. .gold-cup-image{
  1864. height: 100%;
  1865. width: 100%;
  1866. object-fit: contain;
  1867. }
  1868. .bottom-box {
  1869. width: 100%;
  1870. height: rpx(40);
  1871. border-radius: 5px;
  1872. display: flex;
  1873. justify-content: center;
  1874. align-items: flex-start;
  1875. gap: rpx(12);
  1876. margin-top: -rpx(10); /* 向上调整位置 */
  1877. }
  1878. .gold-star-image {
  1879. width: rpx(35);
  1880. height: 100%;
  1881. object-fit: contain;
  1882. }
  1883. .star-top {
  1884. object-position: top;
  1885. }
  1886. .star-bottom {
  1887. object-position: bottom;
  1888. }
  1889. //将tileSize属性绑定到CSS变量上
  1890. :root {
  1891. --tile-size: v-bind('tileSize + "px"');
  1892. }
  1893. /* 游戏简介样式 */
  1894. .info-message-container {
  1895. display: flex;
  1896. flex-direction: column;
  1897. align-items: flex-start;
  1898. }
  1899. .message-item {
  1900. display: flex;
  1901. align-items: flex-start;
  1902. width: 100%;
  1903. margin-bottom: rpx(5);
  1904. }
  1905. /* 头像样式 */
  1906. .avatar {
  1907. margin-right: rpx(4);
  1908. flex-shrink: 0;
  1909. }
  1910. .avatar-image {
  1911. width: rpx(30);
  1912. height: rpx(30);
  1913. object-fit: cover;
  1914. }
  1915. /* 消息内容样式 */
  1916. .message-item {
  1917. flex: 1;
  1918. }
  1919. .message-item p {
  1920. margin: rpx(4) 0;
  1921. line-height: 0;
  1922. font-size: rpx(10);
  1923. text-align: left;
  1924. color: black;
  1925. background-color: #e6faff;
  1926. opacity: 0.8;
  1927. border-radius: rpx(4);
  1928. padding: rpx(2) rpx(5);
  1929. max-width: 100%;
  1930. }
  1931. .message-item p:first-child {
  1932. margin-top: 0;
  1933. font-weight: 500;
  1934. }
  1935. .message-item p:last-child {
  1936. margin-bottom: 0;
  1937. }
  1938. .title-box {
  1939. position: relative;
  1940. top: rpx(5);
  1941. padding-left: 15px;
  1942. z-index: 10;
  1943. display: flex;
  1944. justify-content: space-between;
  1945. align-items: center;
  1946. }
  1947. /* 左侧容器样式 */
  1948. .left-container {
  1949. display: flex;
  1950. flex-direction: column;
  1951. align-items: flex-start;
  1952. gap: 10px;
  1953. }
  1954. /* 线路按钮容器样式 */
  1955. .route-buttons {
  1956. display: flex;
  1957. gap: rpx(5);
  1958. margin-left: rpx(3);
  1959. }
  1960. /* 右侧两个角为圆角的长方形格子样式 */
  1961. .game-badge {
  1962. width: rpx(70);
  1963. height: rpx(20);
  1964. background-color: #5fb5dc;
  1965. color: #fff;
  1966. border-radius: 0 rpx(20) rpx(20) 0;
  1967. display: flex;
  1968. align-items: center;
  1969. justify-content: center;
  1970. font-size: rpx(15);
  1971. font-weight: bold;
  1972. border: none;
  1973. cursor: pointer;
  1974. transition: all 0.3s ease;
  1975. }
  1976. .game-badge:hover {
  1977. background-color: #3498db;
  1978. transform: translateY(-2px);
  1979. box-shadow: 0 rpx(5) rpx(10) rgba(0, 0, 0, 0.1);
  1980. }
  1981. .game-badge.active {
  1982. background-color: #e74c3c;
  1983. color: #fff;
  1984. font-weight: bold;
  1985. }
  1986. /* 右侧容器样式 */
  1987. .right-container {
  1988. display: flex;
  1989. gap: 10px;
  1990. padding-right: 20px;
  1991. }
  1992. /* 上下节按钮样式 */
  1993. .section-button {
  1994. padding: rpx(5) rpx(12);
  1995. border: none;
  1996. border-radius: rpx(10);
  1997. background: #3498db;
  1998. color: #fff;
  1999. font-weight: 500;
  2000. cursor: pointer;
  2001. transition: all 0.3s ease;
  2002. font-size: rpx(7);
  2003. }
  2004. .section-button:hover {
  2005. background: #2980b9;
  2006. transform: translateY(-2px);
  2007. box-shadow: 0 5px 10px rgba(0, 0, 0, 0.1);
  2008. }
  2009. .previous-btn {
  2010. background-color: rgba(255, 255, 255, 0.8);
  2011. color: #333;
  2012. }
  2013. .previous-btn:hover {
  2014. background-color: rgba(255, 255, 255, 0.9);
  2015. }
  2016. .next-btn {
  2017. background: #5fb5dc;
  2018. }
  2019. .next-btn:hover {
  2020. background: #2980b9;
  2021. }
  2022. .box-icon {
  2023. display: flex;
  2024. align-items: center;
  2025. margin-top: rpx(5);
  2026. gap: 10px;
  2027. padding: 10px 20px;
  2028. background-color: rgba(255, 255, 255, 0.8);
  2029. border-radius: 30px;
  2030. backdrop-filter: blur(10px);
  2031. cursor: pointer;
  2032. transition: all 0.3s ease;
  2033. font-size: 16px;
  2034. color: #333;
  2035. font-weight: 500;
  2036. width: fit-content;
  2037. }
  2038. .box-icon:hover {
  2039. background-color: rgba(255, 255, 255, 0.9);
  2040. transform: translate(-3px);
  2041. }
  2042. .left-icon {
  2043. font-size: 18px;
  2044. }
  2045. .content {
  2046. display: flex;
  2047. flex-wrap: nowrap;
  2048. gap: 20px;
  2049. padding: 20px;
  2050. min-width: 1160px;
  2051. }
  2052. /* 地图区域样式 */
  2053. .map-section {
  2054. flex: 1;
  2055. min-width: 500px;
  2056. background: rgba(248, 249, 250, 0.82);
  2057. padding: 15px;
  2058. border-radius: 15px;
  2059. }
  2060. .map-container {
  2061. position: relative;
  2062. width: 100%;
  2063. height: 100%;
  2064. overflow: hidden; // 防止内容溢出
  2065. }
  2066. .map-background {
  2067. position: relative;
  2068. width: 100%;
  2069. height: 100%;
  2070. background-size: cover;
  2071. background-position: center;
  2072. background-repeat: no-repeat;
  2073. }
  2074. .map-image {
  2075. width: 100%;
  2076. height: 100%;
  2077. object-fit: cover;
  2078. }
  2079. /* 可行走区域样式 */
  2080. .walkable-point {
  2081. position: absolute;
  2082. //background-color: rgba(52, 152, 219, 0.2);
  2083. //border: 1px solid rgba(52, 152, 219, 0.5);
  2084. //box-sizing: border-box;
  2085. //是否显示可行路线
  2086. opacity: 1;
  2087. }
  2088. /* 玩家样式 */
  2089. .player {
  2090. position: absolute;
  2091. background-image: var(--player-image);
  2092. background-size: contain;
  2093. background-repeat: no-repeat;
  2094. background-position: center;
  2095. border-radius: 5px;
  2096. z-index: 10;
  2097. }
  2098. /* 碰撞动画 */
  2099. .player.collision {
  2100. animation: collision 0.5s ease-in-out;
  2101. }
  2102. @keyframes collision {
  2103. 0% { transform: rotate(var(--player-rotation)) translateX(0) translateY(0) scale(1); }
  2104. 25% { transform: rotate(var(--player-rotation)) translateX(-3px) translateY(-2px) scale(1.2); }
  2105. 50% { transform: rotate(var(--player-rotation)) translateX(3px) translateY(2px) scale(1.2); }
  2106. 75% { transform: rotate(var(--player-rotation)) translateX(-3px) translateY(-2px) scale(1.2); }
  2107. 100% { transform: rotate(var(--player-rotation)) translateX(0) translateY(0) scale(1); }
  2108. }
  2109. /* 滑行动画 */
  2110. @keyframes sliding {
  2111. 0% { transform: rotate(var(--player-rotation)) translateX(0) translateY(0); }
  2112. 25% { transform: rotate(var(--player-rotation)) translateX(2px) translateY(0); }
  2113. 75% { transform: rotate(var(--player-rotation)) translateX(-2px) translateY(0); }
  2114. 100% { transform: rotate(var(--player-rotation)) translateX(0) translateY(0); }
  2115. }
  2116. /* 携带物品容器 */
  2117. .carried-items-container {
  2118. position: absolute;
  2119. top: 20px;
  2120. left: 20px;
  2121. background: rgba(255, 255, 255, 0.8);
  2122. border: 2px solid #3498db;
  2123. border-radius: 10px;
  2124. padding: 10px;
  2125. display: flex;
  2126. gap: 10px;
  2127. z-index: 15;
  2128. backdrop-filter: blur(10px);
  2129. animation: fadeInScale 0.5s ease-out;
  2130. }
  2131. /* 淡入缩放动画 */
  2132. @keyframes fadeInScale {
  2133. 0% {
  2134. opacity: 0;
  2135. transform: scale(0.5) translateY(-10px);
  2136. }
  2137. 100% {
  2138. opacity: 1;
  2139. transform: scale(1) translateY(0);
  2140. }
  2141. }
  2142. /* 携带物品样式 */
  2143. .carried-item {
  2144. animation: bounceIn 0.3s ease-out forwards;
  2145. }
  2146. /* 【暂停】怀表容器样式 */
  2147. .watch-container {
  2148. animation: fadeInCountdown 0.3s ease-out;
  2149. position: relative;
  2150. }
  2151. /* 【暂停】怀表表盘样式 */
  2152. .watch-face {
  2153. width: 100%;
  2154. height: 100%;
  2155. border-radius: 50%;
  2156. background: linear-gradient(145deg, rgba(240, 240, 240, 0.8), rgba(200, 200, 200, 0.6));
  2157. border: 4px solid rgba(100, 100, 100, 0.8);
  2158. box-shadow:
  2159. 0 0 20px rgba(0, 0, 0, 0.3),
  2160. inset 0 0 20px rgba(0, 0, 0, 0.1);
  2161. position: relative;
  2162. display: flex;
  2163. justify-content: center;
  2164. align-items: center;
  2165. backdrop-filter: blur(5px);
  2166. }
  2167. /* 【暂停】怀表中心圆点 */
  2168. .watch-center {
  2169. width: 10%;
  2170. height: 10%;
  2171. background-color: rgba(100, 50, 20, 0.8);
  2172. border-radius: 50%;
  2173. position: absolute;
  2174. z-index: 10;
  2175. }
  2176. /* 【暂停】表针容器 */
  2177. .watch-hands {
  2178. position: absolute;
  2179. width: 100%;
  2180. height: 100%;
  2181. display: flex;
  2182. justify-content: center;
  2183. align-items: center;
  2184. }
  2185. /* 【暂停】表针基础样式 */
  2186. .watch-hand {
  2187. position: absolute;
  2188. background-color: rgba(100, 50, 20, 0.8);
  2189. transform-origin: bottom center;
  2190. border-radius: 2px;
  2191. }
  2192. /* 【暂停】时针样式 */
  2193. .hour-hand {
  2194. width: 4%;
  2195. height: 30%;
  2196. top: 20%;
  2197. animation: rotateHourHand 12s linear infinite;
  2198. }
  2199. /* 【暂停】分针样式 */
  2200. .minute-hand {
  2201. width: 3%;
  2202. height: 40%;
  2203. top: 10%;
  2204. animation: rotateMinuteHand 1s linear infinite;
  2205. }
  2206. /* 【暂停】怀表中心倒计时数字 */
  2207. .watch-countdown-number {
  2208. font-size: calc(var(--tile-size, 143px) * 0.3);
  2209. font-weight: bold;
  2210. color: rgba(100, 50, 20, 0.9);
  2211. text-shadow: 1px 1px 2px rgba(255, 255, 255, 0.8);
  2212. z-index: 5;
  2213. animation: pulseCountdown 1s infinite;
  2214. }
  2215. /* 【暂停】怀表淡入动画 */
  2216. @keyframes fadeInCountdown {
  2217. 0% {
  2218. opacity: 0;
  2219. transform: scale(0.8) rotate(-30deg);
  2220. }
  2221. 100% {
  2222. opacity: 1;
  2223. transform: scale(1) rotate(0deg);
  2224. }
  2225. }
  2226. /* 【暂停】倒计时数字脉冲动画 */
  2227. @keyframes pulseCountdown {
  2228. 0%, 100% {
  2229. opacity: 0.9;
  2230. transform: scale(1);
  2231. }
  2232. 50% {
  2233. opacity: 0.7;
  2234. transform: scale(1.05);
  2235. }
  2236. }
  2237. /* 【暂停】时针旋转动画 */
  2238. @keyframes rotateHourHand {
  2239. from { transform: rotate(0deg); }
  2240. to { transform: rotate(360deg); }
  2241. }
  2242. /* 【暂停】分针旋转动画 */
  2243. @keyframes rotateMinuteHand {
  2244. from { transform: rotate(0deg); }
  2245. to { transform: rotate(360deg); }
  2246. }
  2247. /* 弹入动画 */
  2248. @keyframes bounceIn {
  2249. 0% {
  2250. opacity: 0;
  2251. transform: scale(0.3) translateY(-20px);
  2252. }
  2253. 50% {
  2254. opacity: 0.7;
  2255. transform: scale(1.1) translateY(5px);
  2256. }
  2257. 80% {
  2258. opacity: 0.9;
  2259. transform: scale(0.95) translateY(-2px);
  2260. }
  2261. 100% {
  2262. opacity: 1;
  2263. transform: scale(1) translateY(0);
  2264. }
  2265. }
  2266. /* 成功到达终点动画 */
  2267. .player.success {
  2268. animation: success 1s ease-in-out;
  2269. }
  2270. @keyframes success {
  2271. 0% { transform: rotate(var(--player-rotation)) scale(1); }
  2272. 10% { transform: rotate(var(--player-rotation)) scale(1.2) translateX(-5px) translateY(-5px); }
  2273. 20% { transform: rotate(var(--player-rotation)) scale(1.3) translateX(5px) translateY(5px); }
  2274. 30% { transform: rotate(var(--player-rotation)) scale(1.2) translateX(-5px) translateY(-5px); }
  2275. 40% { transform: rotate(var(--player-rotation)) scale(1.3) translateX(5px) translateY(5px); }
  2276. 50% { transform: rotate(var(--player-rotation)) scale(1.4) translateX(0) translateY(0); }
  2277. 60% { transform: rotate(var(--player-rotation)) scale(1.3) translateX(-3px) translateY(-3px); }
  2278. 70% { transform: rotate(var(--player-rotation)) scale(1.2) translateX(3px) translateY(3px); }
  2279. 80% { transform: rotate(var(--player-rotation)) scale(1.3) translateX(-3px) translateY(-3px); }
  2280. 90% { transform: rotate(var(--player-rotation)) scale(1.2) translateX(3px) translateY(3px); }
  2281. 100% { transform: rotate(var(--player-rotation)) scale(1); }
  2282. }
  2283. /* 游戏消息样式 */
  2284. .game-message {
  2285. position: absolute;
  2286. top: 20px;
  2287. left: 50%;
  2288. transform: translateX(-50%);
  2289. padding: 10px 20px;
  2290. border-radius: 5px;
  2291. font-weight: bold;
  2292. z-index: 20;
  2293. min-width: 200px;
  2294. text-align: center;
  2295. }
  2296. .game-message.success {
  2297. background-color: #d4edda;
  2298. color: #155724;
  2299. border: 1px solid #c3e6cb;
  2300. }
  2301. .game-message.error {
  2302. background-color: #f8d7da;
  2303. color: #721c24;
  2304. border: 1px solid #f5c6cb;
  2305. }
  2306. .game-message.info {
  2307. background-color: #d1ecf1;
  2308. color: #0c5460;
  2309. border: 1px solid #bee5eb;
  2310. }
  2311. .game-message.warning {
  2312. background-color: #baeff8;
  2313. color: #035767;
  2314. border: 1px solid #9be9f6;
  2315. }
  2316. /* Blockly区域样式 */
  2317. .blockly-section {
  2318. flex: 1;
  2319. min-width: 600px;
  2320. display: flex;
  2321. flex-direction: column;
  2322. gap: 20px;
  2323. }
  2324. // 统一区块样式
  2325. .map-section, .workspace-section {
  2326. background: rgba(248, 249, 250, 0.82);
  2327. padding: 15px;
  2328. border-radius: 15px;
  2329. height: 100%;
  2330. }
  2331. .map-section h2, .workspace-section h2 {
  2332. margin-bottom: 15px;
  2333. color: #2c3e50;
  2334. border-bottom: 2px solid #3498db;
  2335. padding-bottom: 8px;
  2336. }
  2337. #blocklyDiv {
  2338. height: 87%;
  2339. min-height: rpx(250);
  2340. width: 100%;
  2341. background: #fff;
  2342. border: 1px solid #ddd;
  2343. border-radius: 8px;
  2344. }
  2345. /* 优化Blockly积木样式 */
  2346. /* 积木高度 */
  2347. .blocklyBlockCanvas .blocklyBlock {
  2348. height: 45px; /* 默认高度 */
  2349. min-height: 45px;
  2350. }
  2351. /* 积木内部元素的行高和间距 */
  2352. .blocklyText {
  2353. font-size: 16px;
  2354. line-height: 20px;
  2355. font-weight: 500;
  2356. }
  2357. /* 输入字段的高度 */
  2358. .blocklyHtmlInput {
  2359. height: 30px;
  2360. font-size: 14px;
  2361. padding: 5px;
  2362. }
  2363. /* 下拉菜单的高度 */
  2364. .blocklyDropdownMenu {
  2365. line-height: 28px;
  2366. font-size: 14px;
  2367. }
  2368. /* 优化积木圆角和阴影效果 */
  2369. .blocklyBlock {
  2370. border-radius: 8px;
  2371. filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.1));
  2372. }
  2373. /* 积木之间的连接点间距 */
  2374. .blocklyConnection {
  2375. height: 20px;
  2376. width: 20px;
  2377. }
  2378. /* 工具箱中积木的高度 */
  2379. .blocklyTreeRow {
  2380. height: 40px;
  2381. line-height: 40px;
  2382. }
  2383. .controls {
  2384. display: flex;
  2385. gap: 10px;
  2386. margin: 15px 0px;
  2387. flex-wrap: wrap;
  2388. }
  2389. button {
  2390. padding: 10px 20px;
  2391. border: none;
  2392. border-radius: 5px;
  2393. background: #3498db;
  2394. color: #fff;
  2395. font-weight: 700;
  2396. cursor: pointer;
  2397. transition: all 0.3s ease;
  2398. }
  2399. .game-badge:hover {
  2400. background-color: #3498db;
  2401. transform: translateY(-2px);
  2402. box-shadow: 0 rpx(5) rpx(10) rgba(0, 0, 0, 0.1);
  2403. }
  2404. .game-badge.active {
  2405. background-color: #e74c3c;
  2406. color: #fff;
  2407. font-weight: bold;
  2408. }
  2409. .game-badge.passed {
  2410. background-color: #27ae60;
  2411. color: #fff;
  2412. }
  2413. .game-badge.disabled {
  2414. background-color: #bdc3c7;
  2415. color: #7f8c8d;
  2416. cursor: not-allowed;
  2417. pointer-events: none;
  2418. }
  2419. .game-badge.disabled:hover {
  2420. background-color: #bdc3c7;
  2421. transform: none;
  2422. box-shadow: none;
  2423. }
  2424. #runCode {
  2425. background: #e74c3c;
  2426. }
  2427. #runCode:hover {
  2428. background: #c0392b;
  2429. }
  2430. /* 响应式布局 */
  2431. @media (max-width: 1200px) {
  2432. .map-section,
  2433. .blockly-section {
  2434. flex: 1;
  2435. min-width: 45%;
  2436. }
  2437. .map-background {
  2438. width: 100%;
  2439. height: 400px;
  2440. }
  2441. }
  2442. </style>