aardio 文档
aardio 范例: 自绘小游戏 - 见缝插针(Core Ball)
import win.ui;
/*DSG{{*/
var winform = win.form(text="自绘小游戏 - 见缝插针(Core Ball)";right=439;bottom=679;bgcolor=0x0B1020;border="dialog frame";max=false)
winform.add(
gameBox={cls="plus";left=0;top=0;right=440;bottom=680;db=1;dl=1;dr=1;dt=1;notify=1;z=1}
)
/*}}*/
import gdip;
import sys.midiOut;
var midiOut = sys.midiOut();
// 逻辑坐标始终为 440 × 680,绘图时整体适配 DPI。
var VIEW_W = 440;
var VIEW_H = 680;
var GEOMETRY = {
cx = 220;
cy = 255;
coreRadius = 52;
pinLength = 88;
ballRadius = 11.5;
launchTipY = 500;
shootSpeed = 1600;
ballGap = 2.5;
}
GEOMETRY.orbitRadius = GEOMETRY.coreRadius + GEOMETRY.pinLength + GEOMETRY.ballRadius;
GEOMETRY.targetTipY = GEOMETRY.cy + GEOMETRY.coreRadius;
GEOMETRY.collisionDistance = GEOMETRY.ballRadius * 2 + GEOMETRY.ballGap;
var LEVELS = [
{mode="STEADY";speed=70;initial=3;shots=7;phase=18;accent=0xFF4CC9F0},
{mode="REVERSE";speed=84;initial=4;shots=8;phase=0;accent=0xFF72EFDD},
{mode="PULSE";speed=92;initial=4;shots=9;phase=27;accent=0xFFFFC857},
{mode="STOP_GO";speed=103;initial=5;shots=9;phase=9;accent=0xFFFF7A90},
{mode="ALTERNATE";speed=112;initial=5;shots=10;phase=31;accent=0xFFC77DFF},
{mode="STEADY";speed=128;initial=6;shots=10;phase=15;accent=0xFF5EE27A},
{mode="REVERSE_PULSE";speed=122;initial=6;shots=11;phase=4;accent=0xFFFF9F43},
{mode="STOP_GO";speed=136;initial=6;shots=11;phase=23;accent=0xFF54A0FF},
{mode="ALTERNATE";speed=142;initial=7;shots=11;phase=11;accent=0xFFFF6B9D},
{mode="PULSE";speed=148;initial=7;shots=12;phase=29;accent=0xFF7BED9F}
];
var MODE_POOL = ["STEADY","REVERSE","PULSE","STOP_GO","ALTERNATE","REVERSE_PULSE"];
var ACCENTS = [0xFF4CC9F0,0xFF72EFDD,0xFFFFC857,0xFFFF7A90,0xFFC77DFF,0xFF5EE27A];
var Game = {
level = 1;
maxUnlocked = 1;
status = "playing"; // playing / paused / won / lost
soundEnabled = true;
mode = "STEADY";
baseSpeed = 70;
currentSpeed = 70;
accent = 0xFF4CC9F0;
rotation = 0;
clock = 0;
pins = [];
shots = [];
queue = 0;
shotCooldown = 0;
particles = [];
ripples = [];
corePulse = 0;
launchKick = 0;
shake = 0;
shakeX = 0;
shakeY = 0;
flashColor = 0xFFFFFFFF;
flashAlpha = 0;
}
var normalizeAngle = function(angle){
angle = angle % 360;
if(angle < 0) angle += 360;
return angle;
}
var angleDistance = function(a,b){
var diff = math.abs(normalizeAngle(a) - normalizeAngle(b));
return (diff > 180) ? 360 - diff : diff;
}
// 点到线段距离平方,用于高速飞针的连续碰撞检测,避免低帧率穿透。
var pointSegmentDistance2 = function(px,py,x1,y1,x2,y2){
var vx = x2 - x1;
var vy = y2 - y1;
var length2 = vx * vx + vy * vy;
if(length2 <= 0.000001){
var dx = px - x1;
var dy = py - y1;
return dx * dx + dy * dy;
}
var t = ((px - x1) * vx + (py - y1) * vy) / length2;
t = math.clamp(t,0,1);
var qx = x1 + vx * t;
var qy = y1 + vy * t;
var dx = px - qx;
var dy = py - qy;
return dx * dx + dy * dy;
}
// 所有已插针共享同一旋转量,所以在局部角度中即可做精确弦长判定。
var checkAngleCollision = function(newLocalAngle,pinList){
for(i=1;#pinList;1){
var diff = angleDistance(newLocalAngle,pinList[i].angle);
var chord = 2 * GEOMETRY.orbitRadius * math.sin(diff * math.pi / 360);
if(chord < GEOMETRY.collisionDistance) return true,i;
}
return false;
}
var getLevelConfig = function(lv){
if(lv <= #LEVELS) return LEVELS[lv];
var modeIndex = ((lv - 1) % #MODE_POOL) + 1;
var accentIndex = ((lv - 1) % #ACCENTS) + 1;
return {
mode = MODE_POOL[modeIndex];
speed = 105 + (lv * 13) % 58;
initial = 5 + (lv % 3);
shots = math.min(14,9 + math.floor(lv / 3));
phase = (lv * 37) % 360;
accent = ACCENTS[accentIndex];
}
}
var getModeName = function(mode){
if(mode == "REVERSE") return "逆向匀速";
if(mode == "PULSE") return "呼吸变速";
if(mode == "STOP_GO") return "节拍启停";
if(mode == "ALTERNATE") return "平滑往复";
if(mode == "REVERSE_PULSE") return "逆向脉冲";
return "匀速旋转";
}
var getAngularSpeed = function(){
var base = Game.baseSpeed;
var t = Game.clock;
if(Game.mode == "REVERSE") return -base;
if(Game.mode == "PULSE") return base * (0.42 + 1.08 * math.abs(math.sin(t * 2.05)));
if(Game.mode == "REVERSE_PULSE") return -base * (0.42 + 1.08 * math.abs(math.sin(t * 2.15)));
if(Game.mode == "ALTERNATE") return base * 1.42 * math.sin(t * 1.72);
if(Game.mode == "STOP_GO"){
var cycle = t % 2.5;
if(cycle < 1.42) return base * 1.34;
if(cycle < 1.72) return base * 1.34 * (1 - (cycle - 1.42) / 0.3);
if(cycle < 2.16) return 0;
return base * 1.34 * ((cycle - 2.16) / 0.34);
}
return base;
}
var playShootSound = function(){
if(Game.soundEnabled && midiOut) midiOut.play("changeInstrument(115),5_","C6",38);
}
var playInsertSound = function(){
if(Game.soundEnabled && midiOut) midiOut.play("changeInstrument(10),1_","C6",42);
}
var playLoseSound = function(){
if(Game.soundEnabled && midiOut) midiOut.play("changeInstrument(47),7_,6_,5__","C3",145,25);
}
var playWinSound = function(){
if(Game.soundEnabled && midiOut) midiOut.play("changeInstrument(10),1_,3_,5_,1'__","C4",105,35);
}
var addRipple = function(x,y,color,startRadius,maxRadius){
table.push(Game.ripples,{
x=x;y=y;color=color;r=startRadius;maxRadius=maxRadius;life=0;maxLife=0.42
});
}
var addBurst = function(x,y,color,count,power,gravity){
for(i=1;count;1){
var angle = math.random() * math.pi * 2;
var speed = power * (0.4 + math.random() * 0.75);
table.push(Game.particles,{
x=x;y=y;
vx=math.cos(angle)*speed;
vy=math.sin(angle)*speed - gravity * 0.12;
gravity=gravity;
life=0;
maxLife=0.34 + math.random() * 0.48;
size=2.2 + math.random() * 3.8;
color=color;
streak=(i % 3 == 0)
});
}
}
var addConfetti = function(){
var colors = [Game.accent,0xFFFFC857,0xFFFF6B9D,0xFF72EFDD,0xFFFFFFFF];
for(i=1;64;1){
var angle = math.random() * math.pi * 2;
var speed = 120 + math.random() * 260;
table.push(Game.particles,{
x=GEOMETRY.cx;y=GEOMETRY.cy;
vx=math.cos(angle)*speed;
vy=math.sin(angle)*speed-95;
gravity=270;
life=0;
maxLife=0.85+math.random()*0.65;
size=3+math.random()*4.5;
color=colors[math.random(1,#colors)];
streak=true
});
}
}
var startLevel = function(lv){
lv = math.max(1,lv);
var cfg = getLevelConfig(lv);
Game.level = lv;
Game.status = "playing";
Game.mode = cfg.mode;
Game.baseSpeed = cfg.speed;
Game.currentSpeed = cfg.speed;
Game.accent = cfg.accent;
Game.rotation = 0;
Game.clock = 0;
Game.queue = cfg.shots;
Game.shotCooldown = 0;
Game.pins = [];
Game.shots = [];
Game.particles = [];
Game.ripples = [];
Game.corePulse = 0;
Game.launchKick = 0;
Game.shake = 0;
Game.shakeX = 0;
Game.shakeY = 0;
Game.flashAlpha = 0;
for(i=1;cfg.initial;1){
table.push(Game.pins,{
angle=normalizeAngle((i-1)*360/cfg.initial+cfg.phase);
num=null;
initial=true;
hit=false
});
}
}
var loseLevel = function(x,y,pinIndex){
if(Game.status != "playing") return;
Game.status = "lost";
Game.shots = [];
Game.shake = 16;
Game.flashColor = 0xFFFF4D67;
Game.flashAlpha = 0.48;
if(pinIndex) Game.pins[pinIndex].hit = true;
addRipple(x,y,0xFFFF4D67,9,62);
addBurst(x,y,0xFFFF4D67,34,260,210);
playLoseSound();
}
var winLevel = function(){
if(Game.status != "playing") return;
Game.status = "won";
Game.maxUnlocked = math.max(Game.maxUnlocked,Game.level+1);
Game.flashColor = Game.accent;
Game.flashAlpha = 0.34;
Game.corePulse = 1;
addRipple(GEOMETRY.cx,GEOMETRY.cy,Game.accent,GEOMETRY.coreRadius,170);
addConfetti();
playWinSound();
}
// 按下立即发射。视觉挤压改为发射后的反冲,不再用松开事件增加输入延迟。
var firePin = function(){
if(Game.status == "lost"){
startLevel(Game.level);
return;
}
if(Game.status == "won"){
startLevel(Game.level+1);
return;
}
if(Game.status == "paused"){
Game.status = "playing";
return;
}
if(Game.status != "playing") return;
if(Game.queue <= 0 || Game.shotCooldown > 0 || #Game.shots >= 3) return;
var num = Game.queue;
Game.queue--;
Game.shotCooldown = 0.075;
Game.launchKick = 1;
table.push(Game.shots,{
tipY=GEOMETRY.launchTipY;
age=0;
num=num
});
addRipple(GEOMETRY.cx,GEOMETRY.launchTipY+GEOMETRY.pinLength+GEOMETRY.ballRadius,Game.accent,7,30);
playShootSound();
}
startLevel(1);
// 常驻绘图资源。画笔使用世界单位,使线宽随 DPI 整体缩放。
var Draw = {
outerBg = gdip.solidBrush(0xFF070B16);
canvasBg = gdip.solidBrush(0xFF0B1020);
panelBrush = gdip.solidBrush(0xB8151D31);
maskBrush = gdip.solidBrush(0xCC080C17);
textBrush = gdip.solidBrush(0xFFF4F7FB);
subTextBrush = gdip.solidBrush(0xFFD0D8E5);
darkTextBrush = gdip.solidBrush(0xFF09101E);
fxBrush = gdip.solidBrush(0xFFFFFFFF);
guidePen = gdip.pen(0x243E5575,1.2);
pinShadowPen = gdip.pen(0x95040911,5.2);
pinPen = gdip.pen(0xFFD7DFEC,2.4);
accentPen = gdip.pen(0xFF4CC9F0,2.5);
fxPen = gdip.pen(0xFFFFFFFF,2);
titleFont = gdip.font("Segoe UI",22,1,2/*_UnitPixel*/);
levelFont = gdip.font("Segoe UI",17,1,2/*_UnitPixel*/);
labelFont = gdip.font("Microsoft YaHei UI",11,0,2/*_UnitPixel*/);
smallFont = gdip.font("Microsoft YaHei UI",10.5,0,2/*_UnitPixel*/);
numberFont = gdip.font("Segoe UI",10,1,2/*_UnitPixel*/);
coreNumberFont = gdip.font("Segoe UI",31,1,2/*_UnitPixel*/);
resultFont = gdip.font("Microsoft YaHei UI",24,1,2/*_UnitPixel*/);
centerFormat = gdip.stringFormat();
leftFormat = gdip.stringFormat();
rightFormat = gdip.stringFormat();
}
Draw.centerFormat.align = 1;
Draw.centerFormat.lineAlign = 1;
Draw.leftFormat.align = 0;
Draw.leftFormat.lineAlign = 1;
Draw.rightFormat.align = 2;
Draw.rightFormat.lineAlign = 1;
Draw.guidePen.dashStyle = 2/*_DashStyleDot*/;
var ARGB = function(alpha,color){
alpha = math.clamp(math.floor(alpha),0,255);
return (alpha << 24) | (color & 0x00FFFFFF);
}
var drawText = function(graphics,text,font,x,y,width,height,format,brush){
graphics.drawString(text,font,::RECTF(x,y,width,height),format,brush);
}
var drawStaticBackdrop = function(graphics){
graphics.fillRectangle(Draw.canvasBg,0,0,VIEW_W,VIEW_H);
// 低成本的固定星点与同心轨道,避免逐帧创建图形资源。
for(i=1;28;1){
var x = 12 + (i * 83) % 416;
var y = 82 + (i * 47) % 350;
var radius = 0.55 + (i % 3) * 0.35;
Draw.fxBrush.color = ARGB(35 + (i % 4) * 12,0xFFFFFFFF);
graphics.fillCircle(Draw.fxBrush,x,y,radius);
}
Draw.fxPen.color = 0x182A3850;
Draw.fxPen.width = 1;
graphics.drawCircle(Draw.fxPen,GEOMETRY.cx,GEOMETRY.cy,GEOMETRY.orbitRadius);
graphics.drawCircle(Draw.fxPen,GEOMETRY.cx,GEOMETRY.cy,GEOMETRY.orbitRadius+18);
// 顶部信息栏。
drawText(graphics,"CORE BALL",Draw.titleFont,20,17,180,34,Draw.leftFormat,Draw.textBrush);
drawText(graphics,"LEVEL " + string.format("%02d",Game.level),Draw.levelFont,20,52,150,26,Draw.leftFormat,Draw.textBrush);
drawText(graphics,"已解锁 " + Game.maxUnlocked,Draw.smallFont,286,24,134,20,Draw.rightFormat,Draw.subTextBrush);
Draw.fxBrush.color = ARGB(32,Game.accent);
graphics.fillRoundRect(Draw.fxBrush,154,52,132,27,13);
Draw.fxPen.color = ARGB(135,Game.accent);
Draw.fxPen.width = 1.2;
graphics.drawRoundRect(Draw.fxPen,154,52,132,27,13);
drawText(graphics,getModeName(Game.mode),Draw.smallFont,154,52,132,27,Draw.centerFormat,Draw.textBrush);
var soundText = Game.soundEnabled ? "M 音效开" : "M 已静音";
drawText(graphics,soundText,Draw.smallFont,294,53,126,22,Draw.rightFormat,Draw.subTextBrush);
// 两侧淡导轨只提示发射方向,不把飞行针与下一针串成一条线。
graphics.drawLine(Draw.guidePen,GEOMETRY.cx-7,GEOMETRY.targetTipY+17,GEOMETRY.cx-7,GEOMETRY.launchTipY-15);
graphics.drawLine(Draw.guidePen,GEOMETRY.cx+7,GEOMETRY.targetTipY+17,GEOMETRY.cx+7,GEOMETRY.launchTipY-15);
}
var drawPin = function(graphics,pin,dx,dy){
var worldAngle = normalizeAngle(Game.rotation+pin.angle);
var rad = worldAngle*math.pi/180;
var cosValue = math.cos(rad);
var sinValue = math.sin(rad);
var x1 = GEOMETRY.cx+dx+GEOMETRY.coreRadius*cosValue;
var y1 = GEOMETRY.cy+dy+GEOMETRY.coreRadius*sinValue;
var x2 = GEOMETRY.cx+dx+(GEOMETRY.orbitRadius-GEOMETRY.ballRadius)*cosValue;
var y2 = GEOMETRY.cy+dy+(GEOMETRY.orbitRadius-GEOMETRY.ballRadius)*sinValue;
var bx = GEOMETRY.cx+dx+GEOMETRY.orbitRadius*cosValue;
var by = GEOMETRY.cy+dy+GEOMETRY.orbitRadius*sinValue;
var pinColor = pin.hit ? 0xFFFF4D67 : (pin.initial ? 0xFFD7DFEC : Game.accent);
Draw.pinPen.color = pinColor;
graphics.drawLine(Draw.pinShadowPen,x1,y1,x2,y2);
graphics.drawLine(Draw.pinPen,x1,y1,x2,y2);
Draw.fxBrush.color = 0x70000000;
graphics.fillCircle(Draw.fxBrush,bx+2.2,by+3,GEOMETRY.ballRadius+1.3);
Draw.fxBrush.color = pinColor;
graphics.fillCircle(Draw.fxBrush,bx,by,GEOMETRY.ballRadius);
Draw.fxBrush.color = ARGB(92,0xFFFFFFFF);
graphics.fillCircle(Draw.fxBrush,bx-3.2,by-3.6,2.5);
if(pin.num){
drawText(graphics,pin.num,Draw.numberFont,bx-GEOMETRY.ballRadius,by-GEOMETRY.ballRadius,
GEOMETRY.ballRadius*2,GEOMETRY.ballRadius*2,Draw.centerFormat,Draw.darkTextBrush);
}
}
var drawCoreAndPins = function(graphics,dx,dy){
var cx = GEOMETRY.cx+dx;
var cy = GEOMETRY.cy+dy;
var pulse = Game.corePulse;
var radius = GEOMETRY.coreRadius*(1+0.075*pulse);
// 光晕与旋向弧放在针杆后面,针杆不会被弧线切断。
Draw.fxBrush.color = ARGB(22+42*pulse,Game.accent);
graphics.fillCircle(Draw.fxBrush,cx,cy,radius+18+8*pulse);
var sweep = (Game.currentSpeed >= 0) ? 58 : -58;
Draw.fxPen.color = ARGB(165,Game.accent);
Draw.fxPen.width = 2;
graphics.drawArc(Draw.fxPen,cx-radius-10,cy-radius-10,(radius+10)*2,(radius+10)*2,
normalizeAngle(Game.rotation-28),sweep);
for(i=1;#Game.pins;1) drawPin(graphics,Game.pins[i],dx,dy);
Draw.fxBrush.color = 0xFF151D2D;
if(Game.status == "lost") Draw.fxBrush.color = 0xFF321826;
graphics.fillCircle(Draw.fxBrush,cx,cy,radius);
Draw.accentPen.color = (Game.status == "lost") ? 0xFFFF4D67 : Game.accent;
Draw.accentPen.width = 3;
graphics.drawCircle(Draw.accentPen,cx,cy,radius);
var pending = Game.queue+#Game.shots;
drawText(graphics,pending,Draw.coreNumberFont,cx-radius,cy-radius-8,radius*2,radius*1.65,
Draw.centerFormat,Draw.textBrush);
drawText(graphics,"待插",Draw.smallFont,cx-radius,cy+22,radius*2,19,
Draw.centerFormat,Draw.subTextBrush);
}
var drawShot = function(graphics,shot,dx,dy){
var tipY = shot.tipY+dy;
var ballY = shot.tipY+GEOMETRY.pinLength+GEOMETRY.ballRadius+dy;
var cx = GEOMETRY.cx+dx;
Draw.fxPen.color = ARGB(55,Game.accent);
Draw.fxPen.width = 6;
graphics.drawLine(Draw.fxPen,cx,tipY+4,cx,ballY-GEOMETRY.ballRadius);
Draw.accentPen.color = Game.accent;
Draw.accentPen.width = 2.6;
graphics.drawLine(Draw.accentPen,cx,tipY,cx,ballY-GEOMETRY.ballRadius);
var squeeze = math.max(0,1-shot.age/0.075);
var ballW = GEOMETRY.ballRadius*2*(1+0.32*squeeze);
var ballH = GEOMETRY.ballRadius*2*(1-0.28*squeeze);
Draw.fxBrush.color = Game.accent;
graphics.fillEllipse(Draw.fxBrush,cx-ballW/2,ballY-ballH/2,ballW,ballH);
Draw.fxBrush.color = ARGB(90,0xFFFFFFFF);
graphics.fillCircle(Draw.fxBrush,cx-3,ballY-3,2.3);
drawText(graphics,shot.num,Draw.numberFont,cx-GEOMETRY.ballRadius,ballY-GEOMETRY.ballRadius,
GEOMETRY.ballRadius*2,GEOMETRY.ballRadius*2,Draw.centerFormat,Draw.darkTextBrush);
}
var drawLauncher = function(graphics){
if(Game.status != "playing" || Game.queue <= 0) return;
var kick = Game.launchKick;
var tipY = GEOMETRY.launchTipY+kick*7;
var ballY = tipY+GEOMETRY.pinLength+GEOMETRY.ballRadius;
var ballW = GEOMETRY.ballRadius*2*(1+0.28*kick);
var ballH = GEOMETRY.ballRadius*2*(1-0.25*kick);
Draw.fxPen.color = ARGB(55+80*kick,Game.accent);
Draw.fxPen.width = 1.5+kick*2;
graphics.drawCircle(Draw.fxPen,GEOMETRY.cx,ballY,GEOMETRY.ballRadius+8+kick*10);
Draw.accentPen.color = Game.accent;
Draw.accentPen.width = 2.3;
graphics.drawLine(Draw.accentPen,GEOMETRY.cx,tipY,GEOMETRY.cx,ballY-ballH/2);
Draw.fxBrush.color = Game.accent;
graphics.fillEllipse(Draw.fxBrush,GEOMETRY.cx-ballW/2,ballY-ballH/2,ballW,ballH);
drawText(graphics,Game.queue,Draw.numberFont,GEOMETRY.cx-GEOMETRY.ballRadius,ballY-GEOMETRY.ballRadius,
GEOMETRY.ballRadius*2,GEOMETRY.ballRadius*2,Draw.centerFormat,Draw.darkTextBrush);
drawText(graphics,"下一针",Draw.smallFont,GEOMETRY.cx+22,ballY-11,64,22,Draw.leftFormat,Draw.subTextBrush);
var showCount = math.min(7,Game.queue-1);
var firstX = GEOMETRY.cx-(showCount-1)*13;
for(i=1;showCount;1){
var alpha = 185-(i-1)*18;
Draw.fxBrush.color = ARGB(alpha,Game.accent);
graphics.fillCircle(Draw.fxBrush,firstX+(i-1)*26,632,4.4);
}
}
var drawEffects = function(graphics,dx,dy){
for(i=1;#Game.ripples;1){
var ripple = Game.ripples[i];
var alpha = 190*(1-ripple.life/ripple.maxLife);
Draw.fxPen.color = ARGB(alpha,ripple.color);
Draw.fxPen.width = 2.2;
graphics.drawCircle(Draw.fxPen,ripple.x+dx,ripple.y+dy,ripple.r);
}
for(i=1;#Game.particles;1){
var particle = Game.particles[i];
var alpha = 255*(1-particle.life/particle.maxLife);
if(particle.streak){
Draw.fxPen.color = ARGB(alpha,particle.color);
Draw.fxPen.width = math.max(1,particle.size*0.42);
graphics.drawLine(Draw.fxPen,particle.x+dx,particle.y+dy,
particle.x-particle.vx*0.025+dx,particle.y-particle.vy*0.025+dy);
}
else{
Draw.fxBrush.color = ARGB(alpha,particle.color);
graphics.fillCircle(Draw.fxBrush,particle.x+dx,particle.y+dy,particle.size);
}
}
}
var drawBottomUi = function(graphics){
var speed = math.floor(math.abs(Game.currentSpeed)+0.5);
var direction = (Game.currentSpeed >= 0) ? "顺时针" : "逆时针";
if(math.abs(Game.currentSpeed) < 1) direction = "停顿";
drawText(graphics,direction+" "+speed+"°/s",Draw.smallFont,20,452,150,22,Draw.leftFormat,Draw.subTextBrush);
drawText(graphics,"按下即发射 · 鼠标 / 空格",Draw.labelFont,50,646,340,24,Draw.centerFormat,Draw.subTextBrush);
}
var drawStateOverlay = function(graphics){
if(Game.status == "playing") return;
Draw.maskBrush.color = 0xBC080C17;
graphics.fillRectangle(Draw.maskBrush,0,0,VIEW_W,VIEW_H);
Draw.panelBrush.color = 0xEC151D31;
graphics.fillRoundRect(Draw.panelBrush,46,250,348,164,18);
var title = "暂停";
var detail = "点击画面或按 P 继续";
var titleColor = Game.accent;
if(Game.status == "lost"){
title = "碰撞了";
detail = "点击重试 · R 重新开始";
titleColor = 0xFFFF4D67;
}
elseif(Game.status == "won"){
title = "关卡完成";
detail = "点击进入下一关";
titleColor = Game.accent;
}
Draw.fxBrush.color = titleColor;
drawText(graphics,title,Draw.resultFont,66,274,308,48,Draw.centerFormat,Draw.fxBrush);
drawText(graphics,detail,Draw.labelFont,66,329,308,30,Draw.centerFormat,Draw.textBrush);
if(Game.status != "paused"){
drawText(graphics,"← / → 选择已解锁关卡",Draw.smallFont,66,368,308,22,Draw.centerFormat,Draw.subTextBrush);
}
}
winform.gameBox.onDrawBackground = function(graphics,rc,backgroundColor,foregroundColor){
graphics.clear(0xFF070B16);
return true;
}
winform.gameBox.onDrawForeground = function(graphics,rc,foregroundRect,foregroundColor,color,font){
graphics.smoothingMode = 4/*_SmoothingModeAntiAlias*/;
graphics.textRenderingHint = 4/*_TextRenderingHintAntiAlias*/;
graphics.fillRectangle(Draw.outerBg,0,0,rc.width,rc.height);
var scale = math.min(rc.width/VIEW_W,rc.height/VIEW_H);
var offsetX = (rc.width-VIEW_W*scale)/2;
var offsetY = (rc.height-VIEW_H*scale)/2;
var state = graphics.save();
graphics.translate(offsetX,offsetY);
graphics.scale(scale,scale);
graphics.setClipRect(0,0,VIEW_W,VIEW_H);
drawStaticBackdrop(graphics);
var dx = Game.shakeX;
var dy = Game.shakeY;
drawCoreAndPins(graphics,dx,dy);
for(i=1;#Game.shots;1) drawShot(graphics,Game.shots[i],dx,dy);
drawLauncher(graphics);
drawEffects(graphics,dx,dy);
drawBottomUi(graphics);
if(Game.flashAlpha > 0){
Draw.fxBrush.color = ARGB(Game.flashAlpha*255,Game.flashColor);
graphics.fillRectangle(Draw.fxBrush,0,0,VIEW_W,VIEW_H);
}
drawStateOverlay(graphics);
graphics.restore(state);
}
var insertShot = function(shot){
var localAngle = normalizeAngle(90-Game.rotation);
var collided,pinIndex = checkAngleCollision(localAngle,Game.pins);
if(collided){
loseLevel(GEOMETRY.cx,GEOMETRY.cy+GEOMETRY.orbitRadius,pinIndex);
return false;
}
table.push(Game.pins,{
angle=localAngle;
num=shot.num;
initial=false;
hit=false
});
Game.corePulse = 1;
Game.shake = math.max(Game.shake,2.6);
addRipple(GEOMETRY.cx,GEOMETRY.cy+GEOMETRY.coreRadius,Game.accent,8,42);
addBurst(GEOMETRY.cx,GEOMETRY.cy+GEOMETRY.orbitRadius,Game.accent,9,115,90);
playInsertSound();
return true;
}
winform.gameBox.onAnimation = function(state,beginning,change,timestamp,duration,deltaMs){
var dt = deltaMs/1000;
if(dt <= 0) dt = 0.016;
dt = math.min(dt,0.055); // 长时间切出后不让游戏在不可见状态跳过半圈。
if(Game.status == "playing" || Game.status == "won"){
Game.clock += dt;
var speed = getAngularSpeed();
if(Game.status == "won") speed *= 0.35;
Game.currentSpeed = speed;
Game.rotation = normalizeAngle(Game.rotation+speed*dt);
}
Game.shotCooldown = math.max(0,Game.shotCooldown-dt);
Game.corePulse = math.max(0,Game.corePulse-dt*4.8);
Game.launchKick = math.max(0,Game.launchKick-dt*8.5);
Game.flashAlpha = math.max(0,Game.flashAlpha-dt*1.9);
if(Game.shake > 0.08){
Game.shake *= math.exp(-12*dt);
Game.shakeX = (math.random()-0.5)*Game.shake*2;
Game.shakeY = (math.random()-0.5)*Game.shake*2;
}
else{
Game.shake = 0;
Game.shakeX = 0;
Game.shakeY = 0;
}
if(Game.status == "playing" && #Game.shots){
var aliveShots = [];
var collisionRadius2 = GEOMETRY.collisionDistance*GEOMETRY.collisionDistance;
for(i=1;#Game.shots;1){
var shot = Game.shots[i];
shot.age += dt;
var oldTipY = shot.tipY;
var newTipY = oldTipY-GEOMETRY.shootSpeed*dt;
var oldBallY = oldTipY+GEOMETRY.pinLength+GEOMETRY.ballRadius;
var newBallY = newTipY+GEOMETRY.pinLength+GEOMETRY.ballRadius;
var collidedIndex = null;
var hitY = newBallY;
for(k=1;#Game.pins;1){
var pin = Game.pins[k];
var worldAngle = normalizeAngle(Game.rotation+pin.angle);
var rad = worldAngle*math.pi/180;
var pinX = GEOMETRY.cx+GEOMETRY.orbitRadius*math.cos(rad);
var pinY = GEOMETRY.cy+GEOMETRY.orbitRadius*math.sin(rad);
if(pointSegmentDistance2(pinX,pinY,GEOMETRY.cx,oldBallY,GEOMETRY.cx,newBallY) < collisionRadius2){
collidedIndex = k;
hitY = math.clamp(pinY,newBallY,oldBallY);
break;
}
}
if(collidedIndex){
loseLevel(GEOMETRY.cx,hitY,collidedIndex);
break;
}
shot.tipY = newTipY;
if(shot.tipY <= GEOMETRY.targetTipY){
shot.tipY = GEOMETRY.targetTipY;
if(!insertShot(shot)) break;
}
else{
table.push(aliveShots,shot);
}
}
if(Game.status == "playing"){
Game.shots = aliveShots;
if(Game.queue == 0 && #Game.shots == 0) winLevel();
}
}
var aliveParticles = [];
for(i=1;#Game.particles;1){
var particle = Game.particles[i];
particle.life += dt;
if(particle.life < particle.maxLife){
particle.x += particle.vx*dt;
particle.y += particle.vy*dt;
particle.vy += particle.gravity*dt;
particle.vx *= math.exp(-1.2*dt);
table.push(aliveParticles,particle);
}
}
Game.particles = aliveParticles;
var aliveRipples = [];
for(i=1;#Game.ripples;1){
var ripple = Game.ripples[i];
ripple.life += dt;
ripple.r = math.lerp(ripple.r,ripple.maxRadius,1-math.exp(-9*dt));
if(ripple.life < ripple.maxLife) table.push(aliveRipples,ripple);
}
Game.ripples = aliveRipples;
return true;
}
winform.gameBox.onMouseDown = function(wParam,lParam){
firePin();
}
winform.gameBox.dlgCode = 4/*_DLGC_WANTALLKEYS*/;
winform.gameBox.onKeyDown = function(keyCode,lParam,repeat){
if((keyCode == 0x20/*_VK_SPACE*/ || keyCode == 0x0D/*_VK_RETURN*/ || keyCode == 0x26/*_VK_UP*/) && !repeat){
firePin();
}
elseif(keyCode == 'R'#){
startLevel(Game.level);
}
elseif(keyCode == 'P'#){
if(Game.status == "paused") Game.status = "playing";
elseif(Game.status == "playing") Game.status = "paused";
}
elseif(keyCode == 'M'#){
Game.soundEnabled = !Game.soundEnabled;
if(Game.soundEnabled && midiOut) midiOut.play("changeInstrument(10),1_","C6",55);
}
elseif(keyCode == 0x25/*_VK_LEFT*/){
if(Game.level > 1) startLevel(Game.level-1);
}
elseif(keyCode == 0x27/*_VK_RIGHT*/){
if(Game.level < Game.maxUnlocked) startLevel(Game.level+1);
}
}
winform.gameBox.onFocusLost = function(){
if(Game.status == "playing") Game.status = "paused";
}
winform.onClose = function(){
if(midiOut){
midiOut.reset();
midiOut.close();
midiOut = null;
}
for name,obj in Draw{
if(obj[["delete"]]) obj.delete();
}
}
winform.gameBox.startAnimation(16);
winform.show();
winform.gameBox.setFocus();
win.loopMessage();
Markdown 格式