はじめに
本記事ではゲームSlayTheSpireのソースコードを探索し、ゲームの細かい仕様を調べる方法を解説します。以下のような読者を想定しています。
- 非プログラマー
- PCをそれなりに使える
- Steam版 SlayTheSpireをインストール済みである
- SlayTheSpireを熟知しており、プログラミングの知識の不足をSlayTheSpireの知識で補える(A20よりは楽だと思います)
準備
jad(Java逆コンパイラ)のダウンロード
こちらのサイトからjadをダウンロードし、適当なディレクトリ D:\Temporary
に配置してください。(このディレクトリは必要に応じて読み替えてください)
テキストエディタのインストール
逆コンパイルしたソースコードを読むためにテキストエディタをインストールしましょう。こだわりがなければサクラエディタあたりが無難かもしれません。
逆コンパイル
まずはSlayTheSpireのjarファイルを見つけ出しましょう。SteamでSlayTheSpireを右クリックし、管理 ⇒ ローカルファイルを閲覧 をクリックしてください。エクスプローラが起動し、SlayTheSpireのインストールディレクトリが表示されるはずです。その中に desktop-1.0.jar
というファイルがあるので、先ほどの作業用ディレクトリにコピーしてください。
次にそのファイルの拡張子をzipに変更し、右クリックで すべて展開
を選択します。展開先ディレクトリを聞かれるので D:\Temporary\desktop-1.0
を選択し、展開します。
次にコマンドプロンプトを起動します。Windowsキーを押しcmdと入力しエンターを押すことで起動できるはずです。
そうしたら
D:
cd Temporary
と打ち込みます。
画面に
D:\Temporary>
と表示されたら成功です。
次に
jad -s java -d src -r desktop-1.0\com\**\*.class
と打ち込みます。
すると D:\Temporary\src
配下に逆コンパイルされたSlay the Spireのソースコードが生成され、仕様調査の準備が整いました。
仕様の調査
ゴールの設定
それではさっそくソースコードを読み解いていきましょう。題材は攻撃時/被攻撃の効果の解決処理が良いでしょう。
皆さまご存じの通り、このゲームではバッファー、猛毒の仕込み、ブーツ、鳥居、静電放電、タングステンロッド等、攻撃時に発動する効果がたくさんあります。せっかくバッファーを張っていてタングステンロッドも持っていたのに、痛みの効果によってバッファーが剥がれてしまった。HP減少は発生していなかったのに何故!?おかげで心臓に負けちゃったよ、というような悲劇には事欠きません。
そのような事故を防ぐため、仕様の詳細な理解は必要不可欠です。さぁ始めましょう。
ダメージ計算の中枢部を探す
とっかかりはどこからでも良いのですが、まずは一番謎の多いタングステンロッドを入り口として調べていきましょう。
D:\Temporary\src
の中をファイル名Tungstenで検索するとD:\Temporary\src\com\megacrit\cardcrawl\relics\TungstenRod.java
というファイルが見つかります。これがどうやらタングステンロッドを表すクラスのようです。
それをテキストエディタで開いてみてください。以下のようなコードが見つかったはずです。
もしあなたが重度のSlayTheSpire中毒患者であれば、プログラミングに詳しくなくても書かれているメソッド名/変数名と計算式の内容から、これはタングステンロッドを表すコードで間違いなさそうだなという確信が得られると思います。
public int onLoseHpLast(int damageAmount)
{
if(damageAmount > 0)
{
flash();
return damageAmount - 1;
} else
{
return damageAmount;
}
}
それでは次に D:\Temporary\src
の中から、 onLoseHpLast
という文字列を含むファイルを検索してみましょう(テキストエディタの検索機能を使うと早いと思います)。
これにより、タングステンロッドの処理を呼び出している部分、すなわちSlayTheSpireにおけるダメージ計算の中枢部が見つけられるはずです。
D:\Temporary\src\com\megacrit\cardcrawl\characters\AbstractPlayer.java
というファイルが見つかりましたね?
ダメージ計算処理の解読
onLoseHPLast
を呼び出している箇所を上に辿っていくと、以下のような行に行き当たると思います。これがダメージ計算処理の中枢です。
public void damage(DamageInfo info)
{
label0:
{
boolean hadBlock;
label1:
{
label2:
{
int damageAmount = info.output;
hadBlock = true;
if(currentBlock == 0)
hadBlock = false;
if(damageAmount < 0)
damageAmount = 0;
if(damageAmount > 1 && hasPower("IntangiblePlayer"))
damageAmount = 1;
これを読み解いていけば、処理の順序がわかってくるはずです。
たとえばこのコードは何でしょうか。
if(info.owner == this)
{
for(Iterator iterator = relics.iterator(); iterator.hasNext();)
{
AbstractRelic r = (AbstractRelic)iterator.next();
damageAmount = r.onAttackToChangeDamage(info, damageAmount);
}
}
どうやら持っている全レリックのonAttackToChangeDamage
メソッドを呼び出しているようですが、onAttackToChangeDamage
とはいったい何なのでしょう。テキストエディタにてソースコードを検索してみれば何かわかるかもしれません。
■ '\<onAttackToChangeDamage\>' を 'D:/Temporary/src/com/megacrit' 以下の '*.java' から 文字コード:自動 で検索中...
"D:/Temporary/src/com/megacrit/cardcrawl/characters/AbstractPlayer.java"
1748: damageAmount = r.onAttackToChangeDamage(info, damageAmount);
1758: damageAmount = p.onAttackToChangeDamage(info, damageAmount);
"D:/Temporary/src/com/megacrit/cardcrawl/powers/AbstractPower.java"
342: public int onAttackToChangeDamage(DamageInfo info, int damageAmount)
"D:/Temporary/src/com/megacrit/cardcrawl/relics/AbstractRelic.java"
783: public int onAttackToChangeDamage(DamageInfo info, int damageAmount)
"D:/Temporary/src/com/megacrit/cardcrawl/relics/Boot.java"
28: public int onAttackToChangeDamage(DamageInfo info, int damageAmount)
'\<onAttackToChangeDamage\>' を 'D:/Temporary/src/com/megacrit' 以下の '*.java' から 文字コード:自動 で検索しました。
検索したファイル拡張子:java
5 行がマッチしました。( 0.79 sec.)
はい、SlayTheSpireプレイヤーの方は一瞬で理解できましたね。みなさんご存じブーツです。
どうやらこのonAttackToChangeDamage
は、バニラでは事実上のブーツ専用メソッドになっている模様。Modまで含めれば、ブーツ以外のレリックもこのメソッドを使っているかもしれません。
このようにして順々に調べていけば全貌が明らかになってくるはず…。
解析結果まとめ
以下がそのようにして調べた結果を日本語のコメントにて書き加えたものです。
貴方に良いSlayTheSpireライフを。
public void damage(DamageInfo info)
{
label0:
{
boolean hadBlock;
label1:
{
label2:
{
// 初期ダメージ
int damageAmount = info.output;
hadBlock = true;
if(currentBlock == 0)
hadBlock = false;
if(damageAmount < 0)
damageAmount = 0;
if(damageAmount > 1 && hasPower("IntangiblePlayer"))
damageAmount = 1;
// ブロックによるダメージ軽減
damageAmount = decrementBlock(info, damageAmount);
if(info.owner == this)
{
for(Iterator iterator = relics.iterator(); iterator.hasNext();)
{
// レリックによるダメージ変更処理(例:ブーツ)
AbstractRelic r = (AbstractRelic)iterator.next();
damageAmount = r.onAttackToChangeDamage(info, damageAmount);
}
}
if(info.owner != null)
{
for(Iterator iterator1 = info.owner.powers.iterator(); iterator1.hasNext();)
{
// バフによるダメージ変更処理(バニラでは対応効果なし)
AbstractPower p = (AbstractPower)iterator1.next();
damageAmount = p.onAttackToChangeDamage(info, damageAmount);
}
}
for(Iterator iterator2 = relics.iterator(); iterator2.hasNext();)
{
// レリックによるダメージ変更処理(バニラでは対応効果なし)
AbstractRelic r = (AbstractRelic)iterator2.next();
damageAmount = r.onAttackedToChangeDamage(info, damageAmount);
}
for(Iterator iterator3 = powers.iterator(); iterator3.hasNext();)
{
// バフによるダメージ変更処理(例:心臓のターン内ダメージ上限, バッファー)
AbstractPower p = (AbstractPower)iterator3.next();
damageAmount = p.onAttackedToChangeDamage(info, damageAmount);
}
if(info.owner == this)
{
// レリックによる攻撃時特殊効果(バニラでは対応効果なし)
AbstractRelic r;
for(Iterator iterator4 = relics.iterator(); iterator4.hasNext(); r.onAttack(info, damageAmount, this))
r = (AbstractRelic)iterator4.next();
}
if(info.owner != null)
{
// 攻撃者側のバフによる攻撃時特殊効果(例:猛毒の仕込み)
AbstractPower p;
for(Iterator iterator5 = info.owner.powers.iterator(); iterator5.hasNext(); p.onAttack(info, damageAmount, this))
p = (AbstractPower)iterator5.next();
for(Iterator iterator6 = powers.iterator(); iterator6.hasNext();)
{
// バフによる攻撃時特殊効果(例: 静電放電, 炎の障壁, マッドグレムリンの怒りによる攻撃力上昇)
AbstractPower p = (AbstractPower)iterator6.next();
damageAmount = p.onAttacked(info, damageAmount);
}
for(Iterator iterator7 = relics.iterator(); iterator7.hasNext();)
{
// レリックによる攻撃時特殊効果(例: 鳥居)
AbstractRelic r = (AbstractRelic)iterator7.next();
damageAmount = r.onAttacked(info, damageAmount);
}
} else
{
logger.info("NO OWNER, DON'T TRIGGER POWERS");
}
for(Iterator iterator8 = relics.iterator(); iterator8.hasNext();)
{
// レリックによる最終ダメージ変更処理(例: タングステンロッド)
AbstractRelic r = (AbstractRelic)iterator8.next();
damageAmount = r.onLoseHpLast(damageAmount);
}
lastDamageTaken = Math.min(damageAmount, currentHealth);
// ここまでの処理でダメージが0になっていたら処理を中断
if(damageAmount <= 0)
break label1;
for(Iterator iterator9 = powers.iterator(); iterator9.hasNext();)
{
// バフによるHP減少時処理(バニラでは対応効果なし)
AbstractPower p = (AbstractPower)iterator9.next();
damageAmount = p.onLoseHp(damageAmount);
}
// レリックよるHP減少時処理(バニラでは対応効果なし)
AbstractRelic r;
for(Iterator iterator10 = relics.iterator(); iterator10.hasNext(); r.onLoseHp(damageAmount))
r = (AbstractRelic)iterator10.next();
// バフによるHP減少時処理(例:プレートアーマー効果減少)
AbstractPower p;
for(Iterator iterator11 = powers.iterator(); iterator11.hasNext(); p.wasHPLost(info, damageAmount))
p = (AbstractPower)iterator11.next();
// レリックによるHP減少時処理(例:ルーニックキューブ, 自己形成粘土)
AbstractRelic r;
for(Iterator iterator12 = relics.iterator(); iterator12.hasNext(); r.wasHPLost(damageAmount))
r = (AbstractRelic)iterator12.next();
if(info.owner != null)
{
// 攻撃者側のバフによるHP減少時特殊効果(例:刺創の本による苦痛の一刺し)
AbstractPower p;
for(Iterator iterator13 = info.owner.powers.iterator(); iterator13.hasNext(); p.onInflictDamage(info, damageAmount, this))
p = (AbstractPower)iterator13.next();
}