feat: 发布咸鱼喵喵全能助手,加入全收集地图与图文说明
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
// 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;v<max;v+=step)out.push(v);if(max!==min)out.push(max);return out;};
|
||||
const candidates=[];
|
||||
for(const px of axis(minX,maxX,tw/2))for(const py of axis(minY,maxY,th/2))candidates.push({px,py,edge:Math.min((px-minX)/tw,(maxX-px)/tw,(py-minY)/th,(maxY-py)/th)});
|
||||
candidates.sort((a,b)=>a.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};
|
||||
@@ -0,0 +1,43 @@
|
||||
function installCombat() {
|
||||
if (window.__nyaruruCombat) return window.__nyaruruCombat;
|
||||
let settings={outgoing:1,incoming:1,oneHit:false}, attacker=null, touched=Date.now(), actors=$gameActors;
|
||||
const prototype=Game_CharacterBase.prototype;
|
||||
const hit=prototype.onAttackHit, direct=prototype.requestDamage, hooked=new WeakMap();
|
||||
const playerSide=c=>c===$gamePlayer || (typeof $gameLily!=='undefined' && c===$gameLily);
|
||||
function reset(){settings={outgoing:1,incoming:1,oneHit:false};}
|
||||
function touch(){if(actors!==$gameActors){reset();actors=$gameActors;}touched=Date.now();}
|
||||
function scale(character,value){
|
||||
if(!(value>0))return value;
|
||||
if(playerSide(character))return value*settings.incoming;
|
||||
const target=character.battleObject?.(), source=attacker?.battleObject?.();
|
||||
if(playerSide(attacker) && target && source && target.team!==source.team){
|
||||
const scaled=value*settings.outgoing;
|
||||
return settings.oneHit && !target.unlimitHp ? Math.max(scaled,target.hp+1) : scaled;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
prototype.onAttackHit=function(info,character,...args){
|
||||
const previous=attacker;attacker=info?.bulletOwner||this;
|
||||
const already=character&&hooked.has(character);
|
||||
if(character&&!already){
|
||||
const descriptor=Object.getOwnPropertyDescriptor(character,'overrideBeAttackDamage'),original=character.overrideBeAttackDamage;
|
||||
hooked.set(character,{descriptor});
|
||||
Object.defineProperty(character,'overrideBeAttackDamage',{configurable:true,writable:true,enumerable:false,value(...values){return scale(this,original.apply(this,values));}});
|
||||
}
|
||||
try{return hit.call(this,info,character,...args);}finally{
|
||||
attacker=previous;
|
||||
if(character&&!already){const {descriptor}=hooked.get(character);if(descriptor)Object.defineProperty(character,'overrideBeAttackDamage',descriptor);else delete character.overrideBeAttackDamage;hooked.delete(character);}
|
||||
}
|
||||
};
|
||||
prototype.requestDamage=function(value,...args){return direct.call(this,playerSide(this)&&value>0?value*settings.incoming:value,...args);};
|
||||
const api={touch,clear:reset,read(){touch();return{ok:true,...settings};},change(patch){
|
||||
touch();const next={...settings};
|
||||
for(const key of ['outgoing','incoming'])if(patch[key]!==undefined){if(typeof patch[key]!=='number'||!Number.isFinite(patch[key])||patch[key]<0||patch[key]>1000)throw Error('倍率必须为 0~1000 的数字');next[key]=patch[key];}
|
||||
if(patch.oneHit!==undefined){if(typeof patch.oneHit!=='boolean')throw Error('秒杀开关无效');next.oneHit=patch.oneHit;}
|
||||
settings=next;return api.read();
|
||||
}};
|
||||
Object.defineProperty(window,'__nyaruruCombat',{value:api,configurable:true});
|
||||
setInterval(()=>{if(Date.now()-touched>120000||actors!==$gameActors){reset();actors=$gameActors;}},1000);
|
||||
return api;
|
||||
}
|
||||
module.exports={installCombat};
|
||||
+1
-1
@@ -7,7 +7,7 @@
|
||||
// unpacked/data/MapInfos.json → 393 张地图
|
||||
// out/triggers.csv → 897 条剧情触发点(其中 731 条有坐标)
|
||||
//
|
||||
// ⚠️ **台词不在这里。** 这是"C 方案"(见开发文档 12.5)的核心约定:
|
||||
// ⚠️ **台词不在这里。** 这是"C 方案"(见 tool/开发文档.md 的运行时数据约定)的核心约定:
|
||||
// 随包分发的只有**结构索引**(哪里有个触发点),
|
||||
// 一个字创作内容都不带。台词在连上游戏后从 `$dataStory` 实时读,
|
||||
// 只存在于内存,**绝不落盘**(读取逻辑在 lib/ops.js 的 readStoryIndex)。
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
// Read-only inventory/location join, evaluated in the game. No progress is modified.
|
||||
function readEncyclopedia(inventory) {
|
||||
const maps = DataManager._cacheMapData;
|
||||
if (!maps || !Object.keys(maps).length) throw new Error('地图资料尚未加载,请先正常读档');
|
||||
const items = inventory.items.map(item => ({ ...item, locations: [] }));
|
||||
items.push({ type: 'coin', id: 0, name: '金币', desc: '可用于商店购买物品。', count: inventory.stats.coin.value, max: 99999, locations: [] });
|
||||
const byKey = new Map(items.map(item => [item.type + ':' + item.id, item]));
|
||||
const shops = [], locations = [], seen = new Set();
|
||||
const collectedKeys = new Set();
|
||||
for (const region of Object.values(typeof $dataRegion === 'undefined' ? {} : $dataRegion)) {
|
||||
for (const [mapId, eventId] of region?.exploreItem || []) collectedKeys.add(mapId + ':' + eventId);
|
||||
}
|
||||
const conditions = cond => {
|
||||
const result = [];
|
||||
for (const n of [1, 2]) if (cond?.['switch' + n + 'Valid']) {
|
||||
const id = cond['switch' + n + 'Id'];
|
||||
result.push({ label: '开关 ' + ($dataSystem.switches[id] || id), met: !!$gameSwitches.value(id) });
|
||||
}
|
||||
if (cond?.variableValid) result.push({ label: '变量 ' + ($dataSystem.variables[cond.variableId] || cond.variableId) + ' ≥ ' + cond.variableValue,
|
||||
met: $gameVariables.value(cond.variableId) >= cond.variableValue });
|
||||
if (cond?.actorValid) result.push({ label: '队伍角色 ' + cond.actorId, met: null });
|
||||
if (cond?.itemValid) result.push({ label: '持有道具 ' + cond.itemId, met: null });
|
||||
return result;
|
||||
};
|
||||
for (const [mapKey, map] of Object.entries(maps)) {
|
||||
const mapId = Number(mapKey);
|
||||
if (!map || !Number.isInteger(mapId)) continue;
|
||||
const regionId = DataManager.mapRegion?.(mapId) || 0;
|
||||
const region = typeof $dataRegion !== 'undefined' && $dataRegion[regionId];
|
||||
const area = region?.name || '未分类区域';
|
||||
for (const event of map.events || []) {
|
||||
if (!event) continue;
|
||||
for (const [pageIndex, page] of (event.pages || []).entries()) {
|
||||
const list = page.list || [];
|
||||
for (const [commandIndex, command] of list.entries()) {
|
||||
if (command.code !== 357) continue;
|
||||
const [, name, , args = {}] = command.parameters || [];
|
||||
const coinPile = name === 'ai' && Number(args.type) === 60;
|
||||
if (!coinPile && !['addItem', 'add_coin', 'shop'].includes(name)) continue;
|
||||
if (name === 'add_coin' && !(Number(args.num) > 0)) continue;
|
||||
const base = { mapId, mapName: $dataMapInfos?.[mapId]?.name || '地图 ' + mapId, area, regionId,
|
||||
eventId: event.id, eventName: event.name || '', x: event.x, y: event.y, pageIndex,
|
||||
conditions: conditions(page.conditions), branch: Number(command.indent) > 0 };
|
||||
if (page.conditions?.selfSwitchValid) base.conditions.push({ label: '独立开关 ' + page.conditions.selfSwitchCh,
|
||||
met: !!$gameSelfSwitches.value([mapId, event.id, page.conditions.selfSwitchCh]) });
|
||||
if (name === 'shop') { shops.push({ ...base, kind: 'shop' }); continue; }
|
||||
const type = name === 'add_coin' || coinPile ? 'coin' : args.type;
|
||||
const id = name === 'add_coin' || coinPile ? 0 : Number(args.idx);
|
||||
const item = byKey.get(type + ':' + id);
|
||||
if (!item) continue;
|
||||
const key = `${type}:${id}:${mapId}:${event.id}:${pageIndex}:${commandIndex}`;
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
const official = collectedKeys.has(mapId + ':' + event.id);
|
||||
// A later marker is reliable only for a straight-line pickup. Branch
|
||||
// rewards may reuse A for another path, so those remain unknown.
|
||||
const marker = list.slice(commandIndex + 1).find(c => c.code === 123 && Number(c.indent || 0) === Number(command.indent || 0) && c.parameters?.[1] === 0);
|
||||
const nextPageHides = (event.pages || []).some(pg => pg.conditions?.selfSwitchValid && pg.conditions.selfSwitchCh === marker?.parameters?.[0]);
|
||||
const reliable = official || (!base.branch && marker && nextPageHides);
|
||||
const status = reliable ? !!$gameSelfSwitches.value([mapId, event.id, official ? 'A' : marker.parameters[0]]) : null;
|
||||
const location = { ...base, key, type, id, kind: coinPile ? 'coinpile' : 'pickup', collected: status, official,
|
||||
sourceNote: coinPile ? '猫币堆:击打 8 次耗尽,掉落数量受配饰影响' : '',
|
||||
amount: coinPile ? null : name === 'add_coin' ? Number(args.num) : 1, antiCollection: args.antiCollection === 'true' };
|
||||
item.locations.push(location); locations.push(location);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const actor = $gamePlayer.actor();
|
||||
for (const item of items) {
|
||||
let definition = null;
|
||||
if (['consumables','weapons','stones','ornaments'].includes(item.type)) definition = actor['_' + item.type].data()[item.id];
|
||||
else if (item.type === 'bell') definition = actor._ornaments.bellData();
|
||||
else if (item.type === 'hpst') definition = actor.hpStrengthData();
|
||||
else if (item.type === 'skillst') definition = actor.skillStrengthData();
|
||||
else if (item.type === 'joycoin') definition = actor.joyCoinData();
|
||||
item.price = definition?.price ?? null;
|
||||
item.shopCandidate = Number(definition?.price) > 0;
|
||||
item.openTask = definition?.openTask ?? null;
|
||||
}
|
||||
const counted = new Map();
|
||||
for (const location of locations) if (location.official) counted.set(location.mapId + ':' + location.eventId, location);
|
||||
const summaries = new Map();
|
||||
for (const location of counted.values()) {
|
||||
if (!summaries.has(location.area)) summaries.set(location.area, { area: location.area, total: 0, collected: 0 });
|
||||
const s = summaries.get(location.area); s.total++; if (location.collected) s.collected++;
|
||||
}
|
||||
const regions = [...summaries.values()];
|
||||
return { ok: true, items, locations, shops, regions, subject: inventory.subject,
|
||||
stats: { items: items.length, locations: locations.length, shops: shops.length,
|
||||
total: counted.size, collected: [...counted.values()].filter(l => l.collected).length },
|
||||
coverage: '来源为当前游戏已加载的地图物品指令、猫币堆、收集点和商店入口。商店候选需结合任务进度在店内确认;脚本动态奖励及普通敌人掉落不在地图索引中。未知状态不会按未收集处理。' };
|
||||
}
|
||||
module.exports = { readEncyclopedia };
|
||||
@@ -0,0 +1,31 @@
|
||||
// Renders only inventory thumbnails through the game's own UI icon mapping.
|
||||
// Data URLs stay in memory; no game textures are exported to the filesystem.
|
||||
async function readItemIcons(items) {
|
||||
if (typeof Sprite_ItemIcon === 'undefined') throw new Error('游戏图标组件尚未就绪');
|
||||
const bitmaps = new Map();
|
||||
for (const item of items) {
|
||||
if (!bitmaps.has(item.type)) bitmaps.set(item.type,Sprite_ItemIcon.prototype.itemBitmap(item.type));
|
||||
}
|
||||
await Promise.all([...new Set(bitmaps.values())].map(bitmap=>new Promise((resolve,reject)=>{
|
||||
if (bitmap.isReady()) return resolve();
|
||||
if (bitmap.isError?.()) return reject(new Error('物品图标加载失败'));
|
||||
const timer=setTimeout(()=>reject(new Error('物品图标加载超时,请重试')),8000);
|
||||
bitmap.addLoadListener(()=>{ clearTimeout(timer); resolve(); });
|
||||
})));
|
||||
const icons = {};
|
||||
for (const item of items) {
|
||||
const bitmap = bitmaps.get(item.type);
|
||||
let frame;
|
||||
const proxy = { _type:item.type, bitmap, setFrame(x,y,width,height) { frame={x,y,width,height}; } };
|
||||
Sprite_ItemIcon.prototype.refreshIdx.call(proxy,item.id);
|
||||
const {x,y,width,height} = frame;
|
||||
if (![x,y,width,height].every(Number.isFinite) || width <= 0 || height <= 0 || width > 512 || height > 512 ||
|
||||
x < 0 || y < 0 || x+width > bitmap.width || y+height > bitmap.height) throw new Error('物品图标裁切范围无效');
|
||||
const canvas=document.createElement('canvas'); canvas.width=width; canvas.height=height;
|
||||
const context=canvas.getContext('2d'); context.imageSmoothingEnabled=false;
|
||||
context.drawImage(bitmap.canvas,x,y,width,height,0,0,width,height);
|
||||
icons[item.type+':'+item.id]=canvas.toDataURL('image/png');
|
||||
}
|
||||
return {ok:true,icons};
|
||||
}
|
||||
module.exports = { readItemIcons };
|
||||
@@ -0,0 +1,168 @@
|
||||
// Runs inside the game. Hooks live outside saved actors and can be fully removed.
|
||||
function installMotion() {
|
||||
if (window.__nyaruruMotion?.version === 5) { window.__nyaruruMotion.touch(); return window.__nyaruruMotion; }
|
||||
// Preserve an active hover when upgrading the in-game helper after a service restart.
|
||||
const previous = window.__nyaruruMotion;
|
||||
const resume = [$gamePlayer, typeof $gameLily !== 'undefined' ? $gameLily : null]
|
||||
.filter(c => c && previous?.has(c)).map(c => ({ character:c, noclip:!!c._suspendPhysicsHitTest, vx:c._vx, vy:c._vy }));
|
||||
if (previous?.dispose) previous.dispose(); else previous?.clear();
|
||||
const records = new Map();
|
||||
const actorSet = () => typeof $gameActors !== 'undefined' ? $gameActors : null;
|
||||
let touched = Date.now(), player = $gamePlayer, data = $dataMap, map = $gameMap, actors = actorSet();
|
||||
const reset = c => {
|
||||
c.vx = c.vy = c._vx = c._vy = c.dotX = c.dotY = 0;
|
||||
c.clearImpact?.(); c._impact = null; c.clearGrounding?.();
|
||||
c._groundingX = c._terrainGroundingX = c.px;
|
||||
c._groundingY = c._terrainGroundingY = c.py;
|
||||
};
|
||||
function disable(c) {
|
||||
const r = records.get(c);
|
||||
if (!r) return;
|
||||
if (r.gravity) Object.defineProperty(c, 'doGravity', r.gravity); else delete c.doGravity;
|
||||
if (r.transfer) Object.defineProperty(c, 'updateMapBoundTransfer', r.transfer); else delete c.updateMapBoundTransfer;
|
||||
if (r.cache) Object.defineProperty(c, 'cacheDeltaTimeSpeed', r.cache); else delete c.cacheDeltaTimeSpeed;
|
||||
if (r.suspendCheck) Object.defineProperty(c, 'isSuspendGravity', r.suspendCheck); else delete c.isSuspendGravity;
|
||||
for (const [name, descriptor] of Object.entries(r.controls)) {
|
||||
if (descriptor) Object.defineProperty(c,name,descriptor); else delete c[name];
|
||||
}
|
||||
for (const [key, descriptor] of Object.entries(r.flags)) {
|
||||
if (descriptor) Object.defineProperty(c,key,{...descriptor,value:c[key]});
|
||||
else delete c[key];
|
||||
}
|
||||
records.delete(c); reset(c);
|
||||
}
|
||||
function clear() {
|
||||
for (const c of [...records.keys()]) {
|
||||
disable(c); c.resumeGravity?.(); c.resumePhysicsHitTest?.();
|
||||
}
|
||||
}
|
||||
function sync() {
|
||||
// Normal exits reuse these objects; loading a save replaces them.
|
||||
if (player !== $gamePlayer || map !== $gameMap || actors !== actorSet()) {
|
||||
clear(); player = $gamePlayer; map = $gameMap; actors = actorSet(); data = $dataMap;
|
||||
} else if (data !== $dataMap && $dataMap && $gameMap.mapId() > 0 &&
|
||||
SceneManager._scene?.constructor.name === 'Scene_Map' && !SceneManager.isSceneChanging?.()) {
|
||||
data = $dataMap;
|
||||
for (const [c, r] of records) {
|
||||
reset(c); c.suspendGravity();
|
||||
if (r.noclip) c.suspendPhysicsHitTest();
|
||||
}
|
||||
}
|
||||
}
|
||||
function touch() { sync(); touched = Date.now(); }
|
||||
function keepInMap(c) {
|
||||
if (!c.tileXToCenterX || !c.tileYToCenterY || !c.unflush) return;
|
||||
const tw=$gameMap.tileWidth(), th=$gameMap.tileHeight();
|
||||
const left=c.hitOffsetL?.() ?? c.tileXToCenterX(0), top=c.hitOffsetT?.() ?? c.tileYToCenterY(0);
|
||||
const right=$gameMap.width()*tw-(c.hitOffsetR?.() ?? tw-c.tileXToCenterX(0));
|
||||
const bottom=$gameMap.height()*th-(c.hitOffsetB?.() ?? th-c.tileYToCenterY(0));
|
||||
const px=Math.max(left,Math.min(right,c.px)),py=Math.max(top,Math.min(bottom,c.py));
|
||||
if(px===c.px && py===c.py)return;
|
||||
const blockedX=px!==c.px,blockedY=py!==c.py;
|
||||
const x=(px-c.tileXToCenterX(0))/tw,y=(py-c.tileYToCenterY(0))/th;
|
||||
const locate=typeof Game_CharacterBase!=='undefined' ? Game_CharacterBase.prototype.locate : c.locate;
|
||||
locate.call(c,x,y);c.unflush(x,y);
|
||||
if(blockedX){c.vx=0;c.dotX=0;}if(blockedY){c.vy=0;c.dotY=0;}
|
||||
}
|
||||
function enable(c, noclip = false) {
|
||||
if (!c) return;
|
||||
touch();
|
||||
if (!records.has(c)) {
|
||||
const gravity = Object.getOwnPropertyDescriptor(c, 'doGravity');
|
||||
const transfer = Object.getOwnPropertyDescriptor(c, 'updateMapBoundTransfer'), originalTransfer = c.updateMapBoundTransfer;
|
||||
const cache = Object.getOwnPropertyDescriptor(c, 'cacheDeltaTimeSpeed'), originalCache = c.cacheDeltaTimeSpeed;
|
||||
const originalGravity = c.doGravity;
|
||||
const suspendCheck = Object.getOwnPropertyDescriptor(c, 'isSuspendGravity');
|
||||
const flags = Object.fromEntries(['_suspendGravity','_suspendPhysicsHitTest'].map(key=>[key,Object.getOwnPropertyDescriptor(c,key)]));
|
||||
const controls = Object.fromEntries(['controlJump','controlDoubleJump'].map(key=>[key,Object.getOwnPropertyDescriptor(c,key)]));
|
||||
const record = { gravity, transfer, cache, suspendCheck, flags, controls, handledJump:false, noclip:!!(noclip || c._suspendPhysicsHitTest) };
|
||||
records.set(c, record);
|
||||
// Native skill/stagger cleanup calls resumeGravity(). Keep the tool's
|
||||
// hover independent, without blocking those native state transitions.
|
||||
Object.defineProperty(c, 'isSuspendGravity', { configurable:true, writable:true, enumerable:false, value() { return true; } });
|
||||
const skillActive = () => !!c._bodyAttacking;
|
||||
const descending = () => c === $gamePlayer && !c.isOutControl?.() && !!c.isPressDown?.();
|
||||
const extraJump = actor => {
|
||||
if (c !== $gamePlayer || c.isFlexibleGrounding?.() || !c.isTriggeredJump?.() || c.isOutControl?.()) return false;
|
||||
const sign = c._invertMode ? -1 : 1;
|
||||
const native = c.jumpStrength?.(actor);
|
||||
const strength = Number.isFinite(native) && native !== 0 ? Math.abs(native) : Math.abs(actor?.doubleJumpStrength?.() || 10);
|
||||
c.vy = -sign * strength;
|
||||
c.frameJump = true; c._nextFrameImmueElasticCollision = true; c._impact = null;
|
||||
if (typeof AudioManager !== 'undefined') AudioManager.playNormalSe?.('mtv_jump2');
|
||||
return true;
|
||||
};
|
||||
for (const name of ['controlJump','controlDoubleJump']) {
|
||||
const original = c[name];
|
||||
if (typeof original !== 'function') continue;
|
||||
Object.defineProperty(c,name,{configurable:true,writable:true,enumerable:false,value(...args) {
|
||||
if (name === 'controlJump') record.handledJump = false;
|
||||
else if (record.handledJump) return true;
|
||||
if (extraJump(args[0])) { record.handledJump = true; return true; }
|
||||
return original.apply(this,args);
|
||||
}});
|
||||
}
|
||||
Object.defineProperty(c, 'doGravity', { configurable: true, writable: true, enumerable: false, value() {
|
||||
sync();
|
||||
if (!records.has(c)) return originalGravity.apply(this,arguments);
|
||||
// Positive Y is down; reversed-gravity scenes use the other sign.
|
||||
const sign = this._invertMode ? -1 : 1;
|
||||
const vy = Number.isFinite(this._vy) ? this._vy : Number(this.vy) || 0;
|
||||
const g = Math.max(0.1, Math.abs(Number($gameMap.g?.() || 0) + Number(this.g?.() || 0)) || 0.35);
|
||||
if (!skillActive()) {
|
||||
if (descending()) this.vy = sign * 4;
|
||||
else this.vy = vy * sign < 0 ? sign * Math.min(0, vy * sign + g * Math.max(0.1, Math.min(2, this.deltaTime?.() || 1))) : 0;
|
||||
}
|
||||
this.setTempGravity?.(0); this.setTempMGravity?.(0);
|
||||
} });
|
||||
// Apply intent once BEFORE time scaling and collision resolution.
|
||||
// makeMomentum is also called AFTER collisions by endTestV; changing
|
||||
// velocity there overwrites the resolved floor contact and tunnels down.
|
||||
if (typeof originalCache === 'function') Object.defineProperty(c, 'cacheDeltaTimeSpeed', {
|
||||
configurable:true, writable:true, enumerable:false, value(...args) {
|
||||
sync();
|
||||
if (records.has(c) && !this._testV && !skillActive()) {
|
||||
if (descending()) this.vy = (this._invertMode ? -1 : 1) * 4;
|
||||
else if (this._vy * (this._invertMode ? -1 : 1) > 0) this.vy = 0;
|
||||
}
|
||||
return originalCache.apply(this,args);
|
||||
}
|
||||
});
|
||||
if(c === $gamePlayer && typeof originalTransfer === 'function') Object.defineProperty(c,'updateMapBoundTransfer',{
|
||||
configurable:true,writable:true,enumerable:false,value(...args){
|
||||
sync();
|
||||
const result=originalTransfer.apply(this,args);
|
||||
if(!records.has(c) || this.isTransferring?.() || $gameMap.isEventRunning?.())return result;
|
||||
// Side walls sit outside the map, and noclip has no impact.
|
||||
// Resolve real exits before our tighter fallback boundary.
|
||||
const left=this.px-(this.hitOffsetL?.()||0)<0;
|
||||
const right=this.px+(this.hitOffsetR?.()||0)>$gameMap.width()*$gameMap.tileWidth();
|
||||
const down=this.py+(this.hitOffsetB?.()||0)>$gameMap.height()*$gameMap.tileHeight();
|
||||
if((left && this.autoTransfer?.(3)) || (right && this.autoTransfer?.(4)) || (down && this.autoTransfer?.(2)))return result;
|
||||
if(!this.isTransferring?.())keepInMap(this);
|
||||
return result;
|
||||
}
|
||||
});
|
||||
}
|
||||
records.get(c).noclip=!!(noclip || c._suspendPhysicsHitTest);
|
||||
reset(c); c.suspendGravity();
|
||||
if (noclip) c.suspendPhysicsHitTest();
|
||||
// These are temporary tool states. Omitting them from saves avoids loading
|
||||
// suspended gravity without the per-session controller that stops a jump.
|
||||
for (const key of ['_suspendGravity','_suspendPhysicsHitTest']) {
|
||||
const descriptor=Object.getOwnPropertyDescriptor(c,key);
|
||||
if (descriptor?.configurable && 'value' in descriptor) Object.defineProperty(c,key,{...descriptor,enumerable:false});
|
||||
}
|
||||
}
|
||||
let timer;
|
||||
const api = { version:5, touch, enable, disable, clear, has: c => records.has(c), reset,
|
||||
setNoclip(c,on) { if(on)enable(c,true);else {const r=records.get(c);if(r)r.noclip=false;c.resumePhysicsHitTest();} },
|
||||
dispose() { clear(); if (typeof clearInterval === 'function') clearInterval(timer); } };
|
||||
Object.defineProperty(window, '__nyaruruMotion', { value: api, configurable: true });
|
||||
if (typeof setInterval === 'function') timer = setInterval(() => {
|
||||
if (Date.now() - touched > 120000) clear(); else sync();
|
||||
}, 1000);
|
||||
for (const r of resume) { enable(r.character,r.noclip); r.character._vx=r.vx; r.character._vy=r.vy; }
|
||||
return api;
|
||||
}
|
||||
module.exports = { installMotion };
|
||||
+121
-30
@@ -1,6 +1,13 @@
|
||||
const { installPhotoTools } = require('./photo-state');
|
||||
const { chooseStoryPosition } = require('./story-position');
|
||||
const { installTrainer } = require('./trainer');
|
||||
const { installMotion } = require('./motion');
|
||||
const { readEncyclopedia } = require('./encyclopedia');
|
||||
const { readWorldMap } = require('./world-map');
|
||||
const { readItemIcons } = require('./item-icons');
|
||||
const { installCombat } = require('./combat');
|
||||
const { catalogCellBounds,findCatalogLanding,planCatalogLanding } = require('./catalog-position');
|
||||
const CATALOG = `const catalogCellBounds=(${catalogCellBounds.toString()}),findCatalogLanding=(${findCatalogLanding.toString()}),planCatalogLanding=(${planCatalogLanding.toString()});`;
|
||||
const PHOTO = `const photo = (${installPhotoTools.toString()})();`;
|
||||
// 游戏操作脚本库
|
||||
//
|
||||
@@ -8,12 +15,12 @@ const PHOTO = `const photo = (${installPhotoTools.toString()})();`;
|
||||
// 之所以存成字符串而非直接执行,是因为这些代码必须在游戏进程里跑,
|
||||
// 才能访问 $gamePlayer / $gameMap 这些游戏全局对象。
|
||||
//
|
||||
// ⚠️ 关键设计约束(来自 2026-10-02 实机探查,详见 aitemp/探查结果.md):
|
||||
// ⚠️ 关键设计约束(当前规则见 tool/开发文档.md):
|
||||
// 1. 传送必须用 $gamePlayer.locate(x, y),不能手写 _x/_y/_realX/_realY
|
||||
// —— 后者会漏掉物理引擎的 px/py,导致"画面过去了、碰撞体还在原地"
|
||||
// 2. 游戏有 4 套坐标系,locate() 内部会一次同步全部
|
||||
// 3. 无敌用 lockInvincible()(常驻)而非 makeInvincible()(会自己失效)
|
||||
// 4. 工具不负责读档 —— CDP 读档会崩(物理网格无法反序列化)
|
||||
// 4. 读档由用户在游戏正常菜单完成,不能裸调 loadGame(还需重建物理网格)
|
||||
|
||||
/**
|
||||
* 包一层 IIFE,统一处理:游戏未进地图的守卫、异常捕获、JSON 返回格式。
|
||||
@@ -23,12 +30,14 @@ const PHOTO = `const photo = (${installPhotoTools.toString()})();`;
|
||||
* CDP 那边开了 `awaitPromise: true`,所以返回 Promise 会被自动等待。
|
||||
* 同步的操作写成 async 也无妨,只是多包一层 Promise。
|
||||
*/
|
||||
function wrap(body, { needMap = true } = {}) {
|
||||
function wrap(body, { needMap = true, allowMenu = false } = {}) {
|
||||
const guard = needMap ? `
|
||||
if (typeof $gamePlayer === 'undefined' || !$gamePlayer ||
|
||||
typeof $gameMap === 'undefined' || !$gameMap || $gameMap.mapId() === 0 ||
|
||||
(typeof SceneManager !== 'undefined' && SceneManager._scene?.constructor.name !== 'Scene_Map')) {
|
||||
return JSON.stringify({ ok: false, error: '游戏还没进入地图。请先在游戏里读档或开始游戏。' });
|
||||
(typeof SceneManager !== 'undefined' && !${JSON.stringify(allowMenu ? ['Scene_Map', 'Scene_Menu', 'Scene_Shop'] : ['Scene_Map'])}.includes(SceneManager._scene?.constructor.name))) {
|
||||
return JSON.stringify({ ok: false, error: typeof SceneManager !== 'undefined' && ['Scene_Menu','Scene_Shop'].includes(SceneManager._scene?.constructor.name)
|
||||
? '请先关闭游戏菜单,回到地图后再执行移动或剧情操作。'
|
||||
: '游戏还没进入地图。请先在游戏里读档或开始游戏。' });
|
||||
}
|
||||
` : '';
|
||||
return `(async function(){ try { ${guard} ${body} } catch (e) { return JSON.stringify({ ok:false, error: String(e && e.message || e) }); } })()`;
|
||||
@@ -46,6 +55,7 @@ const SLEEP = `const sleep = (ms) => new Promise(r => setTimeout(r, ms));`;
|
||||
* 机制见开发文档第 6 节。
|
||||
*/
|
||||
const SAFE_CHAR = `
|
||||
const motion = (${installMotion.toString()})();
|
||||
// 清残留碰撞 + 重置落地锚点,否则 isFallDown() 会一直为真
|
||||
const safeChar = (c) => {
|
||||
if (!c) return;
|
||||
@@ -63,7 +73,7 @@ const SAFE_CHAR = `
|
||||
const floatChar = (c) => {
|
||||
if (!c) return;
|
||||
safeChar(c);
|
||||
try { c.suspendGravity(); c.suspendPhysicsHitTest(); } catch (e) {}
|
||||
motion.enable(c, true);
|
||||
};
|
||||
// 莉莉娅在不在(她被移除时不该动她)
|
||||
const lilyAny = () => typeof $gameLily !== 'undefined' ? $gameLily : null;
|
||||
@@ -110,13 +120,38 @@ const OPS = {
|
||||
`, { needMap: false });
|
||||
},
|
||||
|
||||
worldMapTravel(mapId,cellX,cellY,page){
|
||||
if(!Number.isInteger(mapId)||mapId<1||![cellX,cellY,page].every(Number.isInteger))throw Error('地图分块坐标无效');
|
||||
return this.teleport(0,0,{mapId,catalog:{cellX,cellY,page}});
|
||||
},
|
||||
worldMap(){
|
||||
return wrap(`const trainer=(${installTrainer.toString()})();
|
||||
const encyclopedia=(${readEncyclopedia.toString()})(trainer.read());
|
||||
return JSON.stringify((${readWorldMap.toString()})(encyclopedia));`,{allowMenu:true});
|
||||
},
|
||||
catalogTravel(mapId,eventId){
|
||||
if(!Number.isInteger(mapId)||mapId<1||!Number.isInteger(eventId)||eventId<1)throw Error('收集地点无效');
|
||||
return this.teleport(0,0,{mapId,catalog:{eventId}});
|
||||
},
|
||||
combat(body=null){return wrap(`const api=(${installCombat.toString()})();return JSON.stringify(${body?`api.change(${JSON.stringify(body)})`:'api.read()'});`,{allowMenu:true});},
|
||||
itemIcons() {
|
||||
return wrap(`const trainer = (${installTrainer.toString()})();
|
||||
return JSON.stringify(await (${readItemIcons.toString()})(trainer.read().items));`, { allowMenu: true });
|
||||
},
|
||||
encyclopedia() {
|
||||
return wrap(`const trainer = (${installTrainer.toString()})();
|
||||
return JSON.stringify((${readEncyclopedia.toString()})(trainer.read()));`, { allowMenu: true });
|
||||
},
|
||||
trainer(body = null) {
|
||||
return wrap(`const trainer = (${installTrainer.toString()})();
|
||||
return JSON.stringify(${body ? `trainer.change(${JSON.stringify(body)})` : 'trainer.read()'});`);
|
||||
return JSON.stringify(${body ? `trainer.change(${JSON.stringify(body)})` : 'trainer.read()'});`, { allowMenu: true });
|
||||
},
|
||||
/** 读取当前游戏状态(供控制面板顶栏显示) */
|
||||
snapshot() {
|
||||
return wrap(`
|
||||
if (window.__nyaruruMotion && window.__nyaruruMotion.version !== 5) (${installMotion.toString()})();
|
||||
window.__nyaruruMotion?.touch();
|
||||
window.__nyaruruCombat?.touch();
|
||||
if (window.__nyaruruPhoto?.touch) window.__nyaruruPhoto.touch();
|
||||
if (window.__nyaruruTrainer) window.__nyaruruTrainer.touch();
|
||||
const p = $gamePlayer, m = $gameMap;
|
||||
@@ -129,12 +164,12 @@ const OPS = {
|
||||
mapId: m.mapId(),
|
||||
mapName: mi ? mi.name : ('地图 ' + m.mapId()),
|
||||
mapSize: [m.width(), m.height()],
|
||||
x: p._x,
|
||||
y: p._y,
|
||||
x: p.tileXToCenterX ? Math.round((p.px - p.tileXToCenterX(0)) / m.tileWidth() * 10000) / 10000 : p._x,
|
||||
y: p.tileYToCenterY ? Math.round((p.py - p.tileYToCenterY(0)) / m.tileHeight() * 10000) / 10000 : p._y,
|
||||
realX: p._realX,
|
||||
realY: p._realY,
|
||||
hp: bo ? bo.hp : null,
|
||||
gravitySuspended: !!p.isSuspendGravity(),
|
||||
gravitySuspended: !!window.__nyaruruMotion?.has(p),
|
||||
invincible: !!p.isInvincible(),
|
||||
noclip: !!p._suspendPhysicsHitTest,
|
||||
// 莉莉娅在不在画面里(通关后她会一直跟着,可以用开关藏起来)
|
||||
@@ -147,12 +182,14 @@ const OPS = {
|
||||
uiHidden: SceneManager._scene.__nyaruruUiHidden || {message:false, artwork:false, hud:false},
|
||||
switchesOn: (function(){ let n=0; for (let i=1;i<$dataSystem.switches.length;i++) if ($gameSwitches.value(i)) n++; return n; })(),
|
||||
});
|
||||
`);
|
||||
`, { allowMenu: true });
|
||||
},
|
||||
|
||||
/** 实机 NW.js 用 chrome-extension URL;身份以 Node 入口和实际存档目录为准。 */
|
||||
selfPath() {
|
||||
return wrap(`
|
||||
window.__nyaruruMotion?.touch();
|
||||
window.__nyaruruCombat?.touch();
|
||||
if (window.__nyaruruTrainer) window.__nyaruruTrainer.touch();
|
||||
if (!window.__nyaruruIdentity) Object.defineProperty(window, '__nyaruruIdentity', {
|
||||
value: { maps: new WeakMap(), next: 0, epoch: Date.now().toString(36) + Math.random().toString(36).slice(2) }
|
||||
@@ -167,14 +204,15 @@ const OPS = {
|
||||
const saveDir = typeof StorageManager !== 'undefined' ? StorageManager.fileDirectoryPath() : '';
|
||||
return JSON.stringify({ ok: true, gameDir, saveDir, href: String(location.href), pathname: String(location.pathname),
|
||||
mapToken: state.epoch + ':' + (player ? state.maps.get(player) : 0) + ':' + mapId + ':' + (mapData ? state.maps.get(mapData) : 0),
|
||||
mapId, inMap: typeof SceneManager !== 'undefined' && SceneManager._scene?.constructor.name === 'Scene_Map' });
|
||||
mapId, inMap: typeof SceneManager !== 'undefined' && SceneManager._scene?.constructor.name === 'Scene_Map',
|
||||
inMenu: mapId > 0 && ['Scene_Menu','Scene_Shop'].includes(SceneManager._scene?.constructor.name) });
|
||||
`, { needMap: false });
|
||||
},
|
||||
|
||||
/**
|
||||
* 传送到指定格。
|
||||
*
|
||||
* ⚠️ 这个函数处理了一个**实测踩出来的坑**,改动前务必先读 `探查结果.md` 与开发文档第 6 节。
|
||||
* ⚠️ 这个函数处理了游戏物理坐标同步问题,改动前先读 `tool/开发文档.md` 的「功能模块与边界」。
|
||||
*
|
||||
* 游戏的"坠落复位"机制(nya_statem.js:5986):
|
||||
* isFallDown() 在玩家碰到 type===2 的地形(坑、虚空)时返回 true,
|
||||
@@ -194,19 +232,23 @@ const OPS = {
|
||||
*
|
||||
* 用户想真正站到地上走动时,调用 land()。
|
||||
*/
|
||||
teleport(x, y, { float = true, mapId = null, story = null } = {}) {
|
||||
teleport(x, y, { float = true, mapId = null, story = null, catalog = null } = {}) {
|
||||
if (x === null || y === null || x === '' || y === '' || !Number.isFinite(Number(x)) || !Number.isFinite(Number(y))) throw new Error('坐标必须是有限数值');
|
||||
if (mapId !== null && (!Number.isInteger(Number(mapId)) || Number(mapId) < 1)) throw new Error('地图编号无效');
|
||||
return wrap(`
|
||||
${SLEEP}
|
||||
${catalog ? CATALOG : ''}
|
||||
const p = $gamePlayer, m = $gameMap;
|
||||
const targetMap = ${mapId !== null && mapId !== undefined ? Number(mapId) : 'null'};
|
||||
const sameMap = (targetMap === null || targetMap === m.mapId());
|
||||
const fromMap = m.mapId();
|
||||
const previous = { x: p._x, y: p._y };
|
||||
const priorPhysical={px:p.px,py:p.py};
|
||||
let catalogPlacement = ${catalog ? `planCatalogLanding(targetMap,${JSON.stringify(catalog.eventId ?? catalog)})` : 'null'};
|
||||
const destination=catalogPlacement||{x:${Number(x)},y:${Number(y)}};
|
||||
|
||||
const tx = Math.max(0, Math.min(m.width() - 1, Math.round(${Number(x)})));
|
||||
const ty = Math.max(0, Math.min(m.height() - 1, Math.round(${Number(y)})));
|
||||
const tx = catalogPlacement?destination.x:Math.max(0, Math.min(m.width() - 1, Math.round(destination.x)));
|
||||
const ty = catalogPlacement?destination.y:Math.max(0, Math.min(m.height() - 1, Math.round(destination.y)));
|
||||
|
||||
if (!sameMap) {
|
||||
if (typeof $dataMapInfos === 'undefined' || !$dataMapInfos[targetMap]) return JSON.stringify({ok:false,error:'目标地图不存在'});
|
||||
@@ -242,13 +284,34 @@ const OPS = {
|
||||
});
|
||||
}
|
||||
// 换图后坐标可能被目标地图的出生点逻辑改过,再定一次位
|
||||
const nx = Math.max(0, Math.min(m.width() - 1, Math.round(${Number(x)})));
|
||||
const ny = Math.max(0, Math.min(m.height() - 1, Math.round(${Number(y)})));
|
||||
p.locate(nx, ny);
|
||||
if(!catalogPlacement){
|
||||
const nx = Math.max(0, Math.min(m.width() - 1, Math.round(destination.x)));
|
||||
const ny = Math.max(0, Math.min(m.height() - 1, Math.round(destination.y)));
|
||||
p.locate(nx, ny);
|
||||
}
|
||||
} else {
|
||||
p.locate(tx, ty);
|
||||
if(!catalogPlacement)p.locate(tx, ty);
|
||||
}
|
||||
|
||||
${catalog ? `
|
||||
try{catalogPlacement=planCatalogLanding(targetMap,${JSON.stringify(catalog.eventId ?? catalog)},true);}
|
||||
catch(error){
|
||||
// A live gate/actor can differ from the cached terrain preview.
|
||||
// Return to the starting map/position instead of forcing a landing.
|
||||
if(!sameMap){
|
||||
p.reserveTransfer(fromMap,previous.x,previous.y,2,0);
|
||||
const rollbackStart=Date.now();
|
||||
while(Date.now()-rollbackStart<5000){await sleep(100);if($gameMap!==m||$gamePlayer!==p)break;if(m.mapId()===fromMap&&!p.isTransferring()&&SceneManager._scene?.constructor.name==='Scene_Map')break;}
|
||||
if($gameMap!==m||$gamePlayer!==p||m.mapId()!==fromMap||p.isTransferring())throw Error('目标分块未找到落点,返回原地图超时或游戏状态已变化,请检查游戏状态');
|
||||
const bx=(priorPhysical.px-p.tileXToCenterX(0))/m.tileWidth(),by=(priorPhysical.py-p.tileYToCenterY(0))/m.tileHeight();
|
||||
Game_CharacterBase.prototype.locate.call(p,bx,by);p.unflush(bx,by);
|
||||
}
|
||||
throw Error('目标分块暂时被地形或角色占用,已保留或返回原位置:'+error.message);
|
||||
}
|
||||
Game_CharacterBase.prototype.locate.call(p,catalogPlacement.x,catalogPlacement.y);
|
||||
p.unflush(catalogPlacement.x,catalogPlacement.y);
|
||||
` : ''}
|
||||
|
||||
let storyPlacement = null;
|
||||
const storyRequest = ${JSON.stringify(story ? { eventId: Number(story.eventId), pageIndex: story.pageIndex == null ? null : Number(story.pageIndex), storyId: Number(story.storyId) } : null)};
|
||||
if (storyRequest) {
|
||||
@@ -279,10 +342,14 @@ const OPS = {
|
||||
const floatMode = ${float ? 'true' : 'false'};
|
||||
if (floatMode) {
|
||||
${SAFE_CHAR}
|
||||
floatChar(p);
|
||||
${catalog ? 'motion.enable(p);motion.setNoclip(p,false);' : 'floatChar(p);'}
|
||||
const companion = lilyHere();
|
||||
if (!sameMap && storyPlacement?.ok && storyPlacement.controller && companion?.locate) companion.locate(p._x, p._y);
|
||||
floatChar(companion);
|
||||
${catalog ? `if(companion){
|
||||
const lx=(catalogPlacement.px-companion.tileXToCenterX(0))/m.tileWidth(),ly=(catalogPlacement.py-companion.tileYToCenterY(0))/m.tileHeight();
|
||||
Game_CharacterBase.prototype.locate.call(companion,lx,ly);companion.unflush(lx,ly);
|
||||
motion.enable(companion);motion.setNoclip(companion,false);
|
||||
}` : 'floatChar(companion);'}
|
||||
}
|
||||
|
||||
// 检测是否正在播放剧情 / 有事件在跑。
|
||||
@@ -299,7 +366,8 @@ const OPS = {
|
||||
|
||||
return JSON.stringify({
|
||||
ok: storyPlacement?.ok !== false, error: storyPlacement?.error,
|
||||
positionNote: storyPlacement?.positionNote,
|
||||
positionNote: catalogPlacement?.positionNote||storyPlacement?.positionNote,
|
||||
catalogCell:catalogPlacement?.bounds,
|
||||
x: p._x, y: p._y, px: p.px, py: p.py,
|
||||
mapId: m.mapId(),
|
||||
changedMap: fromMap !== m.mapId(),
|
||||
@@ -325,6 +393,22 @@ const OPS = {
|
||||
* 插件指令参数格式实测为 [插件名, 命令名, 显示文本, 参数对象] —— 4 个元素,
|
||||
* 参数在**第 3 位**(rmmz_objects.js:11315 的 command357 取的就是 params[3])。
|
||||
*/
|
||||
nudge(dx, dy, pixels = 4) {
|
||||
if (![dx, dy].every(Number.isInteger) || Math.abs(dx) + Math.abs(dy) !== 1 || !Number.isFinite(pixels) || pixels < 1 || pixels > 96) throw new Error('微调方向或像素步长无效');
|
||||
return wrap(`
|
||||
${SAFE_CHAR}
|
||||
const p = $gamePlayer, m = $gameMap;
|
||||
const x = Math.max(0, Math.min(m.width() - 1, (p.px - p.tileXToCenterX(0) + ${dx * pixels}) / m.tileWidth()));
|
||||
const y = Math.max(0, Math.min(m.height() - 1, (p.py - p.tileYToCenterY(0) + ${dy * pixels}) / m.tileHeight()));
|
||||
// Base locate synchronizes physical coordinates without player locate's
|
||||
// automatic floor search or companion transfer, which defeats tiny steps.
|
||||
if (typeof Game_CharacterBase !== 'undefined') Game_CharacterBase.prototype.locate.call(p, x, y);
|
||||
else p.locate(x, y);
|
||||
p.unflush(x, y); floatChar(p);
|
||||
const lily = lilyHere(); if (lily) floatChar(lily);
|
||||
return JSON.stringify({ok:true,x,y,px:p.px,py:p.py});
|
||||
`);
|
||||
},
|
||||
playStory(storyId) {
|
||||
return wrap(`
|
||||
${SLEEP}
|
||||
@@ -1116,6 +1200,7 @@ const OPS = {
|
||||
}
|
||||
|
||||
// 恢复碰撞;锚点设到落点,确保万一出岔子也落在附近而不是旧地图的位置
|
||||
window.__nyaruruMotion?.disable(p);
|
||||
p.resumePhysicsHitTest();
|
||||
p.clearImpact && p.clearImpact();
|
||||
p._impact = null;
|
||||
@@ -1132,7 +1217,7 @@ const OPS = {
|
||||
const hits = (lily.hitPreview(0, dy) || []).filter(o => o && o.terrain === true);
|
||||
if (hits.length) { safe = !hits.some(o => o.type === 2); break; }
|
||||
}
|
||||
if (safe) { safeChar(lily); lily.resumeGravity(); lily.resumePhysicsHitTest(); lilyLanded = true; }
|
||||
if (safe) { motion.disable(lily); safeChar(lily); lily.resumeGravity(); lily.resumePhysicsHitTest(); lilyLanded = true; }
|
||||
else { floatChar(lily); warning = '主角已落地;莉莉娅脚下不安全,仍保持悬浮。请移动到安全地面再点落地'; }
|
||||
}
|
||||
return JSON.stringify({ ok: true, dropDistance: found, lilyLanded, warning });
|
||||
@@ -1147,7 +1232,7 @@ const OPS = {
|
||||
out[i] = $gameSwitches.value(i) ? 1 : 0;
|
||||
}
|
||||
return JSON.stringify({ ok: true, values: out });
|
||||
`);
|
||||
`, { allowMenu: true });
|
||||
},
|
||||
|
||||
/** 设置单个开关 */
|
||||
@@ -1178,14 +1263,15 @@ const OPS = {
|
||||
* 并触发坠落复位循环(用户实测反馈过)。
|
||||
*/
|
||||
setGravity(suspended) {
|
||||
if (!suspended) return this.land();
|
||||
const on = suspended ? 'true' : 'false';
|
||||
return wrap(`
|
||||
${SAFE_CHAR}
|
||||
const p = $gamePlayer;
|
||||
const lh = lilyAny();
|
||||
if (${on}) {
|
||||
floatChar(p);
|
||||
if (lh) floatChar(lh);
|
||||
safeChar(p); motion.enable(p);
|
||||
if (lh) { safeChar(lh); motion.enable(lh); }
|
||||
} else {
|
||||
p.resumeGravity();
|
||||
p.resumePhysicsHitTest();
|
||||
@@ -1221,13 +1307,16 @@ const OPS = {
|
||||
return wrap(`
|
||||
${SAFE_CHAR}
|
||||
const p = $gamePlayer;
|
||||
if (!${on ? 'true' : 'false'} && (p.hitPreview(0, 0) || []).some(o => o?.terrain === true)) return JSON.stringify({ok:false,error:'当前与地形重叠,请先微调到空处再关闭穿墙'});
|
||||
let warning = '';
|
||||
for (const c of [p, lilyAny()]) {
|
||||
if (!c) continue;
|
||||
safeChar(c);
|
||||
if (${on ? 'true' : 'false'}) c.suspendPhysicsHitTest();
|
||||
else c.resumePhysicsHitTest();
|
||||
if (${on ? 'true' : 'false'}) motion.enable(c, true);
|
||||
else if (c !== p && (c.hitPreview(0,0) || []).some(o => o?.terrain === true)) warning = '莉莉娅与地形重叠,仍保持穿墙悬停';
|
||||
else motion.setNoclip(c,false);
|
||||
}
|
||||
return JSON.stringify({ok:true,noclip:!!p._suspendPhysicsHitTest});
|
||||
return JSON.stringify({ok:true,noclip:!!p._suspendPhysicsHitTest,warning});
|
||||
`);
|
||||
},
|
||||
|
||||
@@ -1242,6 +1331,8 @@ const OPS = {
|
||||
const lh = lilyAny();
|
||||
if (window.__nyaruruPhoto) window.__nyaruruPhoto.unpinAll();
|
||||
if (window.__nyaruruTrainer) window.__nyaruruTrainer.clear();
|
||||
window.__nyaruruCombat?.clear();
|
||||
motion.clear();
|
||||
// 玩家和莉莉娅都要清 —— 只清玩家的话,同伴会继续坠落复位循环
|
||||
for (const c of [p, lh]) {
|
||||
if (!c) continue;
|
||||
|
||||
@@ -20,6 +20,15 @@ function contains(a, b) {
|
||||
}
|
||||
function validateConfig(config, gameDir) {
|
||||
const next = { ...config };
|
||||
// 独立工作区可显式指定游戏;旧版放在游戏/aitemp 下的布局仍然可用。
|
||||
if (next.gameDir !== undefined) {
|
||||
next.gameDir = canonical(String(next.gameDir).trim());
|
||||
for (const name of ['package.json', 'index.html']) {
|
||||
const file = path.join(next.gameDir, name);
|
||||
if (!fs.existsSync(file) || !fs.statSync(file).isFile()) throw new Error('游戏目录不完整,缺少:' + file);
|
||||
}
|
||||
gameDir = next.gameDir;
|
||||
}
|
||||
for (const key of ['cdpPort', 'webPort']) {
|
||||
const n = Number(next[key]);
|
||||
if (!Number.isInteger(n) || n < 1024 || n > 65535) throw new Error(`${key} 必须是 1024~65535 的整数`);
|
||||
|
||||
+109
-27
@@ -1,6 +1,8 @@
|
||||
// Injected into the game. Uses NYA's actor inventory/battle values, not RPG Maker HP/gold.
|
||||
function installTrainer() {
|
||||
if (window.__nyaruruTrainer) return window.__nyaruruTrainer;
|
||||
if (window.__nyaruruTrainer?.version === 3) return window.__nyaruruTrainer;
|
||||
if (window.__nyaruruTrainer?.dispose) window.__nyaruruTrainer.dispose();
|
||||
else window.__nyaruruTrainer?.clear();
|
||||
const fields = { coin: '_coin', hp: '_hp', sp: 'sp', zp: 'zp' };
|
||||
const locks = new Map();
|
||||
const ids = new WeakMap();
|
||||
@@ -54,7 +56,7 @@ function installTrainer() {
|
||||
}
|
||||
function touch() { sync(); touched = Date.now(); }
|
||||
// Only one timer per game page; neither hooks nor lock settings enter save data.
|
||||
setInterval(() => { try { sync(); } catch { clear(); } }, 100);
|
||||
const timer = setInterval(() => { try { sync(); } catch { clear(); } }, 100);
|
||||
|
||||
const categories = ['consumables', 'weapons', 'stones', 'ornaments'];
|
||||
const specials = {
|
||||
@@ -87,7 +89,67 @@ function installTrainer() {
|
||||
const data = actor[dataMethod]();
|
||||
items.push({ type, id: 0, name: localized(data.name), desc: localized(data.desc), count: actor[countMethod](), max: 99999, equipped: false });
|
||||
}
|
||||
return { ok: true, subject, role: $gamePlayer.isLilyMode() ? '莉莉娅' : actor.name(), stats, items };
|
||||
if (actor._ornaments.bellData) {
|
||||
const data = actor._ornaments.bellData(), fromMap = actor._ornaments.eventBellGainNum();
|
||||
items.push({ type: 'bell', id: 0, name: localized(data.name), desc: localized(data.desc) + ' 默认保留地图铃铛;允许重置收集点时可减少到 0。',
|
||||
count: actor.bellNum(), min: fromMap, max: 25, equipped: false });
|
||||
}
|
||||
const capacity = actor._ornaments.maxEquip ? { used: actor._ornaments.hasEquipNum(), max: actor._ornaments.maxEquip(),
|
||||
mapBells: actor._ornaments.eventBellGainNum(), extraBells: actor._ornaments._hasBuyBellNum } : null;
|
||||
return { ok: true, subject, role: $gamePlayer.isLilyMode() ? '莉莉娅' : actor.name(), stats, items, capacity };
|
||||
}
|
||||
function validateItem(actor, body) {
|
||||
const { type } = body, itemId = integer(body.id, 99999);
|
||||
if (type === 'bell') {
|
||||
if (itemId !== 0 || !actor._ornaments.bellData) throw new Error('铃铛数据不可用');
|
||||
const count = integer(body.count, 25), minimum = actor._ornaments.eventBellGainNum();
|
||||
if (count < minimum) {
|
||||
if (body.adjustMapBells !== true) throw new Error(`已从地图取得 ${minimum} 个铃铛;勾选允许重置地图铃铛后可继续减少`);
|
||||
const keys = typeof DataManager !== 'undefined' && DataManager._cacheBell;
|
||||
if (!Array.isArray(keys) || keys.filter(key => $gameSelfSwitches.value(key)).length !== minimum) throw new Error('地图铃铛记录不完整,不能减少');
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(specials, type)) {
|
||||
if (itemId !== 0) throw new Error('物品编号无效');
|
||||
integer(body.count, 99999); return;
|
||||
}
|
||||
if (!categories.includes(type)) throw new Error('物品分类无效');
|
||||
const container = actor['_' + type], data = container.data();
|
||||
if (!Object.prototype.hasOwnProperty.call(data, itemId) || !data[itemId]) throw new Error('物品不存在');
|
||||
integer(body.count, type === 'consumables' ? (body.overCap === true ? 99999 : container.maxNum(itemId)) : 1);
|
||||
}
|
||||
function applyItem(actor, body) {
|
||||
const { type, id: itemId, count } = body;
|
||||
if (type === 'bell') {
|
||||
const o = actor._ornaments;
|
||||
if (count < o.eventBellGainNum()) {
|
||||
const acquired = DataManager._cacheBell.filter(key => $gameSelfSwitches.value(key));
|
||||
// Remove the highest map/event keys first for deterministic results.
|
||||
const ordered = [...acquired].sort((a,b)=>a[0]-b[0] || a[1]-b[1]);
|
||||
for (const key of ordered.slice(count)) $gameSelfSwitches.setValue(key, false);
|
||||
}
|
||||
o._hasBuyBellNum = count - o.eventBellGainNum();
|
||||
while (o.hasEquipNum() > o.maxEquip()) {
|
||||
const removable = [...o._equip].reverse().find(id => id !== 20);
|
||||
if (removable === undefined) break;
|
||||
o.unequip(removable);
|
||||
}
|
||||
} else if (Object.prototype.hasOwnProperty.call(specials, type)) actor[specials[type][2]] = count;
|
||||
else {
|
||||
const container = actor['_' + type];
|
||||
if (type === 'consumables') container.add(itemId, count - container.itemNum(itemId), body.overCap === true);
|
||||
else if (count) container.unlock(itemId);
|
||||
else if (type === 'weapons') {
|
||||
const remaining = container._unlock.filter(n => n !== itemId);
|
||||
if (container.currentWeapon() === itemId) { actor.changeWeapon(remaining[0]); $gamePlayer.setWeaponChange(); }
|
||||
container._unlock = remaining;
|
||||
} else if (type === 'ornaments') container.remove(itemId);
|
||||
else {
|
||||
if (container.isEquip(itemId)) container.equip(itemId);
|
||||
container._unlock = container._unlock.filter(n => n !== itemId);
|
||||
}
|
||||
}
|
||||
}
|
||||
function change(body) {
|
||||
touch();
|
||||
@@ -114,35 +176,55 @@ function installTrainer() {
|
||||
locks.set(field, { field, target: value, object: null });
|
||||
try { sync(); } catch (e) { clear(); throw e; }
|
||||
}
|
||||
} else if (body.action === 'item') {
|
||||
const { type } = body;
|
||||
const itemId = integer(body.id, 99999);
|
||||
if (Object.prototype.hasOwnProperty.call(specials, type)) {
|
||||
if (itemId !== 0) throw new Error('物品编号无效');
|
||||
actor[specials[type][2]] = integer(body.count, 99999);
|
||||
} else {
|
||||
if (!categories.includes(type)) throw new Error('物品分类无效');
|
||||
const container = actor['_' + type], data = container.data();
|
||||
if (!Object.prototype.hasOwnProperty.call(data, itemId) || !data[itemId]) throw new Error('物品不存在');
|
||||
const max = type === 'consumables' ? (body.overCap === true ? 99999 : container.maxNum(itemId)) : 1;
|
||||
const count = integer(body.count, max);
|
||||
if (type === 'consumables') container.add(itemId, count - container.itemNum(itemId), body.overCap === true);
|
||||
else if (count) container.unlock(itemId);
|
||||
else if (type === 'weapons') {
|
||||
const remaining = container._unlock.filter(n => n !== itemId);
|
||||
if (!remaining.length) throw new Error('至少需要保留一件武器,避免游戏攻击时找不到武器');
|
||||
if (container.currentWeapon() === itemId) { actor.changeWeapon(remaining[0]); $gamePlayer.setWeaponChange(); }
|
||||
container._unlock = remaining;
|
||||
} else if (type === 'ornaments') container.remove(itemId);
|
||||
else {
|
||||
if (container.isEquip(itemId)) container.equip(itemId);
|
||||
container._unlock = container._unlock.filter(n => n !== itemId);
|
||||
} else if (body.action === 'equip') {
|
||||
const { type, id: itemId } = body;
|
||||
if (!['weapons','stones','ornaments'].includes(type) || typeof body.equipped !== 'boolean') throw new Error('装备参数无效');
|
||||
integer(itemId, 99999);
|
||||
const container = actor['_' + type];
|
||||
if (!Object.hasOwn(container.data(), itemId) || !container.isUnlock(itemId)) throw new Error('需要先获得该物品');
|
||||
if (type === 'weapons') { if (!body.equipped) throw new Error('当前武器不能卸为空'); actor.changeWeapon(itemId); $gamePlayer.setWeaponChange(); }
|
||||
else if (type === 'stones') { if (container.isEquip(itemId) !== body.equipped) container.equip(itemId); }
|
||||
else if (!body.equipped) container.unequip(itemId);
|
||||
else if (!container.isEquip(itemId) && !container.equip(itemId)) throw new Error('配饰栏位不足,请增加铃铛或先卸下其他配饰');
|
||||
} else if (body.action === 'batch' || body.action === 'item') {
|
||||
const changes = body.action === 'item' ? [body] : body.changes;
|
||||
if (!Array.isArray(changes) || !changes.length || changes.length > 100) throw new Error('批量物品列表无效');
|
||||
const seen = new Set();
|
||||
for (const item of changes) {
|
||||
if (!item || typeof item !== 'object') throw new Error('物品参数无效');
|
||||
validateItem(actor, item);
|
||||
const key = item.type + ':' + item.id;
|
||||
if (seen.has(key)) throw new Error('批量列表含重复物品');
|
||||
seen.add(key);
|
||||
}
|
||||
const weapons = new Set(actor._weapons._unlock);
|
||||
for (const item of changes) if (item.type === 'weapons') { if (item.count) weapons.add(item.id); else weapons.delete(item.id); }
|
||||
if (!weapons.size) throw new Error('至少需要保留一件武器,避免游戏攻击时找不到武器');
|
||||
const objects = categories.map(type => actor['_' + type]);
|
||||
const snapshots = objects.map(object => Object.fromEntries(Reflect.ownKeys(object).map(key => {
|
||||
const d = Object.getOwnPropertyDescriptor(object, key);
|
||||
if ('value' in d && d.value && typeof d.value === 'object') d.value = Array.isArray(d.value) ? [...d.value] : { ...d.value };
|
||||
return [key, d];
|
||||
})));
|
||||
const materialKeys = Object.values(specials).map(v => v[2]);
|
||||
const materials = materialKeys.map(key => actor[key]);
|
||||
const bellFlags = changes.some(i=>i.type==='bell' && i.adjustMapBells === true) ? DataManager._cacheBell.map(key=>[key,$gameSelfSwitches.value(key)]) : [];
|
||||
try {
|
||||
const ordered = [...changes].sort((a,b) => Number(b.type === 'weapons' && b.count === 1) - Number(a.type === 'weapons' && a.count === 1));
|
||||
for (const item of ordered) applyItem(actor, item);
|
||||
} catch (e) {
|
||||
for (let i = 0; i < objects.length; i++) {
|
||||
for (const key of Reflect.ownKeys(objects[i])) if (!Object.hasOwn(snapshots[i], key)) delete objects[i][key];
|
||||
Object.defineProperties(objects[i], snapshots[i]);
|
||||
}
|
||||
materialKeys.forEach((key, i) => { actor[key] = materials[i]; });
|
||||
for (const [key,value] of bellFlags) $gameSelfSwitches.setValue(key,value);
|
||||
throw e;
|
||||
}
|
||||
} else throw new Error('未知修改操作');
|
||||
return read();
|
||||
}
|
||||
return window.__nyaruruTrainer = { read, change, touch, clear };
|
||||
return window.__nyaruruTrainer = { version: 3, read, change, touch, clear, dispose() { clear(); if (typeof clearInterval === 'function') clearInterval(timer); } };
|
||||
}
|
||||
|
||||
module.exports = { installTrainer };
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
// Evaluated in the game: read original schematic cells, never activate exploration.
|
||||
function readWorldMap(encyclopedia) {
|
||||
const source = Object.values(DataManager._thumbnailMapCell || {});
|
||||
if (!source.length) throw Error('世界地图资料尚未加载,请先正常读档');
|
||||
const keyOf = c => `${c.page}:${c.x}:${c.y}`;
|
||||
const cells = source.map(c => ({ key: keyOf(c), page: c.page, regionId: c.regionId, mapId: c.mapId,
|
||||
x: c.x, y: c.y, hidden: !!c.hidden,
|
||||
mapName: $dataMapInfos?.[c.mapId]?.name || '地图 ' + c.mapId,
|
||||
explored: !!$gameSystem.isThumbnailActive(DataManager.makePosKey(c.x,c.y)),
|
||||
walls: { left:!!c.l, top:!!c.t, right:!!c.r, bottom:!!c.b },
|
||||
exits: { left:!!c.pl, top:!!c.pt, right:!!c.pr, bottom:!!c.pb } }));
|
||||
const byCell = new Map(cells.map(c => [c.key,c]));
|
||||
const definitions = new Map(encyclopedia.items.map(i => [i.type + ':' + i.id,i]));
|
||||
const markers = [], unmapped = [];
|
||||
for (const location of encyclopedia.locations) {
|
||||
const c = DataManager.getThumbnailCell(location.x,location.y,location.mapId);
|
||||
const cell = c && byCell.get(keyOf(c));
|
||||
if (!cell || cell.mapId !== location.mapId) { unmapped.push(location.key); continue; }
|
||||
const iconKey = location.type + ':' + location.id, item = definitions.get(iconKey);
|
||||
markers.push({ ...location, cellKey:cell.key, page:cell.page, regionId:cell.regionId,
|
||||
name:item?.name || iconKey, desc:item?.desc || '', iconKey });
|
||||
}
|
||||
const official = new Set();
|
||||
for (const region of Object.values($dataRegion || {}))
|
||||
for (const [mapId,eventId] of region?.exploreItem || []) official.add(mapId + ':' + eventId);
|
||||
const mapped = new Set(markers.filter(m=>m.official).map(m=>m.mapId + ':' + m.eventId));
|
||||
const regions = [...new Set(cells.map(c=>c.regionId))].sort((a,b)=>a-b).map(id=>({id,
|
||||
name:$dataRegion?.[id]?.name || '区域 ' + id,
|
||||
color:'#' + (DataManager.regionColor?.(id) || 0).toString(16).padStart(6,'0')}));
|
||||
const pages = [...new Set(cells.map(c=>c.page))].sort((a,b)=>a-b).map(id=>{
|
||||
const group=cells.filter(c=>c.page===id);
|
||||
return {id,label:'页面 ' + id,regionIds:[...new Set(group.map(c=>c.regionId))],
|
||||
minX:Math.min(...group.map(c=>c.x)),maxX:Math.max(...group.map(c=>c.x)),
|
||||
minY:Math.min(...group.map(c=>c.y)),maxY:Math.max(...group.map(c=>c.y))};
|
||||
});
|
||||
const current=DataManager.getCurrentThumbnailCell();
|
||||
const playerCell=current && byCell.get(keyOf(current));
|
||||
const player=playerCell ? {cellKey:playerCell.key,page:playerCell.page,regionId:playerCell.regionId,
|
||||
mapId:$gameMap.mapId(),x:$gamePlayer._x,y:$gamePlayer._y} : {cellKey:null,mapId:$gameMap.mapId()};
|
||||
const missing=[...official].filter(k=>!mapped.has(k)), mappedOfficial=official.size-missing.length;
|
||||
return {ok:true,regions,pages,cells,markers,player,coverage:{cells:cells.length,markers:markers.length,
|
||||
officialTotal:official.size,mappedOfficial,officialMapped:mappedOfficial,missing,missingOfficial:missing,unmapped,
|
||||
message:`原版收集点已映射 ${mappedOfficial}/${official.size};地图 ${cells.length} 格`}};
|
||||
}
|
||||
module.exports={readWorldMap};
|
||||
Reference in New Issue
Block a user