// Pure geometry helpers plus an adapter evaluated inside the game. function catalogCellBounds(data, thumbnail, cell) { if (!data || !thumbnail || !cell || !thumbnail.data?.some(p=>p.x===cell.x&&p.y===cell.y)) throw Error('这个地点缺少可核实的小地图分块信息,未执行传送'); const columns=thumbnail.maxX-thumbnail.minX+1,rows=thumbnail.maxY-thumbnail.minY+1; const ox=Number(data.meta?.cellOx)||0,oy=Number(data.meta?.cellOy)||0; const width=data.width/columns,height=data.height/rows; const left=Math.max(0,(cell.x-thumbnail.minX)*width-ox),top=Math.max(0,(cell.y-thumbnail.minY)*height-oy); const right=Math.min(data.width,(cell.x-thumbnail.minX+1)*width-ox),bottom=Math.min(data.height,(cell.y-thumbnail.minY+1)*height-oy); if(![left,top,right,bottom].every(Number.isFinite)||right<=left||bottom<=top)throw Error('小地图分块范围无效,未执行传送'); return{left,top,right,bottom,cellX:cell.x,cellY:cell.y}; } function findCatalogLanding({bounds,shape,tileWidth:tw,tileHeight:th,hitsAt,maxProbes=14000}) { const minX=Math.ceil(bounds.left*tw+shape.left+8),maxX=Math.floor(bounds.right*tw-shape.right-8); const minY=Math.ceil(bounds.top*th+shape.top+8),maxY=Math.floor(bounds.bottom*th-shape.bottom-8); if(minX>maxX||minY>maxY)return null; const axis=(min,max,step)=>{const out=[min];for(let v=min+step;va.edge-b.edge||b.py-a.py||a.px-b.px); let probes=0,floating=null; const budgetError=Error('分块落点检查达到上限'); const hit=(px,py)=>{if(++probes>maxProbes)throw budgetError;return hitsAt(px,py)||[];}; const hazard=o=>o.type===2||!!o.triggerDamage||Number(o.upElastic)>0; try{ for(const candidate of candidates){ const{px,py}=candidate; if(hit(px,py).length)continue; let supported=null,danger=false; for(let d=1;d<=Math.min(2*th,maxY-py);d+=Math.max(1,th/8)){ const hits=hit(px,py+d); if(!hits.length)continue; danger=hits.some(hazard)||hits.some(o=>!o.terrain&&!o.fix); if(!danger&&hits.some(o=>o.terrain||o.fix)){ let low=0,high=d; while(high-low>1){const mid=Math.floor((low+high)/2);if(hit(px,py+mid).length)high=mid;else low=mid;} supported={px,py:py+low,grounded:true}; } break; } if(supported)return{...supported,probes}; if(!danger&&!floating)floating={px,py,grounded:false}; } }catch(error){if(error!==budgetError||!floating)throw error;} return floating?{...floating,probes}:null; } function planCatalogLanding(mapId,target,live=false) { const data=DataManager.getCachedData(mapId); const thumbnail=DataManager.getThumbnailMapInfo(mapId); let cell; if(target && typeof target==='object') { if(![target.cellX,target.cellY,target.page].every(Number.isInteger))throw Error('地图分块坐标无效'); cell=Object.values(DataManager._thumbnailMapCell||{}).find(c=>c.mapId===mapId&&c.x===target.cellX&&c.y===target.cellY&&c.page===target.page); if(!cell)throw Error('目标分块不属于指定地图或页面,未执行传送'); catalogCellBounds(data,thumbnail,cell); } else { const event=data?.events?.[target]; if(!event)throw Error('收集来源事件不存在,请刷新百科'); cell=DataManager.getThumbnailCell(event.x,event.y,mapId); } let targetBounds=null; if(cell?.mapId===mapId)try{targetBounds=catalogCellBounds(data,thumbnail,cell);}catch{} const current=mapId===$gameMap.mapId(); if(live&&!current)throw Error('目标地图尚未载入'); let terrain; if(current)terrain=$gameMap._terrain; else{ const preview=Object.create(Game_Map.prototype); Object.assign(preview,{_mapId:mapId,_tmxExternalVertices:{},_terrain:new PhysicsGrid()}); // No setup(), events, scene loading, or swaps of the real $gameMap/$dataMap. preview.tilemapTerrain();preview._terrain.rebuild();terrain=preview._terrain; } if(!terrain)throw Error('目标地图碰撞资料尚未就绪,未执行传送'); const tw=$gameMap.tileWidth(),th=$gameMap.tileHeight(),characters=[$gamePlayer]; if(typeof $gameLily!=='undefined'&&$gameLily&&!$gameLily.isRemove())characters.push($gameLily); const shape={left:Math.max(...characters.map(p=>p.hitOffsetL()))+4,right:Math.max(...characters.map(p=>p.hitOffsetR()))+4, top:Math.max(th*0.8,...characters.map(p=>p.hitOffsetT()))+4,bottom:Math.max(...characters.map(p=>p.hitOffsetB()))+4}; const probe=new Game_CharacterBase(); Object.assign(probe,{_hitOffsetL:shape.left,_hitOffsetR:shape.right,_hitOffsetT:shape.top,_hitOffsetB:shape.bottom, _direction:6,_invertMode:false,_physicsFlag:0,_isFastRect:true,vertices:null}); probe.hitTest=function(other){return !characters.includes(other)&&this.baseHitTest(other);}; const grids=[terrain]; if(current)grids.push($gameMap._physicsFixGrid,$gameMap._physicsDynamicGrid); const hitsAt=(px,py)=>{ probe.px=probe.rx=px;probe.py=probe.ry=py; const hits=[];for(const grid of grids)grid?.getObjGrid(probe,hits);return hits; }; const areas=targetBounds?[targetBounds]:[]; const otherCells=[...(thumbnail?.data||[])].filter(p=>!cell||p.x!==cell.x||p.y!==cell.y) .sort((a,b)=>Math.abs(a.x-(cell?.x||0))+Math.abs(a.y-(cell?.y||0))-Math.abs(b.x-(cell?.x||0))-Math.abs(b.y-(cell?.y||0))); for(const p of otherCells)try{areas.push(catalogCellBounds(data,thumbnail,p));}catch{} areas.push({left:0,top:0,right:data.width,bottom:data.height,cellX:null,cellY:null}); for(const bounds of areas){ let result; try{result=findCatalogLanding({bounds,shape,tileWidth:tw,tileHeight:th,hitsAt});} catch(error){if(!error.message.includes('检查达到上限'))throw error;} if(!result)continue; const alternate=bounds!==targetBounds; return{...result,bounds,targetBounds,alternate,x:(result.px-$gamePlayer.tileXToCenterX(0))/tw,y:(result.py-$gamePlayer.tileYToCenterY(0))/th, positionNote:alternate?'目标分块受阻,已改到同图其他可用位置':result.grounded?'目标分块边缘附近的可落脚空处':'目标分块内的空处,保持悬停'}; } // Only an entirely blocked/unavailable map fails; a blocked target cell alone // always expands the search instead of cancelling the user's journey. throw Error('已检查目标分块、邻近分块和整张地图,仍未找到能容纳角色的空处'); } module.exports={catalogCellBounds,findCatalogLanding,planCatalogLanding};