aardio 文档

aardio 范例: 自绘小游戏 - 螺丝木板解谜(Screw Wood Puzzle)

import win.ui;
/*DSG{{*/
var winform = win.form(text="自绘小游戏 - 螺丝木板解谜(Screw Wood Puzzle)";right=1020;bottom=720;bgcolor=0x171821;border="dialog frame";max=false)
winform.add(
screwCanvas={cls="plus";left=0;top=0;right=1020;bottom=720;db=1;dl=1;dr=1;dt=1;notify=1;z=1}
)
/*}}*/

import gdip;

/*
螺丝木板解谜:
- 点击露出的彩色螺丝,把它们送入右侧 6 格工具盒;3 枚同色螺丝会自动收纳。
- 木板剩一枚螺丝时会围绕支点摆动,最后一枚被拆掉后会旋转坠落并揭开下层螺丝。
- 工具盒塞满且没有凑成三枚同色会触发一次紧急清盒并降低星级。
- H 提示最适合当前工具盒的螺丝,R 或右键重置;通关后单击、空格或回车进入下一关。
- 5 关分别包含 6 / 9 / 12 / 15 / 18 枚螺丝。最优拆解序列均已验证可见、可解且不会堵塞工具盒。
- 木板与螺丝使用 DPI 原尺寸透明精灵;逐帧仅贴图、变换少量木板并绘制短寿命粒子。
*/

var CANVAS_W=1020;
var CANVAS_H=720;
var WORK_X=28;
var WORK_Y=112;
var WORK_W=675;
var WORK_H=570;
var PANEL_X=724;
var PANEL_Y=104;
var PANEL_W=268;
var PANEL_H=568;
var TRAY_CAPACITY=6;
var UNSCREW_TIME=0.28;
var FLY_TIME=0.46;
var STATE_PLAYING=1;
var STATE_SOLVED=2;

var screwColors=[
    0xFFFF5F6D,0xFF54DDF2,0xFFFFC857,
    0xFFA879FF,0xFF55DB91,0xFFFF8B52
];

var woodStyles=[
    {top=0xFFF0B868;bottom=0xFF9B572E;edge=0xFF6A351F;grain=0x7A6E361D;name="橡木"},
    {top=0xFFB96B58;bottom=0xFF633340;edge=0xFF482431;grain=0x75622D2A;name="红木"},
    {top=0xFF54B7A2;bottom=0xFF235F5D;edge=0xFF173F42;grain=0x68345D50;name="青木"},
    {top=0xFF6D91D8;bottom=0xFF34466E;edge=0xFF232C4D;grain=0x69405277;name="蓝木"},
    {top=0xFFC680D5;bottom=0xFF684472;edge=0xFF482C52;grain=0x6D59365F;name="紫木"},
    {top=0xFFE19A5A;bottom=0xFF7B4930;edge=0xFF50301F;grain=0x78643825;name="胡桃木"}
];

// 六种木板模板共用同一工作台坐标;points 为多边形,pins 为三处可用螺孔。
var shapes={
    A={
        points=[{x=82;y=190},{x=646;y=205},{x=635;y=300},{x=72;y=284}];
        pins=[{x=145;y=235},{x=355;y=244},{x=585;y=250}]
    };
    B={
        points=[{x=126;y=218},{x=205;y=156},{x=594;y=575},{x=505;y=646}];
        pins=[{x=275;y=320},{x=356;y=405},{x=526;y=570}]
    };
    C={
        points=[{x=297;y=150},{x=407;y=164},{x=391;y=647},{x=283;y=631}];
        pins=[{x=351;y=226},{x=347;y=402},{x=339;y=582}]
    };
    D={
        points=[{x=73;y=430},{x=621;y=399},{x=637;y=505},{x=88;y=537}];
        pins=[{x=145;y=484},{x=411;y=458},{x=573;y=447}]
    };
    E={
        points=[{x=519;y=157},{x=601;y=224},{x=207;y=642},{x=123;y=570}];
        pins=[{x=485;y=315},{x=360;y=405},{x=190;y=571},{x=420;y=350}]
    };
    F={
        points=[{x=145;y=149},{x=246;y=145},{x=257;y=645},{x=149;y=650}];
        pins=[{x=195;y=335},{x=205;y=460},{x=195;y=400}]
    }
};

var P=function(slot,color,seq,cover){
    return {slot=slot;color=color;seq=seq;cover=cover};
}
var B=function(key,style,pins){
    return {key=key;style=style;pins=pins};
}

// boards 按从底层到顶层排列。seq 同时定义了无堵盒的推荐拆解序列。
var levels=[
    {
        name="初识摆臂";subtitle="先让三块木板学会摆动";accent=0xFFFFC857;
        boards=[
            B("E",3,[P(3,1,3),P(1,2,6)]),
            B("B",2,[P(1,1,2),P(3,2,5)]),
            B("A",1,[P(1,1,1),P(3,2,4)])
        ]
    },
    {
        name="三色工具盒";subtitle="不要按木板顺序拆,要按颜色收纳";accent=0xFF55DFF2;
        boards=[
            B("E",5,[P(3,1,3),P(4,2,6),P(1,3,9)]),
            B("B",3,[P(1,1,2),P(2,2,5),P(3,3,8)]),
            B("A",1,[P(1,1,1),P(2,2,4),P(3,3,7)])
        ]
    },
    {
        name="交叠机关";subtitle="上层木板坠落后,藏住的螺丝才会出现";accent=0xFFA879FF;
        boards=[
            B("D",6,[P(1,2,6),P(3,3,9),P(2,4,12,"B")]),
            B("C",4,[P(3,1,3),P(1,3,8,"A"),P(2,4,11)]),
            B("B",2,[P(1,1,2),P(2,2,5),P(3,4,10)]),
            B("A",1,[P(1,1,1),P(2,2,4),P(3,3,7)])
        ]
    },
    {
        name="五彩连锁";subtitle="让摆臂与坠落依次揭开下一层";accent=0xFF55DB91;
        boards=[
            B("E",5,[P(1,2,6),P(2,5,15,"C"),P(3,1,3)]),
            B("D",6,[P(1,1,2),P(2,4,12,"B"),P(3,5,14)]),
            B("C",4,[P(1,3,9,"A"),P(2,4,11),P(3,5,13)]),
            B("B",2,[P(1,2,5),P(2,3,8),P(3,4,10)]),
            B("A",1,[P(1,1,1),P(2,2,4),P(3,3,7)])
        ]
    },
    {
        name="大师工坊";subtitle="六色、六层支点,完成最后的机械瀑布";accent=0xFFFF6F91;
        boards=[
            B("F",3,[P(1,1,3),P(2,6,18,"D"),P(3,2,6)]),
            B("E",5,[P(1,6,17),P(2,5,15,"C"),P(3,1,2)]),
            B("D",6,[P(1,5,14),P(2,4,12,"B"),P(3,6,16)]),
            B("C",4,[P(1,3,9,"A"),P(2,4,11),P(3,5,13)]),
            B("B",2,[P(1,2,5),P(2,3,8),P(3,4,10)]),
            B("A",1,[P(1,1,1),P(2,2,4),P(3,3,7)])
        ]
    }
];

var game={
    state=STATE_PLAYING;
    level=1;
    cycle=1;
    boards=[];
    boardByKey={};
    screws=[];
    screwByKey={};
    solution=[];
    tray=[];
    action=null;
    removedScrews=0;
    totalScrews=0;
    moves=0;
    matches=0;
    jams=0;
    misses=0;
    hoverId=null;
    hintId=null;
    hintTimer=0;
    blockedBoardKey=null;
    blockedTimer=0;
    statusText="点击露出的螺丝,同色三枚会自动收纳";
    statusTime=2600;
    particles=[];
    pulse=0;
    trayFlash=0;
    solveGlow=0;
    pendingSolve=false;
};

var CACHE_DPI_X=winform.dpiScale(1);
var CACHE_DPI_Y=CACHE_DPI_X;
var sceneCache;
var sceneDirty=true;
var screwSprites=[];

// 木板按 z 序分段缓存:连续静止木板合并为一张透明位图,
// 摆动或坠落木板保留为动态层,避免高 DPI 下逐帧贴 6 张大尺寸木板精灵。
var BOARD_CACHE_W=710;
var BOARD_CACHE_H=720;
var layerCaches=[];
var renderItems=[];
var layersDirty=true;
var sceneContainsBoards=false;
var sceneCachedBoardEnd=0;
var drawBoardRangeToGraphics;

var dustMotes=[];
for(i=1;58){
    table.push(dustMotes,{
        x=math.random()*CANVAS_W;y=math.random()*CANVAS_H;
        size=0.5+math.random()*1.6;alpha=18+math.random(0,44)
    });
}

import sys.midiOut;
var midiOut=sys.midiOut();

// -------------------- 基础工具 --------------------
var alphaColor=function(color,alpha){
    return ((math.clamp(math.floor(alpha),0,255)<<24) | (color&0xFFFFFF));
}

var shadeColor=function(color,amount){
    var r=math.clamp(((color>>16)&0xFF)+amount,0,255);
    var g=math.clamp(((color>>8)&0xFF)+amount,0,255);
    var b=math.clamp((color&0xFF)+amount,0,255);
    return 0xFF000000 | (math.floor(r)<<16) | (math.floor(g)<<8) | math.floor(b);
}

var drawText=function(graphics,text,size,x,y,width,height,color,bold,align){
    var textFont=gdip.font("Microsoft YaHei UI",size,bold ? 1/*_FontStyleBold*/ : 0,2/*_UnitPixel*/);
    var format=gdip.stringFormat();
    format.align=align;
    format.lineAlign=1/*_StringAlignmentCenter*/;
    var brush=gdip.solidBrush(color);
    graphics.drawString(text,textFont,::RECTF(x,y,width,height),format,brush);
    brush.delete();
    format.delete();
    textFont.delete();
}

var setStatus=function(text,milliseconds){
    game.statusText=text;
    game.statusTime=milliseconds;
    sceneDirty=true;
}

var defaultStatus=function(){
    if(game.state==STATE_SOLVED) return "机关已清空!单击或按空格进入下一关";
    return "按颜色规划拆解:三枚同色自动收纳,六格塞满会堵盒";
}

var normalizeAngle=function(angle){
    while(angle>180) angle-=360;
    while(angle<-180) angle+=360;
    return angle;
}

var boardPoint=function(board,x,y){
    var rad=math.rad(board.angle);
    var dx=x-board.baseCx;
    var dy=y-board.baseCy;
    return board.cx+dx*math.cos(rad)-dy*math.sin(rad),
        board.cy+dx*math.sin(rad)+dy*math.cos(rad);
}

var screwPosition=function(screw){
    return boardPoint(screw.board,screw.x,screw.y);
}

var traySlotPoint=function(index){
    var col=(index-1)%3;
    var row=math.floor((index-1)/3);
    return 770+col*82,342+row*76;
}

var remainingScrews=function(board){
    var remaining=[];
    for(i,screwId in board.screwIds){
        var screw=game.screws[screwId];
        if(!screw.removed && !screw.removing) table.push(remaining,screw);
    }
    return remaining;
}

var pointInBoard=function(x,y,board){
    if(!board || board.gone) return false;
    var inside=false;
    var j=#board.points;
    for(i=1;#board.points){
        var ax,ay=boardPoint(board,board.points[i].x,board.points[i].y);
        var bx,by=boardPoint(board,board.points[j].x,board.points[j].y);
        if(((ay>y)!=(by>y)) && (x<(bx-ax)*(y-ay)/(by-ay)+ax)) inside=!inside;
        j=i;
    }
    return inside;
}

var isScrewVisible=function(screw){
    if(!screw || screw.removed || screw.removing || screw.board.gone) return false;
    if(screw.cover){
        var coverBoard=game.boardByKey[screw.cover];
        if(coverBoard && coverBoard.covering) return false;
    }

    var x,y=screwPosition(screw);
    for(i=#game.boards;1;-1){
        var board=game.boards[i];
        if(pointInBoard(x,y,board)) return board==screw.board;
    }
    return false;
}

var allBoardsGone=function(){
    for(i,board in game.boards){
        if(!board.gone) return false;
    }
    return true;
}

var disposeLayerCaches=function(){
    for(i,cache in layerCaches){
        if(cache.bitmap) cache.bitmap.delete();
    }
    layerCaches=[];
    renderItems=[];
}

var disposeSprites=function(){
    disposeLayerCaches();
    sceneContainsBoards=false;
    sceneCachedBoardEnd=0;
    for(i,board in game.boards){
        if(board.sprite){
            board.sprite.bitmap.delete();
            board.sprite=null;
        }
        if(board.motionSprite){
            board.motionSprite.bitmap.delete();
            board.motionSprite=null;
        }
    }
    for(i,sprite in screwSprites){
        if(sprite) sprite.bitmap.delete();
    }
    screwSprites=[];
    layersDirty=true;
}

// -------------------- 关卡与有限状态物理 --------------------
var initLevel=function(levelIndex){
    disposeSprites();

    game.level=levelIndex;
    game.state=STATE_PLAYING;
    game.boards=[];
    game.boardByKey={};
    game.screws=[];
    game.screwByKey={};
    game.solution=[];
    game.tray=[];
    game.action=null;
    game.removedScrews=0;
    game.totalScrews=0;
    game.moves=0;
    game.matches=0;
    game.jams=0;
    game.misses=0;
    game.hoverId=null;
    game.hintId=null;
    game.hintTimer=0;
    game.blockedBoardKey=null;
    game.blockedTimer=0;
    game.statusText="点击露出的螺丝,同色三枚会自动收纳";
    game.statusTime=2800;
    game.particles=[];
    game.trayFlash=0;
    game.solveGlow=0;
    game.pendingSolve=false;

    var levelData=levels[levelIndex];
    for(i,boardDef in levelData.boards){
        var shape=shapes[boardDef.key];
        var minX=10000,minY=10000,maxX=-10000,maxY=-10000;
        var sumX=0,sumY=0;
        for(j,p in shape.points){
            minX=math.min(minX,p.x);minY=math.min(minY,p.y);
            maxX=math.max(maxX,p.x);maxY=math.max(maxY,p.y);
            sumX+=p.x;sumY+=p.y;
        }
        var board={
            id=i;key=boardDef.key;style=boardDef.style;
            points=shape.points;pinPoints=shape.pins;
            screwIds=[];
            baseCx=sumX/#shape.points;baseCy=sumY/#shape.points;
            cx=sumX/#shape.points;cy=sumY/#shape.points;
            minX=minX;minY=minY;maxX=maxX;maxY=maxY;
            angle=0;angularVel=0;targetAngle=0;settleTime=0;
            mode="fixed";covering=true;gone=false;
            vx=0;vy=0;omega=0;fallTime=0;
        };
        table.push(game.boards,board);
        game.boardByKey[board.key]=board;

        for(j,pinDef in boardDef.pins){
            var pos=shape.pins[pinDef.slot];
            var screw={
                id=#game.screws+1;key=board.key++j;
                board=board;boardKey=board.key;
                x=pos.x;y=pos.y;color=pinDef.color;seq=pinDef.seq;cover=pinDef.cover;
                removed=false;removing=false;
            };
            table.push(game.screws,screw);
            table.push(board.screwIds,screw.id);
            game.screwByKey[screw.key]=screw;
            game.solution[screw.seq]=screw;
        }
    }
    game.totalScrews=#game.screws;
    sceneDirty=true;
    layersDirty=true;
}

var nextLevel=function(){
    var next=game.level+1;
    if(next>#levels){
        next=1;
        game.cycle++;
    }
    initLevel(next);
    if(midiOut) midiOut.play("changeInstrument(10), 1_,3_,5_", "C5", 55);
}

var spawnParticles=function(x,y,count,color,kind){
    for(i=1;count){
        var speed=kind=="dust" ? 55+math.random()*95 : 90+math.random()*190;
        var angle=(kind=="dust" ? -math.pi*0.95+math.random()*math.pi*0.9 : math.random()*math.pi*2);
        table.push(game.particles,{
            x=x;y=y;
            vx=math.cos(angle)*speed;vy=math.sin(angle)*speed;
            gravity=kind=="dust" ? 150 : 210;
            life=0.42+math.random()*0.48;maxLife=0.42+math.random()*0.48;
            size=kind=="dust" ? 2+math.random()*3.6 : 1.5+math.random()*3;
            color=color;kind=kind;spin=math.random()*math.pi*2
        });
    }
    while(#game.particles>110) table.remove(game.particles,1);
}

var beginHanging=function(board,pivotScrew){
    board.mode="hanging";
    board.pivotLocalX=pivotScrew.x;
    board.pivotLocalY=pivotScrew.y;
    var pivotX,pivotY=screwPosition(pivotScrew);
    board.pivotX=pivotX;
    board.pivotY=pivotY;

    var dx=board.baseCx-board.pivotLocalX;
    var dy=board.baseCy-board.pivotLocalY;
    var target=normalizeAngle(90-math.deg(math.atan2(dy,dx)));
    if(math.abs(target)>124) target=target>0 ? 124 : -124;
    if(math.abs(target)<24) target=(board.id%2 ? 1 : -1)*58;
    board.targetAngle=target;
    board.angularVel=(target>=0 ? 1 : -1)*(35+board.id*4);
    board.settleTime=0;
    layersDirty=true;
    sceneDirty=true;
    setStatus("木板只剩一个支点,正在围绕最后一枚螺丝摆动",1200);
}

var beginFalling=function(board){
    board.mode="falling";
    board.vx=(board.cx-(WORK_X+WORK_W/2))*0.12+(board.id%2 ? -18 : 18);
    board.vy=35+board.id*5;
    board.omega=(board.id%2 ? -1 : 1)*(42+board.id*7);
    board.fallTime=0;
    board.covering=true;
    board.settleTime=0;
    layersDirty=true;
    sceneDirty=true;
    var style=woodStyles[board.style];
    spawnParticles(board.cx,board.cy,13,shadeColor(style.top,22),"dust");
    setStatus("最后一个支点已释放,木板正在旋转坠落",1100);
    if(midiOut) midiOut.play("changeInstrument(116), 1_,5_", "C3", 48);
}

var updateBoardState=function(board){
    var remaining=remainingScrews(board);
    if(#remaining==0){
        beginFalling(board);
    }
    elseif(#remaining==1 && board.mode=="fixed"){
        beginHanging(board,remaining[1]);
    }
}

var detachScrew=function(action){
    var screw=action.screw;
    if(action.detached) return;
    action.detached=true;
    screw.removed=true;
    screw.removing=false;
    game.removedScrews++;
    game.hoverId=null;
    game.hintId=null;
    game.hintTimer=0;

    updateBoardState(screw.board);
    spawnParticles(action.startX,action.startY,10,screwColors[screw.color],"spark");
    action.targetSlot=#game.tray+1;
    action.targetX,action.targetY=traySlotPoint(action.targetSlot);
    sceneDirty=true;
    layersDirty=true;
    if(midiOut) midiOut.play("changeInstrument(13), 1_,5_", "C6", 34);
}

var removeMatchedColor=function(color){
    var indexes=[];
    for(i,value in game.tray){
        if(value==color) table.push(indexes,i);
    }
    if(#indexes<3) return false;

    for(i=#indexes;1;-1) table.remove(game.tray,indexes[i]);
    game.matches++;
    game.trayFlash=1;
    spawnParticles(850,378,24,screwColors[color],"spark");
    setStatus("三枚同色螺丝已自动收纳,工具盒腾出了空间",1450);
    if(midiOut) midiOut.play("changeInstrument(10), 1_,3_,5_,1'_", "C5", 48);
    return true;
}

var finishLevel=function(){
    if(game.state==STATE_SOLVED) return;
    game.state=STATE_SOLVED;
    game.pendingSolve=false;
    game.tray=[];
    game.solveGlow=0;
    setStatus("机关已清空!单击或按空格进入下一关",100000);
    spawnParticles(WORK_X+WORK_W/2,WORK_Y+WORK_H/2,58,levels[game.level].accent,"spark");
    if(midiOut) midiOut.play("changeInstrument(10), 1_,3_,5_,1'__,3'__,5'__", "C4", 78,70);
}

var finishScrewAction=function(action){
    table.push(game.tray,action.screw.color);
    var matched=removeMatchedColor(action.screw.color);
    if(!matched && #game.tray>=TRAY_CAPACITY){
        game.jams++;
        game.tray=[];
        game.trayFlash=-1;
        spawnParticles(850,378,28,0xFFFF5F6D,"spark");
        setStatus("工具盒堵住了!已紧急清盒,本关星级会下降",1900);
        if(midiOut) midiOut.play("changeInstrument(47), '5_,'3_,'1__", "C3", 80);
    }
    elseif(!matched){
        setStatus("螺丝已放入工具盒:继续寻找相同颜色",950);
    }
    game.action=null;
    sceneDirty=true;

    if(game.removedScrews>=game.totalScrews){
        game.pendingSolve=true;
        if(allBoardsGone()) finishLevel();
    }
}

var startRemoveScrew=function(screw){
    if(game.state!=STATE_PLAYING || game.action || !isScrewVisible(screw)) return;
    var x,y=screwPosition(screw);
    screw.removing=true;
    game.action={
        screw=screw;time=0;detached=false;
        startX=x;startY=y;targetX=x;targetY=y;targetSlot=#game.tray+1
    };
    game.moves++;
    game.hoverId=null;
    setStatus("正在旋出彩色螺丝……",650);
    if(midiOut) midiOut.play("changeInstrument(12), 1_,#1_", "C6", 30);
}

var findHintScrew=function(){
    var counts=[];
    for(i=1;#screwColors) counts[i]=0;
    for(i,color in game.tray) counts[color]++;

    var preferredColor=null;
    var preferredCount=0;
    for(color=1;#screwColors){
        if(counts[color]>preferredCount){
            preferredCount=counts[color];
            preferredColor=color;
        }
    }
    if(preferredColor){
        for(i,screw in game.solution){
            if(screw.color==preferredColor && isScrewVisible(screw)) return screw;
        }
    }
    for(i,screw in game.solution){
        if(isScrewVisible(screw)) return screw;
    }
    for(i,screw in game.screws){
        if(isScrewVisible(screw)) return screw;
    }
}

initLevel(1);

// -------------------- 输入 --------------------
var logicalMousePos=function(lParam){
    var x,y=win.getMessagePos(lParam);
    var dpiX,dpiY=winform.dpiScale(1,1);
    return x/dpiX,y/dpiY;
}

var hitScrew=function(x,y,onlyVisible){
    for(i=#game.screws;1;-1){
        var screw=game.screws[i];
        if(!screw.removed && !screw.removing && (!onlyVisible || isScrewVisible(screw))){
            var sx,sy=screwPosition(screw);
            if(math.dist(x,y,sx,sy)<=19) return screw;
        }
    }
}

winform.screwCanvas.onMouseMove=function(wParam,lParam){
    if(game.action || game.state!=STATE_PLAYING){
        game.hoverId=null;
        return;
    }
    var x,y=logicalMousePos(lParam);
    var screw=hitScrew(x,y,true);
    game.hoverId=screw ? screw.id;
}

winform.screwCanvas.onMouseLeave=function(){
    game.hoverId=null;
}

winform.screwCanvas.onMouseDown=function(wParam,lParam){
    if(game.state==STATE_SOLVED){
        nextLevel();
        return;
    }
    if(game.action) return;

    var x,y=logicalMousePos(lParam);
    var screw=hitScrew(x,y,true);
    if(screw){
        startRemoveScrew(screw);
        return;
    }

    var hidden=hitScrew(x,y,false);
    if(hidden && !isScrewVisible(hidden)){
        game.blockedBoardKey=hidden.cover;
        game.blockedTimer=1500;
        setStatus("这枚螺丝仍被上层木板压住,先释放发红光的木板",1500);
        if(midiOut) midiOut.play("changeInstrument(116), 1_", "C3", 45);
    }
    else{
        game.misses++;
        setStatus("请点击露出的彩色螺丝头;木板本身不能直接拖动",1100);
    }
}

winform.screwCanvas.onRightMouseUp=function(){
    initLevel(game.level);
}

winform.screwCanvas.dlgCode=4/*_DLGC_WANTALLKEYS*/;
winform.screwCanvas.onKeyDown=function(keyCode,lParam,repeat){
    if(repeat) return;
    if(keyCode==0x52/*_VK_R*/){
        initLevel(game.level);
    }
    elseif(keyCode==0x48/*_VK_H*/ && game.state==STATE_PLAYING && !game.action){
        var screw=findHintScrew();
        if(screw){
            game.hintId=screw.id;
            game.hintTimer=2300;
            setStatus("提示:发出明亮脉冲的螺丝最适合当前工具盒",2300);
            if(midiOut) midiOut.play("changeInstrument(10), 1_,3_", "C6", 42);
        }
        else{
            setStatus("正在等待坠落木板移开,下一枚螺丝马上出现",1200);
        }
    }
    elseif((keyCode==0x20/*_VK_SPACE*/ || keyCode==0x0D/*_VK_RETURN*/) && game.state==STATE_SOLVED){
        nextLevel();
    }
}

// -------------------- 木板与螺丝精灵 --------------------
var offsetPoints=function(points,offsetX,offsetY){
    var result=[];
    for(i,p in points) table.push(result,{x=p.x+offsetX;y=p.y+offsetY});
    return result;
}

var drawBoardVector=function(graphics,board,offsetX,offsetY){
    var style=woodStyles[board.style];
    var points=offsetPoints(board.points,offsetX,offsetY);
    var minX=board.minX+offsetX;
    var minY=board.minY+offsetY;
    var maxX=board.maxX+offsetX;
    var maxY=board.maxY+offsetY;

    var softShadowPoints=[];
    for(i,p in points) table.push(softShadowPoints,{x=p.x+12;y=p.y+16});
    var softShadow=gdip.solidBrush(0x36000000);
    graphics.fillPolygon(softShadow,softShadowPoints);
    softShadow.delete();

    var shadowPoints=[];
    for(i,p in points) table.push(shadowPoints,{x=p.x+7;y=p.y+10});
    var shadow=gdip.solidBrush(0x82000000);
    graphics.fillPolygon(shadow,shadowPoints);
    shadow.delete();

    var edge=gdip.pen(style.edge,7);
    graphics.drawPolygon(edge,points);
    edge.delete();

    var path=gdip.path();
    path.addPolygon(points);
    var saved=graphics.save();
    graphics.setClipPath(path);

    var wood=gdip.lineBrush(::POINTF(minX,minY),::POINTF(maxX,maxY),style.top,style.bottom);
    graphics.fillPath(wood,path);
    wood.delete();

    var warmBand=gdip.solidBrush(alphaColor(shadeColor(style.top,38),28));
    graphics.fillPolygon(warmBand,[
        {x=minX-30;y=minY+5},{x=maxX+30;y=minY+5},
        {x=maxX+30;y=minY+(maxY-minY)*0.34},{x=minX-30;y=minY+(maxY-minY)*0.52}
    ]);
    warmBand.delete();

    for(lineIndex=1;7){
        var ratio=lineIndex/8;
        var y=minY+(maxY-minY)*ratio;
        var wave=7*math.sin(board.id*1.7+lineIndex);
        var grainColor=shadeColor(style.edge,22);
        var grain=gdip.pen(alphaColor(grainColor,76+lineIndex*4),1.2+(lineIndex%2)*0.55);
        graphics.drawBezier(grain,
            minX-35,y,
            minX+(maxX-minX)*0.32,y+wave,
            minX+(maxX-minX)*0.67,y-wave*0.7,
            maxX+35,y+wave*0.25
        );
        grain.delete();

        if(lineIndex%2){
            var grainLight=gdip.pen(alphaColor(shadeColor(style.top,55),58),0.8);
            graphics.drawBezier(grainLight,
                minX-30,y-3,
                minX+(maxX-minX)*0.35,y+wave-4,
                minX+(maxX-minX)*0.70,y-wave*0.55-3,
                maxX+30,y+wave*0.2-3
            );
            grainLight.delete();
        }
    }

    for(knot=1;2){
        var kx=minX+(maxX-minX)*(0.28+knot*0.23);
        var ky=minY+(maxY-minY)*(0.35+(board.id+knot)%3*0.15);
        var knotPen=gdip.pen(alphaColor(style.grain,88),1.2);
        graphics.drawEllipse(knotPen,kx-13,ky-6,26,12);
        graphics.drawEllipse(knotPen,kx-6,ky-3,12,6);
        knotPen.delete();
    }

    graphics.restore(saved);
    path.delete();

    var rim=gdip.pen(alphaColor(shadeColor(style.top,68),175),1.8);
    graphics.drawPolygon(rim,points);
    rim.delete();
    var inner=gdip.pen(0x4DFFFFFF,1);
    graphics.drawLine(inner,points[1],points[2]);
    inner.delete();

    for(i,screwId in board.screwIds){
        var screw=game.screws[screwId];
        var x=screw.x+offsetX;
        var y=screw.y+offsetY;
        var holeShadow=gdip.solidBrush(0x99000000);
        graphics.fillCircle(holeShadow,x+1.8,y+3,19);
        holeShadow.delete();
        var holeBevel=gdip.lineBrush(::POINTF(x-18,y-18),::POINTF(x+18,y+18),
            shadeColor(style.bottom,-34),shadeColor(style.top,30));
        graphics.fillCircle(holeBevel,x,y,17.8);
        holeBevel.delete();
        var hole=gdip.lineBrush(::POINTF(x-11,y-11),::POINTF(x+11,y+11),0xFF0D0B0A,0xFF3C291E);
        graphics.fillCircle(hole,x,y,11.5);
        hole.delete();
        var holeEdge=gdip.pen(0xA05B3A28,1.4);
        graphics.drawCircle(holeEdge,x,y,17.2);
        holeEdge.delete();
    }
}

var getBoardSprite=function(board){
    if(board.sprite && (math.abs(board.sprite.dpiX-CACHE_DPI_X)>0.001 || math.abs(board.sprite.dpiY-CACHE_DPI_Y)>0.001)){
        board.sprite.bitmap.delete();
        board.sprite=null;
    }
    if(board.sprite) return board.sprite;

    var margin=24;
    var x=board.minX-margin;
    var y=board.minY-margin;
    var width=board.maxX-board.minX+margin*2+12;
    var height=board.maxY-board.minY+margin*2+14;
    var bitmap=gdip.bitmap(math.ceil(width*CACHE_DPI_X),math.ceil(height*CACHE_DPI_Y));
    var graphics=gdip.graphics(bitmap);
    graphics.smoothingMode=4/*_SmoothingModeAntiAlias*/;
    graphics.scale(CACHE_DPI_X,CACHE_DPI_Y);
    drawBoardVector(graphics,board,-x,-y);
    graphics.delete();

    board.sprite={bitmap=bitmap;x=x;y=y;width=width;height=height;dpiX=CACHE_DPI_X;dpiY=CACHE_DPI_Y};
    return board.sprite;
}

var getMotionBoardSprite=function(board){
    if(board.motionSprite && (math.abs(board.motionSprite.dpiX-CACHE_DPI_X)>0.001 || math.abs(board.motionSprite.dpiY-CACHE_DPI_Y)>0.001)){
        board.motionSprite.bitmap.delete();
        board.motionSprite=null;
    }
    if(board.motionSprite) return board.motionSprite;

    // 大木板旋转时使用略低于屏幕 DPI 的临时精灵;运动中几乎看不出差异,
    // 但可显著减少高 DPI 下旋转插值的像素量。静止后仍换回 DPI 原尺寸缓存。
    var motionScale=math.min(CACHE_DPI_X,1.35);
    var margin=24;
    var x=board.minX-margin;
    var y=board.minY-margin;
    var width=board.maxX-board.minX+margin*2+12;
    var height=board.maxY-board.minY+margin*2+14;
    var bitmap=gdip.bitmap(math.ceil(width*motionScale),math.ceil(height*motionScale));
    var graphics=gdip.graphics(bitmap);
    graphics.smoothingMode=4/*_SmoothingModeAntiAlias*/;
    graphics.scale(motionScale,motionScale);
    drawBoardVector(graphics,board,-x,-y);
    graphics.delete();

    board.motionSprite={bitmap=bitmap;x=x;y=y;width=width;height=height;dpiX=CACHE_DPI_X;dpiY=CACHE_DPI_Y};
    return board.motionSprite;
}

var drawBoardSprite=function(graphics,board,motion){
    if(board.gone) return;
    var sprite=motion ? getMotionBoardSprite(board) : getBoardSprite(board);
    var saved=graphics.save();
    graphics.translate(board.cx,board.cy);
    graphics.rotate(board.angle);
    graphics.translate(-board.baseCx,-board.baseCy);
    graphics.drawImageRect(sprite.bitmap,sprite.x,sprite.y,sprite.width,sprite.height);
    graphics.restore(saved);
}

var drawScrewVector=function(graphics,colorIndex){
    var color=screwColors[colorIndex];
    var shadow=gdip.solidBrush(0x76000000);
    graphics.fillCircle(shadow,2,3,16.5);
    shadow.delete();

    var washer=gdip.solidBrush(alphaColor(color,118));
    graphics.fillCircle(washer,0,0,17.5);
    washer.delete();
    var outer=gdip.lineBrush(::POINTF(-14,-14),::POINTF(14,14),0xFFF5F8FA,0xFF59636B);
    graphics.fillCircle(outer,0,0,14.8);
    outer.delete();
    var rim=gdip.pen(0xFF242B31,1.5);
    graphics.drawCircle(rim,0,0,14.6);
    rim.delete();

    var enamel=gdip.lineBrush(::POINTF(-10,-10),::POINTF(10,10),shadeColor(color,62),shadeColor(color,-46));
    graphics.fillCircle(enamel,0,0,10.7);
    enamel.delete();
    var enamelEdge=gdip.pen(alphaColor(shadeColor(color,-75),210),1.2);
    graphics.drawCircle(enamelEdge,0,0,10.7);
    enamelEdge.delete();

    var slotShadow=gdip.pen(0xB0000000,3.5);
    graphics.drawLine(slotShadow,-6,0,6,0);
    graphics.drawLine(slotShadow,0,-6,0,6);
    slotShadow.delete();
    var slot=gdip.pen(0xD9F4F7F8,1.35);
    graphics.drawLine(slot,-5.5,-0.8,5.5,-0.8);
    graphics.drawLine(slot,-0.8,-5.5,-0.8,5.5);
    slot.delete();

    var shine=gdip.solidBrush(0xBFFFFFFF);
    graphics.fillCircle(shine,-5,-5,2.1);
    shine.delete();
}

var getScrewSprite=function(colorIndex){
    var sprite=screwSprites[colorIndex];
    if(sprite && (math.abs(sprite.dpiX-CACHE_DPI_X)>0.001 || math.abs(sprite.dpiY-CACHE_DPI_Y)>0.001)){
        sprite.bitmap.delete();
        screwSprites[colorIndex]=null;
        sprite=null;
    }
    if(sprite) return sprite;

    var size=44;
    var bitmap=gdip.bitmap(math.ceil(size*CACHE_DPI_X),math.ceil(size*CACHE_DPI_Y));
    var graphics=gdip.graphics(bitmap);
    graphics.smoothingMode=4/*_SmoothingModeAntiAlias*/;
    graphics.scale(CACHE_DPI_X,CACHE_DPI_Y);
    graphics.translate(size/2,size/2);
    drawScrewVector(graphics,colorIndex);
    graphics.delete();
    sprite={bitmap=bitmap;size=size;dpiX=CACHE_DPI_X;dpiY=CACHE_DPI_Y};
    screwSprites[colorIndex]=sprite;
    return sprite;
}

var drawScrewSprite=function(graphics,colorIndex,x,y,angle,scale){
    var sprite=getScrewSprite(colorIndex);
    var saved=graphics.save();
    graphics.translate(x,y);
    if(angle) graphics.rotate(angle);
    if(scale!=1) graphics.scale(scale,scale);
    graphics.drawImageRect(sprite.bitmap,-sprite.size/2,-sprite.size/2,sprite.size,sprite.size);
    graphics.restore(saved);
}

// -------------------- 静态场景缓存 --------------------
var starCount=function(){
    if(game.jams==0) return 3;
    if(game.jams==1) return 2;
    return 1;
}

var drawBackground=function(graphics){
    var bg=gdip.lineBrush(::POINTF(0,0),::POINTF(CANVAS_W,CANVAS_H),0xFF101C2A,0xFF25171B);
    graphics.fillRect(bg,0,0,CANVAS_W,CANVAS_H);
    bg.delete();

    var amberGlow=gdip.solidBrush(0x1FFF9B48);
    graphics.fillCircle(amberGlow,140,165,250);
    amberGlow.delete();
    var blueGlow=gdip.solidBrush(0x1654DDF2);
    graphics.fillCircle(blueGlow,930,130,245);
    blueGlow.delete();

    for(i,mote in dustMotes){
        var brush=gdip.solidBrush(alphaColor(0xFFFFE2B0,mote.alpha));
        graphics.fillCircle(brush,mote.x,mote.y,mote.size);
        brush.delete();
    }

    drawText(graphics,"螺 丝 木 板 解 谜",29,0,17,710,43,0xFFF8F4EE,true,1);
    drawText(graphics,"SCREW  •  SWING  •  DROP",10,0,57,710,20,0x9DDBCCBC,false,1);
    drawText(graphics,"按颜色收纳螺丝,让失去支点的木板摆动并坠落",13,0,82,710,26,0xC9E6DED5,false,1);
}

var drawWorkbench=function(graphics){
    var shadow=gdip.solidBrush(0x72000000);
    graphics.fillRoundRect(shadow,WORK_X+5,WORK_Y+8,WORK_W,WORK_H,24);
    shadow.delete();

    var frame=gdip.lineBrush(::POINTF(WORK_X,WORK_Y),::POINTF(WORK_X+WORK_W,WORK_Y+WORK_H),0xFF5B6670,0xFF20282F);
    graphics.fillRoundRect(frame,WORK_X,WORK_Y,WORK_W,WORK_H,22);
    frame.delete();
    var frameEdge=gdip.pen(0x8AB7C0C7,1.4);
    graphics.drawRoundRect(frameEdge,WORK_X,WORK_Y,WORK_W,WORK_H,22);
    frameEdge.delete();

    var plate=gdip.lineBrush(::POINTF(WORK_X+10,WORK_Y+10),::POINTF(WORK_X+10,WORK_Y+WORK_H-10),0xFF252B31,0xFF11171C);
    graphics.fillRoundRect(plate,WORK_X+10,WORK_Y+10,WORK_W-20,WORK_H-20,16);
    plate.delete();

    var hole=gdip.solidBrush(0x6D05080A);
    for(row=0;9){
        for(col=0;11){
            graphics.fillCircle(hole,WORK_X+45+col*52,WORK_Y+42+row*52,2.2);
        }
    }
    hole.delete();

    var blueprint=gdip.pen(0x174FDFF0,1.2);
    graphics.drawCircle(blueprint,WORK_X+WORK_W/2,WORK_Y+WORK_H/2,88);
    graphics.drawCircle(blueprint,WORK_X+WORK_W/2,WORK_Y+WORK_H/2,48);
    for(i=1;12){
        var angle=i*math.pi/6;
        graphics.drawLine(blueprint,
            WORK_X+WORK_W/2+math.cos(angle)*52,WORK_Y+WORK_H/2+math.sin(angle)*52,
            WORK_X+WORK_W/2+math.cos(angle)*82,WORK_Y+WORK_H/2+math.sin(angle)*82
        );
    }
    blueprint.delete();

    var corner=gdip.lineBrush(::POINTF(0,0),::POINTF(12,12),0xFFB7C0C5,0xFF434A50);
    for(i,pt in [
        {x=WORK_X+24;y=WORK_Y+24},{x=WORK_X+WORK_W-24;y=WORK_Y+24},
        {x=WORK_X+24;y=WORK_Y+WORK_H-24},{x=WORK_X+WORK_W-24;y=WORK_Y+WORK_H-24}
    ]){
        graphics.fillCircle(corner,pt.x,pt.y,8);
    }
    corner.delete();
}

var drawProgressGauge=function(graphics){
    var levelData=levels[game.level];
    var cx=858,cy=252;
    var back=gdip.pen(0x5A52616B,8);
    graphics.drawArc(back,cx-46,cy-46,92,92,-220,260);
    back.delete();
    var ratio=game.totalScrews ? game.removedScrews/game.totalScrews : 0;
    if(ratio>0){
        var progress=gdip.pen(alphaColor(levelData.accent,230),8);
        graphics.drawArc(progress,cx-46,cy-46,92,92,-220,260*ratio);
        progress.delete();
    }
    var hub=gdip.lineBrush(::POINTF(cx-23,cy-23),::POINTF(cx+23,cy+23),0xFF40515E,0xFF121C24);
    graphics.fillCircle(hub,cx,cy,27);
    hub.delete();
    var hubEdge=gdip.pen(alphaColor(levelData.accent,175),1.8);
    graphics.drawCircle(hubEdge,cx,cy,27);
    hubEdge.delete();
    drawText(graphics,game.removedScrews++" / "++game.totalScrews,11,cx-40,cy-11,80,22,0xFFF2F6F8,true,1);
}

var drawTray=function(graphics){
    drawText(graphics,"六格磁吸工具盒",11,751,300,214,22,0xFFE3EBEF,true,0);

    var trayShadow=gdip.solidBrush(0x5E000000);
    graphics.fillRoundRect(trayShadow,744,319,230,128,17);
    trayShadow.delete();
    var trayPlate=gdip.lineBrush(::POINTF(744,319),::POINTF(974,447),0xFF344650,0xFF111B21);
    graphics.fillRoundRect(trayPlate,742,316,230,128,17);
    trayPlate.delete();
    var trayEdge=gdip.pen(0x8293A5AE,1.4);
    graphics.drawRoundRect(trayEdge,742,316,230,128,17);
    trayEdge.delete();

    for(slot=1;TRAY_CAPACITY){
        var x,y=traySlotPoint(slot);
        var slotShadow=gdip.solidBrush(0x82000000);
        graphics.fillCircle(slotShadow,x+2,y+3,25);
        slotShadow.delete();
        var slotBrush=gdip.lineBrush(::POINTF(x-22,y-22),::POINTF(x+22,y+22),0xFF30434E,0xFF080E12);
        graphics.fillCircle(slotBrush,x,y,23);
        slotBrush.delete();
        var slotEdge=gdip.pen(0x9AA8B7BE,1.6);
        graphics.drawCircle(slotEdge,x,y,23);
        slotEdge.delete();
        if(game.tray[slot]){
            drawScrewSprite(graphics,game.tray[slot],x,y,0,0.82);
        }
        else{
            var magnet=gdip.pen(0x536E858F,1.2);
            graphics.drawLine(magnet,x-5,y,x+5,y);
            graphics.drawLine(magnet,x,y-5,x,y+5);
            magnet.delete();
        }
    }
}

var drawSidePanel=function(graphics){
    var levelData=levels[game.level];
    var shadow=gdip.solidBrush(0x65000000);
    graphics.fillRoundRect(shadow,PANEL_X+5,PANEL_Y+7,PANEL_W,PANEL_H,22);
    shadow.delete();
    var panel=gdip.lineBrush(::POINTF(PANEL_X,PANEL_Y),::POINTF(PANEL_X+PANEL_W,PANEL_Y+PANEL_H),0xEF253542,0xF20C151C);
    graphics.fillRoundRect(panel,PANEL_X,PANEL_Y,PANEL_W,PANEL_H,22);
    panel.delete();
    var border=gdip.pen(0x4B8195A1,1.2);
    graphics.drawRoundRect(border,PANEL_X,PANEL_Y,PANEL_W,PANEL_H,22);
    border.delete();

    drawText(graphics,"第 "++game.level++" / "++#levels++" 关",11,744,119,228,20,alphaColor(levelData.accent,220),true,1);
    drawText(graphics,levelData.name,22,742,140,232,35,0xFFF5F8FA,true,1);
    drawText(graphics,levelData.subtitle,10,745,174,226,24,0xD0D7E1E6,false,1);
    drawProgressGauge(graphics);
    drawTray(graphics);

    drawText(graphics,"操作 "++game.moves,10,750,454,68,24,0xFFDCE6EC,true,0);
    drawText(graphics,"收纳 "++game.matches,10,825,454,68,24,0xFF9DEBBE,true,0);
    drawText(graphics,"堵盒 "++game.jams,10,900,454,68,24,game.jams ? 0xFFFF9DA8 : 0xFFDCE6EC,true,0);

    var statusBack=gdip.solidBrush(game.state==STATE_SOLVED ? alphaColor(levelData.accent,108) : 0x68101B23);
    graphics.fillRoundRect(statusBack,748,489,220,67,13);
    statusBack.delete();
    var statusEdge=gdip.pen(game.state==STATE_SOLVED ? alphaColor(levelData.accent,238) : 0x354D7484,1.2);
    graphics.drawRoundRect(statusEdge,748,489,220,67,13);
    statusEdge.delete();
    drawText(graphics,game.statusText,10,759,496,198,52,game.state==STATE_SOLVED ? 0xFFFFFFFF : 0xFFD9E4E9,game.state==STATE_SOLVED,1);

    if(game.state==STATE_SOLVED){
        var stars="";
        for(i=1;3) stars++=(i<=starCount() ? '\u2605' : '\u2606');
        drawText(graphics,stars,24,750,568,216,34,0xFFFFD46A,true,1);
        drawText(graphics,"零堵盒可获得三星评价",10,750,605,216,20,0xD6E1E8EC,false,1);
    }
    else{
        drawText(graphics,"H  提示",10,750,576,76,24,0xFFD0DDE4,true,0);
        drawText(graphics,"R / 右键  重置",10,836,576,132,24,0xFFD0DDE4,true,0);
        drawText(graphics,"三枚同色自动收纳",10,750,608,216,20,0xFFDCE6EC,false,1);
        drawText(graphics,"六格无三连会紧急清盒",10,750,632,216,20,0xFFDCE6EC,false,1);
    }
}

var buildSceneCache=function(){
    var width=math.ceil(CANVAS_W*CACHE_DPI_X);
    var height=math.ceil(CANVAS_H*CACHE_DPI_Y);
    if(sceneCache) sceneCache.delete();
    sceneCache=gdip.bitmap(width,height);
    var graphics=gdip.graphics(sceneCache);
    graphics.smoothingMode=4/*_SmoothingModeAntiAlias*/;
    graphics.textRenderingHint=4/*_TextRenderingHintAntiAlias*/;
    graphics.scale(CACHE_DPI_X,CACHE_DPI_Y);
    drawBackground(graphics);
    drawWorkbench(graphics);

    // 把最底部连续静止的木板直接并入完整场景缓存;
    // 动态木板及其上层再按 z 序分段,可少贴一张大尺寸透明图。
    sceneCachedBoardEnd=0;
    for(index=1;#game.boards){
        var board=game.boards[index];
        if(board.mode=="hanging" || board.mode=="falling") break;
        sceneCachedBoardEnd=index;
    }
    sceneContainsBoards=sceneCachedBoardEnd>=#game.boards;
    if(sceneCachedBoardEnd>0 && drawBoardRangeToGraphics) drawBoardRangeToGraphics(graphics,1,sceneCachedBoardEnd);
    drawSidePanel(graphics);
    graphics.delete();
    sceneDirty=false;
}

// -------------------- 动态绘图 --------------------
var drawBoardHighlight=function(graphics,board){
    if(!board || board.gone) return;
    var saved=graphics.save();
    graphics.translate(board.cx,board.cy);
    graphics.rotate(board.angle);
    graphics.translate(-board.baseCx,-board.baseCy);
    var pulse=3+2*math.sin(game.pulse/90);
    var glow=gdip.pen(alphaColor(0xFFFF5F6D,125+55*math.sin(game.pulse/120)),3.4+pulse*0.25);
    graphics.drawPolygon(glow,board.points);
    glow.delete();
    graphics.restore(saved);
}

var drawBoardScrews=function(graphics,board){
    for(i,screwId in board.screwIds){
        var screw=game.screws[screwId];
        if(isScrewVisible(screw)){
            var x,y=screwPosition(screw);
            drawScrewSprite(graphics,screw.color,x,y,0,1);
        }
    }
}

drawBoardRangeToGraphics=function(graphics,startIndex,endIndex){
    for(index=startIndex;endIndex){
        var board=game.boards[index];
        drawBoardSprite(graphics,board);
        drawBoardScrews(graphics,board);
    }
}

var drawScrewHighlights=function(graphics){
    for(i,screw in game.screws){
        if(isScrewVisible(screw)){
            var isHover=game.hoverId==screw.id;
            var isHint=game.hintId==screw.id && game.hintTimer>0;
            if(isHover || isHint){
                var x,y=screwPosition(screw);
                var radius=isHint ? 23+4*math.sin(game.pulse/105) : 21+2*math.sin(game.pulse/130);
                var alpha=isHint ? 105+65*math.sin(game.pulse/125) : 72;
                var halo=gdip.solidBrush(alphaColor(screwColors[screw.color],alpha));
                graphics.fillCircle(halo,x,y,radius);
                halo.delete();
                var ring=gdip.pen(alphaColor(0xFFFFFFFF,isHint ? 175 : 105),isHint ? 2.3 : 1.4);
                graphics.drawCircle(ring,x,y,radius+2);
                ring.delete();
            }
        }
    }
}

var buildLayerCaches=function(){
    disposeLayerCaches();

    var addSegment=function(startIndex,endIndex){
        if(!startIndex) return;
        var bitmap=gdip.bitmap(math.ceil(BOARD_CACHE_W*CACHE_DPI_X),math.ceil(BOARD_CACHE_H*CACHE_DPI_Y));
        var graphics=gdip.graphics(bitmap);
        graphics.smoothingMode=4/*_SmoothingModeAntiAlias*/;
        graphics.interpolationMode=3/*_InterpolationModeBilinear*/;
        graphics.scale(CACHE_DPI_X,CACHE_DPI_Y);
        for(index=startIndex;endIndex){
            var board=game.boards[index];
            if(!board.gone){
                drawBoardSprite(graphics,board);
                drawBoardScrews(graphics,board);
            }
        }
        graphics.delete();
        var cache={bitmap=bitmap;startIndex=startIndex;endIndex=endIndex};
        table.push(layerCaches,cache);
        table.push(renderItems,{kind="cache";cache=cache});
    }

    var segmentStart=null;
    var segmentEnd=null;
    for(index=sceneCachedBoardEnd+1;#game.boards){
        var board=game.boards[index];
        var dynamic=board.mode=="hanging" || board.mode=="falling";
        if(dynamic){
            addSegment(segmentStart,segmentEnd);
            segmentStart=null;
            segmentEnd=null;
            table.push(renderItems,{kind="board";board=board});
        }
        elseif(!board.gone){
            if(!segmentStart) segmentStart=index;
            segmentEnd=index;
        }
    }
    addSegment(segmentStart,segmentEnd);
    layersDirty=false;
}


var drawActionScrew=function(graphics){
    var action=game.action;
    if(!action) return;
    var x=action.startX;
    var y=action.startY;
    var angle=0;
    var scale=1;
    if(action.time<UNSCREW_TIME){
        var p=math.clamp(action.time/UNSCREW_TIME,0,1);
        var eased=1-(1-p)*(1-p);
        y-=eased*12;
        angle=eased*760;
        scale=1+0.12*math.sin(p*math.pi);
        var glow=gdip.solidBrush(alphaColor(screwColors[action.screw.color],48+55*p));
        graphics.fillCircle(glow,x,y,19+5*p);
        glow.delete();
    }
    else{
        var p=math.clamp((action.time-UNSCREW_TIME)/FLY_TIME,0,1);
        var eased=p*p*(3-2*p);
        var controlX=(action.startX+action.targetX)/2;
        var controlY=math.min(action.startY,action.targetY)-110;
        var inv=1-eased;
        x=inv*inv*action.startX+2*inv*eased*controlX+eased*eased*action.targetX;
        y=inv*inv*(action.startY-12)+2*inv*eased*controlY+eased*eased*action.targetY;
        angle=760+eased*540;
        scale=1-0.18*eased;

        for(tail=1;3){
            var old=math.max(0,eased-tail*0.08);
            var oldInv=1-old;
            var tx=oldInv*oldInv*action.startX+2*oldInv*old*controlX+old*old*action.targetX;
            var ty=oldInv*oldInv*(action.startY-12)+2*oldInv*old*controlY+old*old*action.targetY;
            var tailBrush=gdip.solidBrush(alphaColor(screwColors[action.screw.color],10+tail*9));
            graphics.fillCircle(tailBrush,tx,ty,8+tail*2);
            tailBrush.delete();
        }
    }
    drawScrewSprite(graphics,action.screw.color,x,y,angle,scale);
}

var drawParticles=function(graphics){
    for(i,p in game.particles){
        var ratio=math.clamp(p.life/p.maxLife,0,1);
        var size=p.size*(0.45+ratio*0.8);
        var brush=gdip.solidBrush(alphaColor(p.color,ratio*(p.kind=="dust" ? 155 : 235)));
        if(p.kind=="dust"){
            graphics.fillCircle(brush,p.x,p.y,size);
        }
        else{
            var cs=math.cos(p.spin);
            var sn=math.sin(p.spin);
            graphics.fillPolygon(brush,[
                {x=p.x+cs*size*1.7;y=p.y+sn*size*1.7},
                {x=p.x-sn*size*0.55;y=p.y+cs*size*0.55},
                {x=p.x-cs*size*1.7;y=p.y-sn*size*1.7},
                {x=p.x+sn*size*0.55;y=p.y-cs*size*0.55}
            ]);
        }
        brush.delete();
    }
}

var drawDynamicEffects=function(graphics){
    if(game.trayFlash!=0){
        var positive=game.trayFlash>0;
        var strength=math.abs(game.trayFlash);
        var color=positive ? levels[game.level].accent : 0xFFFF5F6D;
        var ring=gdip.pen(alphaColor(color,175*strength),2+5*(1-strength));
        graphics.drawRoundRect(ring,742-8*(1-strength),292-8*(1-strength),232+16*(1-strength),170+16*(1-strength),20);
        ring.delete();
    }

    if(game.state==STATE_SOLVED){
        var glow=math.min(1,game.solveGlow);
        var color=levels[game.level].accent;
        var centerX=WORK_X+WORK_W/2;
        var centerY=WORK_Y+WORK_H/2;
        var ring=gdip.pen(alphaColor(color,(65+45*math.sin(game.pulse/145))*glow),3+glow*3);
        graphics.drawCircle(ring,centerX,centerY,102+5*math.sin(game.pulse/170));
        ring.delete();
        var rays=gdip.pen(alphaColor(color,40*glow),2);
        for(i=1;16){
            var angle=i*math.pi/8+game.pulse/4500;
            graphics.drawLine(rays,
                centerX+math.cos(angle)*112,centerY+math.sin(angle)*112,
                centerX+math.cos(angle)*(138+8*math.sin(game.pulse/150+i)),
                centerY+math.sin(angle)*(138+8*math.sin(game.pulse/150+i))
            );
        }
        rays.delete();

        var bannerGlow=gdip.solidBrush(alphaColor(color,34+25*math.sin(game.pulse/160)));
        graphics.fillRoundRect(bannerGlow,centerX-174,centerY-51,348,102,20);
        bannerGlow.delete();
        var banner=gdip.solidBrush(0xD9141D23);
        graphics.fillRoundRect(banner,centerX-158,centerY-40,316,80,16);
        banner.delete();
        var bannerEdge=gdip.pen(alphaColor(color,210),1.8);
        graphics.drawRoundRect(bannerEdge,centerX-158,centerY-40,316,80,16);
        bannerEdge.delete();
        drawText(graphics,"机 关 清 空",24,centerX-145,centerY-31,290,39,0xFFF8F5EE,true,1);
        drawText(graphics,"LEVEL  CLEAR",9,centerX-145,centerY+10,290,18,alphaColor(color,220),true,1);
    }
}

winform.screwCanvas.onDrawBackground=function(graphics,rc,backgroundColor,color){
    return true;
}

winform.screwCanvas.onDrawForeground=function(graphics,rc,foregroundRect,foregroundColor,color,font){
    var dpiX,dpiY=winform.dpiScale(1,1);
    if(math.abs(CACHE_DPI_X-dpiX)>0.001 || math.abs(CACHE_DPI_Y-dpiY)>0.001){
        disposeSprites();
        CACHE_DPI_X=dpiX;
        CACHE_DPI_Y=dpiY;
        sceneDirty=true;
    }
    if(sceneDirty || !sceneCache) buildSceneCache();
    if(!sceneContainsBoards && (layersDirty || !#renderItems)) buildLayerCaches();

    graphics.interpolationMode=3/*_InterpolationModeBilinear*/;
    graphics.drawImageRect(sceneCache,0,0,rc.width,rc.height);

    var saved=graphics.save();
    graphics.scale(dpiX,dpiY);
    graphics.smoothingMode=4/*_SmoothingModeAntiAlias*/;

    if(!sceneContainsBoards){
        for(i,item in renderItems){
            if(item.kind=="cache"){
                graphics.drawImageRect(item.cache.bitmap,0,0,BOARD_CACHE_W,BOARD_CACHE_H);
            }
            else{
                drawBoardSprite(graphics,item.board,true);
                drawBoardScrews(graphics,item.board);
            }
        }
    }
    drawScrewHighlights(graphics);
    if(game.blockedBoardKey && game.blockedTimer>0) drawBoardHighlight(graphics,game.boardByKey[game.blockedBoardKey]);
    drawActionScrew(graphics);
    drawParticles(graphics);
    drawDynamicEffects(graphics);

    graphics.restore(saved);
}

// -------------------- 动画更新 --------------------
winform.screwCanvas.onAnimation=function(prevState,beginning,change,timestamp,duration,deltaMs){
    deltaMs=math.min(deltaMs,60);
    var dt=deltaMs/1000;
    game.pulse+=deltaMs;

    if(game.statusTime<90000){
        game.statusTime=math.max(0,game.statusTime-deltaMs);
        if(game.statusTime==0 && game.statusText!=defaultStatus()){
            game.statusText=defaultStatus();
            sceneDirty=true;
        }
    }
    game.hintTimer=math.max(0,game.hintTimer-deltaMs);
    game.blockedTimer=math.max(0,game.blockedTimer-deltaMs);
    if(game.blockedTimer==0) game.blockedBoardKey=null;
    if(game.trayFlash>0) game.trayFlash=math.max(0,game.trayFlash-dt*2.6);
    elseif(game.trayFlash<0) game.trayFlash=math.min(0,game.trayFlash+dt*2.2);
    if(game.state==STATE_SOLVED) game.solveGlow=math.min(1,game.solveGlow+dt*1.8);

    if(game.action){
        var action=game.action;
        action.time+=dt;
        if(!action.detached && action.time>=UNSCREW_TIME) detachScrew(action);
        if(game.action && action.time>=UNSCREW_TIME+FLY_TIME) finishScrewAction(action);
    }

    for(i,board in game.boards){
        if(board.mode=="hanging"){
            var diff=normalizeAngle(board.targetAngle-board.angle);
            board.angularVel+=diff*20*dt;
            board.angularVel*=0.88**(dt*60);
            board.angle+=board.angularVel*dt;
            var rad=math.rad(board.angle);
            var dx=board.baseCx-board.pivotLocalX;
            var dy=board.baseCy-board.pivotLocalY;
            board.cx=board.pivotX+dx*math.cos(rad)-dy*math.sin(rad);
            board.cy=board.pivotY+dx*math.sin(rad)+dy*math.cos(rad);

            if(math.abs(diff)<0.28 && math.abs(board.angularVel)<0.55) board.settleTime+=dt;
            else board.settleTime=0;
            if(board.settleTime>0.22){
                board.mode="hangingSettled";
                board.angularVel=0;
                layersDirty=true;
                sceneDirty=true;
            }
        }
        elseif(board.mode=="falling"){
            board.fallTime+=dt;
            board.vy+=420*dt;
            board.cx+=board.vx*dt;
            board.cy+=board.vy*dt;
            board.angle+=board.omega*dt;
            if(board.fallTime>0.17 && board.covering){
                board.covering=false;
                layersDirty=true;
            }
            if(board.cy>CANVAS_H+250 || board.cx<-320 || board.cx>CANVAS_W+320){
                board.gone=true;
                board.covering=false;
                board.mode="gone";
                layersDirty=true;
                sceneDirty=true;
            }
        }
    }

    for(i=#game.particles;1;-1){
        var p=game.particles[i];
        p.life-=dt;
        p.vy+=p.gravity*dt;
        p.x+=p.vx*dt;
        p.y+=p.vy*dt;
        p.vx*=0.985**(dt*60);
        p.spin+=dt*5;
        if(p.life<=0) table.remove(game.particles,i);
    }

    if(game.pendingSolve && allBoardsGone() && !game.action) finishLevel();
    return true;
}

winform.screwCanvas.cursor="hand";
winform.screwCanvas.startAnimation(24);
winform.screwCanvas.setFocus();

winform.show();
win.loopMessage();

Markdown 格式