| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802 |
- <template>
- <div class="title-box">
- <!-- 左侧标题部分 -->
- <div class="left-container">
- <!-- 线路切换按钮 -->
- <div class="route-buttons">
- <button
- v-for="(route, index) in parsedRouteList"
- :key="index"
- class="game-badge"
- :class="{ 'active': currentRouteIndex === index }"
- >
- {{ gameSort }}{{ index + 1 }}
- </button>
- </div>
- </div>
- </div>
- <!-- 中间地图组件部分-->
- <div class="content">
- <!-- 地图显示区域 -->
- <div class="map-section">
- <!-- 内容简介提示 -->
- <div v-if="currentGameData?.info" class="info-message-container">
- <div class="message-item">
- <div class="avatar">
- <img src="@/assets/images/xiaozhi2.png" alt="头像" class="avatar-image" />
- </div>
- <p v-if="currentGameData?.info" v-html="currentGameData?.info"></p>
- </div>
- </div>
- <div class="map-container">
- <!-- 地图背景 -->
- <div class="map-background">
- <img :src="mapBackground" alt="地图背景" class="map-image" @load="updateMapContainerDimensions" />
- <!-- 可行走区域标记 -->
- <div
- v-for="(point, index) in walkablePoints"
- :key="index"
- class="walkable-point"
- :style="getPointStyle(point)"
- >
- </div>
- <!-- 玩家角色 -->
- <div
- class="player"
- :style="playerStyle"
- :class="{ 'collision': isColliding, 'success': hasReachedEnd }"
- >
- </div>
- <!-- 怀表倒计时容器 -->
- <div v-if="showCountdown" class="watch-container" :style="countdownStyle">
- <div class="watch-face">
- <div class="watch-center"></div>
- <div class="watch-hands">
- <div class="watch-hand hour-hand"></div>
- <div class="watch-hand minute-hand"></div>
- </div>
- <div class="watch-countdown-number">{{ countdownValue }}</div>
- </div>
- </div>
- <!-- 携带物品容器 -->
- <div class="carried-items-container" v-show="gameState.player.carriedItems.length > 0">
- <div
- v-for="(item, index) in gameState.player.carriedItems"
- :key="index"
- class="carried-item"
- :style="getCarriedItemStyle(index, item)"
- ></div>
- </div>
- </div>
- <!-- 游戏状态提示 -->
- <div v-if="gameMessage" :class="['game-message', messageType]">
- {{ gameMessage }}
- </div>
- </div>
- </div>
- <!-- Blockly工作区 -->
- <div class="blockly-section">
- <div class="workspace-section">
- <div class="controls">
- <button id="runCode" @click="resetPlayer();runCode();" :disabled="isRunning">运行代码</button>
- <!-- <button @click="generatePythonCode">生成Python代码</button>-->
- <button @click="resetPlayer" >重置玩家</button>
- <button @click="clearWorkspace">清空工作区</button>
- </div>
- <!-- 工具箱-->
- <div id="toolbox"></div>
- <!-- 工作区-->
- <div id="blocklyDiv"></div>
- </div>
- </div>
- </div>
- <!-- 全屏遮罩盒子 通关后弹框得奖杯 -->
- <div class="fullscreen-overlay" v-if="showOverlay">
- <div
- class="centered-box"
- :style="{
- backgroundImage: `url(${passConfig.passBackground})`,
- backgroundSize: '110%',
- backgroundPosition: 'center',
- backgroundRepeat: 'no-repeat'
- }">
- <!-- 彩带飘落效果 -->
- <div class="confetti-container" v-if="passConfig.passStar > 0">
- <div v-for="n in 50" :key="n" class="confetti" :class="`confetti-${n % 12 + 1}`"></div>
- </div>
- <!-- 关闭按钮 -->
- <div class="close-button" @click="closeOverlay">×</div>
- <!-- 题目 -->
- <div class="top-box">
- <div class="title-text">{{passConfig.title}}</div>
- </div>
- <!-- 奖杯 -->
- <div class="middle-box">
- <img :src="passConfig.passTrophy" alt="奖杯" class="gold-cup-image" />
- </div>
- <!-- 星星 -->
- <div class="bottom-box">
- <img
- v-for="index in passConfig.starTotal"
- :key="index"
- :src="index <= passConfig.passStar ? star01 : star02"
- alt="星星"
- class="gold-star-image"
- :class="index % 2 === 0 ? 'star-bottom' : 'star-top'"
- />
- </div>
- </div>
- </div>
- </template>
- <script setup>
- import {ref, onMounted, onUnmounted, reactive, computed, nextTick, watch, defineEmits} from 'vue';
- import { javascriptGenerator } from "blockly/javascript";
- import { pythonGenerator } from "blockly/python";
- import playerImage from '@/assets/images/blockly/user.png';
- // 导入音频文件
- import passMp3 from '@/assets/music/blockly/pass.MP3';
- import failureMp3 from '@/assets/music/blockly/failure.MP3';
- import passRouteMp3 from '@/assets/music/blockly/pass_route.MP3';
- import errorMp3 from '@/assets/music/blockly/error.MP3'
- // 游戏接口数据
- import { registerCustomBlocks, registerJavaScriptGenerators, registerPythonGenerators, initBlockly, BLOCKLY_MAP_TYPE_DICT, BLOCKLY_MAP_SPECIAL_DICT } from '@/api/blockly/blockly.js';
- import {playMp3} from "@/api/blockly/music.js";
- import cupbg from '@/assets/blockly/cupbg.png' // 通关后奖杯底图
- import nocupbg from '@/assets/blockly/nocupbg.png' // 通关失败后奖杯底图
- import goldcup from '@/assets/blockly/goldcup.png' // 金杯
- import silvercup from '@/assets/blockly/silvercup.png' // 银杯
- import coppercup from '@/assets/blockly/coppercup.png' // 铜杯
- import nocup from '@/assets/blockly/nocup.png' // 无杯状态
- import star02 from '@/assets/blockly/star02.png' // 星星
- import star01 from '@/assets/blockly/star01.png' // 金星
- import passFailure from '@/assets/music/blockly/pass_failure.MP3' // 通关失败音效
- import passFireworks from '@/assets/music/blockly/pass_fireworks.MP3' // 通关小号音效
- import passTrump from '@/assets/music/blockly/pass_trumpet.MP3' // 通关庆祝礼炮奖杯
- // 星星数量,可根据需要调整
- const passConfig = ref({
- starTotal: 3,// 总星星数量
- title: 'YOU WIN!',// 通关标题
- passStar: 0,// 已通关星星数量
- passTrophy: "",// 通关奖杯
- passBackground: "",// 通关后奖杯底图
- });
- // 定义emits
- const emits = defineEmits(['saveProgress'])
- // 定义组件属性
- const props = defineProps({
- // 游戏ID
- gameId: {
- type: [String, Number],
- default: ''
- },
- // 地图背景
- mapBackground: {
- type: String,
- default: ''
- },
- // 地图瓦片大小
- mapTileSize: {
- type: [String, Object],
- default: ''
- },
- // 可行走点
- mapWalkablePoints: {
- type: [String, Array],
- default: ''
- },
- // 用户图片
- userImage: {
- type: String,
- default: ''
- },
- // 游戏信息
- info: {
- type: String,
- default: ''
- },
- // 路线列表
- routeList: {
- type: [String, Array],
- default: ''
- },
- // 课程列表
- courseList: {
- type: Array,
- default: () => []
- },
- // 当前课程索引
- currentIndex: {
- type: Number,
- default: 0
- },
- // 特殊积木方法标识集合
- blocklySpecialBlocks: {
- type: Array,
- default: () => []
- }
- });
- // 配置常量
- const CONFIG = {
- // 动画时长配置(毫秒)
- ANIMATION: {
- MOVE_DURATION: 500, // 移动动画持续时间
- ROTATE_DURATION: 500, // 旋转动画持续时间(左转/右转)
- TURN_AROUND_DURATION: 750, // 向后转动画持续时间
- },
- // 延迟配置(毫秒)
- DELAY: {
- ACTION_DELAY: 200, // 每次动作后的延迟时间
- COLLISION_DELAY: 300, // 碰撞后的延迟时间
- RESET_DELAY: 300, // 重置后的延迟时间
- MESSAGE_DISPLAY: 2000, // 消息显示时间
- ICE_MESSAGE_DISPLAY: 300, // 冰块消息显示时间
- COLLISION_RESET: 1000, // 碰撞状态重置时间
- LOOP_PREVENTION: 10, // 循环防止UI阻塞的延迟
- ITEMS_APPEAR: 300, // 物品出现延迟时间
- ITEMS_FLIGHT_ANIMATION_DELAY: 800, // 飞行动画延迟时间
- PALY_MP3_TIMES: 1000, // 播放MP3文件的时间间隔
- VIDEO_TIMEOUT: 5000, // 视频播放超时处理时间
- VIDEO_FADE_DURATION: 500, // 视频淡入淡出持续时间
- PLAYER_FADE_DURATION: 500, // 玩家淡入淡出持续时间
- TEMP_ITEM_FADE_DURATION: 500, // 临时物品淡入淡出持续时间
- COMPLETION_DISPLAY: 1000, // 任务完成后显示延迟时间
- },
- // 游戏配置
- GAME: {
- MAX_LOOP_COUNT: 100, // 最大循环次数
- DIRECTIONS: {
- UP: 0,
- RIGHT: 1,
- DOWN: 2,
- LEFT: 3
- }
- },
- // 样式配置
- STYLES: {
- DEFAULT_TILE_SIZE: 143, // 默认瓦片大小
- PLAYER_SIZE_RATIO: 0.8, // 玩家大小占瓦片的比例
- PLAYER_SIZE_MARGIN: 0.1, // 玩家大小占瓦片边距
- IMG_SIZE_RATIO: 0.8, // 地图素材大小占瓦片的比例
- IMG_SIZE_MARGIN: 0.1, // 地图素材大小占瓦片边距
- ITEM_CONTAINER_POSITION: { x: 30, y: 30 }, // 物品容器位置
- ITEM_CONTAINER_RATIO: 0.4, // 物品大小占容器的比例
- ITEM_CONTAINER_SPACING: 10 // 物品大小占容器的间距
- },
- //提示语
- TIPS: {
- NO_ENTRY: '当前位置无通路,无法移动',
- UNFINISHED: '任务未完成!',
- EFFORT: '再接再厉!',
- PASS_ROUTE: '恭喜你通过了当前路线,自动进入下一条路线!',
- FINISH: '恭喜你到达终点!',
- PICKUP_ITEM: '拾取物品成功!',
- NULL_PICKUP_ITEM: '当前位置没有可拾取的物品',
- USE_ITEM_SUCCESS: '使用物品成功',
- USE_SPECIAL_ITEM: '需要特殊物品才能使用',
- NO_USE_ITEM: '当前位置不需要使用物品',
- ERROR_ERROR: '错了错了',//任务消失时的错误提示
- }
- };
- // 路由和游戏状态
- const currentGameData = ref(null);
- const playerInitialDirection = ref(0); // 人物初始朝向
- const gameSort = ref('路线'); // 默认排序
- const currentRouteIndex = ref(0); // 当前线路索引
- // 路线通过状态:存储各路线的通过状态
- const routePassedStatus = ref([]);
- // 解析routeList
- const parsedRouteList = computed(() => {
- if (!props.routeList) return [];
- try {
- const routeData = typeof props.routeList === 'string' ? JSON.parse(props.routeList) : props.routeList;
- const routes = Array.isArray(routeData) ? routeData : [];
-
- // 初始化路线通过状态
- if (routePassedStatus.value.length === 0 && routes.length > 0) {
- routePassedStatus.value = routes.map((_, index) => index === 0); // 第一条路线默认可用
- }
-
- return routes;
- } catch (error) {
- console.error('解析routeList失败:', error);
- return [];
- }
- });
- // 运行控制标志
- let shouldStopExecution = false;
- let currentExecutionPromise = null;
- let executionAbortController = null;
- // 添加响应式的容器尺寸
- const mapContainerDimensions = ref({ width: 0, height: 0 });
- // 用于控制运行/重置按钮状态
- const isRunning = ref(false);
- // 存储生成的Python代码
- const generatedPythonCode = ref('');
- // 暂停模块-倒计时相关状态
- const showCountdown = ref(false);
- const countdownValue = ref(0);
- // Blockly相关状态
- let workspace = null;
- // 使用 Map 存储可行走点及其类型,提高查询效率
- let walkablePointsMap = new Map();
- // 创建游戏状态的响应式对象
- const gameState = reactive({
- // 地图配置信息
- mapConfig: {
- // 地图背景图片路径
- background: '',
- // 每个瓦片的尺寸(像素)
- tileSize: CONFIG.STYLES.DEFAULT_TILE_SIZE,
- },
- // 玩家相关状态
- player: {
- // 玩家当前位置坐标
- position: {},
- // 玩家当前朝向:0=上, 1=右, 2=下, 3=左
- direction: 1,
- // 是否正在发生碰撞
- isColliding: false,
- // 是否已到达终点
- hasReachedEnd: false,
- // 是否正在冰块上滑行
- isSliding: false,
- // 携带的物品数组
- carriedItems: [],
- },
- // 游戏状态信息
- status: {
- // 当前显示的游戏消息
- message: '',
- // 消息类型(如success、error、info等)
- messageType: ''
- },
- // 地图数据信息
- mapData: {
- // 游戏起点位置
- startPoint: {},
- // 游戏终点位置
- endPoint: {},
- // 地图上所有可行走的点坐标集合,添加type属性区分普通点和冰块
- walkablePoints: [],
- // 路线列表
- routeList: [],
- // 保存原始的可行走点数据,用于重置
- originalWalkablePoints: [],
- }
- });
- // 控制遮罩层显示的状态
- const showOverlay = ref(false);
- // 字典常量已从blockly.js中导入
- // 计算属性 - 提高性能和可读性
- const mapBackground = computed(() => gameState.mapConfig.background);
- // const tileSize = computed(() => gameState.mapConfig.tileSize);
- const walkablePoints = computed(() => gameState.mapData.walkablePoints);
- const startPoint = computed(() => gameState.mapData.startPoint);
- const endPoint = computed(() => gameState.mapData.endPoint);
- const playerPosition = computed(() => gameState.player.position);
- const playerDirection = computed(() => gameState.player.direction);
- const isColliding = computed(() => gameState.player.isColliding);
- const hasReachedEnd = computed(() => gameState.player.hasReachedEnd);
- const gameMessage = computed(() => gameState.status.message);
- const messageType = computed(() => gameState.status.messageType);
- const isSliding = computed(() => gameState.player.isSliding);
- // 计算玩家图片路径,优先使用接口数据
- const playerImageSrc = computed(() => {
- if (currentGameData.value && currentGameData.value.userImage) {
- return currentGameData.value.userImage.trim();
- }
- return playerImage;
- });
- // 计算实际瓦片大小(基于容器尺寸和地图数据)
- const tileSize = computed(() => {
- if (mapContainerDimensions.value.width === 0 || mapContainerDimensions.value.height === 0) {
- return gameState.mapConfig.tileSize;
- }
- // 获取地图数据中的最大坐标
- let size = JSON.parse(gameState.mapConfig.tileSize);
- // 计算基于容器的瓦片大小,确保地图完全可见
- const tileWidth = mapContainerDimensions.value.width / size.x;
- const tileHeight = mapContainerDimensions.value.height / size.y;
- // 返回较小的值以确保地图完全可见
- return Math.min(tileWidth, tileHeight);
- });
- // 计算玩家样式
- const playerStyle = computed(() => ({
- left: playerPosition.value.x * tileSize.value - tileSize.value + 'px',
- top: playerPosition.value.y * tileSize.value - tileSize.value + 'px',
- transform: `rotate(${playerDirection.value * 90}deg)`,
- '--player-rotation': `${playerDirection.value * 90}deg`,
- '--player-image': `url(${playerImageSrc.value})`,
- width: (tileSize.value * CONFIG.STYLES.PLAYER_SIZE_RATIO) + 'px',
- height: (tileSize.value * CONFIG.STYLES.PLAYER_SIZE_RATIO) + 'px',
- margin: (tileSize.value * CONFIG.STYLES.PLAYER_SIZE_MARGIN) + 'px',
- }));
- // 暂停倒计时样式计算
- const countdownStyle = computed(() => {
- return {
- position: 'absolute',
- left: playerPosition.value.x * tileSize.value - tileSize.value - (tileSize.value * 0.2) + 'px',
- top: playerPosition.value.y * tileSize.value - tileSize.value - (tileSize.value * 0.2) + 'px',
- width: tileSize.value * 0.5 + 'px',
- height: tileSize.value * 0.5 + 'px',
- display: 'flex',
- justifyContent: 'center',
- alignItems: 'center',
- zIndex: 100
- };
- });
- // 监听props变化,当地图数据变化时更新游戏数据
- watch(() => [props.gameId, props.mapBackground, props.mapWalkablePoints], async () => {
- // 获取游戏数据
- await fetchGameData();
- // 初始化可行走点集合
- initWalkablePointsSet();
- // 重置玩家位置
- await resetPlayer();
- await nextTick();
- // 更新容器尺寸
- updateMapContainerDimensions();
- }, { deep: true });
- // 生命周期钩子
- onMounted(async () => {
- // 获取游戏数据
- await fetchGameData();
- // 初始化可行走点集合
- initWalkablePointsSet();
- // 注册自定义积木
- registerCustomBlocks(props.blocklySpecialBlocks);
- // 注册JavaScript生成器
- registerJavaScriptGenerators(props.blocklySpecialBlocks);
- // 注册Python生成器
- registerPythonGenerators(props.blocklySpecialBlocks);
- // 动态生成工具箱XML
- const toolboxContainer = document.getElementById('toolbox');
- toolboxContainer.innerHTML = generateToolboxXml();
- // 初始化Blockly工作区
- workspace = initBlockly();
- // 重置玩家位置
- await resetPlayer();
- await nextTick();
- // 添加对窗口大小变化的监听
- updateMapContainerDimensions();
- window.addEventListener('resize', updateMapContainerDimensions);
- });
- //================初始化=====================
- // 动态生成工具箱XML
- function generateToolboxXml() {
- let toolboxXml = `
- <category name="移动控制" colour="230">
- <block type="move_forward"></block>
- <block type="move_backward"></block>
- <block type="turn_left"></block>
- <block type="turn_right"></block>
- <block type="turn_around"></block>
- <block type="move_forward_param"></block>
- <block type="move_backward_param"></block>
- </category>
- <category name="功能" colour="120">
- <block type="pickup_item"></block>
- <block type="use_item"></block>
- <block type="when_passed"></block>
- `;
- // 确保blocklySpecialBlocks是数组
- const specialBlocks = Array.isArray(props.blocklySpecialBlocks) ? props.blocklySpecialBlocks : [];
- // 获取所有特殊积木类型
- const specialBlockTypes = Object.values(BLOCKLY_MAP_SPECIAL_DICT);
- specialBlockTypes.forEach(blockType => {
- if (specialBlocks.includes(blockType)) {
- toolboxXml += `<block type="${blockType}"></block>`;
- }
- })
- // 根据允许的特殊积木动态添加
- // if (specialBlocks.includes(BLOCKLY_MAP_SPECIAL_DICT.PAUSE)) {
- // toolboxXml += '<block type="pause"></block>';
- // }
- // if (specialBlocks.includes(BLOCKLY_MAP_SPECIAL_DICT.PLAY_SOUND)) {
- // toolboxXml += '<block type="play_sound"></block>';
- // }
- // if (specialBlocks.includes(BLOCKLY_MAP_SPECIAL_DICT.CONSTRUCT)) {
- // toolboxXml += '<block type="construct"></block>';
- // }
- toolboxXml += `
- </category>
- <category name="逻辑" colour="210">
- <block type="controls_if"></block>
- <block type="logic_compare"></block>
- <block type="logic_operation"></block>
- <block type="logic_boolean"></block>
- </category>
- <category name="循环" colour="160">
- <block type="controls_repeat_ext"></block>
- </category>
- <category name="数学" colour="60">
- <block type="math_number"></block>
- <block type="math_arithmetic"></block>
- </category>
- `;
- return toolboxXml;
- }
- // 获取游戏数据
- const fetchGameData = async () => {
- try {
- // 优先使用props传递的数据
- if (props.gameId && props.mapBackground) {
- // 使用props数据构建游戏数据对象
- currentGameData.value = {
- mapBackground: props.mapBackground,
- mapTileSize: props.mapTileSize,
- mapWalkablePoints: props.mapWalkablePoints,
- routeList: props.routeList,
- userImage: props.userImage,
- info: props.info,
- };
- // 直接更新游戏状态
- await updateGameStateFromData(currentGameData.value);
- // 数据更新后强制刷新容器尺寸(等待DOM更新)
- await nextTick();
- updateMapContainerDimensions();
- } else {
- // 注释:如果没有props数据,则从API获取
- }
- } catch (error) {
- console.error('获取游戏数据失败:', error);
- }
- };
- // 切换线路
- const switchRoute = (index) => {
- // 检查路线是否可用
- if (index < 0 || index >= parsedRouteList.value.length || !isRouteAvailable(index)) return;
-
- currentRouteIndex.value = index;
- const route = parsedRouteList.value[index];
-
- // 更新人物朝向
- if (route.direction !== undefined) {
- playerInitialDirection.value = route.direction;
- gameState.player.direction = route.direction;
- }
-
- // 更新开始坐标
- if (route.startPoint) {
- const startPoint = typeof route.startPoint === 'string' ? JSON.parse(route.startPoint) : route.startPoint;
- gameState.mapData.startPoint = { x: startPoint.x, y: startPoint.y };
- gameState.player.position = { x: startPoint.x, y: startPoint.y };
- }
-
- // 更新结束坐标
- if (route.endPoint) {
- const endPoint = typeof route.endPoint === 'string' ? JSON.parse(route.endPoint) : route.endPoint;
- gameState.mapData.endPoint = { x: endPoint.x, y: endPoint.y };
- }
-
- // 重新初始化可行走点集合【目前没有单独配置路线的可行走集合,一张地图通用】
- // initWalkablePointsSet();
- };
- // 检查路线是否可用
- const isRouteAvailable = (index) => {
- // 第一条路线始终可用
- if (index === 0) return true;
- // 其他路线需要前一条路线通过
- return routePassedStatus.value[index - 1];
- };
- // 根据获取到的数据更新游戏状态
- const updateGameStateFromData = (gameData) => {
- try {
- // 更新地图配置
- gameState.mapConfig.background = gameData.mapBackground ? gameData.mapBackground.trim() : '';
- gameState.mapConfig.tileSize = gameData.mapTileSize || CONFIG.STYLES.DEFAULT_TILE_SIZE;
- // 更新玩家方向
- if (gameData.userDirection) {
- playerInitialDirection.value = gameData.userDirection;
- }
- // 更新地图数据
- // 地图起点
- if (gameData.mapStartPoint) {
- const startPoint = JSON.parse(gameData.mapStartPoint);
- gameState.mapData.startPoint = { x: startPoint.x, y: startPoint.y };
- gameState.player.position = { x: startPoint.x, y: startPoint.y };
- }
- // 地图终点
- if (gameData.mapEndPoint) {
- const endPoint = JSON.parse(gameData.mapEndPoint);
- gameState.mapData.endPoint = { x: endPoint.x, y: endPoint.y };
- }
- if (gameData.mapWalkablePoints) {
- gameState.mapData.walkablePoints = JSON.parse(gameData.mapWalkablePoints);
- }
- // 路线列表
- if (gameData.routeList) {
- gameState.mapData.routeList = JSON.parse(gameData.routeList);
- }
-
- // 初始加载时选中第一条线路
- if (parsedRouteList.value.length > 0) {
- switchRoute(0);
- }
-
- // 重新初始化可行走点集合
- initWalkablePointsSet();
- } catch (error) {
- console.error('更新游戏状态失败:', error);
- }
- };
- // 更新地图容器尺寸的函数
- function updateMapContainerDimensions() {
- const mapContainer = document.querySelector('.map-container');
- if (mapContainer) {
- const rect = mapContainer.getBoundingClientRect();
- // 确保获取到有效的尺寸值
- if (rect.width > 0 && rect.height > 0) {
- mapContainerDimensions.value = {
- width: rect.width,
- height: rect.height
- };
- } else {
- // 若尺寸无效,使用默认容器尺寸(可根据实际情况调整)
- mapContainerDimensions.value = {
- width: 800,
- height: 600
- };
- }
- }
- };
- // 初始化可行走点映射
- function initWalkablePointsSet() {
- walkablePointsMap.clear();
- gameState.mapData.walkablePoints.forEach(point => {
- walkablePointsMap.set(`${point.x},${point.y}`, point);
- });
- // 保存原始的可行走点数据,用于重置
- gameState.mapData.originalWalkablePoints = JSON.parse(JSON.stringify(gameState.mapData.walkablePoints));
- }
- // 计算点的样式
- function getPointStyle(point) {
- const style = {
- left: point.x * tileSize.value - tileSize.value + 'px',
- top: point.y * tileSize.value - tileSize.value + 'px',
- width: tileSize.value + 'px',
- height: tileSize.value + 'px',
- };
- // 如果point有img属性,则添加图标
- if (point.img) {
- const iconSize = tileSize.value * CONFIG.STYLES.IMG_SIZE_RATIO;
- const marginSize = tileSize.value * CONFIG.STYLES.IMG_SIZE_MARGIN;
- // 重置可能影响背景图显示的样式
- style.backgroundColor = 'transparent';
- style.backgroundImage = `url(${point.img})`;
- style.backgroundSize = 'contain';
- style.backgroundPosition = 'center';
- style.backgroundRepeat = 'no-repeat';
- // 设置与玩家相同的宽高和边距
- style.width = iconSize + 'px';
- style.height = iconSize + 'px';
- style.margin = marginSize + 'px';
- }
- return style;
- }
- // 计算携带物品样式
- function getCarriedItemStyle(index, item) {
- const baseSize = tileSize.value * CONFIG.STYLES.ITEM_CONTAINER_RATIO;
- return {
- position: 'relative',
- width: baseSize + 'px',
- height: baseSize + 'px',
- backgroundSize: 'contain',
- backgroundPosition: 'center',
- backgroundRepeat: 'no-repeat',
- backgroundImage: `url(${item.img})`,
- animationDelay: index * 0.1 + 's'
- };
- }
- //================操作按钮=====================
- // 运行代码
- const runCode = async () => {
- isRunning.value = true;
- try {
- await new Promise(resolve => setTimeout(resolve, CONFIG.DELAY.RESET_DELAY));
- // 重置执行标志,允许新的执行
- shouldStopExecution = false;
- // 创建新的AbortController用于取消执行
- executionAbortController = new AbortController();
- const signal = executionAbortController.signal;
- // 确保生成器和工作区都存在
- if (!javascriptGenerator || !workspace) {
- throw new Error('生成器或工作区未正确初始化');
- }
- // 生成JavaScript代码
- const code = javascriptGenerator.workspaceToCode(workspace) + "await isFinish();";
- try {
- // 增强的安全检查
- const unsafePatterns = [
- 'eval(', 'Function(', 'document.write', 'window.location',
- 'document.createElement', 'XMLHttpRequest', 'fetch',
- 'setInterval', 'setTimeout', 'window.',
- 'alert(', 'confirm(', 'prompt(',
- 'document.cookie', 'localStorage', 'sessionStorage'
- ];
- const hasUnsafeCode = unsafePatterns.some(pattern => code.includes(pattern));
- if (hasUnsafeCode) {
- throw new Error('代码包含不安全的操作');
- }
- // 包装代码为异步函数执行,并设置超时保护
- currentExecutionPromise = new Promise(async (resolve, reject) => {
- try {
- // 检查信号是否已中止
- if (signal.aborted) {
- throw new Error('执行已取消');
- }
- // 添加信号监听
- signal.addEventListener('abort', () => {
- reject(new Error('执行已取消'));
- });
- const wrappedCode = `(async () => { ${code} })()`;
- await new Function(wrappedCode)();
- resolve();
- } catch (error) {
- reject(error);
- }
- });
- } catch (error) {
- // 捕获并显示执行错误
- if (error.message !== '执行已取消') {
- const errorMsg = error.message || '未知错误';
- showGameMessage(`代码执行错误: ${errorMsg}`, 'error');
- console.error('代码执行错误:', error);
- }
- } finally {
- // 清除当前执行的Promise引用
- currentExecutionPromise = null;
- }
- } catch (error) {
- showGameMessage(`运行时错误: ${error.message || '未知错误'}`, 'error');
- console.error('运行时错误:', error);
- } finally {
- isRunning.value = false;
- }
- };
- // 重置玩家位置和状态
- const resetPlayer = async () => {
- isRunning.value = false;
- // 设置标志强制停止所有执行
- shouldStopExecution = true;
- // 清除倒计时显示和重置倒计时值
- showCountdown.value = false;
- countdownValue.value = 0;
- // 取消任何正在执行的代码
- if (executionAbortController) {
- executionAbortController.abort();
- executionAbortController = null;
- }
- if (currentExecutionPromise) {
- currentExecutionPromise = null;
- }
- // 清除所有视频组件
- const mapBackground = document.querySelector('.map-background');
- if (mapBackground) {
- // 清除视频元素
- const videoElements = mapBackground.querySelectorAll('video');
- videoElements.forEach(video => {
- if (mapBackground.contains(video)) {
- mapBackground.removeChild(video);
- }
- });
- // 清除临时动画元素
- const tempElements = mapBackground.querySelectorAll('div[style*="backgroundImage"]');
- tempElements.forEach(temp => {
- if (mapBackground.contains(temp)) {
- mapBackground.removeChild(temp);
- }
- });
- }
- // 确保小智显形(不隐形)
- const playerElement = document.querySelector('.player');
- if (playerElement) {
- playerElement.style.opacity = '1';
- playerElement.style.transition = 'none'; // 取消过渡效果,立即显示
- }
- // 重置携带的物品回地图
- if (gameState.mapData.originalWalkablePoints.length > 0) {
- gameState.mapData.walkablePoints = JSON.parse(JSON.stringify(gameState.mapData.originalWalkablePoints));
- }
- // 清空携带物品
- gameState.player.carriedItems = [];
- // 重新初始化可行走点集合
- initWalkablePointsSet();
- gameState.player.position = { ...startPoint.value };
- gameState.player.direction = playerInitialDirection.value; // 重置为初始方向
- gameState.player.isColliding = false; //碰撞标志
- gameState.player.hasReachedEnd = false;
- gameState.player.isSliding = false; // 重置滑行状态
- showOverlay.value = false; // 隐藏遮罩层
- // 处理多路线情况:重置回初始的第一条路线并清空路线过关标识
- if (parsedRouteList.value.length > 1) {
- // 重置路线通过状态:只有第一条路线可用
- routePassedStatus.value = parsedRouteList.value.map((_, index) => index === 0);
- // 切换回第一条路线
- switchRoute(0);
- }
- };
- // 清空工作区
- const clearWorkspace = () => {
- workspace.clear();
- showGameMessage('工作区已清空', 'info');
- };
- // 生成Python代码
- const generatePythonCode = () => {
- if (!workspace) {
- return;
- }
- try {
- // 生成Python代码
- const pythonCode = pythonGenerator.workspaceToCode(workspace);
- // 存储到常量中
- generatedPythonCode.value = pythonCode;
- console.log('生成的Python代码:', pythonCode);
- // 可以添加提示信息
- showGameMessage('Python代码生成成功!', 'success');
- } catch (error) {
- console.error('生成Python代码时出错:', error);
- showGameMessage('生成Python代码失败', 'error');
- }
- };
- // 统一处理撞到墙时的停止逻辑
- async function handleWallCollision(endMsg = CONFIG.TIPS.NO_ENTRY) {
- // 设置碰撞状态
- gameState.player.isColliding = true;
- // 显示错误消息
- showGameMessage(endMsg, 'error');
- await playMp3(errorMp3);
- await new Promise(resolve => setTimeout(resolve, CONFIG.DELAY.PALY_MP3_TIMES));
- // 立即中止整个代码执行
- if (executionAbortController) {
- executionAbortController.abort();
- }
- // 所有动画和移动操作立即停止
- shouldStopExecution = true;
- // 碰撞状态重置时间后取消碰撞状态
- setTimeout(() => {
- gameState.player.isColliding = false;
- }, CONFIG.DELAY.COLLISION_RESET);
- // 返回一个Promise,允许调用者等待碰撞延迟
- return new Promise(resolve => setTimeout(resolve, CONFIG.DELAY.COLLISION_DELAY));
- }
- // 显示游戏消息
- function showGameMessage(message, type = 'info', duration = CONFIG.DELAY.MESSAGE_DISPLAY) {
- gameState.status.message = message;
- gameState.status.messageType = type;
- // 消息显示时间后自动清除消息
- setTimeout(() => {
- gameState.status.message = '';
- }, duration);
- }
- // 关闭遮罩层
- const closeOverlay = () => {
- gameState.player.hasReachedEnd = false;
- showOverlay.value = false;
- };
- //================积木组件方法=====================
- // 向前移动
- window.moveForward = async function(stepCount = 1) {
- if (shouldStopExecution || isColliding.value || isSliding.value) {
- return;
- }
- for (let i = 1; i <= stepCount; i++) {
- let newX = playerPosition.value.x;
- let newY = playerPosition.value.y;
- // 向前移动
- switch(playerDirection.value) {
- case CONFIG.GAME.DIRECTIONS.UP: newY--; break;
- case CONFIG.GAME.DIRECTIONS.RIGHT: newX++; break;
- case CONFIG.GAME.DIRECTIONS.DOWN: newY++; break;
- case CONFIG.GAME.DIRECTIONS.LEFT: newX--; break;
- }
- await moveStep(newX, newY);
- }
- };
- // 向后移动
- window.moveBackward = async function(stepCount = 1) {
- // 如果已经发生过碰撞,不再执行任何移动
- if (shouldStopExecution || isColliding.value || isSliding.value) {
- return;
- }
- for (let i = 1; i <= stepCount; i++) {
- let newX = playerPosition.value.x;
- let newY = playerPosition.value.y;
- // 根据当前方向计算新位置(向后移动)
- switch(playerDirection.value) {
- case CONFIG.GAME.DIRECTIONS.UP: newY++; break;
- case CONFIG.GAME.DIRECTIONS.RIGHT: newX--; break;
- case CONFIG.GAME.DIRECTIONS.DOWN: newY--; break;
- case CONFIG.GAME.DIRECTIONS.LEFT: newX++; break;
- }
- await moveStep(newX, newY, 1);
- }
- };
- //向左转(逆时针旋转90度)
- window.turnLeft = async function() {
- // 如果已经发生过碰撞,不再执行任何旋转
- if (shouldStopExecution || isColliding.value) {
- return;
- }
- // 记录起始方向和目标方向
- const startDirection = playerDirection.value;
- const targetDirection = (playerDirection.value - 1 + 4) % 4;
- // 实现平滑旋转
- const startTime = performance.now();
- // 使用 requestAnimationFrame 实现平滑动画
- await new Promise(resolve => {
- function animate(currentTime) {
- // 检查是否应该停止执行
- if (shouldStopExecution) {
- resolve();
- return;
- }
- const elapsedTime = currentTime - startTime;
- const progress = Math.min(elapsedTime / CONFIG.ANIMATION.ROTATE_DURATION, 1);
- // 在动画过程中更新方向
- gameState.player.direction = startDirection - progress;
- // 如果动画未完成,继续下一帧
- if (progress < 1) {
- requestAnimationFrame(animate);
- } else {
- // 动画完成后设置最终方向
- gameState.player.direction = targetDirection;
- resolve();
- }
- }
- // 开始动画
- requestAnimationFrame(animate);
- });
- await new Promise(resolve => setTimeout(resolve, CONFIG.DELAY.ACTION_DELAY));
- };
- //向右转(顺时针旋转90度)
- window.turnRight = async function() {
- // 如果已经发生过碰撞,不再执行任何旋转
- if (shouldStopExecution || isColliding.value) {
- return;
- }
- // 记录起始方向和目标方向
- const startDirection = playerDirection.value;
- const targetDirection = (playerDirection.value + 1) % 4;
- // 实现平滑旋转
- const startTime = performance.now();
- // 使用 requestAnimationFrame 实现平滑动画
- await new Promise(resolve => {
- function animate(currentTime) {
- // 检查是否应该停止执行
- if (shouldStopExecution) {
- resolve();
- return;
- }
- const elapsedTime = currentTime - startTime;
- const progress = Math.min(elapsedTime / CONFIG.ANIMATION.ROTATE_DURATION, 1);
- // 处理从3到0的边界情况,确保顺时针旋转
- let currentDirection;
- if (startDirection === 3 && targetDirection === 0) {
- // 对于从3到0的顺时针旋转,我们需要模拟+1的效果而不是-3
- currentDirection = startDirection + progress;
- // 当超过3.99时,设置为0(避免显示4)
- if (currentDirection > 3.99) {
- currentDirection = 0;
- }
- } else {
- // 正常情况下的线性插值
- currentDirection = startDirection + (targetDirection - startDirection) * progress;
- }
- // 在动画过程中更新方向
- gameState.player.direction = currentDirection;
- // 如果动画未完成,继续下一帧
- if (progress < 1) {
- requestAnimationFrame(animate);
- } else {
- // 动画完成后设置最终方向
- gameState.player.direction = targetDirection;
- resolve();
- }
- }
- // 开始动画
- requestAnimationFrame(animate);
- });
- await new Promise(resolve => setTimeout(resolve, CONFIG.DELAY.ACTION_DELAY));
- };
- // 向后转(旋转180度)
- window.turnAround = async function() {
- // 如果已经发生过碰撞,不再执行任何旋转
- if (shouldStopExecution || isColliding.value) {
- return;
- }
- // 记录起始方向和目标方向
- const startDirection = playerDirection.value;
- const targetDirection = (playerDirection.value + 2) % 4;
- // 实现平滑旋转
- const startTime = performance.now();
- // 使用 requestAnimationFrame 实现平滑动画
- await new Promise(resolve => {
- function animate(currentTime) {
- // 检查是否应该停止执行
- if (shouldStopExecution) {
- resolve();
- return;
- }
- const elapsedTime = currentTime - startTime;
- const progress = Math.min(elapsedTime / CONFIG.ANIMATION.TURN_AROUND_DURATION, 1);
- // 在动画过程中更新方向
- gameState.player.direction = startDirection + 2 * progress;
- // 如果动画未完成,继续下一帧
- if (progress < 1) {
- requestAnimationFrame(animate);
- } else {
- // 动画完成后设置最终方向
- gameState.player.direction = targetDirection;
- resolve();
- }
- }
- // 开始动画
- requestAnimationFrame(animate);
- });
- await new Promise(resolve => setTimeout(resolve, CONFIG.DELAY.ACTION_DELAY));
- };
- // 拾取物品函数
- window.pickupItem = async function() {
- if (shouldStopExecution || isColliding.value || isSliding.value) {
- return;
- }
- //取人物当前位置
- let x = playerPosition.value.x;
- let y = playerPosition.value.y;
- let tileMap = walkablePointsMap.get(`${x},${y}`);
- // 判断是否是要拾取的方块类型
- if (tileMap && tileMap.type === BLOCKLY_MAP_TYPE_DICT.ITEM) {
- showGameMessage(tileMap.tip || CONFIG.TIPS.PICKUP_ITEM, 'warning');
- // 处理携带物品逻辑
- if (tileMap && tileMap.img) {
- // 从地图上移除图标
- const pointIndex = gameState.mapData.walkablePoints.findIndex(
- p => p.x === x && p.y === y
- );
- if (pointIndex !== -1) {
- // 保留点但移除img属性
- const updatedPoint = { ...gameState.mapData.walkablePoints[pointIndex] };
- delete updatedPoint.img;
- updatedPoint.status = updatedPoint.must === true;
- gameState.mapData.walkablePoints.splice(pointIndex, 1, updatedPoint);
- // 更新映射
- walkablePointsMap.set(`${x},${y}`, updatedPoint);
- // 执行物品拾取动画:放大晃动两下然后移动到左上角物品容器
- await animateItemPickup(tileMap, x, y);
- // 将物品添加到玩家携带物品中
- gameState.player.carriedItems.push({
- ...tileMap,
- originalX: x,
- originalY: y
- });
- }
- }
- } else {
- showGameMessage(CONFIG.TIPS.NULL_PICKUP_ITEM, 'info');
- }
- await new Promise(resolve => setTimeout(resolve, CONFIG.DELAY.ACTION_DELAY));
- }
- // 使用物品函数
- window.useItem = async function() {
- if (shouldStopExecution || isColliding.value || isSliding.value) {
- return;
- }
- //取人物当前位置
- let x = playerPosition.value.x;
- let y = playerPosition.value.y;
- let tileMap = walkablePointsMap.get(`${x},${y}`);
- // 判断当前位置是否有特殊需求
- if (tileMap && tileMap.type === BLOCKLY_MAP_TYPE_DICT.TASK) {
- // 检查玩家是否携带了需要的物品
- const requiredItems = tileMap.type;
- let hasRequiredItem = false;
- let itemIndex = -1;
- if (gameState.player.carriedItems.length > 0) {
- hasRequiredItem = true;
- itemIndex = 0;
- }
- if (hasRequiredItem) {
- // 获取要使用的物品
- const itemToUse = gameState.player.carriedItems[itemIndex];
- // 从携带物品中移除已使用的物品
- gameState.player.carriedItems.splice(itemIndex, 1);
- // 执行物品使用动画,传递finishAnimation
- await animateItemUse(itemToUse, itemIndex, tileMap.finishAnimation);
- // 使用物品成功
- showGameMessage(tileMap.finishedTip || CONFIG.TIPS.USE_ITEM_SUCCESS, 'success');
- } else {
- // 提示缺少所需物品
- showGameMessage(tileMap.unfinishedTip || CONFIG.TIPS.USE_SPECIAL_ITEM, 'warning');
- }
- } else {
- showGameMessage(CONFIG.TIPS.NO_USE_ITEM, 'info');
- }
- await new Promise(resolve => setTimeout(resolve, CONFIG.DELAY.ACTION_DELAY));
- }
- // 暂停函数
- window.pause = async function(seconds) {
- if (shouldStopExecution || isSliding.value) {
- return;
- }
- // 显示倒计时
- showCountdown.value = true;
- countdownValue.value = seconds;
- // 使用更细粒度的时间间隔,确保能快速响应重置操作
- const interval = 100; // 100毫秒检查一次
- const totalIterations = seconds * 10; // 总迭代次数
- //处理特殊任务消失
- processingSpecialTasksDisappearing();
- for (let i = 1; i <= totalIterations; i++) {
- // 检查是否应该停止执行
- if (shouldStopExecution) {
- break;
- }
- // 每10次迭代(即1秒)减少倒计时值
- if (i % 10 === 0) {
- countdownValue.value--;
- }
- // 等待一小段时间
- await new Promise(resolve => setTimeout(resolve, interval));
- // 检查是否应该停止执行
- if (shouldStopExecution) {
- break;
- }
- }
- // 隐藏倒计时
- showCountdown.value = false;
- await new Promise(resolve => setTimeout(resolve, CONFIG.DELAY.ACTION_DELAY));
- }
- // 声音函数
- window.playSound = async function() {
- if (shouldStopExecution || isColliding.value || isSliding.value) {
- return;
- }
- if(processingSpecialTasksDisappearing()){
- //延迟,确保声音播放完成
- await playMp3(passMp3);
- await new Promise(resolve => setTimeout(resolve, CONFIG.DELAY.PALY_MP3_TIMES));
- return
- }
- //延迟,确保声音播放完成
- await playMp3(failureMp3);
- await new Promise(resolve => setTimeout(resolve, CONFIG.DELAY.PALY_MP3_TIMES));
- };
- // 修建函数
- window.construct = async function() {
- if (shouldStopExecution || isColliding.value || isSliding.value) {
- return;
- }
- await new Promise(resolve => setTimeout(resolve, CONFIG.DELAY.TEMP_ITEM_FADE_DURATION));
- //处理特殊任务消失
- processingSpecialTasksDisappearing()
- };
- // 当经过指定形状时执行的函数(返回布尔值,可作为参数使用)
- window.whenPassed = function(shape) {
- if (shouldStopExecution || isColliding.value || isSliding.value) {
- return false;
- }
- // 取人物当前位置
- let x = playerPosition.value.x;
- let y = playerPosition.value.y;
- let tileMap = walkablePointsMap.get(`${x},${y}`);
- // 检查当前位置的瓦片是否匹配指定形状
- const isMatch = tileMap && tileMap.mark && tileMap.mark.toLowerCase() === shape;
-
- return isMatch;
- };
- //校验是否到达终点
- window.isFinish = async function() {
- // 如果已经发生过碰撞,不再执行任何检查
- if (isColliding.value) {
- return;
- }
- // 校验是否到达终点
- if (gameState.player.position.x === endPoint.value.x && gameState.player.position.y === endPoint.value.y) {
- // 如果通过了当前路线,标记为已通过并自动切换到下一个路线(true:切换下一关,false:全部通关)
- if (await markCurrentRouteAsPassed()){
- //继续执行代码
- await runCode();
- return;
- }
- // 全部通关
- // 统计所有类型为TASK的任务点总数||必须完成拾取物品的人物总数
- const totalTasks = gameState.mapData.walkablePoints.filter(
- p => p.type === BLOCKLY_MAP_TYPE_DICT.TASK ||
- p.type === BLOCKLY_MAP_TYPE_DICT.ITEM && p.must === true
- ).length;
- // 统计其中status为true的完成任务数||完成拾取物品的人物总数
- const completedTasks = gameState.mapData.walkablePoints.filter(
- p => p.type === BLOCKLY_MAP_TYPE_DICT.TASK && p.status === true
- || p.type === BLOCKLY_MAP_TYPE_DICT.ITEM && p.must === true && p.status === true
- ).length;
- //blockly总星星数量
- //无任务情况下直接完成
- if (totalTasks === 0 || completedTasks === totalTasks) {
- gameState.player.hasReachedEnd = true;
- emits('saveProgress', 'blockly', passConfig.value.starTotal * 100)
- showGameMessage(CONFIG.TIPS.FINISH, 'success' );
- // 展示通过动画
- await showPass(passConfig.value.starTotal);
- return;
- }
- //任务失败
- // 计算完成百分比
- const completionPercentage = totalTasks > 0 ? Math.round(completedTasks / totalTasks * passConfig.value.starTotal) : passConfig.value.starTotal;
- if (completionPercentage > 0){
- showGameMessage(CONFIG.TIPS.EFFORT, 'warning');
- }else{
- showGameMessage(CONFIG.TIPS.UNFINISHED, 'error');
- }
- emits('saveProgress', 'blockly', completionPercentage * 100)
- // 展示通过动画
- await showPass(completionPercentage);
- }
- };
- //================特殊组件积木逻辑=====================
- //移动逻辑处理(前后通用)
- async function moveStep(newX, newY, moveDirection = 0){
- // 检查是否应该停止执行
- if (shouldStopExecution) {
- return;
- }
- // 检查是否可以移动
- if (walkablePointsMap.has(`${newX},${newY}`)) {
- // 移动前处理方块类型逻辑
- await switchMapType(0);
- // 使用平滑移动动画
- await smoothMoveTo(newX, newY);
- // 移动后处理方块类型逻辑
- await switchMapType(1, moveDirection);
- await new Promise(resolve => setTimeout(resolve, CONFIG.DELAY.ACTION_DELAY));
- } else {
- // 发生碰撞 使用统一的碰撞处理方法
- await handleWallCollision();
- }
- }
- // 处理地图类型逻辑
- async function switchMapType(type, moveDirection = 0) {
- //取人物当前位置
- let x = playerPosition.value.x;
- let y = playerPosition.value.y;
- let tileMap = walkablePointsMap.get(`${x},${y}`);
- //移动前置
- if (type === 0) {
- //判断方块类型并处理逻辑
- switch (tileMap.type) {
- case BLOCKLY_MAP_TYPE_DICT.TASK:
- await taskLogic(tileMap);
- break;
- }
- }else {//移动后置
- //判断方块类型并处理逻辑
- switch (tileMap.type) {
- case BLOCKLY_MAP_TYPE_DICT.ICE:
- do {
- showGameMessage(tileMap.tip, 'warning',CONFIG.DELAY.ICE_MESSAGE_DISPLAY)
- // 处理方块类型逻辑
- await switchMapType(0);
- console.log("滑行前位置:" + playerPosition.value.x + "," + playerPosition.value.y);
- await slidingLogic(moveDirection);
- tileMap = walkablePointsMap.get(`${playerPosition.value.x},${playerPosition.value.y}`);
- }while (tileMap.type === BLOCKLY_MAP_TYPE_DICT.ICE && !isColliding.value)
- break;
- case BLOCKLY_MAP_TYPE_DICT.TRAP:
- showGameMessage(tileMap.tip, 'error')
- await handleWallCollision(tileMap.tip);
- break;
- }
- }
- }
- // 平滑移动函数
- async function smoothMoveTo(targetX, targetY) {
- const startX = playerPosition.value.x;
- const startY = playerPosition.value.y;
- const startTime = performance.now();
- // 使用Promise包装动画过程,使其可以await
- return new Promise(resolve => {
- function animate(currentTime) {
- // 检查是否应该停止执行
- if (shouldStopExecution) {
- resolve();
- return;
- }
- const elapsed = currentTime - startTime;
- const progress = Math.min(elapsed / CONFIG.ANIMATION.MOVE_DURATION, 1); // 计算进度,最大为1
- // 线性插值计算当前位置
- const currentX = startX + (targetX - startX) * progress;
- const currentY = startY + (targetY - startY) * progress;
- gameState.player = {
- ...gameState.player,
- position: { x: currentX, y: currentY },
- };
- // 检查是否到达终点
- if (progress < 1) {
- // 继续动画
- requestAnimationFrame(animate);
- } else {
- resolve(); // 动画完成
- }
- }
- // 启动动画
- requestAnimationFrame(animate);
- });
- }
- // 特殊任务处理消失(不需要物品)
- function processingSpecialTasksDisappearing(errorTip = CONFIG.TIPS.ERROR_ERROR) {
- let x = playerPosition.value.x;
- let y = playerPosition.value.y;
- let tileMap = walkablePointsMap.get(`${x},${y}`);
- if (tileMap && tileMap.type === BLOCKLY_MAP_TYPE_DICT.TASK) {
- const pointIndex = gameState.mapData.walkablePoints.findIndex(
- p => p.x === x && p.y === y
- );
- if (pointIndex !== -1) {
- const updatedPoint = { ...gameState.mapData.walkablePoints[pointIndex] };
- updatedPoint.img = updatedPoint.endImg;
- updatedPoint.status = true;
- gameState.mapData.walkablePoints.splice(pointIndex, 1, updatedPoint);
- walkablePointsMap.set(`${x},${y}`, updatedPoint);
- }
- showGameMessage(tileMap.finishedTip || CONFIG.TIPS.USE_ITEM_SUCCESS, 'success');
- return true;
- }else{
- showGameMessage(errorTip, 'info');
- return false;
- }
- }
- // 处理冰块滑行逻辑
- async function slidingLogic(moveDirection = 0) {
- if (shouldStopExecution || isColliding.value) {
- return;
- }
- gameState.player.isSliding = true;
- try {
- // 计算下一个位置
- let nextX = playerPosition.value.x;
- let nextY = playerPosition.value.y;
- // 根据当前方向计算下一个位置
- // 如果有指定方向,使用该方向滑行
- if (moveDirection === 0) {
- switch(playerDirection.value) {
- case CONFIG.GAME.DIRECTIONS.UP: nextY--; break;
- case CONFIG.GAME.DIRECTIONS.RIGHT: nextX++; break;
- case CONFIG.GAME.DIRECTIONS.DOWN: nextY++; break;
- case CONFIG.GAME.DIRECTIONS.LEFT: nextX--; break;
- }
- }else{
- switch(playerDirection.value) {
- case CONFIG.GAME.DIRECTIONS.UP: nextY++; break;
- case CONFIG.GAME.DIRECTIONS.RIGHT: nextX--; break;
- case CONFIG.GAME.DIRECTIONS.DOWN: nextY--; break;
- case CONFIG.GAME.DIRECTIONS.LEFT: nextX++; break;
- }
- }
- // 检查下一个位置是否可行走
- if (walkablePointsMap.has(`${nextX},${nextY}`)) {
- // 执行平滑移动到下一个位置
- await smoothMoveTo(nextX, nextY);
- } else {
- // 使用统一的碰撞处理方法,但不等待延迟(因为slidingLogic不需要这个延迟)
- await handleWallCollision();
- }
- } catch (error) {
- console.error('滑行过程中发生错误:', error);
- } finally {
- // 无论如何都要确保滑行状态被重置
- gameState.player.isSliding = false;
- }
- }
- // 处理任务逻辑
- async function taskLogic(tileMap) {
- if (shouldStopExecution || isColliding.value) {
- return;
- }
- try {
- // 判断当前位置是否有特殊需求
- if (tileMap && tileMap.type === BLOCKLY_MAP_TYPE_DICT.TASK && tileMap.noPassing === true && tileMap.status !== true) {
- await handleWallCollision(tileMap.unfinishedTip);
- return;
- }
- } catch (error) {
- console.error('处理任务逻辑发生错误:', error);
- }
- }
- // 物品拾取动画函数
- async function animateItemPickup(item, itemX, itemY) {
- // 创建临时动画元素
- const tempItem = document.createElement('div');
- const iconSize = tileSize.value * CONFIG.STYLES.IMG_SIZE_RATIO;
- const marginSize = tileSize.value * CONFIG.STYLES.IMG_SIZE_MARGIN;
- // 计算物品在地图上的实际位置
- const itemLeft = itemX * tileSize.value - tileSize.value + marginSize;
- const itemTop = itemY * tileSize.value - tileSize.value + marginSize;
- // 平行移动到容器位置,同时调整大小
- const finalSize = tileSize.value * CONFIG.STYLES.ITEM_CONTAINER_RATIO;
- // 获取携带物品容器的位置(左上角)
- // 注意:这里不需要考虑当前已携带物品数量,因为物品还没被添加
- let containerLeft = CONFIG.STYLES.ITEM_CONTAINER_POSITION.x;
- let containerTop = CONFIG.STYLES.ITEM_CONTAINER_POSITION.y;
- // 如果已经有物品,计算新物品应该出现的位置
- if (gameState.player.carriedItems.length > 0) {
- let containerSize = finalSize + CONFIG.STYLES.ITEM_CONTAINER_SPACING;
- containerLeft = CONFIG.STYLES.ITEM_CONTAINER_POSITION.x + gameState.player.carriedItems.length * containerSize;
- }
- // 设置临时元素样式
- Object.assign(tempItem.style, {
- position: 'absolute',
- left: itemLeft + 'px',
- top: itemTop + 'px',
- width: iconSize + 'px',
- height: iconSize + 'px',
- backgroundImage: `url(${item.img})`,
- backgroundSize: 'contain',
- backgroundPosition: 'center',
- backgroundRepeat: 'no-repeat',
- zIndex: '100', // 确保在最上层
- opacity: '0', // 初始完全透明
- transform: 'scale(1)',
- });
- // 将临时元素添加到地图容器
- const mapBackground = document.querySelector('.map-background');
- if (!mapBackground) {
- console.error('地图容器未找到');
- return;
- }
- mapBackground.appendChild(tempItem);
- // 触发淡入动画
- tempItem.offsetHeight;
- tempItem.style.transition = 'opacity ' + CONFIG.DELAY.ITEMS_APPEAR + 'ms ease-out';
- tempItem.style.opacity = '1'; // 完全显示
- await new Promise(resolve => setTimeout(resolve, CONFIG.DELAY.ITEMS_APPEAR));
- // 移动动画
- tempItem.style.transition = 'all ' + CONFIG.DELAY.ITEMS_FLIGHT_ANIMATION_DELAY + 'ms ease-out'; // 减慢动画速度
- Object.assign(tempItem.style, {
- left: containerLeft + 'px',
- top: containerTop + 'px',
- width: finalSize + 'px',
- height: finalSize + 'px',
- opacity: '0.8',
- transform: 'scale(1)',
- });
- // 等待动画完成
- await new Promise(resolve => setTimeout(resolve, CONFIG.DELAY.ITEMS_FLIGHT_ANIMATION_DELAY));
- // 移除临时元素
- if (mapBackground.contains(tempItem)) {
- mapBackground.removeChild(tempItem);
- }
- }
- // 物品使用动画函数
- async function animateItemUse(item, itemIndex, finishAnimation) {
- // 创建临时动画元素
- const tempItem = document.createElement('div');
- const finalSize = tileSize.value * CONFIG.STYLES.ITEM_CONTAINER_RATIO;
- const iconSize = tileSize.value * CONFIG.STYLES.IMG_SIZE_RATIO;
- const marginSize = tileSize.value * CONFIG.STYLES.IMG_SIZE_MARGIN;
- // 计算物品在物品栏中的初始位置
- let startLeft = CONFIG.STYLES.ITEM_CONTAINER_POSITION.x;
- let startTop = CONFIG.STYLES.ITEM_CONTAINER_POSITION.y;
- let containerSize = finalSize + CONFIG.STYLES.ITEM_CONTAINER_SPACING;
- // 根据物品索引计算在物品栏中的位置
- if (itemIndex > 0) {
- startLeft = CONFIG.STYLES.ITEM_CONTAINER_POSITION.x + itemIndex * containerSize;
- }
- // 计算玩家当前位置(物品目标位置)
- const playerLeft = playerPosition.value.x * tileSize.value - tileSize.value + marginSize;
- const playerTop = playerPosition.value.y * tileSize.value - tileSize.value + marginSize;
- // 获取玩家元素
- const playerElement = document.querySelector('.player');
- const originalPlayerZIndex = playerElement ? playerElement.style.zIndex : '';
- const originalPlayerOpacity = playerElement ? playerElement.style.opacity : '1';
- // 将临时元素添加到地图容器
- const mapBackground = document.querySelector('.map-background');
- if (!mapBackground) {
- console.error('地图容器未找到');
- return;
- }
- // 设置临时元素初始样式
- Object.assign(tempItem.style, {
- position: 'absolute',
- left: startLeft + 'px',
- top: startTop + 'px',
- width: finalSize + 'px',
- height: finalSize + 'px',
- backgroundImage: `url(${item.img})`,
- backgroundSize: 'contain',
- backgroundPosition: 'center',
- backgroundRepeat: 'no-repeat',
- zIndex: '120',
- transition: 'transform ' + CONFIG.DELAY.ITEMS_APPEAR + 'ms ease-out',
- transform: 'scale(1)',
- });
- mapBackground.appendChild(tempItem);
- // 1. 首先让小智逐渐消失
- if (playerElement) {
- playerElement.style.transition = 'opacity ' + CONFIG.DELAY.PLAYER_FADE_DURATION + 'ms ease-out';
- playerElement.style.opacity = '0';
- }
- // 2. 准备漂移动画
- await new Promise(resolve => setTimeout(resolve, CONFIG.DELAY.ITEMS_APPEAR));
- // 3. 如果有视频,先创建视频元素但不自动播放
- let videoElement = null;
- if (finishAnimation) {
- // 创建视频元素
- videoElement = document.createElement('video');
- videoElement.src = finishAnimation;
- videoElement.autoplay = false; // 不自动播放
- // videoElement.muted = true; // 视频默认静音
- videoElement.playsinline = true;
- videoElement.style.position = 'absolute';
- videoElement.style.left = playerLeft + 'px';
- videoElement.style.top = playerTop + 'px';
- videoElement.style.width = iconSize + 'px';
- videoElement.style.height = iconSize + 'px';
- videoElement.style.zIndex = '2000'; // 提高z-index确保视频可见
- videoElement.style.objectFit = 'contain';
- // 添加渐变显示效果
- videoElement.style.opacity = '0';
- videoElement.style.transition = 'opacity ' + CONFIG.DELAY.VIDEO_FADE_DURATION + 'ms ease-in-out';
- // 添加视频到地图容器
- mapBackground.appendChild(videoElement);
- // 触发重绘后设置透明度为1,实现渐变效果
- setTimeout(() => {
- videoElement.style.opacity = '1';
- }, CONFIG.DELAY.VIDEO_FADE_DURATION);
- } else {
- console.log('无视频');
- }
- // 4. 设置漂移动画样式
- tempItem.style.transition = 'all ' + CONFIG.DELAY.TEMP_ITEM_FADE_DURATION + 'ms ease-out';
- Object.assign(tempItem.style, {
- left: playerLeft + 'px',
- top: playerTop + 'px',
- width: iconSize + 'px',
- height: iconSize + 'px',
- });
- // 5. 等待漂移到玩家位置
- await new Promise(resolve => setTimeout(resolve, CONFIG.DELAY.ITEMS_FLIGHT_ANIMATION_DELAY));
- // 6. 移除临时元素
- if (mapBackground.contains(tempItem)) {
- mapBackground.removeChild(tempItem);
- }
- let x = playerPosition.value.x;
- let y = playerPosition.value.y;
- // 8. 视频播放完成后替换任务图片
- const pointIndex = gameState.mapData.walkablePoints.findIndex(
- p => p.x === x && p.y === y
- );
- if (pointIndex !== -1) {
- // 保留点但移除img属性并设置完成状态图标
- const updatedPoint = { ...gameState.mapData.walkablePoints[pointIndex] };
- updatedPoint.img = updatedPoint.endImg; // 设置完成状态图标
- updatedPoint.status = true;
- gameState.mapData.walkablePoints.splice(pointIndex, 1, updatedPoint);
- // 更新映射
- walkablePointsMap.set(`${x},${y}`, updatedPoint);
- }
- // 9. 处理视频播放或直接替换任务图片
- if (videoElement) {
- // 开始播放视频
- try {
- const playPromise = videoElement.play();
- if (playPromise !== undefined) {
- playPromise.then(_ => {
- console.log('Video started playing successfully');
- }).catch(error => {
- console.error('Error playing video:', error);
- });
- }
- } catch (error) {
- console.error('Exception when playing video:', error);
- }
- // 等待视频播放完成,添加超时处理
- await new Promise(resolve => {
- videoElement.onended = () => {
- console.log('Video ended');
- resolve();
- };
- // 添加超时处理,防止视频卡住
- setTimeout(() => {
- resolve();
- }, CONFIG.DELAY.VIDEO_TIMEOUT);
- });
- // 10. 移除视频元素 - 添加淡出效果
- if (mapBackground.contains(videoElement)) {
- // 设置淡出效果
- videoElement.style.transition = 'opacity ' + CONFIG.DELAY.VIDEO_FADE_DURATION + 'ms ease-in-out';
- videoElement.style.opacity = '0';
- // 等待淡出动画完成后再移除元素
- setTimeout(() => {
- if (mapBackground.contains(videoElement)) {
- mapBackground.removeChild(videoElement);
- }
- }, CONFIG.DELAY.VIDEO_FADE_DURATION); // 与动画时长一致
- }
- } else {
- // 10.无视频
- }
- // 11. 视频消失后稍作停留,让用户看到完成后的图片
- await new Promise(resolve => setTimeout(resolve, CONFIG.DELAY.COMPLETION_DISPLAY));
- // 逐步显示玩家并将其置顶
- if (playerElement) {
- playerElement.style.transition = 'opacity ' + CONFIG.DELAY.PLAYER_FADE_DURATION + 'ms ease-in';
- playerElement.style.opacity = originalPlayerOpacity;
- playerElement.style.zIndex = originalPlayerZIndex || '100';
- // 额外设置一个较高的z-index确保盖住其他人物图片
- setTimeout(() => {
- playerElement.style.zIndex = '150';
- }, CONFIG.DELAY.PLAYER_FADE_DURATION);
- }
- }
- // 展示通过动画
- async function showPass(passStar) {
- // 记录通过的星星数量
- passConfig.value.passStar = passStar;
- passConfig.value.title = 'YOU WIN!';
- passConfig.value.passBackground = cupbg;
- switch (passStar) {
- case 0:
- passConfig.value.title = 'YOU LOSE!';
- passConfig.value.passTrophy = nocup;
- passConfig.value.passBackground = nocupbg;
- await playMp3(passFailure);
- break;
- case 1:
- passConfig.value.passTrophy = coppercup;
- await playMp3(passTrump);
- break;
- case 2:
- passConfig.value.passTrophy = silvercup;
- await playMp3(passTrump);
- break;
- case 3:
- passConfig.value.passTrophy = goldcup;
- await playMp3(passFireworks);
- break;
- }
- showOverlay.value = true;
- }
- // 标记当前路线为已通过
- const markCurrentRouteAsPassed = async () => {
- routePassedStatus.value[currentRouteIndex.value] = true;
- // 自动切换到下一个可用路线
- if (currentRouteIndex.value < parsedRouteList.value.length - 1) {
- // 解锁下一个路线
- routePassedStatus.value[currentRouteIndex.value + 1] = true;
- // 自动切换到下一个路线
- switchRoute(currentRouteIndex.value + 1);
- //通关路线提示
- showGameMessage(CONFIG.TIPS.PASS_ROUTE, 'success');
- //播放通过路线声音
- await playMp3(passRouteMp3);
- await new Promise(resolve => setTimeout(resolve, CONFIG.DELAY.PALY_MP3_TIMES));
- return true;
- }
- return false;
- };
- //================卸载区=====================
- // 组件卸载时清理
- onUnmounted(() => {
- // 清理Blockly工作区
- if (workspace) {
- workspace.dispose();
- workspace = null;
- }
- // 移除事件监听器
- window.removeEventListener('resize', updateMapContainerDimensions);
- // 停止当前正在执行的代码
- shouldStopExecution = true;
- // 取消任何正在进行的执行
- if (executionAbortController) {
- executionAbortController.abort();
- executionAbortController = null;
- }
- // 清除当前执行的Promise引用
- currentExecutionPromise = null;
- // 清理所有临时DOM元素
- const mapBackground = document.querySelector('.map-background');
- if (mapBackground) {
- // 清除视频元素
- const videoElements = mapBackground.querySelectorAll('video');
- videoElements.forEach(video => {
- if (mapBackground.contains(video)) {
- mapBackground.removeChild(video);
- }
- });
- // 清除临时动画元素
- const tempElements = mapBackground.querySelectorAll('div[style*="backgroundImage"]');
- tempElements.forEach(temp => {
- if (mapBackground.contains(temp)) {
- mapBackground.removeChild(temp);
- }
- });
- }
- // 清理全局函数引用
- window.pickupItem = null;
- window.useItem = null;
- // 重置游戏状态
- gameState.player.carriedItems = [];
- gameState.player.position = { ...startPoint.value };
- });
- </script>
- <style scoped lang="scss">
- @use "sass:math";
- @function rpx($px) {
- @return math.div($px, 750) * 100vw;
- }
- /* 全屏遮罩盒子样式 */
- .fullscreen-overlay {
- position: fixed;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- background-color: rgba(0, 0, 0, 0.5); /* 半透明黑色 遮罩 */
- z-index: 9999; /* 确保在最上层 */
- // pointer-events: none; /* 允许点击穿透 */
- display: flex;
- justify-content: center;
- align-items: center;
- }
- /* 垂直居中盒子样式 */
- .centered-box {
- border-radius: rpx(10);
- z-index: 10000;
- width: rpx(220);
- height: rpx(255);
- overflow: auto;
- pointer-events: auto;
- // display: flex;
- flex-direction: column;
- position: relative;
- /* 初始状态:缩小、透明 */
- transform: scale(0.7);
- opacity: 0;
- /* 动画效果 */
- animation: popUp 0.5s ease-out forwards;
- }
- /* 弹出动画定义 */
- @keyframes popUp {
- 0% {
- transform: scale(0.7);
- opacity: 0;
- }
- 70% {
- transform: scale(1.05);
- opacity: 0.9;
- }
- 100% {
- transform: scale(1);
- opacity: 1;
- }
- }
- /* 彩带容器样式 */
- .confetti-container {
- position: absolute;
- width: 100%;
- height: 100%;
- overflow: hidden;
- z-index: 9999;
- pointer-events: none;
- }
- /* 彩带基本样式 */
- .confetti {
- position: absolute;
- width: rpx(5);
- height: rpx(15);
- opacity: 0;
- animation-timing-function: ease-in-out;
- animation-iteration-count: infinite;
- animation-fill-mode: forwards;
- }
- /* 彩带颜色和动画 */
- .confetti-1 { background-color: #FF5252; animation: confetti-fall 2s 0.2s linear infinite; left: 5%; }
- .confetti-2 { background-color: #536DFE; animation: confetti-fall 2.7s 0.9s linear infinite; left: 15%; }
- .confetti-3 { background-color: #FFC107; animation: confetti-fall 2.3s 1.8s linear infinite; left: 25%; }
- .confetti-4 { background-color: #4CAF50; animation: confetti-fall 2.1s 1.4s linear infinite; left: 35%; }
- .confetti-5 { background-color: #9C27B0; animation: confetti-fall 2.5s 1.8s linear infinite; left: 45%; }
- .confetti-6 { background-color: #2196F3; animation: confetti-fall 2.2s 0.2s linear infinite; left: 55%; }
- .confetti-7 { background-color: #FF9800; animation: confetti-fall 2.4s 1.8s linear infinite; left: 65%; }
- .confetti-8 { background-color: #795548; animation: confetti-fall 2s 1.4s linear infinite; left: 75%; }
- .confetti-9 { background-color: #607D8B; animation: confetti-fall 2.6s 3.4s linear infinite; left: 85%; }
- .confetti-10 { background-color: #E91E63; animation: confetti-fall 2.3s 1.8s linear infinite; left: 95%; }
- /* 彩带飘落动画 */
- @keyframes confetti-fall {
- 0% {
- top: -10%;
- opacity: 0;
- transform: rotate(0deg);
- }
- 10% {
- opacity: 1;
- }
- 90% {
- opacity: 1;
- }
- 100% {
- top: 100%;
- opacity: 0;
- transform: rotate(360deg);
- }
- }
- /* 关闭按钮样式 */
- .close-button {
- position: absolute;
- top: rpx(0);
- right: rpx(2);
- font-size: rpx(20);
- color: #2b8fdd;
- font-weight: bold;
- cursor: pointer;
- width: rpx(25);
- height: rpx(25);
- display: flex;
- justify-content: center;
- align-items: center;
- z-index: 10001;
- text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
- // -webkit-text-stroke: 1px white; // 描
- }
- .close-button:hover {
- transform: scale(1.1);
- }
- .top-box {
- width: 100%;
- height: rpx(60);
- border-radius: 5px;
- display: flex;
- justify-content: center;
- align-items: center;
- }
- .title-text {
- font-size: rpx(20);
- font-weight: bold;
- color: white;
- font-family: 'SourceHanSansCN-Bold_0';
- text-align: center;
- width: 100%;
- height: 100%;
- display: flex;
- justify-content: center;
- align-items: center;
- text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
- }
- .middle-box {
- width: 100%;
- height: rpx(130);
- border-radius: 5px;
- display: flex;
- justify-content: center;
- align-items: center;
- }
- .gold-cup-image{
- height: 100%;
- width: 100%;
- object-fit: contain;
- }
- .bottom-box {
- width: 100%;
- height: rpx(40);
- border-radius: 5px;
- display: flex;
- justify-content: center;
- align-items: flex-start;
- gap: rpx(12);
- margin-top: -rpx(10); /* 向上调整位置 */
- }
- .gold-star-image {
- width: rpx(35);
- height: 100%;
- object-fit: contain;
- }
- .star-top {
- object-position: top;
- }
- .star-bottom {
- object-position: bottom;
- }
- //将tileSize属性绑定到CSS变量上
- :root {
- --tile-size: v-bind('tileSize + "px"');
- }
- /* 游戏简介样式 */
- .info-message-container {
- display: flex;
- flex-direction: column;
- align-items: flex-start;
- }
- .message-item {
- display: flex;
- align-items: flex-start;
- width: 100%;
- margin-bottom: rpx(5);
- }
- /* 头像样式 */
- .avatar {
- margin-right: rpx(4);
- flex-shrink: 0;
- }
- .avatar-image {
- width: rpx(30);
- height: rpx(30);
- object-fit: cover;
- }
- /* 消息内容样式 */
- .message-item {
- flex: 1;
- }
- .message-item p {
- margin: rpx(4) 0;
- line-height: 0;
- font-size: rpx(10);
- text-align: left;
- color: black;
- background-color: #e6faff;
- opacity: 0.8;
- border-radius: rpx(4);
- padding: rpx(2) rpx(5);
- max-width: 100%;
- }
- .message-item p:first-child {
- margin-top: 0;
- font-weight: 500;
- }
- .message-item p:last-child {
- margin-bottom: 0;
- }
- .title-box {
- position: relative;
- top: rpx(5);
- padding-left: 15px;
- z-index: 10;
- display: flex;
- justify-content: space-between;
- align-items: center;
- }
- /* 左侧容器样式 */
- .left-container {
- display: flex;
- flex-direction: column;
- align-items: flex-start;
- gap: 10px;
- }
- /* 线路按钮容器样式 */
- .route-buttons {
- display: flex;
- gap: rpx(5);
- margin-left: rpx(3);
- }
- /* 右侧两个角为圆角的长方形格子样式 */
- .game-badge {
- width: rpx(70);
- height: rpx(20);
- background-color: #5fb5dc;
- color: #fff;
- border-radius: 0 rpx(20) rpx(20) 0;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: rpx(15);
- font-weight: bold;
- border: none;
- cursor: pointer;
- transition: all 0.3s ease;
- }
- .game-badge:hover {
- background-color: #3498db;
- transform: translateY(-2px);
- box-shadow: 0 rpx(5) rpx(10) rgba(0, 0, 0, 0.1);
- }
- .game-badge.active {
- background-color: #e74c3c;
- color: #fff;
- font-weight: bold;
- }
- /* 右侧容器样式 */
- .right-container {
- display: flex;
- gap: 10px;
- padding-right: 20px;
- }
- /* 上下节按钮样式 */
- .section-button {
- padding: rpx(5) rpx(12);
- border: none;
- border-radius: rpx(10);
- background: #3498db;
- color: #fff;
- font-weight: 500;
- cursor: pointer;
- transition: all 0.3s ease;
- font-size: rpx(7);
- }
- .section-button:hover {
- background: #2980b9;
- transform: translateY(-2px);
- box-shadow: 0 5px 10px rgba(0, 0, 0, 0.1);
- }
- .previous-btn {
- background-color: rgba(255, 255, 255, 0.8);
- color: #333;
- }
- .previous-btn:hover {
- background-color: rgba(255, 255, 255, 0.9);
- }
- .next-btn {
- background: #5fb5dc;
- }
- .next-btn:hover {
- background: #2980b9;
- }
- .box-icon {
- display: flex;
- align-items: center;
- margin-top: rpx(5);
- gap: 10px;
- padding: 10px 20px;
- background-color: rgba(255, 255, 255, 0.8);
- border-radius: 30px;
- backdrop-filter: blur(10px);
- cursor: pointer;
- transition: all 0.3s ease;
- font-size: 16px;
- color: #333;
- font-weight: 500;
- width: fit-content;
- }
- .box-icon:hover {
- background-color: rgba(255, 255, 255, 0.9);
- transform: translate(-3px);
- }
- .left-icon {
- font-size: 18px;
- }
- .content {
- display: flex;
- flex-wrap: nowrap;
- gap: 20px;
- padding: 20px;
- min-width: 1160px;
- }
- /* 地图区域样式 */
- .map-section {
- flex: 1;
- min-width: 500px;
- background: rgba(248, 249, 250, 0.82);
- padding: 15px;
- border-radius: 15px;
- }
- .map-container {
- position: relative;
- width: 100%;
- height: 100%;
- overflow: hidden; // 防止内容溢出
- }
- .map-background {
- position: relative;
- width: 100%;
- height: 100%;
- background-size: cover;
- background-position: center;
- background-repeat: no-repeat;
- }
- .map-image {
- width: 100%;
- height: 100%;
- object-fit: cover;
- }
- /* 可行走区域样式 */
- .walkable-point {
- position: absolute;
- //background-color: rgba(52, 152, 219, 0.2);
- //border: 1px solid rgba(52, 152, 219, 0.5);
- //box-sizing: border-box;
- //是否显示可行路线
- opacity: 1;
- }
- /* 玩家样式 */
- .player {
- position: absolute;
- background-image: var(--player-image);
- background-size: contain;
- background-repeat: no-repeat;
- background-position: center;
- border-radius: 5px;
- z-index: 10;
- }
- /* 碰撞动画 */
- .player.collision {
- animation: collision 0.5s ease-in-out;
- }
- @keyframes collision {
- 0% { transform: rotate(var(--player-rotation)) translateX(0) translateY(0) scale(1); }
- 25% { transform: rotate(var(--player-rotation)) translateX(-3px) translateY(-2px) scale(1.2); }
- 50% { transform: rotate(var(--player-rotation)) translateX(3px) translateY(2px) scale(1.2); }
- 75% { transform: rotate(var(--player-rotation)) translateX(-3px) translateY(-2px) scale(1.2); }
- 100% { transform: rotate(var(--player-rotation)) translateX(0) translateY(0) scale(1); }
- }
- /* 滑行动画 */
- @keyframes sliding {
- 0% { transform: rotate(var(--player-rotation)) translateX(0) translateY(0); }
- 25% { transform: rotate(var(--player-rotation)) translateX(2px) translateY(0); }
- 75% { transform: rotate(var(--player-rotation)) translateX(-2px) translateY(0); }
- 100% { transform: rotate(var(--player-rotation)) translateX(0) translateY(0); }
- }
- /* 携带物品容器 */
- .carried-items-container {
- position: absolute;
- top: 20px;
- left: 20px;
- background: rgba(255, 255, 255, 0.8);
- border: 2px solid #3498db;
- border-radius: 10px;
- padding: 10px;
- display: flex;
- gap: 10px;
- z-index: 15;
- backdrop-filter: blur(10px);
- animation: fadeInScale 0.5s ease-out;
- }
- /* 淡入缩放动画 */
- @keyframes fadeInScale {
- 0% {
- opacity: 0;
- transform: scale(0.5) translateY(-10px);
- }
- 100% {
- opacity: 1;
- transform: scale(1) translateY(0);
- }
- }
- /* 携带物品样式 */
- .carried-item {
- animation: bounceIn 0.3s ease-out forwards;
- }
- /* 【暂停】怀表容器样式 */
- .watch-container {
- animation: fadeInCountdown 0.3s ease-out;
- position: relative;
- }
- /* 【暂停】怀表表盘样式 */
- .watch-face {
- width: 100%;
- height: 100%;
- border-radius: 50%;
- background: linear-gradient(145deg, rgba(240, 240, 240, 0.8), rgba(200, 200, 200, 0.6));
- border: 4px solid rgba(100, 100, 100, 0.8);
- box-shadow:
- 0 0 20px rgba(0, 0, 0, 0.3),
- inset 0 0 20px rgba(0, 0, 0, 0.1);
- position: relative;
- display: flex;
- justify-content: center;
- align-items: center;
- backdrop-filter: blur(5px);
- }
- /* 【暂停】怀表中心圆点 */
- .watch-center {
- width: 10%;
- height: 10%;
- background-color: rgba(100, 50, 20, 0.8);
- border-radius: 50%;
- position: absolute;
- z-index: 10;
- }
- /* 【暂停】表针容器 */
- .watch-hands {
- position: absolute;
- width: 100%;
- height: 100%;
- display: flex;
- justify-content: center;
- align-items: center;
- }
- /* 【暂停】表针基础样式 */
- .watch-hand {
- position: absolute;
- background-color: rgba(100, 50, 20, 0.8);
- transform-origin: bottom center;
- border-radius: 2px;
- }
- /* 【暂停】时针样式 */
- .hour-hand {
- width: 4%;
- height: 30%;
- top: 20%;
- animation: rotateHourHand 12s linear infinite;
- }
- /* 【暂停】分针样式 */
- .minute-hand {
- width: 3%;
- height: 40%;
- top: 10%;
- animation: rotateMinuteHand 1s linear infinite;
- }
- /* 【暂停】怀表中心倒计时数字 */
- .watch-countdown-number {
- font-size: calc(var(--tile-size, 143px) * 0.3);
- font-weight: bold;
- color: rgba(100, 50, 20, 0.9);
- text-shadow: 1px 1px 2px rgba(255, 255, 255, 0.8);
- z-index: 5;
- animation: pulseCountdown 1s infinite;
- }
- /* 【暂停】怀表淡入动画 */
- @keyframes fadeInCountdown {
- 0% {
- opacity: 0;
- transform: scale(0.8) rotate(-30deg);
- }
- 100% {
- opacity: 1;
- transform: scale(1) rotate(0deg);
- }
- }
- /* 【暂停】倒计时数字脉冲动画 */
- @keyframes pulseCountdown {
- 0%, 100% {
- opacity: 0.9;
- transform: scale(1);
- }
- 50% {
- opacity: 0.7;
- transform: scale(1.05);
- }
- }
- /* 【暂停】时针旋转动画 */
- @keyframes rotateHourHand {
- from { transform: rotate(0deg); }
- to { transform: rotate(360deg); }
- }
- /* 【暂停】分针旋转动画 */
- @keyframes rotateMinuteHand {
- from { transform: rotate(0deg); }
- to { transform: rotate(360deg); }
- }
- /* 弹入动画 */
- @keyframes bounceIn {
- 0% {
- opacity: 0;
- transform: scale(0.3) translateY(-20px);
- }
- 50% {
- opacity: 0.7;
- transform: scale(1.1) translateY(5px);
- }
- 80% {
- opacity: 0.9;
- transform: scale(0.95) translateY(-2px);
- }
- 100% {
- opacity: 1;
- transform: scale(1) translateY(0);
- }
- }
- /* 成功到达终点动画 */
- .player.success {
- animation: success 1s ease-in-out;
- }
- @keyframes success {
- 0% { transform: rotate(var(--player-rotation)) scale(1); }
- 10% { transform: rotate(var(--player-rotation)) scale(1.2) translateX(-5px) translateY(-5px); }
- 20% { transform: rotate(var(--player-rotation)) scale(1.3) translateX(5px) translateY(5px); }
- 30% { transform: rotate(var(--player-rotation)) scale(1.2) translateX(-5px) translateY(-5px); }
- 40% { transform: rotate(var(--player-rotation)) scale(1.3) translateX(5px) translateY(5px); }
- 50% { transform: rotate(var(--player-rotation)) scale(1.4) translateX(0) translateY(0); }
- 60% { transform: rotate(var(--player-rotation)) scale(1.3) translateX(-3px) translateY(-3px); }
- 70% { transform: rotate(var(--player-rotation)) scale(1.2) translateX(3px) translateY(3px); }
- 80% { transform: rotate(var(--player-rotation)) scale(1.3) translateX(-3px) translateY(-3px); }
- 90% { transform: rotate(var(--player-rotation)) scale(1.2) translateX(3px) translateY(3px); }
- 100% { transform: rotate(var(--player-rotation)) scale(1); }
- }
- /* 游戏消息样式 */
- .game-message {
- position: absolute;
- top: 20px;
- left: 50%;
- transform: translateX(-50%);
- padding: 10px 20px;
- border-radius: 5px;
- font-weight: bold;
- z-index: 20;
- min-width: 200px;
- text-align: center;
- }
- .game-message.success {
- background-color: #d4edda;
- color: #155724;
- border: 1px solid #c3e6cb;
- }
- .game-message.error {
- background-color: #f8d7da;
- color: #721c24;
- border: 1px solid #f5c6cb;
- }
- .game-message.info {
- background-color: #d1ecf1;
- color: #0c5460;
- border: 1px solid #bee5eb;
- }
- .game-message.warning {
- background-color: #baeff8;
- color: #035767;
- border: 1px solid #9be9f6;
- }
- /* Blockly区域样式 */
- .blockly-section {
- flex: 1;
- min-width: 600px;
- display: flex;
- flex-direction: column;
- gap: 20px;
- }
- // 统一区块样式
- .map-section, .workspace-section {
- background: rgba(248, 249, 250, 0.82);
- padding: 15px;
- border-radius: 15px;
- height: 100%;
- }
- .map-section h2, .workspace-section h2 {
- margin-bottom: 15px;
- color: #2c3e50;
- border-bottom: 2px solid #3498db;
- padding-bottom: 8px;
- }
- #blocklyDiv {
- height: 87%;
- min-height: rpx(250);
- width: 100%;
- background: #fff;
- border: 1px solid #ddd;
- border-radius: 8px;
- }
- /* 优化Blockly积木样式 */
- /* 积木高度 */
- .blocklyBlockCanvas .blocklyBlock {
- height: 45px; /* 默认高度 */
- min-height: 45px;
- }
- /* 积木内部元素的行高和间距 */
- .blocklyText {
- font-size: 16px;
- line-height: 20px;
- font-weight: 500;
- }
- /* 输入字段的高度 */
- .blocklyHtmlInput {
- height: 30px;
- font-size: 14px;
- padding: 5px;
- }
- /* 下拉菜单的高度 */
- .blocklyDropdownMenu {
- line-height: 28px;
- font-size: 14px;
- }
- /* 优化积木圆角和阴影效果 */
- .blocklyBlock {
- border-radius: 8px;
- filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.1));
- }
- /* 积木之间的连接点间距 */
- .blocklyConnection {
- height: 20px;
- width: 20px;
- }
- /* 工具箱中积木的高度 */
- .blocklyTreeRow {
- height: 40px;
- line-height: 40px;
- }
- .controls {
- display: flex;
- gap: 10px;
- margin: 15px 0px;
- flex-wrap: wrap;
- }
- button {
- padding: 10px 20px;
- border: none;
- border-radius: 5px;
- background: #3498db;
- color: #fff;
- font-weight: 700;
- cursor: pointer;
- transition: all 0.3s ease;
- }
- .game-badge:hover {
- background-color: #3498db;
- transform: translateY(-2px);
- box-shadow: 0 rpx(5) rpx(10) rgba(0, 0, 0, 0.1);
- }
- .game-badge.active {
- background-color: #e74c3c;
- color: #fff;
- font-weight: bold;
- }
- .game-badge.passed {
- background-color: #27ae60;
- color: #fff;
- }
- .game-badge.disabled {
- background-color: #bdc3c7;
- color: #7f8c8d;
- cursor: not-allowed;
- pointer-events: none;
- }
- .game-badge.disabled:hover {
- background-color: #bdc3c7;
- transform: none;
- box-shadow: none;
- }
- #runCode {
- background: #e74c3c;
- }
- #runCode:hover {
- background: #c0392b;
- }
- /* 响应式布局 */
- @media (max-width: 1200px) {
- .map-section,
- .blockly-section {
- flex: 1;
- min-width: 45%;
- }
- .map-background {
- width: 100%;
- height: 400px;
- }
- }
- </style>
|