MapGame.vue 86 KB

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