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 格式