feat: 发布咸鱼喵喵全能助手,加入全收集地图与图文说明

This commit is contained in:
Kitasami_Nya
2026-10-24 01:37:15 +08:00
parent 8f6ac4ff04
commit 3823e38b07
39 changed files with 2918 additions and 194 deletions
+187
View File
@@ -0,0 +1,187 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const vm = require('node:vm');
const { installMotion } = require('../lib/motion');
const { OPS } = require('../lib/ops');
function fixture() {
let now = 0;
const timers = [];
const character = () => ({ _x: 3, _y: 4, px: 3 * 96 + 40, py: 4 * 96 + 80, _vx: 0, _vy: 12,
get vx() { return this._vx; }, set vx(v) { this._vx = v; },
get vy() { return Math.round(this._vy); }, set vy(v) { this._vy = v; },
g: () => 0, deltaTime: () => 1, doGravity() { this._vy += 0.5; }, updateMapBoundTransfer() { this.transfer = true; },
makeMomentum() { this.rx = this.px + this._vx; this.ry = this.py + this._vy; },
cacheDeltaTimeSpeed() { this._vx *= this.deltaTime(); this._vy *= this.deltaTime(); },
startTestV() { if(!this._testV){this._testV=true;this._tvx=this._vx;this._tvy=this._vy;} },
testV(v) {this.vx=v.x;this.vy=v.y;this.makeMomentum();},
endTestV() {if(this._testV){this.vx=this._tvx;this.vy=this._tvy;this._testV=false;this.makeMomentum();}},
tileXToCenterX: x => x * 96 + 40, tileYToCenterY: y => y * 96 + 80,
locate(x,y) { this._x = Math.round(x); this._y = Math.round(y); this.unflush(x,y); },
unflush(x,y) { this.px = Math.round(x * 96 + 40); this.py = Math.round(y * 96 + 80); },
clearImpact() {}, clearGrounding() {}, suspendGravity() { this._suspendGravity = true; }, resumeGravity() { this._suspendGravity = false; },
suspendPhysicsHitTest() { this._suspendPhysicsHitTest = true; }, resumePhysicsHitTest() { this._suspendPhysicsHitTest = false; },
isSuspendGravity() { return !!this._suspendGravity; }, isRemove() { return false; },
hitPreview() { return []; },
isFlexibleGrounding() { return !!this.grounded; }, isTriggeredJump() { return !!this.jumpPressed; },
isPressDown() { return !!this.downPressed; }, isOutControl() { return !!this.blocked; }, jumpStrength() { return -8; },
controlJump() { if(this.grounded && this.jumpPressed)this.vy=-8; },
controlDoubleJump() { if(!this.grounded && !this.useDoubleJump && this.jumpPressed) { this.vy=-6; this.useDoubleJump=true; } },
});
const p = character(), lily = character();
const context = vm.createContext({ window: {}, $gamePlayer:p, $gameLily:lily, $dataMap: {},
$gameMap: { g: () => 0.5, width: () => 20, height: () => 12, tileWidth: () => 96, tileHeight: () => 96, mapId: () => 1 },
SceneManager: { _scene: { constructor: { name:'Scene_Map' } } },
Date: { now: () => now }, setInterval: fn => timers.push(fn),
});
const api = vm.runInContext(`(${installMotion.toString()})()`, context);
return { context, api, p, lily, advance(ms) { now += ms; timers.forEach(fn=>fn()); } };
}
test('hover stops falling, slows upward jump to zero, supports inverted gravity and restores hooks', () => {
const f=fixture(), original=f.p.doGravity, transfer=f.p.updateMapBoundTransfer;
f.api.enable(f.p); assert.equal(f.p._vy,0); assert.equal(f.p._suspendGravity,true);
const saved=JSON.parse(JSON.stringify(f.p));
assert.equal(Object.hasOwn(saved,'_suspendGravity'),false);
assert.equal(Object.hasOwn(saved,'_suspendPhysicsHitTest'),false);
f.p._vy=-8; const speeds=[];
for(let i=0;i<40;i++) { f.p.doGravity(); speeds.push(f.p._vy); }
assert.equal(speeds[0],-7.5); assert.equal(speeds.at(-1),0); assert.ok(speeds.every(v=>v<=0));
f.p._vy=20; f.p.doGravity(); assert.equal(f.p._vy,0);
f.p._vy=20; f.p.cacheDeltaTimeSpeed(); f.p.makeMomentum(); assert.equal(f.p.ry,f.p.py);
f.p._testV=true; f.p._vy=5; f.p.makeMomentum(); assert.equal(f.p.ry,f.p.py+5); f.p._testV=false;
f.p._invertMode=true; f.p._vy=8;
for(let i=0;i<40;i++) f.p.doGravity();
assert.equal(f.p._vy,0);
f.p.updateMapBoundTransfer(); assert.equal(f.p.transfer,true);
assert.equal(Object.keys(f.p).includes('doGravity'),false);
f.api.clear(); assert.equal(f.p.doGravity,original); assert.equal(f.p.updateMapBoundTransfer,transfer);
assert.equal(f.p._suspendGravity,false); assert.equal(f.p._suspendPhysicsHitTest,false);
});
test('noclip enables hover for both characters and refuses to restore collision inside terrain', async () => {
const f=fixture();
const run=async code=>JSON.parse(await vm.runInContext(code,f.context));
assert.equal((await run(OPS.setNoclip(true))).ok,true);
for(const c of [f.p,f.lily]) { assert.equal(c._suspendGravity,true); assert.equal(c._suspendPhysicsHitTest,true); c._vy=99; c.doGravity(); assert.equal(c._vy,0); }
f.p.hitPreview=()=>[{terrain:true,type:0}];
assert.equal((await run(OPS.setNoclip(false))).ok,false); assert.equal(f.p._suspendPhysicsHitTest,true);
f.p.hitPreview=()=>[];
assert.equal((await run(OPS.setNoclip(false))).ok,true); assert.equal(f.p._suspendPhysicsHitTest,false); assert.equal(f.p._suspendGravity,true);
});
test('pixel nudge uses physical position, accumulates exact small steps and respects map bounds', async () => {
const f=fixture(), run=async code=>JSON.parse(await vm.runInContext(code,f.context));
f.p.px=500; f.p.py=600;
const first=await run(OPS.nudge(1,0,4)); assert.equal(first.px,504); assert.equal(first.py,600);
const next=await run(OPS.nudge(1,0,4)); assert.equal(next.px,508);
f.p.px=40; assert.equal((await run(OPS.nudge(-1,0,16))).px,40);
f.p.py=80; assert.equal((await run(OPS.nudge(0,-1,1))).py,80);
f.p.px=99999; f.p.py=99999; f.p.updateMapBoundTransfer(); assert.equal(f.p.px,19*96+40); assert.equal(f.p.py,11*96+80);
assert.throws(()=>OPS.nudge(1,1,4)); assert.throws(()=>OPS.nudge(1,0,Infinity));
});
test('hover survives normal map changes but releases on save load and heartbeat expiry', () => {
const f=fixture(); f.api.enable(f.p,true); f.advance(120001);
assert.equal(f.api.has(f.p),false); assert.equal(f.p._suspendGravity,false);
f.api.enable(f.p,true); f.context.$dataMap={}; f.p.resumeGravity();f.p.resumePhysicsHitTest();f.advance(1);
assert.equal(f.api.has(f.p),true);assert.equal(f.p.isSuspendGravity(),true);assert.equal(f.p._suspendPhysicsHitTest,true);
f.context.$gameMap={...f.context.$gameMap};f.advance(1);
assert.equal(f.api.has(f.p),false);
});
test('hover allows repeated airborne jumps, manual descent, and preserves downward skill movement', () => {
const f=fixture(), jump=f.p.controlJump, double=f.p.controlDoubleJump;
f.p.useDoubleJump=true; f.api.enable(f.p); f.p.jumpPressed=true;
for(let i=0;i<5;i++) { f.p._vy=0; f.p.controlJump({}); f.p.controlDoubleJump({}); assert.equal(f.p._vy,-8); }
assert.equal(f.p.useDoubleJump,true);
f.p.jumpPressed=false; f.p.downPressed=true; f.p.doGravity(); f.p.makeMomentum();
assert.equal(f.p.ry-f.p.py,4);
f.p.downPressed=false; f.p.doGravity(); f.p.makeMomentum(); assert.equal(f.p.ry,f.p.py);
f.p._bodyAttacking='toru_down_dash_attack'; f.p._vy=22; f.p.doGravity(); f.p.makeMomentum(); assert.equal(f.p.ry-f.p.py,22);
f.p.downPressed=true; f.p._vy=25; f.p.doGravity(); f.p.makeMomentum(); assert.equal(f.p.ry-f.p.py,25);
f.p._bodyAttacking=''; f.p.downPressed=false; f.p.doGravity(); f.p.makeMomentum(); assert.equal(f.p.ry,f.p.py);
f.p.blocked=true; f.p.downPressed=true; f.p.doGravity(); assert.equal(f.p._vy,0);
f.api.clear(); assert.equal(f.p.controlJump,jump); assert.equal(f.p.controlDoubleJump,double);
});
test('native skill and stagger cleanup cannot cancel tool hover; disabling restores the native query', async () => {
const f=fixture(), originalQuery=f.p.isSuspendGravity;
f.api.enable(f.p);
// The original game does this when a skill or stagger ends.
f.p.resumeGravity();
assert.equal(f.p._suspendGravity,false);
assert.equal(f.api.has(f.p),true);
assert.equal(f.p.isSuspendGravity(),true);
f.p._vy=20; f.p.doGravity(); f.p.makeMomentum();
assert.equal(f.p.ry,f.p.py);
f.advance(15000);
f.p.isInvincible=()=>false;
Object.assign(f.context,{$dataMapInfos:[], $dataSystem:{switches:[]}, $gameSwitches:{value:()=>false}, Graphics:{}});
const snap=JSON.parse(await vm.runInContext(OPS.snapshot(),f.context));
assert.equal(snap.ok,true);
assert.equal(snap.gravitySuspended,true);
assert.equal(f.p.isSuspendGravity(),true);
assert.equal(Object.keys(f.p).includes('isSuspendGravity'),false);
f.api.disable(f.p);
assert.equal(f.p.isSuspendGravity,originalQuery);
assert.equal(f.p.isSuspendGravity(),false);
// Native skill suspension must not turn the panel's tool switch back on.
f.p.suspendGravity();
assert.equal(JSON.parse(await vm.runInContext(OPS.snapshot(),f.context)).gravitySuspended,false);
});
test('hot upgrading an active v4 helper preserves hover, noclip and velocity', () => {
const f=fixture(), nativeQuery=f.p.isSuspendGravity;
f.api.enable(f.p,true); f.p._vx=3; f.p._vy=-7;
f.api.version=4;
const next=vm.runInContext(`(${installMotion.toString()})()`,f.context);
assert.equal(next.version,5); assert.equal(next.has(f.p),true);
assert.equal(f.p._suspendPhysicsHitTest,true);
assert.equal(f.p._vx,3); assert.equal(f.p._vy,-7);
f.p.resumeGravity(); assert.equal(f.p.isSuspendGravity(),true);
next.clear(); assert.equal(f.p.isSuspendGravity,nativeQuery);
assert.equal(f.p.isSuspendGravity(),false);
});
test('collision-resolved motion is preserved through grounding probes while holding down', () => {
const f=fixture(), momentum=f.p.makeMomentum,cache=f.p.cacheDeltaTimeSpeed;
f.api.enable(f.p); f.p.downPressed=true;
assert.equal(f.p.makeMomentum,momentum);
for(let i=0;i<30;i++){
f.p.doGravity();f.p.cacheDeltaTimeSpeed();f.p.makeMomentum();
assert.equal(f.p._vy,4);
// Native solid-floor collision resolves to no vertical displacement.
f.p._vy=0;f.p.makeMomentum();
// generateObjectPhysicsInfo probes below, then restores resolved velocity.
f.p.startTestV();f.p.testV({x:0,y:1});f.p.endTestV();
assert.equal(f.p._vy,0);assert.equal(f.p.ry,f.p.py);
}
f.p.deltaTime=()=>0.5;f.p.cacheDeltaTimeSpeed();assert.equal(f.p._vy,2);
f.api.clear();assert.equal(f.p.cacheDeltaTimeSpeed,cache);
});
test('native map exits run before the safety clamp; noclip side exits use native destinations', () => {
const f=fixture();let nativeCalls=0,exitType=null,transferring=false;
f.p.hitOffsetL=f.p.hitOffsetR=()=>12;f.p.hitOffsetT=()=>64;f.p.hitOffsetB=()=>16;
f.p.updateMapBoundTransfer=()=>{nativeCalls++;};f.p.isTransferring=()=>transferring;
f.p.autoTransfer=type=>{exitType=type;transferring=true;return true;};
f.api.enable(f.p,true);f.p.px=-5;f.p.py=600;f.p.updateMapBoundTransfer();
assert.equal(nativeCalls,1);assert.equal(exitType,3);assert.equal(f.p.px,-5);
transferring=false;f.p.autoTransfer=()=>false;f.p.updateMapBoundTransfer();assert.equal(f.p.px,12);
f.p.updateMapBoundTransfer();assert.equal(nativeCalls,3);
f.api.setNoclip(f.p,false);f.context.$dataMap={};f.api.touch();assert.equal(f.p._suspendPhysicsHitTest,false);
assert.equal(f.api.has(f.p),true);
});
test('normal map transitions retain helpers; transient loading and replaced actors do not revive old helpers', () => {
const f=fixture();f.context.$gameActors={};f.api.touch();f.api.enable(f.p,true);
f.context.$dataMap=null;f.context.SceneManager.isSceneChanging=()=>true;
f.api.touch();assert.equal(f.api.has(f.p),true);
f.context.$dataMap={};f.context.SceneManager.isSceneChanging=()=>false;
f.p.resumePhysicsHitTest();f.p._vy=40;f.api.touch();
assert.equal(f.p._suspendPhysicsHitTest,true);assert.equal(f.p._vy,0);
f.context.$gameActors={};f.api.touch();
assert.equal(f.api.has(f.p),false);assert.equal(f.p.isSuspendGravity(),false);
f.context.$dataMap={};f.api.touch();assert.equal(f.api.has(f.p),false);
});