カードゲームの(オンライン)テストプレイと入稿用ファイル作成を自動化する

# 概要
* 初めてアナログゲームを作るにあたり、「テストプレイ用」と「印刷所入稿用」のデータ作成をスクリプトで半自動化した
* テストプレイは[ユドナリウム](https://udonarium.app/)を使ってオンラインで何度か行った。テストプレイの度にデータを修正するので、その都度カード画像を書き出す必要があった
* 印刷所への入稿は印刷所指定のIllustratorテンプレートファイルでカード1枚につき1ファイル作成する必要があった
* それら2種類のファイルを1つのマスターファイル（TSV）から繰り返し作成する為の手順の備忘録

# 作るゲームについて
* 「推しの名前を叫びたい」という、カードに書かれた属性を満たすキャラクターを宣言するゲーム
<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/17941/3651688a-83a6-7948-fc57-1c09acdba4b1.png">


* 「メガネをかけてる」「ライバルがいる」「髪が青/緑」みたいなキャラクターの特徴が書かれたカードが150枚入ってるいる
* カードには「カテゴリ」「タイトル（キャラクターの特徴）」「補足説明（箇条書き）」「難易度（★）」という4つの要素がある
* 今回はマスターデータからこれらの要素を画像ファイル（テストプレイ）、.aiファイル（入稿）として書き出したい
* （宣伝）ゲームマーケット2023秋 2023年12月9日(土)にて頒布予定
* 詳しいルール等については[Xアカウント](https://twitter.com/daydreamori)にて告知予定です！
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr"><a href="https://twitter.com/hashtag/%E3%82%B2%E3%83%A0%E3%83%9E2023%E7%A7%8B?src=hash&amp;ref_src=twsrc%5Etfw">#ゲムマ2023秋</a> 用ブースカットを入稿完了！<br>徐々にこのアカウントも動かしていきます <a href="https://t.co/6vSct8xmCQ">pic.twitter.com/6vSct8xmCQ</a></p>&mdash; Daydream@ゲムマ2023秋(土) (@daydreamori) <a href="https://twitter.com/daydreamori/status/1701179913628438843?ref_src=twsrc%5Etfw">September 11, 2023</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

# マスターファイルの準備
```
...
自信家	そういう描写や言及があればOK	肩書・内面	2	1
自信が無い	そういう描写や言及があればOK	肩書・内面	2	1
マザコン/ファザコン/ブラコン/シスコン		肩書・内面	2	1
ギャル/チャラ男	周囲にそう思われてるだけでもOK	肩書・内面	3	1
優等生/真面目	そういう描写や言及があればOK	肩書・内面	2	1
...
```
* TSVで「タイトル」「説明」「カテゴリ」「難易度（★）」の順に並べただけのファイル
* Googleスプレッドシートで管理してTSVとして書き出す

# 方針
* 最初は「Imagemagickを使って、ユドナリウム用PNG画像とXMLを書き出すRubyスクリプト」と「Illustratorファイルを書き出すイラレscript」の2つを使っていた
```mermaid
flowchart LR
  DN[(マスターファイル)]-->|Ruby with ImageMagick| f1[ユドナリウム用PNG,XML]
  DN[(マスターファイル)]-->|イラレscript| f2[イラレファイル]
```
* しかしカードの要素やレイアウトを試行錯誤する度に2つのスクリプトを修正するのは面倒なので、まずイラレファイルを生成し、それを元にPNG画像を書き出すフローに変更。ユドナリウム用XML生成は別途元のRubyスクリプトを使う（こちらはカードの要素やレイアウトの影響を受けないので修正の必要が無い）
```mermaid
flowchart LR
  DN[(マスターファイル)]-->|イラレscript1| f2[イラレファイル]
  f2-->|イラレscript2| f1[ユドナリウム用PNG]
```
```mermaid
flowchart LR
  DN[(マスターファイル)]-->|Ruby| f2[ユドナリウム用XML]
```

# マスターファイルからIllustratorデータを作る
### テンプレートファイルを作る
* 今回は萬印堂さんに印刷をご依頼するので、萬印堂さん指定のIllustratorテンプレートファイルを[こちら](https://www.mnd.co.jp/template/)からDLする
* そのテンプレートファイルを元に、当ゲーム用テンプレートファイルを作る
* 以下のようにテキストフレームを配置
  * スクリプトでテキストフレームの生成や各種設定を行えばこの工程は不要だが、非直感的でわざわざスクリプトでやる意味が無い
<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/17941/17b53aa8-4b92-b5f3-bfad-d33be7c0e09b.png">

### Illustratorファイル生成スクリプト
```js
app.userInteractionLevel = UserInteractionLevel.DONTDISPLAYALERTS;
var csvPath = "/{任意のディレクトリ}/card_csv.csv";
var csvFile = new File(csvPath);
csvFile.open('r');

var templatePath = "/{任意のディレクトリ}/template_card.ai";
var titleIndex = 2;
var descIndex = 1;
var categoryIndex = 3;
var starIndex = 0;

var i = 0;
while (!csvFile.eof) {
  i++;
  var line = csvFile.readln();
  var values = line.split("\t");

  var templateFile = new File(templatePath);
  app.open(templateFile);

  var doc = app.activeDocument;

  var category = values[2];
  var categoryItem = app.activeDocument.textFrames[categoryIndex];
  categoryItem.contents = category;

  var starNum = parseInt(values[3]);
  var starText = "";
  for (var j = 0; j < starNum; j++) {
    starText += "★";
  }
  var starItem = app.activeDocument.textFrames[starIndex];
  starItem.contents = starText;

  var textItem = app.activeDocument.textFrames[titleIndex];
  textItem.contents = values[0];

  var desc = app.activeDocument.textFrames[descIndex];
  desc.contents = values[1];

  var outputFile = new File("/{任意のディレクトリ}/tmp_ai/card_"+i+"_front.ai");
  doc.saveAs(outputFile);
  doc.close();
}
csvFile.close();
```
* jsでDOMを操作するようなノリで、要素を取得してフィールドにセットすればOK
* 実際はカードのカテゴリによってテンプレートファイルを変えたり、「補足説明」を箇条書きに変えたりといった細かな処理を足している
* 最終的な入稿用データとしては、機械的なルールでは処理できない部分（フォントサイズの細かな調整等）を手動で行った

# ユドナリウム用データを作る
### 最終的に作りたいもの
* ユドナリウムにはルームデータをzipでインポート/エクスポートできる仕組みがある
* ルームデータはXMLで管理されているので、そのXMLと、XMLから参照される画像データを生成できればルームデータが作れる
* こんな感じで8つの山札が置かれたルームを生成したい（テーブル画像等は後から設定すればOK）
<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/17941/e49a4224-00b9-1198-861c-52bb1058ee65.png">

### カード画像を作る
* 「方針」の通り、まず入稿用IllustratorファイルからPNG画像を書き出す
* 入稿用ファイルはカード画像以外の要素が色々あるので、「お客様の作業領域」というレイヤーのみをPNG書き出しする
* そのままだと解像度が低いので3倍にスケールさせる
```js
var options = new ExportOptionsPNG24();
options.antiAliasing = true;
options.transparency = true;
options.horizontalScale = 300;
options.verticalScale = 300;

var inputDir = "/{任意のディレクトリ}/fix/";
var outputDir = "/{任意のディレクトリ}/v1/";
for (var i = 1; i <= 150; i++) {
  var file = new File(inputDir + "card_"  + i + "_front.ai");
  var doc = app.open(file);

  for (var j = 0; j < doc.layers.length; j++) {
    doc.layers[j].visible = false;
  }
  var targetLayer = doc.layers.getByName("お客様の作業領域");
  targetLayer.visible = true;

  var outputFile = new File(outputDir + "card_" + i + ".png");
  doc.exportFile(outputFile, ExportType.PNG24, options);
  doc.close(SaveOptions.DONOTSAVECHANGES);
}
```
<details><summary>room_template.xml</summary>

```xml
<?xml version="1.0" encoding="UTF-8"?>
<room>
  <game-table name="最初のテーブル" width="20" height="15" gridSize="50" imageIdentifier="testTableBackgroundImage_image" backgroundImageIdentifier="imageIdentifier" backgroundFilterType="" selected="false" gridType="0" gridColor="#000000e6"></game-table>

  <card-stack location.name="table" location.x="0.0" location.y="452.8333688050696" posZ="0" rotate="0" zindex="5" owner="" isShowTotal="true">
    <data name="card-stack">
      <data name="image">
        <data type="image" name="imageIdentifier"></data>
      </data>
      <data name="common">
        <data name="name">名前</data>
      </data>
      <data name="detail"></data>
    </data>
    <node name="cardRoot">
      __card1__
    </node>
  </card-stack>
  <card-stack location.name="table" location.x="300.0" location.y="452.8333688050696" posZ="0" rotate="0" zindex="6" owner="" isShowTotal="true">
    <data name="card-stack">
      <data name="image">
        <data type="image" name="imageIdentifier"></data>
      </data>
      <data name="common">
        <data name="name">年齢</data>
      </data>
      <data name="detail"></data>
    </data>
    <node name="cardRoot">
      __card2__
    </node>
  </card-stack>
  <card-stack location.name="table" location.x="600.0" location.y="452.8333688050696" posZ="0" rotate="0" zindex="7" owner="" isShowTotal="true">
    <data name="card-stack">
      <data name="image">
        <data type="image" name="imageIdentifier"></data>
      </data>
      <data name="common">
        <data name="name">特徴</data>
      </data>
      <data name="detail"></data>
    </data>
    <node name="cardRoot">
      __card3__
    </node>
  </card-stack>
    <card-stack location.name="table" location.x="0.0" location.y="660" posZ="0" rotate="0" zindex="5" owner="" isShowTotal="true">
      <data name="card-stack">
        <data name="image">
          <data type="image" name="imageIdentifier"></data>
        </data>
        <data name="common">
          <data name="name">名前</data>
        </data>
        <data name="detail"></data>
      </data>
      <node name="cardRoot">
        __card4__
      </node>
    </card-stack>
    <card-stack location.name="table" location.x="300.0" location.y="660" posZ="0" rotate="0" zindex="6" owner="" isShowTotal="true">
      <data name="card-stack">
        <data name="image">
          <data type="image" name="imageIdentifier"></data>
        </data>
        <data name="common">
          <data name="name">年齢</data>
        </data>
        <data name="detail"></data>
      </data>
      <node name="cardRoot">
        __card5__
      </node>
    </card-stack>
    <card-stack location.name="table" location.x="600.0" location.y="660" posZ="0" rotate="0" zindex="7" owner="" isShowTotal="true">
      <data name="card-stack">
        <data name="image">
          <data type="image" name="imageIdentifier"></data>
        </data>
        <data name="common">
          <data name="name">特徴</data>
        </data>
        <data name="detail"></data>
      </data>
      <node name="cardRoot">
        __card6__
      </node>
    </card-stack>
      <card-stack location.name="table" location.x="0.0" location.y="960" posZ="0" rotate="0" zindex="5" owner="" isShowTotal="true">
        <data name="card-stack">
          <data name="image">
            <data type="image" name="imageIdentifier"></data>
          </data>
          <data name="common">
            <data name="name">名前</data>
          </data>
          <data name="detail"></data>
        </data>
        <node name="cardRoot">
          __card7__
        </node>
      </card-stack>
      <card-stack location.name="table" location.x="300.0" location.y="960" posZ="0" rotate="0" zindex="6" owner="" isShowTotal="true">
        <data name="card-stack">
          <data name="image">
            <data type="image" name="imageIdentifier"></data>
          </data>
          <data name="common">
            <data name="name">年齢</data>
          </data>
          <data name="detail"></data>
        </data>
        <node name="cardRoot">
          __card8__
        </node>
      </card-stack>
</room>
```
</details>

### ルームデータを作る
* マスターファイルに従ってXMLを作る
* ドキュメント等は無いのでエクスポートしたファイルを見てみた所、ユドナリウムでは「data.xml」というファイルにルームデータを記述すれば良いようなので、そのようにzipファイルを作る
* 画像アセットをzipに含める方法は分からなかった（そもそもできるのかな？）ので、画像はクラウドストレージに置いてそのURLをXMLに記述する
* 後はユドナリウムを開いて「ZIP読み込み」すればOK！