RPGの魔法を実装する-実装編

前回、魔法の設計を行いましたが引き続き実装していきたいと思います(jQueryが必要です)。
余談ですが、仮にも設計をOOでやってしまったので実装もそれを引きずるわけですが、昔は結構、インスタンス生成の実装方法に悩んだりしていました。

例えばMagicオブジェクトのストラテジの生成です。クラス図上では既にストラテジを含んでいますが、これはいつ誰が用意するのか?等。ですので先にインスタンスの生成部分を片付けておきたいと思います。
(自明ですが下記の方法が唯一の解法ではありませんw)。

Magic

まず各魔法のデータはJSONでサーバー上に保存するものとします。

【magic.txt】
{
'catalog' : [
  { 'id': 10001, 'title' : "heal" , 'h' : 25, 'm' : 0, 'poison' : false },
  { 'id': 10002, 'title' : "poison" , 'h' : 5, 'm' : 0, 'poison' : true }]
}

そして、Magicインスタンスの生成の責任を誰に持たせるかですが、これはMagicオブジェクトのコンテナとなるMagicCatalogオブジェクトを用意してMagicCatalogが生成するものとします。

[MagicCatalog]++-[Magic]

MagicCatalog = function(url) {
  this.url = url || "data/magic.txt";
  this.load(this.url);
}
MagicCatalog.prototype.load = function(url) {
  var me = this;
  $.get(url, "", function(data) {
      eval('var s=' + data); //error 処理は割愛
      me.catalog = new Array();
      for(var i = 0 ; i < s.catalog.length ; i++ ) {
          me.catalog[s.catalog[i].id] = new Magic(s.catalog[i]);
      }
  });
}

そしてStrategyの選択です。(悩ましいのですが)今回はMagicオブジェクトに任せます。

Magic = function (options) {
    options = options || {};
    for (var nm in options) {
        options.hasOwnProperty(nm) && (this[nm] = options[nm]);
    }
    this._strategy = this._buildStrategy(options);
    this._text = "";
}
Magic.prototype._buildStrategy = function (state) { //ストラテジ生成
    switch (state.title.toLowerCase()) {
        case "heal":
            return new Magic.Heal(this);
        case "posion":
            return new Magic.Poison(this);
        default:
            return new MagicStg(this);
    }
}
MagicStg = function (owner) {
    this.owner = owner;
}
MagicStg.prototype = {
    _state: {},
    chant: function (from, to) {
    },
    update: function () {
    }
}
Magic.Heal = dqextend(MagicStg, function (owner) { //MagicStgを継承
    this.base(owner);
}, {
});
Magic.Poison = dqextend(MagicStg, function (owner) {
    this.base(owner);
}, {
});

Creatureを実装

Playerや敵を抽象化したCreatureについてです。とりあえず魔法についてだけ考えます。前回の設計から永続状態と一次状態、および魔法の一覧を持つ事にします。
敵については魔法と同様にJSONの元データとカタログを用意して管理します(説明は割愛)。
保有する魔法はIDで管理し、実際に使用するときにカタログからインスタンスを取得して利用します。

[Creature|parmanentState;magics]-temporaryState 1[Chain]

Creature = function (options) {
    options = options || {};
    for (var nm in options) {
        options.hasOwnProperty(nm) && (this[nm] = options[nm]);
    }
    this._temporary = new Chain(this);
}
Creature.prototype = {
    parmanent: {},
    temporary: null,
    magicIDs : {}
}

またCreatureはTargetインターフェースを持ちます。現状では単純にprototypeをコピーすることで実現します。

function __interface__(c,s){
    for (var nm in c.prototype) {
        s.prototype[nm] = c.prototype[nm];
    }
}
Target = function() {}
Target.prototype = {
  getParmanent: function() {
      return this.parmanent;
  }
}
__interface__(Target, Creature);

よく見るとTargetインターフェースの意義が見えてこない(緩い型付けのJavaScriptでかつ抽出したインターフェースが少ない)のでTargetの使用は保留します。

Stateの実装

Stateも一時的状態を表すときはStrategyを持ちます(設計ではStateパターンとしましたがやはりStrategyとします)。
一次状態の元データはMagic用のJSONとして、生成時に引き渡します。

State = function(options) {
    options = options || {};
    for (var nm in options) {
        options.hasOwnProperty(nm) && (this[nm] = options[nm]);
    }
    this._strategy = this._buildStrategy(options);
}
State.prototype._buildStrategy = function (state) {
    if(state.poison) {
        return new State.Poison(this);
    } else if(state.stan) {
        return new State.Stan(this);
    } else {
        return new StateStg(this);
    }
}
State.prototype.enter = function() {}
State.prototype.leave = function() {}
State.prototype.update = function() {}

State.Poison = function(owner) {
    this.owner = owner;
}

余談ですがStrategyパターンの場合、ストラテジー追加の都度build()メソッドを修正する必要があって、それが嫌われる事があるようです。
JavaScriptならトリッキーなやり方もありますね(実際には使わないけど・・・)。

//源泉が  { h : 5, m : 0, Poison : true, Stan: false } だとして
State.prototype._buildStrategy = function (status) {
    for(var nm in status) {
        if(status.hasOwnProperty(nm) &&
          typeof status[nm] == "boolean" && status[nm] == true) {
            return new State[nm](this);
        }
    }
}

閑話休題

Engine

直接のテーマでは無いので最小限に戦闘Engineについて検討します。まず、戦闘が開始されると敵味方のPartyがEngineに登録されます。
今回は簡略化して1対1とします。
それから戦闘の参加者の一覧から順番を決めてコマンドを実行していきます。コマンドは敵ならAI、PlayerならUIからの入力ですが、このあたりは省略して、特定の魔法が選択された所から始めます。

[BattleEngine||setup()]<>- cast>*[Creature]

BEngine = function() {}
BEngine.prototype.setup(player,enemy) {
    this._cast = new Array();
    this._cast.push(player);
    this._cast.push(enemy);
    this._cast.sort(this.compare);
    ...
}

Healの実装

まずは簡単そうなHealの実装をしてみます。増加する値は固定値です。値のばらつきやfromの能力に応じた増減は枝葉なので省略します。
既にHPが上限値なら失敗とみなします。後はmaxHPを上限にHPを増加させ、UIへ表示するテキストを準備しておきます。

Magic.prototype.chant = function(from, to) {
    if(!this._strategy.judge(from, to)) {
        this._text = this._strategy.getText(from, to, false);
        return false;
    }

    this._strategy.chant(from, to);
    this._text = this._strategy.getText(from, to, true);
    return true;
}
Magic.Heal.prototype.getText = function(from, to, success) {
    return  success ? this._text : to.name + "は回復しなかった。";
}
Magic.Heal.prototype.judge = function(from, to) {
    return to.parmanent.hp != to.parmanent.maxHp;
}
Magic.Heal.prototype.chant = function(from, to) {
    var st = to.parmanent,
        delta = (st.hp + this.owner.hp >= st.maxHP) ?
                    st.maxHp - this.owner.hp : st.hp + this.owner.hp;

    this._text = to.name + "は " + delta + "のHPを回復した。";
    to.parmanent.hp += delta;
}

Magic.Poisonの実装

poisonはState.Poisonオブジェクトを対象(to)に付与します。実際の状態の変化(HPの減少等)は付与されたState.Poisonのupdate()を呼び出すことで実施します。

Magic.Poison.prototype.getText = function(from, to, success) {
    return  to.name + (success ? "は毒に侵された。" : "は平気だった。");
}
Magic.Poison.prototype.judge = function(from, to) {
    return true;//省略します。
}
Magic.Poison.prototype.chant = function(from, to) {
    to.temporary.push(new State.Poison(to)); //状態を追加する
}

State.Poisonの実装

State.Poisonは毒に侵された状態を担当します。
Stateは発動(entry)、更新(update)、解除(leave)処理があり、Poisonの場合、発動と更新処理でhpを削ります。
それからもう一つ、活動可能問い合わせ(canAction)も用意します。Poisonの場合は戦闘自体へのペナルティーは無しです。
update()BEngineからターンごとに呼び出されるものとします。

State.prototype.canAction = function() {
    return this._strategy.canAction();
}
State.prototype.enter = function() {
    return this._strategy.enter(this.owner);
}
State.prototype.leave = function() {
    return this._strategy.leave(this.owner);
}
State.prototype.update = function() {
    if(this._strategy.update(this.owner)) {
        this._text = this._strategy.getText();
        return true;
    } else {
         return false;
    }
}
State.Poison.prototype.canAction = function() { return true; }
State.Poison.prototype.enter = function(to) {
    this._count = 1;
    this.update(to);
}
State.Poison.prototype.update = function(to) {
    this.count--;
    if(this.count) {
        return false;
    }
    var st = to.parmanent,
         hp = st.hp - this.owner.hp >= 0 ? this.owner.hp : st.hp;
    st.hp -= hp;
    this._text = to.name +
                         "は毒に侵されている。\n" + hp + "のダメージを受けた。";
    return true;
}
State.Poison.prototype.leave = function(to) {}

BEngineからupdate()を呼ぶ

一次状態の更新部分を実装します。一次状態はターンの開始前に更新される物とします。
仮に一ターン分の処理を実施するBEngine.turn()メソッドを用意します。
そこから戦闘参加者の一次状態を更新します。その後、死亡と行動可否の確認をしながら全ての参加者のコマンドを実行します。
Chainは既出なので説明は省略します。

BEngine.prototype.turn = function() {
     //preprocess
     for(var i = 0 ;i < this.cast.length ; i++ ) {
         var c = this.cast[i];
         c.temporary.or("update");
     }
     //敵味方のパーティーが全滅していれば終了
     if() {} //省略
     for(var i = 0 ; i < this.cast.length ; i++ ) {
         if(this.cast[i].isDead()) {
             continue;
         }
         if(!this.cast[i].temporary.logicalAnd("canAction")) {
            this.messageBox.push(this.cast[i].name + "は行動できない。");
            continue;
         }
     }
}

ちょっとずるをしたのですが、このままではState.Poison.update()で生成したテキストが表示できません。
そこで、Chainオブジェクトが収集することにします(その判定に至った理由を残しておくというニュアンス)。

Chain.prototype.or(command) {
    var result = false;
    this.reason = new Array();
    for(var i = 0 ; i < this.length ; i++) {
        result |= this[i][command].apply(this[i]);
        this[i].getText && this[i].getText() && this.reason.push(this[i].getText());
    }
}
BEngine.prototype.turn = function() {
     //preprocess
     for(var i = 0 ;i < this.cast.legnth ; i++ ) {
         var c = this.cast[i];
         c.temporary.or("update");
         c.temporary.reason.length && this.messageBox.push(c.temporary.reason);
     }
     //敵味方のパーティーが全滅していれば終了
     if() {} //省略
     for(var i = 0 ; i < this.cast.length ; i++ ) {
         var c = this.cast[i];
         if(c.isDead()) {
             continue;
         }
         if(c.temporary.length && !c.temporary.logicalAnd("canAction")) {
            c.temporary.reason.length && this.messageBox.push(c.temporary.reason);
            continue;
         }
     }
}

一次状態の解除

最後に一次状態の解除させるためにMagic.Cureを実装します。
Magic.Cureは毒を治療する魔法です。

まずは元データに追加します。

【magic.txt】
{
'catalog' : [
  { 'id': 10001, 'title' : "heal" , 'h' : 25, 'm' : 0, 'poison' : false },
  { 'id': 10002, 'title' : "poison" , 'h' : 5, 'm' : 0, 'poison' : true },
  { 'id': 10003, 'title' : "cure" , 'h' : 0, 'm' : 0, 'poison' : false }
]
}

さらにMagic.Cureを実装します。

Magic.prototype._buildStrategy = function (state) {
    switch (state.title.toLowerCase()) {
        case "heal":
            return new Magic.Heal(this);
        case "posion":
            return new Magic.Poison(this);
        case "Cure":
            return new Magic.Cure(this);
        default:
            return new MagicStg(this);
    }
}
Magic.Cure.prototype.judge = function(from, to) {
     for(var i = 0 ; i < to.temporary.length ; i++ ) {
        if(to.temporary[i] instanceof State.Poison) {
            return true; //100%成功
        }
    }
    return false;
}
Magic.Cure.prototype.getText = function(from, to, success) {
    return to.name + (success ? "は毒が治った。" : "は毒に侵されていない。";
}
Magic.Cure.prototype.chant = function(from, to) {
    for(var i = 0 ; i < to.temporary.length ; i++ ) {
        if(to.temporary[i] instanceof State.Poison) {
            to.temporary[i].leave();
            to.temporary.remove(i); //要素の削除
        return true;
        }
    }
    return false;
}

解除を実装するときに気づきましたが、一次状態を付与するときに二重呼び出しのチェックをしないといけないですね(効果が強化されるのか無効なのかは仕様次第ですが)。
そこについては動作サンプルを作るときに組み込むことにします。

動作サンプルを作成しました。

免責

ここに示したコードは、机上で作成したものなので動作確認していません。typoがあったりちゃんと動かないかもしれませんが、怒らないでください。

優しく指摘して下されば、適宜修正致します。

M. K. の紹介

IT屋さんです。プログラミングが大好きで今はJavascriptがお気に入りです。
カテゴリー: JavaScript, ゲーム作成, プログラミング   パーマリンク