Stone Story RPG で利用できる Stonescript のまとめ
最近 Stone Story RPG という放置系のゲームを少しやってまして。 普通にプレイするだけだとまぁ普通によくある放置ゲーなんですが、ゲーム内言語の Stonescript に手を出したら案外面白い。 しかしながら、よく使う処理系のまとまった日本語の記事が見つからなかったので備忘録がてら書き残しておくことにしました。
基本的にはプログラムを少しでも齧ったことのある人向けの話になります。
今回は Script の解説と、実際に使える簡単なスクリプトの紹介のみです。
時間ができたら実用編も書いておきます。
iOS (無料)
apps.apple.comAndroid(無料)
Steam(3400円)
Stone Story RPG とは
Stone Story RPG(以下、Stone Story)は、いわゆる放置ゲーの一種で、放置をしてアイテムを集めて強化してより難しいダンジョンへ挑んでいくタイプのゲームです。
進行中に装備の変更等、手動での変更が可能で、雑魚的とボスとで立ち回りを変えるといったことが可能です。
また、他の放置ゲーと一線を画すのが、キャラクターの行動を Stonescript というスクリプト言語によってそのあたりの操作を自動化することができる点にあります。
Stone Story の放置関連仕様
今回の説明に関わる部分の仕様についてザックリ触れていきます。
Stone Story ではゲームを進めていくと、二つの放置モードが追加されます。
一つはアプリケーション起動中にキャラクターが死ぬか、プレイヤーが任意のタイミングで終了させるまで指定したダンジョン内をループするモード。
もう一つは指定したダンジョンをこれまでの実績を元にアプリケーションを閉じた状態でも進行するモード。
前者では経験値が手に入り、後者では経験値が手に入りません。*1
後者は前者のモードでダンジョンをクリアした時間、ダンジョン開始時とクリア時のHPの差分、使用したポーション、などの履歴を何周分か見て、それを元に何時間で何ループするか、資源をどれだけ消費するか、何周で死ぬかが計算され、放置時の報酬に反映されます。
つまり、通常のプレイでいかに早く、いかにHPを減らさずに、いかにポーションを使わずに*2クリアするかが重要になってきます。
ではどうやってそれを実現するかというと、そこで自動化するための機能であるところの Stonescript が役立つわけです。
最終的にはタイムアタック的にスクリプトを微調整して楽しむゲームだと思っています。
Stonescript とは
Stone Story を進めてマインドストーンというアイテムを手に入れると使えるようになるゲーム内スクリプト言語です。
これを使うことでゲーム内情報をゲーム画面に出力したり、条件指定で装備を付け外しができるようになります。これができると何が嬉しいのかというと、人間ではおよそ不可能な速度で敵に合わせた装備に切り替えることができるようになります。
Android だと最新の Snapdragon 8Gen2 を積んだ端末でも重い処理系があるので注意。*3
iOS、Android では手入力は非常に面倒なのでPCで作業、またはメモ帳で作業してコピー&ペースト推奨。
Stonescript の入力欄を長押しするとコピーなどのメニューが表示されるのでそれを使います。この際、ソフトウェアキーボードが立ち上がっていると反応しないので、キーボードは閉じてから長押ししましょう。
おおまかな仕様
記述されたスクリプトは1フレームに1回実行されます。
変数は1フレーム目に初期化され、2フレーム目以降は再利用されます。
ユーザーが利用できる型は主に 真偽
数値
文字列
の3種で、他の型については用意されたオブジェクト経由で触れることになります。
条件分岐やループ処理も記述可能。関数宣言も可能。ブロックの範囲は Python のようにインデントで管理します。
1行当たりの最大文字数が決まっているため、適宜改行して継続処理をする必要があります。
比較演算子の内 =
と !
の処理系が少し特殊で、フィルターのような役割を持っています。
最大文字数は2000文字程度らしい(未確認)
言語仕様
それでは早速言語の仕様に入りたいと思います。と言っても、網羅的には解説しないので、詳しくは公式サイトをご確認ください。
以下では攻略に有用となる処理系を主として解説していきます。
基本構文
セミコロンレスな言語で、インデントベースでブロックが決まる Python のような構文。
1行あたり48文字まで利用可。それ以上書きたい場合は継続キーワード ^
を利用して改行して入力する。
インデントに使う文字は半角スペース、全角スペース、タブどれでも良いと思われますが、基本的には半角スペースを使うのが良いでしょう。
1インデントあたりの文字数に関しては自由ですが、本記事では2文字で統一します。
サンプルコード
// 使う装備を定義しておく var crossbow = "aether heavy crossbow D" // 地獄の鉱山である ?loc=mine // ステージのボスである、かつ攻撃可能な状態かどうか ?foe=boss&foe=bronze&foe.state>1 // 遠距離攻撃でしか当たらない状態かどうか ?foe.state=2|(foe.state=33&foe.time>190) ^|(foe.state=32&foe.time<15) equip @crossbow@ // ボスの攻撃をいい感じに回避する :?foe.state=32&foe.time=18 equipL mind // 以下略
これだけ見てもわかる通り、相手の状態や、その状態になってからの時間経過を指定して武器を切り替えたりできるので、人間にはおよそ実現不可能な動きができることが見て取れるかと思います。
画面出力
UIを自分なりにアレンジしたりするのもいいですが、自分の場合は主にデバッグ用途やスクリプト更新の参考用に使用します。
左上を起点とした出力をしたい場合は以下。
x が横軸、yが縦軸。x:0, y:0 が画面左上。input には文字列とステートメントが利用可能。
>`x,y,input
起点は色んなパターンがあるのでそれらは公式サイトを見てください。
実際の利用時はこんな感じ
>`0,1,foe.state: @foe.state@
コメント
//
/* */
//
が行コメント。
/* */
がブロックコメント。
// コメント
/* ブロック コメント */
変数宣言
var
1フレーム目に宣言時の値に初期化。以降は再利用。
var x = False var y = 1 var z = "string" >`0,0,x:@x@ @y@ @z@ x = True y = 2 z = "String"
1フレーム目は False 1 string
と表示され、2フレーム目以降は変数が再利用されるので True 2 String
と表示されます。
装備する
equip
equipR
equipL
equip
は両手持ちの装備用。
文字列をスペース区切りで8個まで指定できる。
部分一致で装備してくれるので割と雑に指定ができる。
例えば
equipL triskel stone
とも記述できるが、面倒な場合は
equipL tri
でも装備できます。
また、変数で装備を指定したい場合は @variable@
とすることで可能です。
var mainEquipment = "aether long sword D" equipL @mainEquipment@
何かを使う
activate
特定の文字列を渡すことで様々なものを使うことができる。
P
potion
→ 現在瓶の中に入っているポーションを使う
L
left → 左手に持った装備のスキルを使う
R
right → 右手に持った装備のスキルを使う
activate potion
特定の装備セットを装備する
loadout
画面右にある装備セットを装備する。上から順に1, 2, 3, 4, 5 を指定することで切り替えられる。
loadout 1
条件分岐
?
:
いわゆる if
や else
の事。?
と :
で表現されます。
// 一番近い敵がボスの時 ?foe = boss equip nanika tsuyoi sword : equip splash dekiru sword
else if
や elif
や elsif
や elseif
のようなことをしたい場合は :
の後に ?
を繋げることで実現できます。
// 一番近い敵との距離が20よりも遠いとき ?foe.distance > 20 equipL triskel stone equipR vigor shield // 一番近い敵との距離が20以下で11よりも遠いとき :?foe.distance > 11 equipL nanika tsuyoi sword equipR dashing_shield // 一番近い敵との距離が11以下のとき : equipL nanika tsuyoi sword equipR chotto tsuyoi sword
前行の継続
前述の通り1行当たり48文字までしか使えないので、それ以上書きたい場合は ^
を行頭に置いて2行目以降に記述する
?foe.id = skeleton ^&foe ! boss equipR vigor sword
インデントが上がっている状態で使うときは対象のブロックのインデントに合わせる。
?loc = halls ?foe.id = skeleton ^& foe ! boss equipR vigor long sword
等値、非等値比較
=
!
等値、非等値比較、と書きましたが、これはそう見せかけた文字列のフィルタリング機能です。
かなり重要かつ使うには多少コツが要るのでちゃんと解説します。
最初のサンプルコードのこの行を見てみます。
// 地獄の鉱山である ?loc=mine // ステージのボスである、かつ攻撃可能な状態かどうか ?foe=boss&foe=bronze&foe.state>1
まず ?loc=mine
から。
>@loc@
で出力してみると bronze_mine Boiling Mine ☆3
とか表示されると思います。
=
はフィルタリングなので、bronze_mine Boiling Mine ☆3
に指定した文字列が含まれているか、を示すことになります。
この場合、bronze_mine
の内 mine
に引っかかるため、この記述で成立するのです。
同様に、foe
は多岐に渡る情報が文字列として含まれており、その中に指定した文字が含まれていれば成立するわけです。
具体的に地獄の鉱山のボスであるところの銅のガーディアンを >@foe@
で確認すると以下の文字列が表示されます。*4
bronze_guardian Bronze Guardian boss Fire unpushable machine phase1 immune_to_stan immune_to_debuaff_chill
foe=boss
で指定している通り、この文字列の中には boss が含まれています。また同様に foe=bronze
で指定している通り、bronze が含まれています。*5
!
はこれとは逆で、指定した文字列が含まれなかったら、という条件となります。
スクリプトを改善するために
実際にスクリプトを書く際には先ほどのように、一旦 >@foe@
のような形で出力してどのような文字列が含まれているかを確認して、それから条件文を考える、といった作業を行います。
先ほどの文字列を簡単に読み解いてみると以下のようになります。
文字列 | 推定される意味 |
---|---|
bronze_guardian | IDのようなもの。おそらくこれは一意になるので厳密に指定する場合はこれがよさそう。 |
Bronze Guardian | 英語の表示名?半角スペースが混ざるし大文字小文字混在なのでスクリプトには使いにくい |
boss | ボスである |
Fire | 炎属性である |
unpushable | ノックバックしない? |
machine | 機械属性? |
phase1 | 複数フェーズある内の1つ目のフェーズである |
immune_to_stan | スタン耐性持ち |
immune_to_debuaff_chill | 氷のデバフ耐性持ち |
こんな感じで読み解いていくことで、こいつは氷デバフ耐性以外のデバフ耐性は無いから他のデバフは積極的にあてるようにスクリプト組もう、などができるようになっていきます。
その他にも、スクリプトでは非常に多くの情報が提供されています。
その内、敵関連に絞ってもこれだけの情報量があります。
foe foe.id foe.name foe.damage foe.distance foe.count foe.GetCount(int) foe.hp foe.maxhp foe.armor foe.maxarmor foe.buffs.count foe.buffs.string foe.debuffs.count foe.debuffs.string foe.state foe.time foe.level
これらを >
を使って画面上に出力したり、特定のタイミングで変数に入れて画面上に出力してみたりしたものを動画を撮って、このタイミングでこれをしたらよさそう、だとかこのスキルはこのタイミングで発動できるのか、とかを地道に調べていくことで、条件分岐を探っていくことで、スクリプトが洗練されていくかと思います。
画面上に出力するにあたって、たまに困るのが文字数があふれたり、長くなりすぎて必要な情報に重なってしまったりして困ったりすることがあるかと思います。その際は以下のスクリプトを利用することで対応可能です。
var array = string.Break(foe, 20) for i = 0..array.Count() - 1 >`0,@i@,@array[i]@
string.Break
は指定した文字列を指定した文字数毎に分割してくれる関数になります。半角スペースが含まれる場合、いい感じに分割してくれるので途中で改行されて読みにくくなるということはほとんどないので便利に利用できます。
for に関してはプログラムに触れたことがあるなら何となくわかると思われるので説明を省きます。
先ほどの例で挙げた文字列の場合、以下のような表示になります。
bronze_guardian Bronze Guardian boss Fire unpushable machine phase1 immune_to_stan immune_to_debuaff_chi ll
このように、改行が含まれない状態で20文字を超えた場合のみ変なタイミングで改行されますが、そうでない限り半角スペースのところで折り返してくれていることがわかると思います。
他に、foe 関連で情報源として自分がよく使うものをいくつか、どのように使うのかも軽く触れつつ紹介して今回は終わりにします。
情報 | 使い道 |
---|---|
foe.distance | 装備の切り替えタイミングを計るのに利用。スキル発動可能距離とか、攻撃可能距離とかに |
foe.count | 範囲攻撃を使うか、単体攻撃を使うかの切り替え等に利用。あとは堕神の剣のスキル使用のタイミングを計るのに利用したり |
foe.armor | ハンマー仕様するかどうかの判定に利用。ヘビーハンマーなどなかった |
foe.debuffs.string | 未付与のバフを与えるかどうか。スマイトバフ(堕神の剣のスキル効果)が掛かっているときに特に使う。 |
foe.state | ボスが攻撃してくるタイミングを計るのに使う。mind stone を使って、避けられるなら避ける |
foe.time | state が切り替わってからの経過時間(フレーム数)で、攻撃モーションに入ってから実際に攻撃されるまでのラグを調整したりするのに利用 |
foe 以外にも色々使います。例えば item.left.state
や item.right.state
なんかもハック的に利用しますが、これは別の記事で紹介できれば。
とりあえず書いた実践編その1。
実践編その2。
実践編その3。
*1:経験値を入手してレベルを上げると最大HPが上がり、アーマー値の最大値も増えるほか、アプリケーションを閉じた状態で放置した際に取得できる宝箱の数の最大値が上がります。放置時の上限を増やすためにも放置せずに頑張りましょう。本末転倒ですが致し方なし。
*2:より良い報酬を時間効率良く獲得するため、最上位ダンジョンは早くクリアするために使う、という選択肢もアリだと思いますが、資源の消費が激しいのと、資源集めが面倒なのが悩ましい所
*3:import UI/BetterInfo2 って書くとつらみが出ます。iPhone は割と余裕。多分処理系の得意不得意問題。
*4:実際には長いので見切れます
*5:多少ネタバレになりますが、foe=boss だけではなく名前まで指定しているのには理由があって、黄色の☆以降はボス手前に中ボスが出現して、そいつが boss という文字列を含むから、それを弁別するために指定する必要があるためです