MapGame.vue 78 KB

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