🦑

discord.jsでnode-canvasを利用したゲーム募集コマンド

2023/09/20に公開

Splatoon3のコミュニティサーバーで使用しているDiscord Botの開発をお手伝いしています。
その中で得た知見としてnode-canvasを使用した画像の動的生成について貼っておきます。

動作例

募集時にステージ情報などをAPIから取得して画像を作成したり、ボタンを押して参加状況が変わると画像を再生成しています。

コマンドからのインタラクション処理

ここではSplatoon3のサーモンランの募集の一部について抜粋して紹介します。

interactionから各情報を取得し、splatoon3.inkのAPIにステージ情報やブキ情報の問い合わせを行い、そのデータをキャンバス作成を行う関数に投げます。

    let recruitBuffer;
    let ruleBuffer;
    if (type === RecruitType.SalmonRecruit) {
        recruitBuffer = await recruitSalmonCanvas(
            RecruitOpCode.open,
            recruitNum,
            count,
            recruiter,
            attendee1,
            attendee2,
            null,
            condition,
            channelName,
        );
        ruleBuffer = await ruleSalmonCanvas(await getSalmonData(schedule, 0));
    } else if (type === RecruitType.BigRunRecruit) {
        recruitBuffer = await recruitBigRunCanvas(
            RecruitOpCode.open,
            recruitNum,
            count,
            recruiter,
            attendee1,
            attendee2,
            null,
            condition,
            channelName,
        );
        ruleBuffer = await ruleBigRunCanvas(schedule);
    } else if (type === RecruitType.TeamContestRecruit) {
        recruitBuffer = await recruitSalmonCanvas(
            RecruitOpCode.open,
            recruitNum,
            count,
            recruiter,
            attendee1,
            attendee2,
            null,
            condition,
            channelName,
            'コンテスト',
        );
        ruleBuffer = await ruleSalmonCanvas(await getTeamContestData(schedule, 0));
    }

バッファーをチャンネルに投稿

作成されたCanvasのCanvasRenderingContext2DをAttachmentBuilderで送信可能な画像に変換し、channel.sendのファイルオプションとして追加します。

 const recruit = new AttachmentBuilder(recruitBuffer, {
        name: 'ikabu_recruit.png',
 });
 const rule = new AttachmentBuilder(ruleBuffer, { name: 'schedule.png' });

 const image2Message = await recruitChannel.send({ files: [rule] });
 const sentMessage = await recruitChannel.send({
     content:
	 mention + ` ボタンを押して参加表明するでし!\n${getMemberMentions(recruitNum, [])}`,
 });

 // 募集文を削除してもボタンが動くように、bot投稿メッセージのメッセージIDでボタンを作る
 const deleteButtonMsg = await recruitChannel.send({
     components: [recruitDeleteButton(sentMessage, image1Message, image2Message)],
 });    

キャンバスの作成例

  • Canvasの作成
const recruitCanvas = Canvas.createCanvas(720, 550);
const recruitCtx = recruitCanvas.getContext('2d');
  • 四角形の作成
// 角丸の四角形を作成する関数
export function createRoundRect(
    ctx: CanvasRenderingContext2D,
    x: number,
    y: number,
    width: number,
    height: number,
    radius: number,
) {
    ctx.moveTo(x + radius, y);
    ctx.lineTo(x + width - radius, y);
    ctx.arcTo(x + width, y, x + width, y + radius, radius);
    ctx.lineTo(x + width, y + height - radius);
    ctx.arcTo(x + width, y + height, x + width - radius, y + height, radius);
    ctx.lineTo(x + radius, y + height);
    ctx.arcTo(x, y + height, x, y + height - radius, radius);
    ctx.lineTo(x, y + radius);
    ctx.arcTo(x, y, x + radius, y, radius);
    ctx.closePath();
}

// 使用例
    createRoundRect(recruitCtx, 1, 1, 718, 548, 30);
    recruitCtx.fillStyle = '#2F3136';
    recruitCtx.fill();
    recruitCtx.strokeStyle = '#FFFFFF';
    recruitCtx.lineWidth = 4;
    recruitCtx.stroke();
  • 画像の読み込み
const salmonIcon = await Canvas.loadImage('icon_url_here');
recruitCtx.drawImage(salmonIcon, 22, 32, 82, 60);

募集関連のコード

https://github.com/shngmsw/ikabu/tree/3b72edc0addd4d9347a4778e1701d1d0e0f83a12/src/app/feat-recruit

Discussion