Module Parties(集落や施設)

(訳注: モジュール名「Parties」はワールド・マップ上を動き回る全部隊を連想させますが、実際は違います。Native や多くの既製 MOD で この Parties モジュールに置いているのは、主人公の隊など一部の形式的な隊を除くと、街、城、村、悪漢の住みか、廃虚、橋といった静的なものばかりです。そして全レコードの第 5、7~11 項目 つまり攻撃性などの「性格」や「敵対相手」といった「部隊」に関する項目は 0 です。一方、よく似た項目を持つ別の「集団テンプレート」(Party Templates)モジュールのほうに動的な各種集団(候伯以外の、「隊商」「強盗騎士」「身代金を待つ賊」「鹿の群れ」など)を集め、候伯などヒーローは兵種(Troops)モジュール、というように役割を分けています。

そのため、ここでは実状に合わせて「集落や施設」と訳します

モジュール名から離れて役割が分かれた経緯については、右の投稿などにヒントがありそう。 Zsar, Documentation & Tutorials。)

module_parties.py 冒頭に書かれた指定方法
「集落・施設」(party)のレコードには下記の項目がある。
(1) 集落・施設 ID: 他のファイルから このレコードを特定するために使われる。
    ID の手前に接頭子 p_ が自動的に付加される。
(2) 名前。ゲーム中で表示される集落・施設の名称。ID とは別で、任意に指定できる。
(3) フラグ群。指定可能なフラグは header_parties.py を参照のこと。pf_disabled フラグを指定しない限り、マップ・アイコンの ID(icon_XXXX)を一つだけ論理和しなければならない。 (訳注: この XXXX の部分は、module_map_icons.py 内の各レコードの第 1 項目で "XXXX"(例: "town")のように指定したマップ・アイコン ID で、他ファイルから参照時は icon_XXXX のように接頭子を付け引用符を付けない。実際の icon_XXXX はコンパイル時に ID_map_icons.py に出力される。デフォルトではフラグが 0x100 以上のビット位置を使うので、255 を超える数のマップ・アイコンを扱えない。)
(4) メニュー。この集落・施設に着いた時に表示するメニューの ID。値が 0(あるいは定数 no_menu)なら、デフォルトの集落施設遭遇システムが使われる。 (訳注: 本文で後述される通り、この第 4 項目は不用。)
(5) 集団テンプレート(Party-template)。この集落・施設が属する集団テンプレートの ID。pt_none ならデフォルト値となる。
(6) 勢力。module_factions.py で定義した勢力。
(7) 性格フラグ群。header_parties.py に説明あり。 (訳注: 度胸(大胆さ)、攻撃性、悪漢か否かなど。)
(8) AI 動作。AI の集団がワールド・マップでどう振る舞うかを指定。
(9) AI の敵対相手。AI が敵と見なす集団。
(10) 初期座標 (x, y) 。ワールド・マップ上の座標。
(11) スタックのリスト。各スタックのレコードは、次のような 3 項目からなる。 (訳注: 標準ではスタックは最大 6 個まで。増やす方法については本文で後述。)
  11.1) 兵種 ID。module_troops.py で定義された一般兵またはヒーロー。
  11.2) Number of troops in this stack; does not vary. The number you input here is the number of troops the town will have.
  11.3) Member flags. Optional. Use pmf_is_prisoner to note that this member is a prisoner.
      Note: There can be at most 6 stacks.
(12) 向き(単位: 度)。省略可。

ファイル module_parties.py の先頭にあるように、定数を宣言しておけば、値を変更する場合の作業が簡単になります。ここでも独自の定数を自由に追加できます。

no_menu = 0
#pf_town = pf_is_static|pf_always_visible|pf_hide_defenders|pf_show_faction
pf_town = pf_is_static|pf_always_visible|pf_show_faction|pf_label_large
pf_castle = pf_is_static|pf_always_visible|pf_show_faction|pf_label_medium
pf_village = pf_is_static|pf_always_visible|pf_hide_defenders|pf_label_small

パーティ(Party)になり得るものは、主人公がマップ上で接触するあらゆるものです。移動する集団、静止した街や村、独自に設計したその他の場所など。重要な点の 1 つは、2 つ以上のものが出会うと、出会いのトリガーが発火するということです。一般に、静的なものは(Native の通常の街、城、村のように)module_parties.py に置き、動的なものは party_templates.py に集団テンプレートとして置きます。静的なものは有効無効を切り替えることができます。街を表示するとしたら、それらはすべて既知の ID を持っています。一方、動的なものは作成と破棄ができます。新たに出現したり作られたものは全て一意の識別子として新しい ID を得ます。だから、特定の ID がまだ有効であるかどうかを確認し、まだ存在しているか、すでに破棄されているかを制御する必要があります。

例を見てみましょう。少し下がった所に次のようなタプルがあります。

("town_1","Sargoth", icon_town|pf_town, no_menu, pt_none, fac_neutral,0,ai_bhvr_hold,0,(-1.55, 66.45),[], 170),

サルゴス(サーゴス)という街をワールド・マップ上に配置しています。各項目は次のようになっています。

(1) Party-id = "town_1"
(2) Party name = "Sargoth"
(3) Party flags = icon_town|pf_town
(4) Menu = no_menu
(5) Party-template = pt_none
(6) Party faction = fac_neutral
(7) Party personality = 0
(8) AI-behaviour = ai_bhvr_hold
(9) AI-target party = 0
(10) Initial coordinates = (-1.55, 66.45)
(11) List of troop stacks = [] (None)
(12) Party direction = 170

第 4 項目は廃止予定で、もう使われなくなりました。M&B バージョン 0.730 の時点では、このフィールドはゲームにまったく影響を与えません。代わりに、プレイヤーの隊が別の隊やワールド・マップ上の戦闘に遭遇するたびに、スクリプト game_event_party_encounter がゲーム・エンジンから呼び出されます。 同じことが module_party_templates.py の第 4 項目にも当てはまります。

独自の街、城、村を追加する場合は、次の点に注意して下さい。module_parties.py と他の特定のモジュール・ファイルの場合、新しい街をリストの最後に追加してはいけません。命令が中断される可能性があるからです。そのことは これらのファイル内のコメントで警告しています。module_parties.py に新しい街を追加するなら、例えば「town_1」と「castle_1」の間に書くことが推奨されます。これは、module_constants.py で定義された街の範囲です。そのファイルの中にある、マップ・アイコンの様々なカテゴリに対して定義された範囲を読んでみて下さい。街、村、城は通常は「中立」として追加されます。これは script_game_start で勢力(fiction)と候伯(lord)に割り当てられるからです。

フラグ群

フラグ群には下記を指定できます。

pf_always_visible 村、城、街が(たとえプレイヤーから離れた所にあっても)ワールド・マップ上に常に表示される(プレイヤーから見える)ようになる。AI の集団から見た可視不可視には影響しない [1]。指定しない場合、プレイヤーは視界内でしか見ることができない。
pf_auto_remove_in_town 街に着き次第 旅の一行を解散する(消滅させる)。隊商、戦闘後の敗残兵(安全な集落や道に逃げる)、伝令などに使う [2]
pf_civilian 民間人として扱い、pf_dont_attack_civilians フラグを持つ集団が攻撃しないようにする。
pf_default_behavior 1 を指定すると、ゲーム・エンジンによる AI 動作をオフにし、MOD 側の単純トリガーなどによって AI 動作をさせる。0 を指定すると ゲーム・エンジンが攻撃や撤退などの AI の決定を制御する [3]
pf_disabled ワールド・マップで集団を非表示にする。盗賊の「出現点」や一時的に生成する集団などに使う。ただし、(街などの場合)場所は依然として勢力リストに表示される。このフラグを指定しても、スクリプトと try_for_parties ループのコードからは「見える」。
(訳注: 原文の location について、出現点は勢力リストに表示されないので、ここでは)
pf_dont_attack_civilians この集団が pf_civilian フラグを持つ集団を攻撃しないようにする。
pf_hide_defenders hides the garrison numbers on the world map. ワールド・マップ上で城や街の守備兵数を非表示にする。
pf_is_hidden コメント・アウトされたフラグ。 エンジンが使用するので、このフラグを上書きしてはいけない!
pf_is_ship 集団が水上を移動できるようにする。通行可能な地形は、rt_water、rt_river、rt_bridge 。
pf_is_static 移動しないものに指定する。例えば「街」など。
pf_label_large ワールド・マップ上で大型の名札を割り当てる。街などに使う。
pf_label_medium ワールド・マップ上で中型の名札を割り当てる。城などに使う。
pf_label_small ワールド・マップ上で小型の名札を割り当てる。村などに使う。
pf_limit_members この隊の統率者のリーダー・スキルに応じて隊の規模が制限される。Native ではプレイヤーの隊のみに使われる。実際には このフラグはどの隊でも機能するし、プレイヤーの隊から指定を外して上限を無効にすることもできる。add_companion_party 命令は、出現させた隊に対しこのフラグを設定する。party_force_add_members 命令や party_force_add_prisoners 命令を使う場合、このフラグは無視される。
pf_no_label ワールド・マップ上で名札を付けない。プレイヤーは この相手と やりとりできない。
pf_quest_party プレイヤーの隊(main_party を指定した集団)に対し、スクリプト経由で使う。1 を指定すると、プレイヤーがクエスト中であることがゲーム・エンジンに伝えられる。クエストがキャンセルされた場合に(スクリプトを書いておいて)0 に戻すことができる。チュートリアルとゲームに新設した一連のクエストに使われる [2]
pf_show_faction 集団の勢力名を示す。このフラグを指定しないと、その集団は略奪者や狩猟者のように見える。
pmf_is_prisoner レコードの第 12 項目を介して隊に指定されると、選択した兵種を捕虜に変える。 (訳注: 第 11 ?)
carries_goods(x) 隊の移動速度を低下させるために使われる。荷物の重量計数 x が大きいほど速度が下がる。x を 50 倍した値が、隊が余計に負う実際の重量。このフラグを指定すると、追加の戦利品を引き起こさない (訳注: 原文は "This flag does not cause additional loot."。この loot が比喩的な何かなのか戦闘で得る物品なのかが不明瞭。) エンジンには次の式が含まれている。
float totalWeight = ((m_flags & pf_carry_goods_mask) >> pf_carry_goods_bits) * 50.0f;

下記 2 つのフラグが使えるのか、また どのように使えるかは不明。 恐らく非推奨で、エンジンで不用。

pf_carry_gold_multiplier defines the multiplicator effect for carries_gold(x).
carries_gold(x) deprecated, unused by the engine.

AI 動作

AI 動作には下記を指定できます。

ai_bhvr_attack_party 予め定めた相手を攻撃するようにする。
ai_bhvr_avoid_party 予め定めた相手を避けるようにする。
ai_bhvr_driven_by_party 予め定めた相手によって この集団を動かす。Native では牛の群れのクエストに使われる。
ai_bhvr_escort_party 予め定めた相手を同行させる。
ai_bhvr_hold この集団の属する勢力が他の全勢力に対して中立であることを前提として、この集団をワールド・マップ上で完全に静止させる。
ai_bhvr_in_town この集団が街の中にいるという情報を AI に与える。
ai_bhvr_negotiate_party 不明。(要調査)
ai_bhvr_patrol_location この隊に、予め定めた地域を巡回させる。例えば、Native の馬賊(Steppe Bandits, 草原の賊)のように。
ai_bhvr_patrol_party この隊に、予め定めた相手の周りを巡回させる。例えば、Native の馬賊(Steppe Bandits, 草原の賊)のように。
ai_bhvr_track_party 廃止。代わりに別名の ai_bhvr_attack_party を使用のこと。
ai_bhvr_travel_to_party この集団に、予め定めた目的地へ向かわせる。この集団は目的地に到着すると(隊商がそうであるように)不可視になるだけで存続する。この隊との出会いを、コードや会話では検出できない。 (訳注: 街など目的地にいる間に検出できない、という意味? 原文は "there is no party encounter you can pick up in code or dialogs."。)
ai_bhvr_travel_to_point この集団に対し、ワールド・マップ上の所定の位置を移動の目的地として指定する。
ai_bhvr_travel_to_ship 不明。移動の目的地が船?

ワールド・マップの座標

レコードの第 10 項目には、ワールド・マップ上の座標を指定します。ゲーム中に主人公の座標を知りたければ、ゲーム起動直後の小さなメニューで Configure を選び、「映像」(Video)タブの「フレームレートを表示する」と「詳細」(Advanced)タブの「エディット モードを有効にする」にチェックを入れて、プレイ開始します。ワールド・マップに出ると、画面左上にフレーム・レート(fps)が表示されるので、Ctrl e キーを押すと、その少し下に緑色で主人公の隊(p_main_party)の座標が表示されます [4]。もし左下の「キャンプ」ボタンの左に「地形」(Terrain)ボタンが表示されていないなら、編集モードになっていない可能性があるので、起動時メニューを再確認して下さい [5]。また、チート・モードをオンにした場合は、Ctrl キーを押しながらマップ上の任意の点をクリックすると座標を得られます。

(訳注: 下図は訳者が追加。Native では、座標の原点はマップの中心付近、「ディリム」(Dhirim)という街の少し西辺りにあります。方位磁針が上(北)を向いている時、右が +x 方向、上が +y 方向です。)


コードからは下記のように party_get_position 命令を使って座標を取得します。

(set_fixed_point_multiplier, <精度>),
(party_get_position, pos1, "p_main_party")
(position_get_x, reg1, pos1),
(position_get_y, reg2, pos1),
(display_message, "@Player is at ({reg1}, {reg2})"),

ここで <精度> には整数を指定します。よくあるのは 100 か 1000 です。単位は「基本距離」で、ワールド・マップ上で、速度 1.0 の集団が 1 秒間に移動する距離です [6]。また、次のような方法もあります [7]

module_constants.py:
cmenu_locate = 10
  ("game_context_menu_get_buttons",
    [
    (store_script_param, ":party_no", 1),
    (context_menu_add_item, "@Where am I?", cmenu_locate),
    (try_begin),
      (neq, ":party_no", "p_main_party"),
      (context_menu_add_item, "@Move here", cmenu_move),
    (try_end),
...
  ("game_event_context_menu_button_clicked",
    [
    (store_script_param, ":party_no", 1),
    (store_script_param, ":button_value", 2),
    (try_begin),
      (eq, ":button_value", cmenu_center_note),
      (change_screen_notes, 3, ":party_no"),
    (else_try),
      (eq, ":button_value", cmenu_locate),
      (party_get_position, pos1, ":party_no"),
      (str_store_party_name, s1, ":party_no"),
      (position_get_x, reg1, pos1),
      (position_get_y, reg2, pos1),
      (display_message, "@{s1} is at x:{reg1}, y:{reg2}"),
...

マップ上の位置を取得するもう 1 つの方法は、Thorgrim's Map Editor を使って、Z キーを押したままマップ上でカーソルを移動することです。Z キーを放すと、その場所もクリップ・ボードにコピーされるので、Python スクリプトなどに貼り付けることができます [8]。このマップ・エディタで様々な変更をしてから保存することで parties.txt が更新されるので、その後、HokieBT's UpdateModulePartieskt0's Party Script を使って座標をコピーし、開発中の Python コードで使うことができます [9]

スタックの最大数

隊のレコードには標準ではスタックを最大 6 つまでしか持てませんが、party_add_members 命令、party_add_prisoners 命令、またはそれらの強制バージョンを使ってテンプレートを生成した後、スクリプトを使って最大 255 個のスタックを追加できます。change_screen 命令で隊員一覧画面を開いた場合であれば、スクロールバー付きのリストが表示されるので問題ありません。しかし、グローバル・マップのツール・チップの場合はスクロールしないので その数は収まりきらず、昔からの制限である最大 32 個の隊員スタックが表示されます。この古い制限は、あなたが既に遭遇したかもしれない、またはこれから遭遇するかもしれない別のバグにも通じています。つまり、33 個以上のスタックの更新分が保存されません。それらを保存したければ、手動で 32 番目から上の位置に移動する必要がありますが、大抵は煩わしいでしょう [10]。なお、一つのスタックあたりの兵の数には制限がありません。

集団の移動速度 [11]

ある集団がマップ上をどのくらい速く移動するか、の度合いです。士気、隊の中で最高の経路探索スキルを持つ者、騎乗している隊員の数、捕虜、地形、昼と夜のサイクル、持ち物(鉄など)、天候、といった多くの要因の影響を受けます。一部の兵には tf_mounted フラグを指定でき、たとえ傭兵(Hired Blades, 一部の日本語版では「古参傭兵」や「傭剣士」)のような非騎乗の兵であっても、マップ上での移動速度は、乗馬スキルの影響を受けるようになります。士気は計算式の中で最も単純な要素の一つで、リーダー・スキル、食料の種類、隊員数の少なさ、「戦闘での勝利など最近の明るい出来事」によって高めることができます。

持ち物、特に交易品の重量 [12] は、集団の移動速度を著しく低下させます。前述の carries_goods(x) フラグ が関係します。予備の馬を在庫に入れれば荷馬として機能するので、この持ち物の不利を軽減できます。馬の種類は不問ですが、馬自体も速度を低下させるので、多くの馬を持ちすぎると悪影響があります。また、予備の馬を持っても、隊員数の多さによる速度ペナルティに影響しません。

ゲーム・エンジンは次のような計算式で移動速度を求めます [13]

(訳注: 下記は C++ 系のコード。)

float mbParty::getMovementSpeed()
{
  if (isShip())
  {
    return m_speedMultiplier * 15.0f;
  }
  else
  {
    float speed = m_speed * m_speedMultiplier;

    if (mbRegionTypeIsForest(m_regionType))
      speed *= 0.7f;

    if (g_game->isNight())
      speed *= 0.6f;

    return speed;
  }
}

float mbParty::calculateSpeed()
{
  float totalSpeed = 0.0f;
  float slowestTroopSpeed = 100000.0f;
  float totalWeight = ((m_flags & pf_carry_goods_mask) >> pf_carry_goods_shift) * 50.0f;
  int pathfinding = getSkill(skl_pathfinding);
  int numTroops = 0;
  int numRegulars = 0;
  int numPrisoners = 0;
  int numHorses = 0;
  mbParty tempParty;

  g_game->collectPartyAttachmentsToParty(m_no, tempParty);

  for (int i = 0; i < tempParty.m_numStacks; ++i)
  {
    mbPartyStack *stack = tempParty.getStack(i);
    mbTroop *troop = g_game->getTroop(stack->m_troopNo);
    float troopSpeed = troop->getMapSpeed();

    if (stack->isPrisoner())
      numPrisoners += stack->m_numTroops;
    else
      numRegulars += stack->m_numTroops;

    totalSpeed += stack->m_numTroops * troopSpeed;
    slowestTroopSpeed = rglMin(slowestTroopSpeed, troopSpeed);
    numTroops += stack->m_numTroops;

    if (troop->isHero())
    {
      int numInventorySlots = troop->getNumInventorySlots();

      for (int j = 0; j < numInventorySlots; ++j)
      {
        if (troop->m_inventory[j].isValid())
        {
          mbItemKind *itemKind = troop->m_inventory[j].getItemKind();

          if (itemKind->getType() == itp_type_goods)
            totalWeight += itemKind->m_weight;
          else if (itemKind->getType() == itp_type_horse)
            numHorses++;
        }
      }
    }
  }

  float moraleFactor = (m_morale - 0.5f) * 0.05f + 1.0f;

  if (slowestTroopSpeed > 10000.0f)
      slowestTroopSpeed = 0.0f;

  if (totalSpeed == 0.0f)
      return 0.0f;

  float partyFactor = 1.0f / (totalWeight / (numTroops + 4 * numHorses + 3) * 0.007f + 1.0f) * (pathfinding * 0.03f + 1.0f) * 2.6f * moraleFactor;

  return (partyFactor * ((totalSpeed / numTroops) * 0.8f + slowestTroopSpeed * 0.2f)) / log((float)(numPrisoners / 2 + numRegulars + numHorses / 2 + 16));
}

void mbParty::updateSpeedMultiplier()
{
  g_basicGame.m_triggerResult = 100;
  g_game->executeMappedScript(game_get_party_speed_multiplier, m_no, 0);
  m_speedMultiplier = g_basicGame.m_triggerResult / 100.0f;
}

(詳細 未調査)

ワールド・マップでの移動速度。 Dusk Voyager, Modding Q&A

Experience Constants

header_parties.py にある下記 2 つの定数(いわゆる経験値係数)は様々なスクリプトの結果に影響を与えます。

player_loot_share = 10
hero_loot_share = 3

これらは calculate_main_party_shares、player_loot_share、party_calculate_loot、party_give_xp_and_gold というスクリプトで使われ、戦闘で得られる経験値、金銭、戦利品に影響します。開発者が この定数の宣言場所としてここが理想的と考えた理由は わかりません。

村、街、城の主従関係

ゲーム・エンジンは、slot_village_bound_center というスロットを見て、領地(村、街、城)同士がどのように関連付けられているかを理解します。新規にゲームを開始すると、スクリプト game_start が全てをセットアップし、その中で各「村」が どの町や城に属すかを決めます。Native では慣例的に、各城には村を一つずつ、街には一つ以上の村を割り当てます。中心間の距離を用いて、どれがどれに属するかを選んでいます。詳しくは、module_scripts.py の中で上記スロットを使っている箇所を探してコードを確認してみて下さい [14]

これは Native での話で、独自 MOD では自由に設定できます。経済、募兵、隊商による物流、物品の生産、街の繁栄などは全てモジュール・システム上で指定できます。削除や置換などを行ないたければ、各コードを編集します。その作業は「このコード行を編集する」といった単純なものではなく、「複数ファイルの計 50 箇所に散らばった、100 行のコードを編集する」というったようなものです。だから まずは M&B の MOD 開発言語と機能テストの経験が、ある程度 求められます。Native をベースにしてもよいし、あるいは既存のオープン・ソース MOD をベースにしてもよいでしょう [15]

その他の注意点

スロットの、上のほうの番号の用途は? Lumos, Modding Q&A

隊と候伯 AI (AI に関する章?)。 Lav, Modding Q&A

街を割り当てると、隊の勢力の割り当てが上書きされる(意見)。 Fire_and_Blood (credit), Modding Q&A

物品の携行。 Ikaguia, Modding Q&A

party_set_ai_initiative の効果。 kalarhan, Modding Q&A

Party に指定するフラグ群の機能を知りたければ、modsys と各種チュートリアルを調べるといい。 kalarhan, Modding Q&A