aardio 文档

aardio 范例: 自绘小游戏 - 魔法阵修复(绳结解谜变体)

import win.ui;
/*DSG{{*/
var winform = win.form(text="自绘小游戏 - 魔法阵修复(绳结解谜变体)";right=760;bottom=650;bgcolor=0x110A24;border="dialog frame";max=false)
winform.add(
magicCanvas={cls="plus";left=0;top=0;right=760;bottom=650;db=1;dl=1;dr=1;dt=1;notify=1;z=1}
)
/*}}*/

import gdip;

/*
- 玩法:拖动符文节点,消除边与边交叉、节点重叠、连线穿过无关节点等“裂隙”;5 个递增关卡,R 或右键重置,通关后单击/空格进入下一关。
- 关卡使用平面轮图(外环 + 中心辐射边),只打乱外环节点位置;标准环形布局保证每关都有 0 冲突解。已用自动测试验证 5 关初始冲突分别为 10/13/14/21/30,标准布局均为 0。
- 几何判定:严格线段相交(共享端点不算)、节点最小距离、点到非关联线段距离;避免把节点堆叠或让线穿过节点形成“伪通关”。
- 视觉经验:初始坏边很多时,发光线不宜过粗,否则粉色光晕会糊成一片;本例将坏边主线/光晕控制在约 2.5/8 逻辑像素,并用小亮点标交叉。完成提示应放在顶部信息区,不要用中央面板遮挡修复后的阵眼和完整法阵。
- 性能策略:每条边仅一个循环流光,反馈粒子数量少且寿命短;约 42 FPS(24 ms 间隔),不做大量常驻粒子。
*/
var NODE_RADIUS = 23;
var BOARD_LEFT = 54;
var BOARD_TOP = 116;
var BOARD_RIGHT = 706;
var BOARD_BOTTOM = 618;
var BOARD_CX = 380;
var BOARD_CY = 370;
var BOARD_RX = 250;
var BOARD_RY = 205;

var STATE_PLAYING = 1;
var STATE_SOLVED = 2;

var runeColors = [
    0xFF58F5FF,0xFF8B7CFF,0xFFFF66C4,0xFFFF9E57,0xFFFFE16B,
    0xFF65F0A8,0xFF54A7FF,0xFFCF7CFF,0xFFFF6D7A,0xFF79F6D2
];

var levels = [
    {name="星芒初醒";order=[1,3,5,2,4];accent=0xFF59F3FF},
    {name="月影回环";order=[1,4,2,6,3,5];accent=0xFF8E83FF},
    {name="焰心之轮";order=[1,4,7,3,6,2,5];accent=0xFFFF6ABF},
    {name="时隙罗盘";order=[1,5,2,7,4,8,3,6];accent=0xFFFFB65C},
    {name="群星秘仪";order=[1,6,3,8,2,7,4,9,5];accent=0xFFFFE576}
];

var game = {
    state=STATE_PLAYING;
    level=1;
    cycle=1;
    nodes=[];
    edges=[];
    edgeBad=[];
    nodeBad=[];
    crossings=[];
    conflictCount=0;
    moves=0;
    selected=null;
    dragOffsetX=0;
    dragOffsetY=0;
    dragStartX=0;
    dragStartY=0;
    dragDirty=false;
    conflictBeforeDrag=0;
    pulse=0;
    solveGlow=0;
    particles=[];
};

var stars = [];
for(i=1;72){
    table.push(stars,{
        x=math.random()*760;
        y=math.random()*650;
        size=0.6+math.random()*1.8;
        phase=math.random()*math.pi*2;
    });
}

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

// -------------------- 几何与关卡 --------------------
var crossProduct = function(ax,ay,bx,by,cx,cy){
    return (bx-ax)*(cy-ay) - (by-ay)*(cx-ax);
}

// 只统计真正穿过的线段;共享端点的边会在调用前排除。
var segmentsIntersect = function(a,b,c,d){
    var d1 = crossProduct(a.x,a.y,b.x,b.y,c.x,c.y);
    var d2 = crossProduct(a.x,a.y,b.x,b.y,d.x,d.y);
    var d3 = crossProduct(c.x,c.y,d.x,d.y,a.x,a.y);
    var d4 = crossProduct(c.x,c.y,d.x,d.y,b.x,b.y);
    return d1*d2 < -0.0001 && d3*d4 < -0.0001;
}

var intersectionPoint = function(a,b,c,d){
    var denominator = (a.x-b.x)*(c.y-d.y) - (a.y-b.y)*(c.x-d.x);
    if(math.abs(denominator) < 0.0001) return null;
    var n1 = a.x*b.y-a.y*b.x;
    var n2 = c.x*d.y-c.y*d.x;
    return {
        x=(n1*(c.x-d.x)-(a.x-b.x)*n2)/denominator;
        y=(n1*(c.y-d.y)-(a.y-b.y)*n2)/denominator;
    };
}

var pointSegmentDistance = function(p,a,b){
    var dx = b.x-a.x;
    var dy = b.y-a.y;
    var length2 = dx*dx+dy*dy;
    if(length2 <= 0.0001) return math.dist(p.x,p.y,a.x,a.y);
    var t = ((p.x-a.x)*dx+(p.y-a.y)*dy)/length2;
    t = math.clamp(t,0,1);
    return math.dist(p.x,p.y,a.x+dx*t,a.y+dy*t);
}

var makeEdges = function(rimCount){
    var edges = [];
    var centerIndex = rimCount+1;
    for(i=1;rimCount){
        table.push(edges,[i,i%rimCount+1]);
        table.push(edges,[i,centerIndex]);
    }
    return edges;
}

var evaluateBoard = function(){
    game.edgeBad = table.array(#game.edges,false);
    game.nodeBad = table.array(#game.nodes,false);
    game.crossings = [];
    var conflicts = 0;

    // 边与边的交叉。
    for(i=1;#game.edges-1){
        var edge1 = game.edges[i];
        for(j=i+1;#game.edges){
            var edge2 = game.edges[j];
            var shareNode = edge1[1] == edge2[1]
                || edge1[1] == edge2[2]
                || edge1[2] == edge2[1]
                || edge1[2] == edge2[2];
            if(!shareNode){
                var a = game.nodes[edge1[1]];
                var b = game.nodes[edge1[2]];
                var c = game.nodes[edge2[1]];
                var d = game.nodes[edge2[2]];
                if(segmentsIntersect(a,b,c,d)){
                    conflicts++;
                    game.edgeBad[i] = true;
                    game.edgeBad[j] = true;
                    var pt = intersectionPoint(a,b,c,d);
                    if(pt) table.push(game.crossings,pt);
                }
            }
        }
    }

    // 节点不能重叠,能量线也不能从无关符文下面穿过。
    for(i=1;#game.nodes-1){
        for(j=i+1;#game.nodes){
            var a = game.nodes[i];
            var b = game.nodes[j];
            if(math.dist(a.x,a.y,b.x,b.y) < NODE_RADIUS*2.25){
                conflicts++;
                game.nodeBad[i] = true;
                game.nodeBad[j] = true;
            }
        }
    }

    for(nodeIndex=1;#game.nodes){
        var node = game.nodes[nodeIndex];
        for(edgeIndex=1;#game.edges){
            var edge = game.edges[edgeIndex];
            if(nodeIndex != edge[1] && nodeIndex != edge[2]){
                if(pointSegmentDistance(node,game.nodes[edge[1]],game.nodes[edge[2]]) < NODE_RADIUS+5){
                    conflicts++;
                    game.nodeBad[nodeIndex] = true;
                    game.edgeBad[edgeIndex] = true;
                }
            }
        }
    }

    game.conflictCount = conflicts;
    return conflicts;
}

var spawnParticles = function(x,y,count,color,burst){
    for(i=1;count){
        var angle = math.random()*math.pi*2;
        var speed = 25+math.random()*(burst ? 150 : 70);
        var life = 0.45+math.random()*0.8;
        table.push(game.particles,{
            x=x;y=y;
            vx=math.cos(angle)*speed;
            vy=math.sin(angle)*speed;
            life=life;maxLife=life;
            size=1.5+math.random()*3.5;
            color=color;
        });
    }
}

var finishLevel = function(){
    if(game.state == STATE_SOLVED) return;
    game.state = STATE_SOLVED;
    game.selected = null;
    game.solveGlow = 0;

    var accent = levels[game.level].accent;
    spawnParticles(BOARD_CX,BOARD_CY,42,accent,true);
    for(i,node in game.nodes){
        spawnParticles(node.x,node.y,5,node.color,false);
    }
    if(midiOut) midiOut.play("changeInstrument(10), 1_,3_,5_,1'__,5_,1'__", "C4", 95, 70);
}

var initLevel = function(levelIndex){
    game.level = levelIndex;
    game.state = STATE_PLAYING;
    game.moves = 0;
    game.selected = null;
    game.dragDirty = false;
    game.solveGlow = 0;
    game.particles = [];

    var levelData = levels[levelIndex];
    var rimCount = #levelData.order;
    var nodes = table.array(rimCount+1,0);

    // 把编号符文放到被打乱的圆周槽位;中心符文保持在法阵核心。
    for(slot=1;rimCount){
        var angle = -math.pi/2+(slot-1)*math.pi*2/rimCount;
        var nodeIndex = levelData.order[slot];
        nodes[nodeIndex] = {
            x=BOARD_CX+math.cos(angle)*BOARD_RX;
            y=BOARD_CY+math.sin(angle)*BOARD_RY;
            color=runeColors[(nodeIndex-1)%#runeColors+1];
            center=false;
        };
    }
    nodes[rimCount+1] = {
        x=BOARD_CX;y=BOARD_CY;
        color=0xFFFFE477;
        center=true;
    };

    game.nodes = nodes;
    game.edges = makeEdges(rimCount);
    evaluateBoard();
}

var nextLevel = function(){
    var nextIndex = game.level+1;
    if(nextIndex > #levels){
        nextIndex = 1;
        game.cycle++;
    }
    initLevel(nextIndex);
}

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 hitNode = function(x,y){
    for(i=#game.nodes;1;-1){
        var node = game.nodes[i];
        if(math.dist(x,y,node.x,node.y) <= NODE_RADIUS+9) return i;
    }
}

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

    var x,y = logicalMousePos(lParam);
    var nodeIndex = hitNode(x,y);
    if(nodeIndex){
        var node = game.nodes[nodeIndex];
        game.selected = nodeIndex;
        game.dragOffsetX = x-node.x;
        game.dragOffsetY = y-node.y;
        game.dragStartX = node.x;
        game.dragStartY = node.y;
        game.dragDirty = false;
        game.conflictBeforeDrag = game.conflictCount;
        if(midiOut) midiOut.play("changeInstrument(98), 1_", "C6", 35);
    }
}

// onMouseDrag 会自动捕获鼠标,拖出控件边缘也不会丢失拖动状态。
winform.magicCanvas.onMouseDrag = function(wParam,lParam){
    if(!game.selected || game.state != STATE_PLAYING) return;
    var x,y = logicalMousePos(lParam);
    var node = game.nodes[game.selected];
    node.x = math.clamp(x-game.dragOffsetX,BOARD_LEFT,BOARD_RIGHT);
    node.y = math.clamp(y-game.dragOffsetY,BOARD_TOP+25,BOARD_BOTTOM);
    if(math.dist(node.x,node.y,game.dragStartX,game.dragStartY) > 3) game.dragDirty = true;
    evaluateBoard();
}

winform.magicCanvas.onMouseUp = function(wParam,lParam){
    if(!game.selected) return;
    var node = game.nodes[game.selected];
    var moved = game.dragDirty;
    var oldConflicts = game.conflictBeforeDrag;
    game.selected = null;
    game.dragDirty = false;

    if(moved){
        game.moves++;
        evaluateBoard();
        if(game.conflictCount == 0){
            finishLevel();
        }
        elseif(game.conflictCount < oldConflicts){
            spawnParticles(node.x,node.y,10,node.color,false);
            if(midiOut) midiOut.play("changeInstrument(10), 3_,5_", "C5", 55);
        }
    }
}

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

winform.magicCanvas.dlgCode = 4/*_DLGC_WANTALLKEYS*/;
winform.magicCanvas.onKeyDown = function(keyCode,lParam,repeat){
    if(repeat) return;
    if(keyCode == 0x52/*_VK_R*/){
        initLevel(game.level);
    }
    elseif((keyCode == 0x20/*_VK_SPACE*/ || keyCode == 0x0D/*_VK_RETURN*/) && game.state == STATE_SOLVED){
        nextLevel();
    }
}

// -------------------- 绘图 --------------------
var drawText = function(graphics,text,size,x,y,width,height,color,bold,align){
    var style = 0;
    if(bold) style = 1/*_FontStyleBold*/;
    var textFont = gdip.font("Microsoft YaHei UI",size,style,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 drawMagicBackdrop = function(graphics){
    var ringPulse = 0.5+math.sin(game.pulse/850)*0.2;
    var ringAlpha = math.floor(24+ringPulse*25);
    var ringColor = (ringAlpha<<24) | 0x7C71FF;
    var penThin = gdip.pen(ringColor,1);
    var penWide = gdip.pen(0x185DF5FF,5);

    graphics.drawEllipse(penWide,BOARD_CX-BOARD_RX-18,BOARD_CY-BOARD_RY-18,(BOARD_RX+18)*2,(BOARD_RY+18)*2);
    graphics.drawEllipse(penThin,BOARD_CX-BOARD_RX-18,BOARD_CY-BOARD_RY-18,(BOARD_RX+18)*2,(BOARD_RY+18)*2);
    graphics.drawEllipse(penThin,BOARD_CX-BOARD_RX+18,BOARD_CY-BOARD_RY+18,(BOARD_RX-18)*2,(BOARD_RY-18)*2);
    graphics.drawEllipse(penThin,BOARD_CX-86,BOARD_CY-70,172,140);

    var diamond = [
        [BOARD_CX,BOARD_CY-BOARD_RY-8],
        [BOARD_CX+BOARD_RX+8,BOARD_CY],
        [BOARD_CX,BOARD_CY+BOARD_RY+8],
        [BOARD_CX-BOARD_RX-8,BOARD_CY]
    ];
    graphics.drawPolygon(penThin,diamond);

    // 环上的刻度像一个缓慢转动的星盘。
    var rotation = game.pulse/6000;
    for(i=1;24){
        var angle = rotation+(i-1)*math.pi*2/24;
        var ca = math.cos(angle);
        var sa = math.sin(angle);
        var x1 = BOARD_CX+ca*(BOARD_RX+23);
        var y1 = BOARD_CY+sa*(BOARD_RY+23);
        var tick = (i%3 == 1) ? 12 : 6;
        var x2 = BOARD_CX+ca*(BOARD_RX+23+tick);
        var y2 = BOARD_CY+sa*(BOARD_RY+23+tick);
        graphics.drawLine(penThin,x1,y1,x2,y2);
    }

    penWide.delete();
    penThin.delete();
}

var drawEnergyEdge = function(graphics,edgeIndex){
    var edge = game.edges[edgeIndex];
    var a = game.nodes[edge[1]];
    var b = game.nodes[edge[2]];
    var bad = game.edgeBad[edgeIndex];
    var baseColor;
    if(game.state == STATE_SOLVED) baseColor = 0xFFFFDA70;
    elseif(bad) baseColor = 0xFFFF4B8B;
    else baseColor = 0xFF55EFFF;

    var glowColor = 0x2855EFFF;
    if(game.state == STATE_SOLVED) glowColor = 0x38FFD86B;
    elseif(bad) glowColor = 0x42FF286F;

    // 初始局面通常有多条坏边,控制线宽避免粉色光晕糊成一片。
    var glowPen = gdip.pen(glowColor,bad ? 8 : 7);
    var corePen = gdip.pen(baseColor,bad ? 2.5 : 2.2);
    graphics.drawLine(glowPen,a,b);
    graphics.drawLine(corePen,a,b);
    glowPen.delete();
    corePen.delete();

    // 一粒流光沿能量线运行,不使用大量粒子也能保持魔法感。
    var t = (game.pulse/1450+edgeIndex/#game.edges)%1;
    var mx = a.x+(b.x-a.x)*t;
    var my = a.y+(b.y-a.y)*t;
    var moteGlow = gdip.solidBrush(glowColor);
    var mote = gdip.solidBrush(baseColor);
    graphics.fillCircle(moteGlow,mx,my,bad ? 7 : 5);
    graphics.fillCircle(mote,mx,my,bad ? 2.5 : 2);
    mote.delete();
    moteGlow.delete();
}

var drawRuneNode = function(graphics,nodeIndex){
    var node = game.nodes[nodeIndex];
    var selected = game.selected == nodeIndex;
    var bad = game.nodeBad[nodeIndex];
    var pulse = 1+math.sin(game.pulse/260+nodeIndex)*0.06;
    var radius = NODE_RADIUS*pulse;
    if(node.center) radius += 5;
    if(selected) radius += 4;

    var glowColor = 0x4058EFFF;
    var rimColor = node.color;
    if(game.state == STATE_SOLVED){
        glowColor = 0x55FFDA70;
        rimColor = 0xFFFFE58A;
    }
    elseif(bad){
        glowColor = 0x55FF296D;
        rimColor = 0xFFFF5A91;
    }

    var glowBrush = gdip.solidBrush(glowColor);
    graphics.fillCircle(glowBrush,node.x,node.y,radius+10);
    glowBrush.delete();

    var outerBrush = gdip.solidBrush(0xFF17142C);
    graphics.fillCircle(outerBrush,node.x,node.y,radius);
    outerBrush.delete();

    var fillBrush = gdip.lineBrush(
        ::POINTF(node.x,node.y-radius),::POINTF(node.x,node.y+radius),
        0xFF312B58,0xFF121023
    );
    graphics.fillCircle(fillBrush,node.x,node.y,radius-4);
    fillBrush.delete();

    var rimPen = gdip.pen(rimColor,selected ? 4 : 2.5);
    graphics.drawCircle(rimPen,node.x,node.y,radius);
    rimPen.delete();

    var innerPen = gdip.pen((selected ? 0xB0FFFFFF : 0x55FFFFFF),1);
    graphics.drawCircle(innerPen,node.x,node.y,radius-7);
    innerPen.delete();

    if(selected){
        var orbitPen = gdip.pen(0x90FFFFFF,1.5);
        graphics.drawArc(orbitPen,node.x-radius-11,node.y-radius-11,(radius+11)*2,(radius+11)*2,game.pulse/4,220);
        graphics.drawArc(orbitPen,node.x-radius-15,node.y-radius-15,(radius+15)*2,(radius+15)*2,-game.pulse/5,110);
        orbitPen.delete();
    }

    var label;
    if(node.center) label = "✦";
    else label = tostring(nodeIndex);
    drawText(graphics,label,node.center ? 24 : 17,node.x-radius,node.y-radius,radius*2,radius*2,0xFFFFFFFF,true,1);
}

winform.magicCanvas.onDrawBackground = function(graphics,rc,backgroundColor,color){
    var background = gdip.lineBrush(::POINTF(0,0),::POINTF(0,rc.bottom),0xFF160C32,0xFF070817);
    graphics.fillRect(background,rc);
    background.delete();
    return true;
}

winform.magicCanvas.onDrawForeground = function(graphics,rc,foregroundRect,foregroundColor,color,font){
    var dpiX,dpiY = winform.dpiScale(1,1);
    var saved = graphics.save();
    graphics.scale(dpiX,dpiY);
    graphics.smoothingMode = 4/*_SmoothingModeAntiAlias*/;
    graphics.textRenderingHint = 4/*_TextRenderingHintAntiAlias*/;

    var width = rc.width/dpiX;
    var height = rc.height/dpiY;

    // 星尘背景。
    for(i,star in stars){
        var alpha = math.floor(45+(math.sin(game.pulse/650+star.phase)+1)*48);
        var starBrush = gdip.solidBrush((alpha<<24) | 0xBBD9FF);
        graphics.fillCircle(starBrush,star.x,star.y,star.size);
        starBrush.delete();
    }

    drawMagicBackdrop(graphics);

    // 标题与状态栏。
    drawText(graphics,"魔 法 阵 修 复",25,0,10,width,36,0xFFF4EFFF,true,1);
    var levelText = "第 " ++ game.level ++ " / " ++ #levels ++ " 阶 · " ++ levels[game.level].name;
    drawText(graphics,levelText,14,24,48,width/2,26,levels[game.level].accent,true,0);
    if(game.state == STATE_SOLVED){
        drawText(graphics,"✦ 法阵已修复 · 单击或按空格进入下一阶 ✦",15,0,72,width,26,0xFFFFE68A,true,1);
    }
    else{
        drawText(graphics,"拖动符文,消除所有裂隙",13,0,74,width,22,0xA9D9D4F5,false,1);
    }

    var statusText = "移动 " ++ game.moves ++ "    裂隙 " ++ game.conflictCount;
    var statusBrush = gdip.solidBrush(game.conflictCount == 0 ? 0x284FFFC5 : 0x28FF5A96);
    graphics.fillRoundRect(statusBrush,width-220,45,194,31,15);
    statusBrush.delete();
    drawText(graphics,statusText,13,width-220,45,194,31,game.conflictCount == 0 ? 0xFF77FFD1 : 0xFFFF84AE,true,1);

    // 先画能量线,再画节点,保证符文始终压在线条上方。
    for(i=1;#game.edges){
        drawEnergyEdge(graphics,i);
    }

    // 交叉处的裂隙火花。
    var crackPulse = 3.5+math.sin(game.pulse/120)*1.3;
    for(i,pt in game.crossings){
        var crackGlow = gdip.solidBrush(0x48FF2369);
        var crackCore = gdip.solidBrush(0xFFFFD8E7);
        graphics.fillCircle(crackGlow,pt.x,pt.y,crackPulse+3);
        graphics.fillCircle(crackCore,pt.x,pt.y,2.3);
        crackCore.delete();
        crackGlow.delete();
    }

    for(i=1;#game.nodes){
        drawRuneNode(graphics,i);
    }

    // 短寿命粒子提供反馈,数量受控,不会长期累积。
    for(i,p in game.particles){
        var alpha = math.floor(math.clamp(p.life/p.maxLife,0,1)*255);
        var particleBrush = gdip.solidBrush((alpha<<24) | (p.color & 0x00FFFFFF));
        graphics.fillCircle(particleBrush,p.x,p.y,p.size*(0.5+p.life/p.maxLife));
        particleBrush.delete();
    }

    if(game.state == STATE_SOLVED){
        var haloAlpha = math.floor(45+game.solveGlow*80);
        var haloPen = gdip.pen((haloAlpha<<24) | 0xFFE58A,4+game.solveGlow*8);
        graphics.drawEllipse(haloPen,BOARD_CX-BOARD_RX-25,BOARD_CY-BOARD_RY-25,(BOARD_RX+25)*2,(BOARD_RY+25)*2);
        haloPen.delete();

        // 通关提示放在顶部信息区,完整展示修复后的阵眼与全部能量线。
    }

    drawText(graphics,"R 或右键:重置本关",12,18,height-28,220,20,0xB8E3DFF2,false,0);
    graphics.restore(saved);
}

// -------------------- 动画 --------------------
winform.magicCanvas.onAnimation = function(prevState,beginning,change,timestamp,duration,deltaMs){
    game.pulse += deltaMs;
    if(game.state == STATE_SOLVED){
        game.solveGlow = math.min(1,game.solveGlow+deltaMs/700);
    }

    var dt = deltaMs/1000;
    for(i=#game.particles;1;-1){
        var p = game.particles[i];
        p.x += p.vx*dt;
        p.y += p.vy*dt;
        p.vx *= 0.985;
        p.vy *= 0.985;
        p.life -= dt;
        if(p.life <= 0) table.remove(game.particles,i);
    }
    return true;
}

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

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

Markdown 格式