【プチコン講座】ATB風バトルシステムを構築しよう:準備編

プチコン

プチコン バトルシステム

こんにちは。継続の錬金術士なおキーヌです。

ブログ毎日更新は150日目になります。

前回「【プチコン講座】ステータスウィンドウを実装してみよう」でステータス画面をXボタンで開閉できるようにしました。

ついにバトルシステム構築編に入りましたね。

RPGの5割はバトルシステムにかかっているとも言えます。

バトルシステムは大きく分けてターン制・疑似リアルタイム制・リアルタイム制といったシステムがあります。

今回は疑似リアルタイム制、つまりFFぽいATB風の戦闘を作っていこうと思います。

それではプチコンでRPG作り第20回目を始めましょう。

うだうだ考えずコードだけ書きたい人は目次の2つ目から見てください。

  1. ATB風バトルシステムについて考えてみる
  2. とにもかくにもバトルシーンを作ってみよう
  3. ATB風のゲージを溜まっていく処理を作ろう

ATB風バトルシステムについて考えてみる

なぜATBじゃなくATB風なのかと言っておくと、ATBは現スクウェア・エニックス(当時のスクウェア)が特許を持っているので
ATBと正式に名乗ることはできません。

同時に特許取得の内容をみると

請求項1
プレイヤーキャラクターと敵キャラクタを画面に表示し入力された、またはあらかじめ定められたコマンドに応じて、キャラクタに対応する時間を設定して行動に移すプロセス。
請求項2
上記の時間の経過に合わせてキャラクタの行動やコマンドの表示が発生する制御システム。
請求項4
アクティブとウエイトのモードをもち、アクティブが選択されたときには上記計時手段に計時動作を行なわせ、ウエイトが選択された時は、一部に計時動作をストップさせる。
請求項5
ATBゲージの速度の設定。
引用元:wikipedia

同人ゲーではよくつかわれていますが、商業ゲーで使っているのはあまりないのじゃないでしょうか?

ツクール4とかツクール2013とかFF8をパロっていたときメモ2やなんかが普通に使っていましたがアレはどうなんでしょうか(笑)

発案者に敬意を払ってマネさせてもらう

仕組みを利用して売るのは法に触れてしまいますが、勉強するのは法に触れません。

なので当時のスクウェアのデバッガーでシステムエンジニアだった伊藤裕之様に敬意を払って勉強させていただきましょう。

私も実はATB風のバトルは初めて組むので、つたない部分がたくさんあるかと思います。

ひとまずはどういった感じに動くのかをFFに例えて考えてみましょう。

ゲージが溜まる仕組み

ATB風バトルを作るにはATBバーという行動準備が出来たかどうかを知らせるゲージがあります。

このゲージは0~100%で設定するとします。

FFで言えばヘイストをかけるとこのゲージの進む速度が速くなりますね。

なのでゲージの溜まる速度は素早さで補正がかかっているということです。

ゲージのため方は、プログラミングではもはや常識になるループ処理です。

0%から100%までの計算は何フレームループすればいいかが楽でしょう。

ゲージが溜まったらコマンド入力開始

ゲージが100%になったのを条件にしてコマンド入力に切り替えます。

選択されたコマンドを行動キューに入れます。

そして行動キューは最初に入った方から取り出されるのでなるべく早く選択するのが勝利のコツですね。

例えば相手が1ターンを使って力を溜めて次のターンにダメージの大きい攻撃をしてくるとわかっている時に
行動せずにタイミングよく防御を選択すれば生き残れる確率が上がります。

逆に力を溜めている間に先手必勝で攻撃を連続で仕掛けてダメージを受ける前に倒すといったこともできますね。

このバトルの駆け引きがATB風バトルの醍醐味です。

プログラム内部ではターン制みたいなもの

いくらATB風とはいえ、プログラムでみれば順番に処理が行われているので
実質ターンを消費して戦うシステムなんですよね。

なので攻撃を受けたり与えたりした時に、お互いのHPを調べたり状態を調べる処理を書く必要があります。

ATB風バトルシステムのフローを簡単に箇条書きするとこんな感じです。

  • 戦闘開始
  • お互いの行動開始ゲージがたまる
  • 溜まったら行動を選択する
  • 選択が早かった順に選択した行動を開始する
  • 行動キューから先に選択した者から行動する
  • 先手はエネミーが攻撃を仕掛けてきた
  • プレイヤーはダメージを受けた
  • ここでダメージを受けた側のステータスをチェック
  • HPが0以下であれば戦闘不能・ゲームオーバー処理へ
  • HPが残っていればプレイヤーの行動キューが取り出される
  • エネミーは行動ゲージ増加開始(プレイヤーアクション中は停止)
  • エネミーにダメージ
  • エネミーのHPが0以下であればエネミー消滅・リザルト画面・戦闘終了
  • エネミーのHPが残っていればお互いのゲージがたまるまでまつ
  • 以下行動開始してお互いのHPがなくなるまでループ

基本的な流れはこのようになります。

ちょっとHPが残っていれば戦闘は続くと言った感じです。

戦闘開始時かイベント中に特定のフラグを立てておいて、
その戦闘で敗北したら負けイベント発動などをしても面白いですね。

今回はそこまで凝ったものは作りませんが(笑)

とにもかくにもバトルシーンを作ってみよう

御託はいいからさっさとバトルシーンを作ろうぜ!って方はこちらから開始してください。

ですが、少しだけ思い出して欲しいです。

シーンフラグを作ってイベントを調べた時にイベントがあればイベントキューに追加され、
イベントシーンに切り替わるということをやったと思います。

あれの応用というかそのままで、イベントキューにバトルタイプがあったら
シーンフラグをバトルに切り替えます。

FFやドラクエ・ポケモンなど言えばトランジションと言われる戦闘に入る暗転が入りますね。

今回あれは無しで直接戦闘シーンに入ります。

それでは戦闘シーンの関数を作ってみましょう。

# シーンの大元
DEF SCENE_PARENT

  IF G_SCENE_FLAG == 0 THEN
    D_CTRL_MAP_MODE
  ELSEIF G_SCENE_FLAG == 1 THEN
    IF G_EV_Q_FLAG == 0 THEN
      D_SHIFT_EV_DATA
    ELSEIF EV_Q_FLAG == 1 THEN
      D_EV_STOP_A_BTN
    ENDIF
  ELSEIF G_SCENE_FLAG == 2 THEN
    D_STATUS_MODE
  ELSEIF G_SCENE_FLAG == 3 THEN
    D_BATTLE_MODE
  ENDIF

  D_BTN_PRESS

END

# バトルシーンループ
DEF BATTLE_MODE
  PRINT "バトルモードダヨ"
END

これでバトルシーンは作れましたね。

後はイベントでシーンフラグを3にしてあげればバトルシーンに突入できます。

コウモリにバトルイベントを仕込む

次にバトルシーンに入るためにコウモリのイベントをバトルにしてみましょう。


# コウモリイベントデータ
@EVENT001
DATA 3,  4,1,0, 3,1,0, 0,1,1
@EVENT001_1
DATA 1,  0,0,0

コウモリイベントを定義しました。

バトルイベントのタイプは4にしておきましょう。

2つ目の数字はモンスターの種類の選択です。

1番目はコウモリにしました。

3つ目の数字は強さの補正にしましょうか。

この数値を上げれば上げる程、同じコウモリでも強さが変化するという仕組みを作れます。

見た目は同じでも奥にいるコウモリは強いが、プレイヤーにはわからない罠

というのも作れます。

ちょっとプレイヤーはストレスがたまると思いますが、
奥に行くほど敵が強くなるということをプレイヤーに覚えさせるためのレベルデザインでもあります。

今回は居ませんが、村があった場合そのような警告をしてくれるNPCが居るとプレイヤーも事前に警戒できます。

今回はチュートリアルということで強くすることはしませんが、おまけでやるかもしれません。

それではこの値を使ってバトルイベントを発生させてみましょう。


# イベントキュー取り出し
DEF D_SHIFT_EV_DATA

  ~~ 省略 ~~

  ELSEIF TYPE == 3 THEN
    IF EVENT == 1 THEN
      SPSET G_EV_ID_NOW, RESERVE
    ENDIF
  ELSEIF TYPE == 4 THEN
    G_SCENE_FLAG = 3
  ELSEIF TYPE == 10 THEN
    BEEP EVENT, RESERVE
  ENDIF

  ~~ 省略 ~~

END

イベントキュー取り出しにタイプ4を追加しました。

中身はシーンを3にしてバトルモードに切り替えています。

しかしまだイベント001を認識するように作られていません。

現在は「D_GET_EV_DATA」関数で引数のIDによって処理を切り分けていますね。

直接ラベル「@EVENT006」を記述していますが、イベントが増えてくるとこれを増やしていかなければいけません。

しかもそれ以外のコードは結構使いまわせそうな感じがしませんか?

はい、じゃあラベルで分岐するところを関数化してみましょう。


# イベントリストからイベントキューへ挿入
DEF D_GET_EV_DATA A_ID

  D_EV_SELECT_ A_ID

  IF LEN(G_EV_QUEUE) > 0 THEN
    G_EV_ID_NOW = A_ID
    G_SCENE_FLAG = 1
  ENDIF
END

# イベントID毎のイベントキュー作成
DEF D_EV_SELECT A_ID

  IF A_ID != 0 THEN
    # IDラベル作成
    VAR LABEL$ = "@EVENT" + FORMAT$("%03D", A_ID)

    VAR EV_COUNT = 0
    VAR EV_DATA = 0

    IF SPVAR(A_ID, 4) == 0 THEN
      RESTORE LABEL$
    ELSEIF SPVAR(A_ID, 4) == 1 THEN
      RESTORE LABEL$ + "_1"
    ENDIF

    READ EV_COUNT
    FOR G_I=1 TO (EV_COUNT * 3)-1
      READ EV_DATA
      PUSH G_EV_QUEUE, EV_DATA
    NEXT

  ENDIF
END

最初プチコンではラベルを変数に入れられないのでちょっと絶望しましたが、
リファレンスをみているとCOPYの項目で文字列でラベルを設定してるところを見つけました。

もしやと思い文字列でRESTOREのラベル指定をしたら動きました。

これで後ろが数値であればゼロの桁数を合わせれば可変ラベル指定を実現できました。

これが出来たらかなりプログラミングの自由度は上がりますね。

プチコン、おそるべし。

ここで重要なのが先ほども言った「ゼロの桁数を合わせる」ことです。

いわゆるゼロパディングという桁数を合わせる初歩的なプログラミングテクニックの一つです。

jsでRPGを作ってた時も私はやってたのでプチコンでも作ろうと思ったのですが、
「FORMAT$()」関数が用意されていることに気付きこれを使いました。

FORMAT$()はプログラミングに慣れていないとリファレンスを見てもよくわからないと思います。

C言語を触った人ならすぐに理解できると思います。

そうでない人は、これをここに指定したらこう動くんだという感覚の方がいいでしょう。

コードを軽く説明すると

FORMAT$("%03D", A_ID)

FORMAT$()は関数ですね。

1つ目の引数ですが、これが結構曲者です。

意味を説明すると長くなるのでこのコードの意味だけを説明します。

「%03D」

とありますが、%は決まり文句だと覚えておいてください。

次の「0」は何で桁合わせをするかを指定しています。

001とか006とかにしたいので0を指定しています。

これを1にすると111とか116になります。

次に「3」ですが、これは見ての通り桁数ですね。

ここを4にすると0001・0006とかになります。

次の「D」は整数です。いわゆる0~10…の10進数ですね。

2つ目の引数は関数の処理を適応する数値です。

A_IDを6として過程して説明すると

数値の6を3桁の形にしたいので、足りない2桁は0で埋めて10進数の文字列にして返すぞ!

という感じですね。

最後のDをXとかにすると16進数で返します。

今は10進数でいいのでDでいいですね。

A_IDが2桁、つまり10以上99以下だと1桁足りないので0を先頭に一つ追加して文字列で返してきます。

ATB風のゲージを溜まっていく処理を作ろう

これで自由にイベントに敵を作ることが出来るようになったので、
バトルに突入したら戦闘を開始したいところです。

その前にATB風バトルにしたいのでゲージが溜まったら行動が出来るようになるという仕組みを作りましょう。

バトルはバトル専用の変数を作って基本そこでしか使わないようにしたいです。

クラスの概念があれば楽なのですが、プチコンにはデフォルトではその機能はないのでグローバル変数を利用します。

バトルのゲージは数値配列を作ってプレイヤーとエネミーの分を作れば管理しやすいですね。

「G_BATTLE_STATUS」という配列を作って偶数はプレイヤー用の変数で奇数はエネミーの変数ってことにしましょう。

ゲージも何の行動を選択したのかも一括で管理できます。

配列へのアクセスは基本的にリテラルでやった方が良いかもしれません。

変数にしておくともし予期せぬ番号が入ってしまった場合最悪ゲームが止まります。

配列0はプレイヤーのゲージ・配列1はエネミーのゲージと決まっているのですから変数にする必要は無いですね。

後で変更する時面倒ですが、見えないところでバグが起るよりよりはマシです。

それでは早速ゲージを実装してみましょう。

DIM G_BATTLE_STATUS[6]

# バトルシーンループ
DEF BATTLE_MODE
  D_BATTLE_GAUGE_LOOP
END

# ゲージループ
DEF D_BATTLE_GAUGE_LOOP
  # プレイヤーゲージ
  IF G_BATTLE_STATUS[0] < 180 THEN
    INC G_BATTLE_STATUS[0]
  ELSEIF G_BATTLE_STATUS[2] == 0 THEN
    BEEP 7, 600
    INC G_BATTLE_STATUS[2]
  ENDIF

  # エネミーゲージ
  IF G_BATTLE_STATUS[1] < 360 THEN
    INC G_BATTLE_STATUS[1]
  ELSEIF G_BATTLE_STATUS[3] != 0 THEN
    BEEP 7, 600
    INC G_BATTLE_STATUS[3]
  ENDIF
END

ゲージが増加するループを作りました。

毎ループゲージが1増加するので、プレイヤーは180カウント後になったらSEがなります。

分かりやすいようにエネミーは360カウント後にSEがなります。

配列の0・1はゲージ

2・3は現在の行動ステータス(動けるかどうか)

一旦これで動かしてみましょう。

視覚的にはわかりませんが、ゲージが溜まったら音をなるようにしているのでよくわかると思います。

これでゲージの基礎は出来上がりました。

後は素早さによってゲージの溜まり具合や、いくつまで溜まれば行動できるかを調整すればいいですね。

今度は視覚的なゲージを作ってみましょう。

表示するのはプレイヤーのみでいいと思います。

エネミー側も見えて良いと思いますが、いつ攻撃されるかがわかったらかなりヌルゲーになりますね。

簡単に作ってみるので一旦グラフィックの線を描写するものを使います。


# バトルシーンループ
DEF BATTLE_MODE
  D_BATTLE_GAUGE_LOOP
  D_BATTLE_GUAGE_GRAPH
END

# ゲージグラフィック表示
DEF D_BATTLE_GUAGE_GRAPH
  GLINE SPVAR(0,0) , SPVAR(0,1)+16, SPVAR(0,0)+G_BATTLE_STATUS[0] , SPVAR(0,1)+16, 
END

数値で大体酷いことになる予感はしますが、実行してみてください。

ゲージの長さはおかしいですが、いい感じにゲージが溜まったらSEが鳴るようになりました。

大きさはプレイヤーの横幅ぐらいでいいと思うので、調整が必要ですね。

スプライトの基本の大きさは16×16です。

なので16ドットで表現する必要があります。

しかしゲージは180溜まったら行動開始になっているので、割合で表示する必要があります。

変数 / 180 = 180の割合ですね。

割合に16を掛けると16のうちのどの数値にいるのかわかります。

半分まで溜まると数値は60ですね。

90 / 180 = 0.5です。

0.5 x 16 = 8 になりますね。 丁度半分です。

その数値グラフィック描写のX座標の終点にすれば表現できます。

中途半端なので16で掛けたあとの小数点は切り捨てましょう。

小数点の切り捨てはFLOOR()関数を使います。

それではゲージグラフに計算式と関数を使ってみましょう。

チップサイズの16は「G_SZ」で取得できますね。


# ゲージグラフィック表示
DEF D_BATTLE_GUAGE_GRAPH
  VAR INC_GUAGE = FLOOR( G_BATTLE_STATUS[0] / 180 * G_SZ )
  GLINE SPVAR(0,0) , SPVAR(0,1)+G_SZ, SPVAR(0,0) + INC_GUAGE , SPVAR(0,1)+G_SZ, 
END

どうでしょうか?

めっちゃいい感じにATB風ゲージを実装出来ました。

しかし1ドットだと3DSでは見辛いかもしれません。

縦3ドットぐらい使って背景を黒にして赤いメーターが溜まるように改良してみましょう。


# ゲージグラフィック表示
DEF D_BATTLE_GUAGE_GRAPH
  # 背景として下地を黒く塗りつぶす
  GFILL SPVAR(0,0)-2, SPVAR(0,1)+G_SZ+2, SPVAR(0,0)+G_SZ+2, SPVAR(0,1)+G_SZ+5, RGB(255,255,255)
  GFILL SPVAR(0,0)-1, SPVAR(0,1)+G_SZ+1, SPVAR(0,0)+G_SZ+1, SPVAR(0,1)+G_SZ+4, RGB(0,0,0)
  VAR INC_GUAGE = FLOOR( G_BATTLE_STATUS[0] / 180 * G_SZ )
  GLINE SPVAR(0,0) , SPVAR(0,1)+G_SZ, SPVAR(0,0) + INC_GUAGE , SPVAR(0,1)+G_SZ+3, 
END

これでどうでしょうか?

プチコン 出力結果

めっちゃいい感じにATB風ゲージが出来上がったのではないでしょうか?

基本的な形なので見た目は好きなように装飾してみるとよいかもですね。

スプライトで実装するのもカッコ良さそうです。

後は上から敵に遭遇した時にゲージが被ってしまうので下を向いている時に、
バトルイベントが発生したら位置を変更する分岐を作っておくといいかもしれません。

行動したと仮定してゲージをリセットしてみよう

バトルコマンドを選択して行動が終わったらゲージがリセットされますよね。

最後にその処理を実装しましょう。

一旦テストなので、ゲージが溜まったときにYボタンを押したらゲージをリセットするようにしてみましょうか。


# バトルゲージリセット
DEF D_BATTLE_GUAGE_RESET A_ACTOR
  IF A_ACTOR == 0 THEN
    G_BATTLE_STATUS[0] = 0
    G_BATTLE_STATUS[2] = 0
  ENDIF
END

# プレイヤーの攻撃
DEF D_BATTLE_ATTACK
  IF G_BATTLE_STATUS[2] == 1 THEN
    IF G_BTN_PRESS_COUNT[7] == 1 THEN
      BEEP 103, 300
      D_BATTLE_GUAGE_RESET 0
    ENDIF
  ENDIF  
END

ゲージのリセット関数と、ゲージリセットをするための攻撃関数を作りました。

ゲージが溜まったら攻撃可能。

攻撃したらゲージをリセット。

一旦こんな感じでいいでしょう。

しかしこのままでは攻撃ができないので、Yボタンを動くようにボタンカウント関数に追記しましょう。


# ボタンカウント関数
DEF D_BTN_PRESS_COUNT
  VAR B = BUTTON()

  # Aボタンカウント
  IF (B AND #A) > 0 THEN
    IF G_BTN_PRESS_COUNT[4] < 255 THEN 
      INC G_BTN_PRESS_COUNT[4]
    ENDIF
  ELSE
    # 1回でも離されたらカウントリセット
    G_BTN_PRESS_COUNT[4] = 0
  ENDIF

  # Xボタンカウント
  IF (B AND #X) > 0 THEN
    IF G_BTN_PRESS_COUNT[6] < 255 THEN 
      INC G_BTN_PRESS_COUNT[6]
    ENDIF
  ELSE
    # 1回でも離されたらカウントリセット
    G_BTN_PRESS_COUNT[6] = 0
  ENDIF

  # Xボタンカウント
  IF (B AND #Y) > 0 THEN
    IF G_BTN_PRESS_COUNT[7] < 255 THEN 
      INC G_BTN_PRESS_COUNT[7]
    ENDIF
  ELSE
    # 1回でも離されたらカウントリセット
    G_BTN_PRESS_COUNT[7] = 0
  ENDIF

END

これも一部同じで同じような処理を書いているので関数化できそうですね。

キャンセル(Bボタン)を実装する時にまとめようと思います。

しかし今出来るなって思った人はやってみるといいでしょう。

講座にそってばかりだとプログラミングスキルは成長しないので、
こういったタイミングで自力でコードを改良してみると一気に成長できますよ。

自分の力でで出来たっていう自信もつきますから、一石二鳥です。

後で私とコードが違っていても余程メモリを食うような書き方じゃなければ、
結果が良ければまったく問題ありません。

これで実行してみましょう。

ゲージが溜まってからYボタンを押すと攻撃音とともにゲージがリセットされます。

──なんかめちゃくちゃゲームっぽくなってきたじゃないですか!

このままYボタンで攻撃にしてXボタンでポーションを使ったりBボタンで防御にしてもいいのですが、
RPGということで次回はコマンド入力(たたかう・ぼうぎょ・かいふく等の選択)を実装したいと思います。

Yボタンを攻撃にするなら、今はグリッド移動ですがドット移動にして攻撃時に剣を突き出すか振るかをすれば
ゼルダの伝説みたいなゲームが作れそうですね。

しかしドット移動となると衝突判定がちょっと厄介になります。

スプライトが持つ衝突判定があるから楽と言えば楽ですが、それでもグリッドの衝突判定よりかは面倒くさいです。

それでは次回にやることをネタバレしておくと、
ウィンドウを生成してその中に選択文字列とスプライトの指カーソルを実装してFFぽくしてみましょう。

ダメージまで一気に進めてもいいのですが、平日はちょっと厳しいので小分けになって申し訳ないです。

最後に恒例のソースコードを載せておきます。

そして最後にバグを見つけてしまいました。

コウモリに移動する瞬間にAボタンを押してみると、アッ!ってなると思います。お試しあれ(笑)

グリッド間の移動中はキー操作完全無効にすべきですね。

それでは。

ACLS

#------------------
# 変数定義
#------------------
VAR G_STOP_FLAG = TRUE # ゲームループフラグ
VAR G_I # 汎用変数

# シーンフラグ
VAR G_SCENE_FLAG = 0
VAR G_STATUS_FLAG = FALSE

# イベント用
VAR G_EV_Q_FLAG = 0 # イベントキューがあるかどうか
DIM G_EV_ID[32] # イベント用情報の配列(初回DATA読み込み用)
VAR G_EV_ID_NOW = 0 # 現在のイベントナンバー
VAR G_EV_STEP = 4 # 1つのイベントデータ数
DIM G_EV_QUEUE[0] # イベントキュー配列

# プレイヤー用
VAR G_PLAYER_NAME$ = ""
VAR G_PLAYER_STATUS_LEN = 8
DIM G_PLAYER_STATUS[0]

# ウィンドウサイズ用
VAR G_WINDOW_SIZES_LEN = 8
DIM G_WINDOW_SIZES[0]

# アイテム名用
VAR G_ITEMS_NAMES_LEN = 2
DIM G_ITEMS_NAMES[0]

# システムワード
VAR G_SYSTEM_WORDS_LEN = 7
DIM G_SYSTEM_WORDS[0]

# 敵用
VAR G_ENEMY_NAMES_LEN = 5
DIM G_ENEMY_NAMES[0]

# メッセージ用
VAR G_MSG_DATAS_LEN = 4
DIM G_MSG_DATAS[0]

# ボタン分の配列を用意
DIM G_BTN_PRESS_COUNT[13] # 全ボタンカウント配列
FILL G_BTN_PRESS_COUNT,0 # 一応定義したボタン配列を0で初期化しておく

# BG関連
VAR G_OX = 0, G_OY = 0 # BG読み込みのオフセット
VAR G_SZ=16, G_MW, G_MH # チップサイズ、マップ幅、マップ高さ
VAR G_BGW = CEIL(400/G_SZ), G_BGH = CEIL(240/G_SZ) # BG読み込み準備
DIM G_MAP[0] # マップレイヤー4枚+イベントレイヤー分

# スプライト用
VAR G_SP_PLAYER = 0 # PLAYERスプライトNo.
DIM G_SP_ANIM_NO[9] # スプライトアニメーション初期値変数 添え字=ID

# バトル用
DIM G_BATTLE_STATUS[6]



#------------------
# データベース定義
#------------------
# 開始時に読み込むDATA軍(ラベル付けたほうが安全かもしれない
DATA 500,1040,920,1000,980,1020,269,269,269
DATA 1,5,5,2,  2,23,13,2,  3,4,11,2,  4,13,3,2,  5,22,4,2
DATA 6,6,7,1,  7,3,11,1,  8,22,3,1

# コウモリイベントデータ
@EVENT001
DATA 3,  4,1,0, 3,1,0, 0,1,1
@EVENT001_1
DATA 1,  0,0,0


# 宝箱イベントデータ
@EVENT006
DATA 7,  10,95,400  3,1,268  2,0,1  1,1,0, 10,12,0  1,2,0,  0,1,1
@EVENT006_1
DATA 2,  1,3,0,  0,0,0

# プレイヤー初期ステータス
@PLAYER_INIT_STATUS
DATA 1,20,20,3,2,3,0,1

# ウィンドウサイズ用
@WINDOWSIZE
DATA 80,150,240,58,  10,10,112,77

# ステータスウィンドウ用ワード
@SYSTEMWORD
DATA "Lv", "HP", "こうげき", "ぼうぎょ", "すばやさ", "けいけんち", "ポーション"
# アイテム名リスト
@ITEMDATA
DATA "ライフポーション", "せいすい"

# モンスター名リスト
@ENEMYNAME
DATA "リトルバット", "ゴブリン", "スケルトン", "マミー", "ゴースト"

# 定型文リスト
@MESSAGEDATA
DATA "は たからばこを あけた", "たからばこ には", "が はいっていた!", "からっぽ!"

# アニメーション配列初期値代入
D_FIRST_DATA_READ G_SP_ANIM_NO
# イベントID,X座標,Y座標代入
D_FIRST_DATA_READ G_EV_ID

# マップデータ準備
LOAD "DAT:TEST", G_MAP, 0 # マップデータのロード
G_MW = SHIFT(G_MAP) # 読み込んだデータ配列の0個目を配列から切り離して取得(1638415という数字が入ってる)
G_MH = G_MW AND &HFFFF : G_MW = G_MW >> 16 AND &HFFFF # 取り出したデータからシフト演算やらビット演算をする

# マップデータ描写
FOR G_I=0 TO 3
  BGSCREEN G_I, G_BGW, G_BGH, G_SZ # 1画面分のBG
  BGOFS 0,0,4-G_I
  BGLOAD I, -G_OX, ( -G_OY - ( G_I * G_MH )), G_MW, G_MH * ( G_I + 1 ), G_MAP # マップ描写
NEXT

# プレイヤー配置
D_SP_SETUP G_SP_PLAYER, 1, 0

# イベント配置
FOR G_I=0 TO LEN(G_EV_ID)-1 STEP G_EV_STEP
  D_SP_SETUP G_EV_ID[G_I], G_EV_ID[G_I+1], EV_ID[G_I+2]
NEXT

# 各種データの読み込み
D_DB_LOAD 0, G_PLAYER_STATUS_LEN, G_PLAYER_STATUS
D_DB_LOAD 1, G_SYSTEM_WORDS_LEN, G_SYSTEM_WORDS
D_DB_LOAD 2, G_ITEM_NAMES_LEN, G_ITEM_NAMES
D_DB_LOAD 3, G_ENEMY_NAMES_LEN, G_ENEMY_NAMES
D_DB_LOAD 4, G_MSG_DATAS_LEN, G_MSD_DATAS

# グラフィック画面を一番手前に
GPRIO 0

# ゲームループ
WHILE G_STOP_FLAG
  D_SCENE_PARENT
  VSYNC 1
WEND

# シーンの大元
DEF SCENE_PARENT

  IF G_SCENE_FLAG == 0 THEN
    D_CTRL_MAP_MODE
  ELSEIF G_SCENE_FLAG == 1 THEN
    IF G_EV_Q_FLAG == 0 THEN
      D_SHIFT_EV_DATA
    ELSEIF EV_Q_FLAG == 1 THEN
      D_EV_STOP_A_BTN
    ENDIF
  ELSEIF G_SCENE_FLAG == 2 THEN
    D_STATUS_MODE
  ELSEIF G_SCENE_FLAG == 3 THEN
    D_BATTLE_SCENE_LOOP
  ENDIF

  D_BTN_PRESS

END

# マップ用コントローラー関数
DEF D_CTRL_MAP_MODE
  IF SPVAR(SP_PLAYER, 2) != 1 THEN
    VAR B = BUTTON()
    IF (B AND #UP) > 0 THEN
      IF D_CHECK_COLLISION(G_SP_PLAYER, #UP) == 1 THEN
        D_DIRECTION_MOVE SP_PLAYER, #UP
      ENDIF
    ELSEIF (B AND #DOWN) > 0 THEN
      IF D_CHECK_COLLISION(G_SP_PLAYER, #DOWN) == 1 THEN
        D_DIRECTION_MOVE SP_PLAYER, #DOWN
      ENDIF
    ELSEIF (B AND #LEFT) > 0 THEN
      IF D_CHECK_COLLISION(G_SP_PLAYER, #LEFT) == 1 THEN
        D_DIRECTION_MOVE SP_PLAYER, #LEFT
      ENDIF
    ELSEIF (B AND #RIGHT) > 0 THEN
      IF D_CHECK_COLLISION(G_SP_PLAYER, #RIGHT) == 1 THEN
        D_DIRECTION_MOVE SP_PLAYER, #RIGHT
      ENDIF
    ENDIF
  ELSE
    D_GRID_MOVE SP_PLAYER, (SPVAR SP_PLAYER, 3)
  ENDIF

  # 移動中もボタンを押せるようにしておく
  IF G_BTN_PRESS_COUNT[4] == 1 THEN
    PRINT "Aボタンが押されたよ!"
    D_EV_CHECK G_SP_PLAYER SPVAR(G_SP_PLAYER, 3)
  ENDIF

  IF G_BTN_PRESS_COUNT[6] == 1 THEN
    PRINT "Xボタンが押されたよ!"
    G_SCENE_FLAG = 2
  ENDIF
END


# 向きと移動フラグを設定
DEF D_DIRECTION_MOVE A_SPRITE_NO, A_SPRITE_DIRECTION
  SPVAR A_SPRITE_NO, 2, 1
  SPVAR A_SPRITE_NO, 3, A_DIRECTION
END

# 向いている方向に止まるまで歩き続ける(グリッド移動)
DEF D_GRID_MOVE SPRITE_NO, SPRITE_DIRECTION
  # 渡されたスプライトと向きによって移動方向を決定
  IF SPRITE_DIRECTION == #UP THEN
    SPVAR SPRITE_NO, 1, (SPVAR SPRITE_NO, 1) - 1
  ELSEIF SPRITE_DIRECTION == #DOWN THEN
    SPVAR SPRITE_NO, 1, (SPVAR SPRITE_NO, 1) + 1
  ELSEIF SPRITE_DIRECTION == #LEFT THEN
    SPVAR SPRITE_NO, 0, (SPVAR SPRITE_NO, 0) - 1
  ELSEIF SPRITE_DIRECTION == #RIGHT THEN
    SPVAR SPRITE_NO, 0, (SPVAR SPRITE_NO, 0) + 1
  ENDIF

  # 移動させる
  SPOFS SPRITE_NO,(SPVAR SPRITE_NO, 0),(SPVAR SPRITE_NO, 1)

  # 割った余りが0になったら動きを止める
  IF (SPVAR SPRITE_NO, 0) MOD G_SZ == 0 && (SPVAR G_SPRITE_NO, 1) MOD G_SZ == 0 THEN
    SPVAR G_SPRITE_NO, 2, 0
  ENDIF

END

# ドット座標からグリッド座標に変換
DEF D_GET_GRID_XY(A_X_OR_Y)
  RETURN A_X_OR_Y / G_SZ
END

# MAP配列のどこにいるかチェック
DEF D_GET_MAP_POSITION(A_X, A_Y, A_LAYER)
  VAR RETURN_POS = 0
  VAR GRID_X = D_GET_GRID_XY(A_X)
  VAR GRID_Y = D_GET_GRID_XY(A_Y)

  VAR PULS_LAYER = (G_MW * G_MH) * A_LAYER

  IF A_Y <= 0 THEN
    RETURN_POS = GRID_X + PLUS_LAYER
  ELSE
    RETURN_POS = (GRID_X + (GRID_Y * G_MW)) + PLUS_LAYER
  ENDIF

  RETURN RETURN_POS
END

# 当たり判定チェック
DEF D_CHECK_COLLISION(A_NO, A_DIRECTION)
  VAR MP1_SUM_POS = 0
  VAR EV_SUM_POS = 0

  VAR DIRECT = D_GET_DIRECT_POINT(A_DIRECTION)

  VAR MAP_POS = D_GET_MAP_POSITION(SPVAR(A_NO,0), SPVAR(A_NO,1), 1)
  VAR EVENT_POS = D_GET_MAP_POSITION(SPVAR(A_NO,0), SPVAR(A_NO,1), 3)

  MP1_SUM_POS = MAP_POS + DIRECT
  EV_SUM_POS = EVENT_POS + DIRECT

  IF G_MAP[MP1_SUM_POS] == 0 THEN
    IF G_MAP[EV_SUM_POS] == 0 THEN
      D_EV_LAYER_REWRITE A_NO, A_DIRECTION, 1
      RETURN 1
    ELSE
      RETURN 0
    ENDIF
  ELSE
    RETURN 0
  ENDIF
END

# スプライトセットアップ
DEF D_SP_SETUP A_NO, A_X, A_Y
  VAR F = 20
  VAR ANIM = G_SP_ANIM_NO[A_NO]

  SPSET A_NO ANIM
  SPVAR A_NO 0, G_SZ * A_X
  SPVAR A_NO 1, G_SZ * A_Y
  SPVAR A_NO 2, 0
  SPVAR A_NO 3, #DOWN
  SPOFS A_NO SPVAR(A_NO,0), SPVAR(A_NO,1),1

  D_EV_LAYER_REWRITE A_NO, 0, 1

  IF A_NO == 0 || G_EV_ID[(G_EV_STEP * A_NO)-1] == 2 THEN
    SPANIM A_NO, "I", F,ANIM, F,ANIM+1, F,ANIM+2, F,ANIM+3, 0 
  ENDIF
END

# イベントレイヤー書き換え
DEF D_EV_LAYER_REWRITE A_NO, A_DIRECT, A_FLAG

  VAR POS = D_GET_MAP_POSITION(SPVAR(A_NO,0), SPVAR(A_NO,1), 3)

  IF A_FLAG > 0 THEN
    IF A_DIRECT > 0 THEN
      VAR AFTER_POS = POS + D_GET_DIRECT_POINT(A_DIRECT)
      G_MAP[AFTER_POS] = A_NO
    ELSE
      G_MAP[POS] = A_NO
    ENDIF
  ELSE
    VAR BEFORE_POS = POS - D_GET_DIRECT_POINT(A_DIRECT)
    G_MAP[BEFORE_POS] = 0
  ENDIF

END

# 方向定数から1歩前のグリッド座標を得るための値を得る
DEF D_GET_DIRECT_POINT(A_DIRECTION)
  IF A_DIRECTION == #UP THEN
    RETURN DIRECT = -MW
  ELSEIF A_DIRECTION == #DOWN THEN
    RETURN DIRECT = MW
  ELSEIF A_DIRECTION == #LEFT THEN
    RETURN DIRECT = -1
  ELSEIF A_DIRECTION == #RIGHT THEN
    RETURN DIRECT = 1
  ENDIF
END

# DATA初期読み込み
DEF D_FIRST_DATA_READ A_ARRAY
  FOR G_I=0 TO LEN(G_EV_ID)-1
    VAR N=0: READ N
    G_EV_ID[G_I] = N
  NEXT
END

# ボタンカウント関数
DEF D_BTN_PRESS_COUNT
  VAR B = BUTTON()

  # Aボタンカウント
  IF (B AND #A) > 0 THEN
    IF G_BTN_PRESS_COUNT[4] < 255 THEN 
      INC G_BTN_PRESS_COUNT[4]
    ENDIF
  ELSE
    # 1回でも離されたらカウントリセット
    G_BTN_PRESS_COUNT[4] = 0
  ENDIF

  # Xボタンカウント
  IF (B AND #X) > 0 THEN
    IF G_BTN_PRESS_COUNT[6] < 255 THEN 
      INC G_BTN_PRESS_COUNT[6]
    ENDIF
  ELSE
    # 1回でも離されたらカウントリセット
    G_BTN_PRESS_COUNT[6] = 0
  ENDIF

  # Yボタンカウント
  IF (B AND #Y) > 0 THEN
    IF G_BTN_PRESS_COUNT[7] < 255 THEN 
      INC G_BTN_PRESS_COUNT[7]
    ENDIF
  ELSE
    # 1回でも離されたらカウントリセット
    G_BTN_PRESS_COUNT[7] = 0
  ENDIF

END


# 前方のイベントをチェックする
DEF D_EV_CHECK A_NO A_DIRECT

  VAR POS = D_GET_MAP_POSITION(SPVAR(A_NO,0), SPVAR(A_NO,1), 3)
  VAR AFTER_POS = POS + D_GET_DIRECT_POINT(A_DIRECT)

  VAR EV_ID = G_MAP[AFTER_POS]

 D_GET_EV_DATA EV_ID
END

# イベントリストからイベントキューへ挿入
DEF D_GET_EV_DATA A_ID

  D_EV_SELECT_ A_ID

  IF LEN(G_EV_QUEUE) > 0 THEN
    G_EV_ID_NOW = A_ID
    G_SCENE_FLAG = 1
  ENDIF
END

# イベントID毎のイベントキュー作成
DEF D_EV_SELECT A_ID

  IF A_ID != 0 THEN
    # IDラベル作成
    VAR LABEL$ = "@EVENT" + FORMAT$("%03D", A_ID)

    VAR EV_COUNT = 0
    VAR EV_DATA = 0

    IF SPVAR(A_ID, 4) == 0 THEN
      RESTORE LABEL$
    ELSEIF SPVAR(A_ID, 4) == 1 THEN
      RESTORE LABEL$ + "_1"
    ENDIF

    READ EV_COUNT
    FOR G_I=1 TO (EV_COUNT * 3)-1
      READ EV_DATA
      PUSH G_EV_QUEUE, EV_DATA
    NEXT

  ENDIF
END


# イベントキュー取り出し
DEF D_SHIFT_EV_DATA
  VAR TYPE = SHIFT(G_EV_QUEUE)
  VAR EVENT = SHIFT(G_EV_QUEUE)
  VAR RESERVE = SHIFT(G_EV_QUEUE)

  IF TYPE== 0 THEN
    IF EVENT == 1 THEN
      SPVAR G_EV_ID_NOW, 4, RESERVE
    ENDIF
  ELSEIF TYPE == 1 THEN
    GCLS
    D_WINDOW_DRAW G_WINDOW_SIZES[0], G_WINDOW_SIZES[1], G_WINDOW_SIZES[2], G_WINDOW_SIZES[3]
    D_MSG_DRAW G_WINDOW_SIZES[0], G_WINDOW_SIZES[1], EVENT RESERVE
    G_EV_Q_FLAG = 1
  ELSEIF TYPE == 2 THEN
    IF EVENT == 1 THEN
      INC G_PLAYER_STATUS[7], RESERVE
    ENDIF
  ELSEIF TYPE == 3 THEN
    IF EVENT == 1 THEN
      SPSET G_EV_ID_NOW, RESERVE
    ENDIF
  ELSEIF TYPE == 4 THEN
    G_SCENE_FLAG = 3
  ELSEIF TYPE == 10 THEN
    BEEP EVENT, RESERVE
  ENDIF

  IF LEN(G_EV_QUEUE) <= 0 THEN
    GCLS
    G_SCENE_FLAG = 0
  ENDIF
END

# ウィンドウクリエイター
DEF D_WINDOW_DRAW A_X, A_Y, A_W, A_H

  # カラー配列を定義
  VAR WC_BACK = RGB(0, 21, 151) # 背景色
  VAR WC_FRAME = RGB(128,126,129) # フレーム色
  VAR WC_HIGHLITE = RGB(255,255,255) # ハイライト色

  # 右下を計算
  VAR EX = A_W + A_X
  VAR EY = A_H + A_Y

  # 背景描写(塗りつぶし有り矩形)
  GFILL A_X, A_Y, EX, EY, WC_BACK

  # 矩形フレーム描写(塗りつぶし無し矩形)
  GBOX A_X-1, A_Y-1, EX+1, EY+1, WC_FRAME
  GBOX A_X-2, A_Y-2, EX+2, EY+2, WC_FRAME
  GBOX A_X-3, A_Y-3, EX+3, EY+3, WC_FRAME

  # ハイライトの角の部分(点)
  GPSET A_X-2, A_Y-2, WC_HIGHLITE
  GPSET A_X-2, EY+2, WC_HIGHLITE
  GPSET EX+2, A_Y-2, WC_HIGHLITE
  GPSET EX+2, EY+2, WC_HIGHLITE

  # ハイライトとフレームの直線部分
  GLINE A_X-3, A_Y-1, A_X-3, EY+1, WC_HIGHLITE
  GLINE A_X-4, A_Y-1, A_X-4, EY+1, WC_FRAME

  GLINE EX+3, A_Y-1, EX+3, EY+1, WC_HIGHLITE
  GLINE EX+4, A_Y-1, EX+4, EY+1, WC_FRAME

  GLINE A_X-1, A_Y-3, EX+1, A_Y-3, WC_HIGHLITE
  GLINE A_X-1, A_Y-4, EX+1, A_Y-4, WC_FRAME

  GLINE A_X-1, EY+3, EX+1, EY+3, WC_HIGHLITE
  GLINE A_X-1, EY+4, EX+1, EY+4, WC_FRAME

END

# メッセージ送り用
DEF D_EV_STOP_A_BTN
  IF G_BTN_PRESS_COUNT[4] == 1 THEN
    G_EV_Q_FLAG = 0
  ENDIF
END

# メッセージイベント
DEF D_MSG_DRAW A_X, A_Y, A_EVENT A_RESERVE
  IF A_EVENT == 1 THEN
    GPUTCHR A_X+5, A_Y+5, "プレイヤー" + G_MSG_DATAS[0]
  ELSEIF A_EVENT == 2 THEN
    GPUTCHR A_X+5, A_Y+5, G_MSG_DATAS[1] + G_ITEMS_NAMES[A_RESERVE] + G_MSG_DATAS[2]
  ELSEIF A_EVENT == 3 THEN
    GPUTCHR A_X+5, A_Y+5, G_MSG_DATAS[3]
  ENDIF
END

# データベースローダー
DEF D_DB_LOAD A_ID, A_LEN, A_ARY

  IF A_ID == 0 THEN
    VAR ARY_WORD = 0
  ELSE
    VAR ARY_WORD = ""
  ENDIF

  IF A_ID == 0 THEN
    RESTORE @PLAYER_INIT_STATUS
  ELSEIF A_ID == 1 THEN
    RESTORE @SYSTEMWORD
  ELSEIF A_ID == 2 THEN
    RESTORE @ITEMNAME
  ELSEIF A_ID == 3 THEN
    RESTORE @ENEMYNAME
  ELSEIF A_ID == 4 THEN
    RESTPRE @MESSAGEDATA
  ENDIF

  FOR G_I=0 to A_LEN-1
    READ ARY_WORD
    PUSH A_ARY, ARY_WORDS
    # 読み込み確認用
    PRINT A_ARY[G_I]
  NEXT
END


# ステータスウィンドウ描写
DEF D_STATUS_WINDOW_DRAW
  # ローカル配列定義
  VAR STATUS = ""
  DIM WS[0]
  # グローバル配列の参照を格納
  WS = G_WINDOW_SIZES

  # ウィンドウの描写
  D_WINDWOW_DRAW WS[4], WS[5], WS[6], WS[7]

  # ステータス文字の描写
  FOR G_I = 0 TO G_SYSTEM_WORDS_LEN-1
    # ステータス文字の描写準備
    IF G_I == 0 THEN
      STATUS = G_SYSTEM_WORDS[G_I]+":"+ STR$(G_PLAYER_STATUS[0])
    ELSEIF G_I == 1 THEN
      STATUS = G_SYSTEM_WORDS[G_I]+":"+ STR$(G_PLAYER_STATUS[1])+"/"+STR$(G_PLAYER_STATUS[2])
    ELSEIF G_I == 2 THEN
      STATUS = G_SYSTEM_WORDS[G_I]+":"+ STR$(G_PLAYER_STATUS[3])
    ELSEIF G_I == 3 THEN
      STATUS = G_SYSTEM_WORDS[G_I]+":"+ STR$(G_PLAYER_STATUS[4])
    ELSEIF G_I == 4 THEN
      STATUS = G_SYSTEM_WORDS[G_I]+":"+ STR$(G_PLAYER_STATUS[5])
    ELSEIF G_I == 5 THEN
      STATUS = G_SYSTEM_WORDS[G_I]+":"+ STR$(G_PLAYER_STATUS[6])+"/999"
    ELSEIF G_I == 6 THEN
      STATUS = G_SYSTEM_WORDS[G_I]+":"+ STR$(G_PLAYER_STATUS[7])
    ENDIF

    # 実際のステータスの描写
    GPUCHR  WS[4]+5, WS[5]+5 + (G_I*10), STATUS
  NEXT
END

# ウィンドウトグル
DEF D_STATUS_WINDOW_TOGGLE
  # フラグを反転させる
  G_STATUS_FLAG = NOT G_STATUS_FLAG

  # フラグによって開くか閉じるかを決める
  IF STATUS_FLAG THEN
    D_STATUS_WINDOW_DRAW
  ELSE
    GCLS
  ENDIF
END

# ステータスモード
DEF D_STATUS_MODE
  # 初回にステータスウィンドウフラグをONにしておく
  IF NOT G_STATUS_FLAG THEN
    D_STATUS_WINDOW_TOGGLE
  ENDIF

  # Xボタン監視
  IF G_BTN_PRESS_COUNT[6] == 1 THEN
    D_STATUS_WINDOW_TOGGLE
  ENDIF

END


# バトルシーンループ
DEF BATTLE_MODE
  D_BATTLE_GAUGE_LOOP
  D_BATTLE_GUAGE_GRAPH
END

# ゲージループ
DEF D_BATTLE_GAUGE_LOOP
  # プレイヤーゲージ
  IF G_BATTLE_STATUS[0] < 180 THEN
    INC G_BATTLE_STATUS[0]
  ELSEIF G_BATTLE_STATUS[2] == 0 THEN
    BEEP 7, 600
    INC G_BATTLE_STATUS[2]
  ENDIF

  # エネミーゲージ
  IF G_BATTLE_STATUS[1] < 360 THEN
    INC G_BATTLE_STATUS[1]
  ELSEIF G_BATTLE_STATUS[3] != 0 THEN
    BEEP 7, 600
    INC G_BATTLE_STATUS[3]
  ENDIF
END


# ゲージグラフィック表示
DEF D_BATTLE_GUAGE_GRAPH
  # 背景として下地を黒く塗りつぶす
  GFILL SPVAR(0,0)-2, SPVAR(0,1)+G_SZ+2, SPVAR(0,0)+G_SZ+2, SPVAR(0,1)+G_SZ+5, RGB(255,255,255)
  GFILL SPVAR(0,0)-1, SPVAR(0,1)+G_SZ+1, SPVAR(0,0)+G_SZ+1, SPVAR(0,1)+G_SZ+4, RGB(0,0,0)
  VAR INC_GUAGE = FLOOR( G_BATTLE_STATUS[0] / 180 * G_SZ )
  GLINE SPVAR(0,0) , SPVAR(0,1)+G_SZ, SPVAR(0,0) + INC_GUAGE , SPVAR(0,1)+G_SZ+3, 
END

# バトルゲージリセット
DEF D_BATTLE_GUAGE_RESET A_ACTOR
  IF A_ACTOR == 0 THEN
    G_BATTLE_STATUS[0] = 0
    G_BATTLE_STATUS[2] = 0
  ENDIF
END

# プレイヤーの攻撃
DEF D_BATTLE_ATTACK
  IF G_BATTLE_STATUS[2] == 1 THEN
    IF G_BTN_PRESS_COUNT[7] == 1 THEN
      BEEP 103, 300
      D_BATTLE_GUAGE_RESET 0
    ENDIF
  ENDIF  
END