# aardio 范例: 自绘小游戏 - 反应堆 Stack



```aardio
import win.ui;
/*DSG{{*/
var winform = win.form(text="自绘小游戏 - 反应堆 Stack";right=400;bottom=600;border="dialog frame";max=false)
winform.add(
plus={cls="plus";left=0;top=0;right=400;bottom=600;db=1;dl=1;dr=1;dt=1;notify=1;z=1}
)
/*}}*/

var STATE_START = 0;
var STATE_PLAYING = 1;
var STATE_GAMEOVER = 2;

var colors = [
    0xFF00b894, 0xFF00cec9, 0xFF0984e3, 0xFF6c5ce7,
    0xFFa29bfe, 0xFFfd79a8, 0xFFe17055, 0xFFfdcb6e,
    0xFF55efc4, 0xFF81ecec, 0xFF74b9ff, 0xFFf39c12
];

var game = {
    state = STATE_START;
    score = 0;
    highScore = 0;
    level = 1;
    blockWidth = 180;
    blockHeight = 28;
    blockSpeed = 3;
    blockDirection = 1;
    currentBlock = null;
    stack = {};
    particles = {};
    glowEffects = {};
    viewOffset = 0; 
    targetViewOffset = 0;
    perfectCount = 0;
    cutBlock = null;
};

var renderedBlock = null;

import sys.midiOut;
var midiOut = sys.midiOut();

var getCanvasSize = function() {
    var rc = winform.plus.getClientRect();
    return rc.right, rc.bottom;
}

var toScreenY = function(gameY) {
    return gameY + game.viewOffset;
}

var createParticles = function(x, y, width, height, color, count, particleType) {
    for(i=1; count) {
        var vxRange = particleType == "perfect" ? 10 : 5;
        var vyBase = particleType == "perfect" ? -8 : -2;
        var particle = {
            x = x + math.random() * width;
            y = y + math.random() * height;
            vx = (math.random() - 0.5) * vxRange;
            vy = (math.random() - 0.5) * 4 + vyBase;
            size = math.random() * 5 + 3;
            color = color;
            alpha = 1.0;
            gravity = particleType == "perfect" ? 0.25 : 0.4;
            rotation = math.random() * 6.28;
            rotationSpeed = (math.random() - 0.5) * 0.3;
        };
        table.push(game.particles, particle);
    }
}

var createGlowEffect = function(x, y, width, height, color) {
    table.push(game.glowEffects, {
        x = x; y = y; width = width; height = height;
        color = color; alpha = 0.8; scale = 1.0;
    });
}

var updateParticles = function() {
    var w, h = getCanvasSize();
    for(i=#game.particles; 1; -1) {
        var p = game.particles[i];
        p.x = p.x + p.vx;
        p.y = p.y + p.vy;
        p.vy = p.vy + p.gravity;
        p.alpha = p.alpha - 0.025;
        p.rotation = p.rotation + p.rotationSpeed;
        p.size = p.size * 0.98;
        
        if(p.alpha <= 0 || toScreenY(p.y) > h + 50) {
            table.remove(game.particles, i);
        }
    }
}

var updateGlowEffects = function() {
    for(i=#game.glowEffects; 1; -1) {
        var g = game.glowEffects[i];
        g.alpha = g.alpha - 0.08;
        g.scale = g.scale + 0.08;
        if(g.alpha <= 0) table.remove(game.glowEffects, i);
    }
}

var initGame = function() {
    var w, h = getCanvasSize();
    var dpiX, dpiY = winform.dpiScale(1,1);
    
    game.score = 0;
    game.level = 1;
    game.blockWidth = 180 * dpiX;
    game.blockHeight = 28 * dpiY;
    game.blockSpeed = 5 * dpiX;
    game.blockDirection = 1;
    game.viewOffset = 0;
    game.targetViewOffset = 0;
    game.perfectCount = 0;
    game.cutBlock = null;
    game.particles = {};
    game.glowEffects = {};
    game.stack = {};
    
    var baseBlock = {
        x = (w - game.blockWidth) / 2;
        y = h - game.blockHeight - 80 * dpiY;
        width = game.blockWidth;
        color = colors[1];
    };
    table.push(game.stack, baseBlock);
    
    game.currentBlock = {
        x = 0;
        y = baseBlock.y - game.blockHeight;
        width = game.blockWidth;
        color = colors[2];
    };
    
    game.state = STATE_PLAYING;
    if(midiOut) midiOut.play("changeInstrument(10), 1_,3_,5_", "C4", 80);
}

var placeBlock = function() {
    if(!game.currentBlock) return; 
    
    var w, h = getCanvasSize();
    var dpiX, dpiY =  winform.dpiScale(1,1);
    var topBlock = game.stack[#game.stack];
    
    var current = game.currentBlock;
    
    var overlapLeft = math.max(current.x, topBlock.x);
    var overlapRight = math.min(current.x + current.width, topBlock.x + topBlock.width);
    var overlapWidth = overlapRight - overlapLeft;
    
    if(overlapWidth <= 0) {
        game.state = STATE_GAMEOVER;
        game.cutBlock = {
            x = current.x; y = current.y;
            width = current.width; color = current.color;
            vy = 0; vx = game.blockDirection * 2 * dpiX; rotation = 0;
        };
        game.currentBlock = null;
        if(game.score > game.highScore) game.highScore = game.score;
        if(midiOut) midiOut.play("changeInstrument(47), '5_,'4_,'3__", "C3", 200);
        winform.plus.redraw(); 
        return;
    }
    
    var isPerfect = math.abs(current.x - topBlock.x) < 6 * dpiX;
     
    var actualBlockX = current.x;
    var actualBlockWidth;
    
    if(isPerfect) { 
        actualBlockWidth = topBlock.width;
        
        game.perfectCount = game.perfectCount + 1;
        game.score = game.score + 10 + game.perfectCount * 5;
         
        createParticles(current.x, current.y, topBlock.width, game.blockHeight, 0xFFFFFF00, 15, "perfect");
        createParticles(current.x, current.y, topBlock.width, game.blockHeight, 0xFFFF6B6B, 10, "perfect");
        if(midiOut) midiOut.play("changeInstrument(10), 1_,3_,5_,1'_", "C5", 80);
    } else {
        game.perfectCount = 0;
        game.score = game.score + 10;
        actualBlockX = overlapLeft;
        actualBlockWidth = overlapWidth;
        
        var cutX = current.x < topBlock.x ? current.x : overlapRight;
        var cutWidth = current.x < topBlock.x ? (topBlock.x - current.x) : (current.x + current.width - overlapRight);
        
        if(cutWidth > 2) {
            game.cutBlock = {
                x = cutX; y = current.y; width = cutWidth; color = current.color;
                vy = 0; vx = (cutX < overlapLeft) ? -2 * dpiX : 2 * dpiX; rotation = 0;
            };
            createParticles(cutX, current.y, cutWidth, game.blockHeight, current.color, 12, "cut");
        }
        if(midiOut) midiOut.play("changeInstrument(115), 5_", "C5", 60);
    }
    
    var newBlock = { x = actualBlockX; y = current.y; width = actualBlockWidth; color = current.color };
    table.push(game.stack, newBlock);
    createGlowEffect(actualBlockX, current.y, actualBlockWidth, game.blockHeight, current.color);

    game.level = game.level + 1;
    game.blockSpeed = math.min((5 + game.level * 0.25) * dpiX, 16 * dpiX);
    
    var newBlockScreenY = toScreenY(newBlock.y);
    var idealScreenY = h * 0.4;
    if(newBlockScreenY < idealScreenY) {
        game.targetViewOffset = game.targetViewOffset + (idealScreenY - newBlockScreenY);
    }
    
    var colorIndex = ((#game.stack) % #colors) + 1;
    game.currentBlock = {
        x = game.blockDirection > 0 ? -actualBlockWidth : w;
        y = newBlock.y - game.blockHeight;
        width = actualBlockWidth;
        color = colors[colorIndex];
    };
    game.blockDirection = -game.blockDirection;
    
    winform.plus.redraw(); 
}

var drawBlock = function(graphics, x, y, width, height, color, isMoving) {
    var path = gdip.path();
    path.addRoundRect(x, y, width, height, 5);
    
    var brush = gdip.solidBrush(color);
    graphics.fillPath(brush, path);
    brush.delete();
    
    if(width > 10) {
        var highlightPath = gdip.path();
        highlightPath.addRoundRect(x + 2, y + 2, width - 4, height / 3, 3);
        var highlightBrush = gdip.solidBrush(isMoving ? 0x50FFFFFF : 0x30FFFFFF);
        graphics.fillPath(highlightBrush, highlightPath);
        highlightBrush.delete();
        highlightPath.delete();
    }
    
    if(isMoving) {
        var glowPen = gdip.pen(0x60FFFFFF, 2);
        graphics.drawPath(glowPen, path);
        glowPen.delete();
    }
    
    path.delete();
}

winform.plus.onDrawBackground = function(graphics,rc,backgroundColor,color){
    var brush = gdip.lineBrush(::POINTF(0, 0), ::POINTF(0, rc.bottom), 0xFF1a1a2e, 0xFF0f0f1a);
    graphics.fillRectangle(brush, rc);
    brush.delete();
}

winform.plus.onDrawForeground = function(graphics,rc,foregroundRect,foregroundColor,color,font){

    graphics.smoothingMode = 4;
    
    var w, h = rc.right, rc.bottom;
    var dpiX, dpiY =  winform.dpiScale(1,1);
    
    var family = gdip.family("Tahoma");
    var format = gdip.stringFormat();
    format.align = 1;
    format.lineAlign = 1;
    
    if(game.state == STATE_START) {
        var titleFont = family.createFont(36, 1, 3/*_UnitPoint*/);
        var layoutRect = ::RECTF(0, 0, w, h);
        var titleBounds = graphics.measureString("反 应 堆", titleFont, layoutRect, format);
        
        var shadowBrush = gdip.solidBrush(0x40000000);
        graphics.drawString("反 应 堆", titleFont, ::RECTF(3, h*0.22 + 3, w, titleBounds.height), format, shadowBrush);
        shadowBrush.delete();
        
        var titleBrush = gdip.lineBrush(::POINTF(0, h*0.22), ::POINTF(0, h*0.22 + titleBounds.height), 0xFFffeaa7, 0xFFfdcb6e);
        graphics.drawString("反 应 堆", titleFont, ::RECTF(0, h*0.22, w, titleBounds.height), format, titleBrush);
        titleBrush.delete();
        titleFont.delete();
        
        var subFont = family.createFont(14, 0, 3/*_UnitPoint*/);
        var subBounds = graphics.measureString("层层堆叠，挑战极限", subFont, layoutRect, format);
        var subBrush = gdip.solidBrush(0xBBFFFFFF);
        graphics.drawString("层层堆叠，挑战极限", subFont, ::RECTF(0, h*0.32, w, subBounds.height), format, subBrush);
        subBrush.delete();
        subFont.delete();
        
        for(i=1; 5) {
            var demoW = 180 * dpiX - (i-1) * 15 * dpiX;
            var demoX = w/2 - demoW/2 + math.sin(i * 0.8) * 15 * dpiX;
            var demoY = h*0.42 + (i-1) * 33 * dpiY;
            drawBlock(graphics, demoX, demoY, demoW, 28 * dpiY, colors[i], false);
        }
        
        var hintAlpha = math.floor(math.abs(math.sin(time.tick() / 500)) * 180 + 75);
        var hintFont = family.createFont(16, 0, 3/*_UnitPoint*/);
        var hintBounds = graphics.measureString("单击或按空格键开始", hintFont, layoutRect, format);
        var hintBrush = gdip.solidBrush((hintAlpha << 24) | 0xFFFFFF);
        graphics.drawString("单击或按空格键开始", hintFont, ::RECTF(0, h*0.78, w, hintBounds.height), format, hintBrush);
        hintBrush.delete();
        hintFont.delete();
        
    } elseif(game.state == STATE_PLAYING || game.state == STATE_GAMEOVER) {
        
        for(i=1; #game.stack) {
            var block = game.stack[i];
            var screenY = toScreenY(block.y);
            if(screenY > -game.blockHeight && screenY < h + game.blockHeight) {
                drawBlock(graphics, block.x, screenY, block.width, game.blockHeight, block.color, false);
            }
        }
        
        for(i=1; #game.glowEffects) {
            var g = game.glowEffects[i];
            var screenY = toScreenY(g.y);
            var expandX = (g.scale - 1) * g.width / 2;
            var expandY = (g.scale - 1) * g.height / 2;
            var alpha = math.floor(g.alpha * 200);
            
            var path = gdip.path();
            path.addRoundRect(g.x - expandX, screenY - expandY, g.width + expandX*2, g.height + expandY*2, 8);
            var brush = gdip.solidBrush((alpha << 24) | 0xFFFFFF);
            graphics.fillPath(brush, path);
            brush.delete();
            path.delete();
        }
        
        if(game.currentBlock) {
            var block = game.currentBlock;
            var screenY = toScreenY(block.y);
            drawBlock(graphics, block.x, screenY, block.width, game.blockHeight, block.color, true);
           
            if(#game.stack > 0) {
                var topBlock = game.stack[#game.stack];
                var topScreenY = toScreenY(topBlock.y);
                if(topScreenY > screenY + game.blockHeight) {
                    var projBrush = gdip.solidBrush(0x15FFFFFF);
                    graphics.fillRectangle(projBrush, ::RECTF(block.x, screenY + game.blockHeight, block.width, topScreenY - screenY - game.blockHeight));
                    projBrush.delete();
                }
            }
        }
        
        if(game.cutBlock) {
            var cut = game.cutBlock;
            var screenY = toScreenY(cut.y);
            if(screenY < h + 100) {
                graphics.save();
                graphics.translateTransform(cut.x + cut.width/2, screenY + game.blockHeight/2);
                graphics.rotateTransform(cut.rotation * 57.3);
                
                var alpha = math.max(0, math.min(255, 255 - (screenY - h/2) * 0.5));
                var path = gdip.path();
                path.addRoundRect(-cut.width/2, -game.blockHeight/2, cut.width, game.blockHeight, 5);
                var brush = gdip.solidBrush((alpha << 24) | (cut.color & 0x00FFFFFF));
                graphics.fillPath(brush, path);
                brush.delete();
                path.delete();
                
                graphics.restore();
            }
        }
        
        for(i=1; #game.particles) {
            var p = game.particles[i];
            var screenY = toScreenY(p.y);
            var alpha = math.floor(p.alpha * 255);
            
            graphics.save();
            graphics.translateTransform(p.x, screenY);
            graphics.rotateTransform(p.rotation * 57.3);
            
            var brush = gdip.solidBrush((alpha << 24) | (p.color & 0x00FFFFFF));
            graphics.fillRectangle(brush, ::RECTF(-p.size/2, -p.size/2, p.size, p.size));
            brush.delete();
            
            graphics.restore();
        }
        
        var layoutRect = ::RECTF(0, 0, w, h);
        
        var scoreFont = family.createFont(28, 1, 3/*_UnitPoint*/);
        var scoreBounds = graphics.measureString(tostring(game.score), scoreFont, layoutRect, format);
        
        var shadowBrush = gdip.solidBrush(0x40000000);
        graphics.drawString(tostring(game.score), scoreFont, ::RECTF(2, 20, w, scoreBounds.height), format, shadowBrush);
        shadowBrush.delete();
        
        var scoreBrush = gdip.solidBrush(0xFFFFFFFF);
        graphics.drawString(tostring(game.score), scoreFont, ::RECTF(0, 18, w, scoreBounds.height), format, scoreBrush);
        scoreBrush.delete();
        scoreFont.delete();
        
        var levelFont = family.createFont(12, 0, 3/*_UnitPoint*/);
        var levelText = "第 " ++ game.level ++ " 层";
        var levelBounds = graphics.measureString(levelText, levelFont, layoutRect, format);
        var levelBrush = gdip.solidBrush(0x99FFFFFF);
        graphics.drawString(levelText, levelFont, ::RECTF(0, 18 + scoreBounds.height + 5, w, levelBounds.height), format, levelBrush);
        levelBrush.delete();
        levelFont.delete();
        
        if(game.perfectCount >= 2) {
            var comboScale = 1 + math.sin(time.tick() / 150) * 0.1;
            var comboFont = family.createFont(16 * comboScale, 1, 3/*_UnitPoint*/);
            var comboText = "✨ 完美 x" ++ game.perfectCount ++ " ✨";
            var comboBounds = graphics.measureString(comboText, comboFont, layoutRect, format);
            var comboBrush = gdip.solidBrush(0xFFFFD700);
            graphics.drawString(comboText, comboFont, ::RECTF(0, 18 + scoreBounds.height + levelBounds.height + 15, w, comboBounds.height), format, comboBrush);
            comboBrush.delete();
            comboFont.delete();
        }
        
        if(game.state == STATE_GAMEOVER) {
            var maskBrush = gdip.solidBrush(0xA0000000);
            graphics.fillRectangle(maskBrush, rc);
            maskBrush.delete();
            
            var titleFont = family.createFont(36, 1, 3/*_UnitPoint*/);
            var titleBounds = graphics.measureString("游戏结束", titleFont, layoutRect, format);
            var titleBrush = gdip.solidBrush(0xFFe74c3c);
            graphics.drawString("游戏结束", titleFont, ::RECTF(0, h*0.32, w, titleBounds.height), format, titleBrush);
            titleBrush.delete();
            titleFont.delete();
            
            var resultFont = family.createFont(24, 1, 3/*_UnitPoint*/);
            var resultText = "得分: " ++ game.score;
            var resultBounds = graphics.measureString(resultText, resultFont, layoutRect, format);
            var resultBrush = gdip.solidBrush(0xFFFFFFFF);
            graphics.drawString(resultText, resultFont, ::RECTF(0, h*0.42, w, resultBounds.height), format, resultBrush);
            resultBrush.delete();
            resultFont.delete();
            
            if(game.score >= game.highScore && game.score > 0) {
                var newRecordFont = family.createFont(16, 0, 3/*_UnitPoint*/);
                var newRecordBounds = graphics.measureString("✨ 新纪录！", newRecordFont, layoutRect, format);
                var newRecordBrush = gdip.solidBrush(0xFFFFD700);
                graphics.drawString("✨ 新纪录！", newRecordFont, ::RECTF(0, h*0.50, w, newRecordBounds.height), format, newRecordBrush);
                newRecordBrush.delete();
                newRecordFont.delete();
            }
            
            var hintAlpha = math.floor(math.abs(math.sin(time.tick() / 400)) * 150 + 100);
            var hintFont = family.createFont(14, 0, 3/*_UnitPoint*/);
            var hintBounds = graphics.measureString("单击重新开始", hintFont, layoutRect, format);
            var hintBrush = gdip.solidBrush((hintAlpha << 24) | 0xFFFFFF);
            graphics.drawString("单击重新开始", hintFont, ::RECTF(0, h*0.62, w, hintBounds.height), format, hintBrush);
            hintBrush.delete();
            hintFont.delete();
        }
    }
    
    format.delete();
    family.delete();
}

winform.plus.onAnimation = function(state, beginning, change, timestamp, duration) {
    var w, h = getCanvasSize();
    var dpiX, dpiY =  winform.dpiScale(1,1);
    
    if(game.state == STATE_PLAYING && game.currentBlock) {
        var block = game.currentBlock;
        block.x = block.x + game.blockSpeed * game.blockDirection;
        
        if(block.x <= -block.width * 0.3) game.blockDirection = 1;
        elseif(block.x + block.width * 0.7 >= w) game.blockDirection = -1;
    }
    
    if(game.cutBlock) {
        game.cutBlock.vy = game.cutBlock.vy + 0.6 * dpiY;
        game.cutBlock.y = game.cutBlock.y + game.cutBlock.vy;
        game.cutBlock.x = game.cutBlock.x + game.cutBlock.vx;
        game.cutBlock.rotation = game.cutBlock.rotation + game.cutBlock.vx * 0.02;
        
        if(toScreenY(game.cutBlock.y) > h + 150) game.cutBlock = null;
    }
    
    var viewDiff = game.targetViewOffset - game.viewOffset;
    if(math.abs(viewDiff) > 0.5) {
        game.viewOffset = game.viewOffset + viewDiff * 0.08;
    }
    
    updateParticles();
    updateGlowEffects();
    
    return true;
}

winform.plus.onMouseDown = function(wParam, lParam) {
    if(game.state == STATE_START) initGame();
    elseif(game.state == STATE_PLAYING) placeBlock();
    elseif(game.state == STATE_GAMEOVER) initGame();
}

winform.plus.dlgCode = 4/*_DLGC_WANTALLKEYS*/;
winform.plus.onKeyDown = function(keyCode, lParam, repeat) {
    if(keyCode == 0x20/*_VK_SPACE*/) {
        if(game.state == STATE_START) initGame();
        elseif(game.state == STATE_PLAYING) placeBlock();
        elseif(game.state == STATE_GAMEOVER) initGame();
    }
}

winform.plus.startAnimation(16);
winform.plus.setFocus();

winform.show();
win.loopMessage();
```

