Module Troops(兵種)

module_troops.py 冒頭に書かれた指定方法
各「兵種」には下記の項目がある。
(1) 兵種 ID (string)。 他のファイルから このレコードを特定するために使われる。接頭子 trp_ が先頭に自動的に付加される。
(2) 兵種名 (string)。
(3) 複数形の兵種名 (string)。
(4) フラグ群 (int)。 指定可能なフラグは header_troops.py を参照のこと。
(5) シーン入場点 (int)。 ヒーロー(訳注: 定義は後述)でのみ有意で、例えば scn_reyvadin_castle|entry(1) なら、この兵を Reyvadin 城の入場点「1 番」に配置。
(6) 予約 (int)。 定数 "reserved" または 0 を指定。
(7) 勢力 (int)。
(8) 装備品 (list)。 物品のリストで、最大 64 個まで。
(9) 属性 (int)。 例: str_6|agi_6|int_4|cha_5|level(5) (訳注: スキル画面の左側に表示される、体力、敏捷性(びんしょうせい)、知性、魅力(カリスマ性)の各初期値を、ビット毎論理和「|」で結んで指定。)
(10) 武器熟練度 (int)。 使用例:
  wp_one_handed(55)|wp_two_handed(90)|wp_polearm(36)|wp_archery(80)|wp_crossbow(24)|wp_throwing(45) (訳注: スキル画面の右側に表示される、片手武器、両手武器、長柄武器、弓、クロスボウ、投擲(とうてき)の各初期値を、ビット毎論理和で結んで指定。)
  関数 wp(x) で値 x 近辺の武器熟練度をランダムに生成できる。例えば熟練射手の他の武器熟練度を 60 前後にしたければ、wp_archery(160) | wp(60) など。
(11) スキル (int): 一覧は header_skills.py にある。例:
  knows_ironflesh_3|knows_power_strike_2|knows_athletics_2|knows_riding_2 (訳注: スキル画面の中央のカラムに表示される各スキル。この例では 強靭さ、強打、アスレチック、乗馬の初期値を指定している。)
(12) 顔コード (int): 顔生成画面で Ctrl e を押すと顔コードを得られる。(訳注: 事前に起動時の小メニューで「編集モード」を有効にしておく必要がある。New Game の際に顔生成画面の左上にコードが表示されるようになり、それをクリックするとクリップボードに値がコピーされる。)
(13) 顔コード2 (int) (一般兵のみに適用され、ヒーローでは省略できる。生成された兵種の場合は、ゲーム・エンジンが 顔コード 1~2 の間でランダムに作り出す。
(14) 兵の静止画 (string): この変数が設定されている兵種は、会話中に 3D ではなく画像を使用。

訳注: troop を「兵種」と訳すことについて。

説明図を、トップ・ページ前半にある「開発言語の書式」のページの「兵種(Troop)とエージェント(Agent)」の項に示しました。

M&B や M&B Warband の「隊員一覧」画面では、同種の兵(一般兵)が 1 行にまとめられ、人数が示されます。例えば どこそこ王国の騎馬槍兵、のような名称です。MOD 開発の文脈では troop という語が その各行(複数名が含まれ得る)に書かれた名称で種類を表わすので、ここでは troop を「兵種」と訳しています。主人公(プレイヤー・キャラクタ)やコンパニオンや特定の街の職人などには 1 人ずつ troop ID と名前を割り当て、複数名が束ねられないように(MOD 開発者が)制御します。また、後述されるように人物以外に一部の物にも troop ID を付与します。

また ここでは、「歩兵」「弓兵」「騎兵」のような class を「兵科」、戦場などで 0~9 キーを使って指示する division を「」と訳します。それらは「兵種, troops」とは別のものです。通常は「兵科」と「班」は一対一に対応しますが、一部の既存 MOD では更に sub-class副班」があり、ある兵科(例えば歩兵)の中の兵種(例えば どこそこ国の正規兵)の一部人員を副班として別の兵科(例えば弓兵)に出向させ、先方の班への 0~9 キーでの指示(例えば弓兵が 2 班なら 2)に従わせるものもあります。一方「グループ」のような語は曖昧なので この訳では使わないようにしました。

module_troops.py では、全ての一般兵、ヒーロー(主人公、コンパニオンなど)、チェスト*1、街の NPC が定義され、各々について、顔、能力スコア、装備品を定義します。新しいタイプの人物や兵種を作成したい場合は、このファイルを改造することになります。 (訳注: *1 このチェスト(chest)は胸部ではなく、tutorial_chest_1 や bonus_chest_1 といった保管箱や、人物の兵種に対応させた「持ち物の置き場」の兵種。)

このファイルの始めのほうには、武器の熟練度を計算する小さなコード・ブロックと、他の改造不可のコードがあります。「troops = [」というリストが始まる行まで読み進んで下さい。そこには、プレイヤーとゲームにとって大事な、様々な「兵種」のレコードがあります。闘技場で出てくる戦士のレコードなども含まれています。そういった一般兵は、「昇格」の説明をするための好例となるので、いくつか見ていきましょう。

["novice_fighter","Novice Fighter","Novice Fighters",tf_guarantee_boots|tf_guarantee_armor,no_scene,reserved,fac_commoners,
[itm_hide_boots],
str_6|agi_6|level(5),wp(60),knows_common,mercenary_face_1, mercenary_face_2],

これは新人闘士(novice fighter)という、ごくありふれた一般兵です。この兵種は、レベルが低く、戦闘にさほど長けておらず、能力スコアが低く、取り柄が特にありません。
レコードを詳しく見てみましょう。

(1) 兵種 ID = "novice_fighter"
(2) 兵種名 = "Novice Fighter"
(3) 複数形の兵種名 = "Novice Fighters"
(4) フラグ群 = tf_guarantee_boots|tf_guarantee_armor
(5) シーン = no_scene
(6) 予約 = reserved
(7) 勢力 = fac_commoners
(8) 装備品 = [itm_hide_boots]
(9) 属性 = str_6|agi_6|level(5)
(10) 武器熟練度 = wp(60)
(11) スキル = knows_common
(12) 顔コード = mercenary_face_1
(13) 顔コード 2 = mercenary_face_2

このレコードで注目すべきことが 3 つあります。

  1. この "novice fighter" という兵種には tf_guarantee_armor が指定されているのに、防具は持っていません。しかし、だからと言って tf_guarantee_armor が冗長というわけではなく、ゲーム実行中に受け取った防具を着用します。
  2. 最初(つまり、レベル 1 で)、この兵種の「体力」は 6、「敏捷性」は 6 です。「レベル」はゲーム開始時に 5 に引き上げられ、それに合わせて通常の数値も全て増加します。
  3. スキル「knows_common」を持っています。それは module_troops.py の始めのほうで定義されていて、複数スキルを集めたものです。その内容は、ファイルを読むと次のようなものです。
    knows_common = knows_riding_1|knows_trade_2|knows_inventory_management_2|knows_prisoner_management_1|knows_leadership_1
    
    この knows_common を持つ兵種は、ここに並んでいる全スキルを持ちます。乗馬 1、取引 2、持ち物管理 2、囚人管理 1、統率力 1 です。knows_common は、いわば定数です。つまり、数値、識別子、別の定数、その他の有効なオブジェクトといったものをまとめて言い換えるものです。このような定数の右辺では、何個でも「|」で連結できます。ただし、この定数を使う場所に相応しいものを集める必要があります (訳注: 原文は ここに更に文がありますが、くどいので省略)

では、リスト中の その次のレコードを見てみましょう。

["regular_fighter","Regular Fighter","Regular Fighters",tf_guarantee_boots|tf_guarantee_armor,no_scene,reserved,fac_commoners,
[itm_hide_boots],
str_8|agi_8|level(11),wp(90),knows_common|knows_ironflesh_1|knows_power_strike_1|knows_athletics_1|knows_riding_1|knows_shield_2,mercenary_face_1, mercenary_face_2],

この例では、先ほどよりも少しだけ強い兵「闘士」だということがわかります。より高い能力スコアを持ち、レベル 11 で、knows_common を上回る いくつかのスキルを持っています。ゲーム内で、隊にいる「新人闘士」がレベル 11 になるための充分な経験値に達している時、彼らを「闘士」に昇格できるようにしたいかもしれません。そういった「兵種ツリー」(訳注: 兵種間の昇格経路を示す木構造)については後述します。

他の兵種でも前の兵種と同様に、定数定義を使って、属性(レベルを含んだり含まなかったり)と武器の熟練度を下記のように宣言できます。

def_attrib = str_7 | agi_5 | int_4 | cha_4
knight_attrib_1 = str_15|agi_14|int_8|cha_16|level(22)
wp_swadia_low_tier = wpex(90,75,75,50,50,60)

(訳注: 上記のうち def_attrib と knight_attrib_1 と関数 wpex() は module_troops.py の始めのほうの離れた行で宣言されています。一方、wp_swadia_low_tier は v1.171 の どのファイルにも見当たらないので、例示用に ここで宣言しているもよう。)

繰り返しになりますが、定数を使う場所に相応しいものだけを右辺に書かねばなりません。新しい定数を宣言する場合には、その点に特に注意が必要です (訳注: 間違った次元のものを右辺に書いても、疑似コンパイル時にエラーになりにくいです)。Native (つまりモジュール・システム)のコードや他のオープン・ソース・コードの MOD の例を調べてみて下さい。

フラグ群

(訳注: 下記のような個々の「フラグ」をビット毎論理和「|」で連結して「フラグ群」に指定します。)
  • tf_allways_fall_dead ヒーローと非ヒーローの兵種に対し、戦闘で常に絶命するようにし、ノックアウトで済んだり後で回復したりすることを無くす。
  • tf_female この人物を女性(第 2 スキンに割り当て)とする。
  • tf_guarantee_armor 装備品に「防具」がある場合、この兵種がそれを装備することを保証する。
  • tf_guarantee_boots 装備品に「靴(ブーツ)」がある場合、この兵種がそれを装備することを保証する。
  • tf_guarantee_gloves 装備品に「手指防具」がある場合、この兵種がそれを装備することを保証する。
  • tf_guarantee_helmet 装備品に「兜(ヘルメット)」がある場合、この兵種がそれを装備することを保証する。
  • tf_guarantee_horse 装備品に「馬」がある場合、この兵種がそれを装備することを保証する。
  • tf_guarantee_polearm 装備品に「長柄武器」がある場合、この兵種がそれを装備することを保証する。
  • tf_guarantee_ranged 装備品に「飛び道具(矢、クロスボウ、鉄砲など)」がある場合、この兵種がそれを装備することを保証し、射手(archer troop)カテゴリに分類する。
  • tf_guarantee_shield 装備品に「盾」がある場合、この兵種がそれを装備することを保証する。
  • tf_hero (ヒーロー)は、主人公(プレイヤー・キャラクタ)だけでなく、主人公がゲーム内で遭遇するあらゆる個別の NPC (訳注: Non-Player Character)にも指定する。例えば「商人」も これに該当する。ヒーローと一般兵の主な違いは次のとおり。
    • ヒーローはユニーク(唯一無二)であり、誤った または意図的な設計で複製されない限り、各ヒーローは 1 人だけ。ただし、意図的設計でヒーローのクローンを作ろうという考えは良くない。各ヒーローの個体は 1 つだけである必要があり、複数の兵種名を持たない。だから、ヒーローのレコードの第 3 項目には第 2 項目と同じものを指定する。 (訳注: ヒーロー以外、つまり一般兵や村人は module_troops.py 内では個々の「兵種」つまり「兵の種類」として くくられます。これは隊員一覧画面で同種の兵が(兵種ごとに)まとめられて人数が示されるのと同じ扱いです。一方、闘技場や宿屋など、どこかのシーンに同種の人物が複数名 現れたりヒーローたちが現れる場合、個々の人物や馬を「エージェント」(agent)と呼びます。)
    • ヒーローは無敵(殺されない)。ヘルス値はパーセントで表わされる。 (訳注: ただし「ヒーローが絶命したことにして」、会話文やメニューを使ってゲーム・オーバーにすることは可能。)
    • ヒーローは、持ちうる「隊スタック」の全てを、各人が独自に持つ。 (訳注: 「スタック」については この web 文書の「Parties (集落や施設)」のページを参照のこと。ただし、その party モジュールの用途は現実には「集落や施設」です。)
    • ヒーローを指定すると、シーンに適宜 表示させることができる。 (訳注: この web 文書冒頭の第 5 項目「シーン」の説明にある「シーンはヒーローでのみ有意」を言い換えているもよう。)
    • 味方のヒーローは、敵に敗北した場合でも主人公側に残る、つまり敵に捕らえられないが、敵のヒーローは通常 主人公に捕らえられることがある。
  • tf_inactive は、チェスト、マーカーなど人物以外の兵種に使われる。このフラグを持つ兵種は、他のフラグが設定されていても防具を身に付けない。通常、シーン小道具またはメッシュが所定の位置に生成される。 (訳注: この和訳では troop を「兵種」としていますが、実際には人物だけでなく このような物体も含まれていることに注意して下さい。)
  • tf_is_merchant (商人)が指定されたヒーローは、その兵種のレコードで初期状態から割り当てられたものを除き、持ち物内の物品を装備できなくなります。言い換えると、これら商人はゲーム中にあらゆる種類の物品を受け取ることができますが、それを着用したり使ったりすることはなく、その物品は単に販売用の「商品」として表示されます。
  • tf_male この人物を男性(第 1 スキンに割り当て)とする。
  • tf_mounted マップ上で この兵種の移動速度が「乗馬スキル」で決まるようにし、この兵種の「兵科」を「騎兵」にする(飛び道具フラグ tf_guarantee_ranged より こちらが優先される)。
  • tf_no_capture_alive これは古い M&B で廃止されたフラグ(「この兵種を生け捕りにできず、捕虜にもならない」とされていたが、現在は使用不可)。
  • tf_randomize_face ゲーム開始時に顔をランダム化。この兵種のレコードには 2 つの顔コードが必要 [1]
  • tf_undead 基本的には第 3 の性別/人種(M&B でも同じ)。無暗に使うことはできず、module_skins.py の変更作業と、デフォルトのアンデッド・メッシュでの不正スキニングの対策が必要。
  • tf_unmoveable_in_party_window は、敵の捕虜には なり得るが、それ以外の状況で、守備兵として駐屯させたり、他の隊に譲渡したりできない。通常はヒーローを指定した兵種に指定する。 (訳注: ヒーロー以外に指定するような例外の説明は原文に無い。)
  • tf_unkillable は廃止。使用不可。

他にも下記のような複合フラグが 2 つ、module_troops.py で定義されています。

tf_guarantee_all = tf_guarantee_boots|tf_guarantee_armor|tf_guarantee_gloves|tf_guarantee_helmet|tf_guarantee_horse|tf_guarantee_shield|tf_guarantee_ranged
tf_guarantee_all_wo_ranged = tf_guarantee_boots|tf_guarantee_armor|tf_guarantee_gloves|tf_guarantee_helmet|tf_guarantee_horse|tf_guarantee_shield

シーン入場点

項目 5「シーン入場点」は、ミッション・テンプレートの入場点フラグ mtef_scene_source と連動します。このフラグは、静的に定義された入場点を持つ兵種に使われます。レコードで定義された入場点にフラグがある場合にのみ、ミッションの最初のフレームでそれらを生成します [2]。他のスクリプトを使用する場合は、準非推奨になる可能性があります。(Somebody, Modding Q&A)。

(訳注: module_troops.py 内を探すと見つかる通り、レコードの第 5 項目には、例えば scn_zendar_center | entry(1) のように、「シーン ID」と「入場点(の番号)」を「|」で連結して指定します。実際の 3D のシーン内の入場点の位置や向きは、Warband に組み込まれたシーン・エディタを使って指定したり変更したりします。詳しくはトップ・ページの前半にある「イン・ゲーム・エディタ」のページへ。下図は訳者が追加。)

その他の投稿での成果は こちら。Lumos, Modding Q&A, MadVader, Modding Q&A, Lumos, Modding Q&A, Docm30, Modding Q&A, その他 note

兵を出現させる 2 つの方法。(kalarhan, Modding Q&A

所属勢力

兵種は複数の勢力に属すことができない。とは言え、士気のスクリプトを調整することで相応のことを実現可能。(Lav, Modding Q&A

属性

(訳注: この項は、MOD 開発者向けというより、Warband をプレイしたことのある人なら知っていそうなルール説明に近いことがほとんどです。しかも Native のことだとしても ごく一部で、不完全に見えます。)

人物には 4 つの主要な属性(Attribute)があり、これらは できることに強く影響します。属性には 2 つの働きがあります。1 つは直接的な利点をもたらすこと、もう 1 つはその属性に依存するスキルを向上させることです。「体力」(Strength)属性により より優れた鎧を使えたり、「知性」(Intelligence)属性により、本を使えたりします。スキルは基礎となる特定の属性の 3 分の 1 までしか上げられません(切り捨て)。例えば、「強靭さ」(Ironflesh)スキルは「体力」属性が基礎なので、体力が14 の人物は「強靭さ」スキルを 4 までしか上げることができず、体力が 15 の人物なら 5 まで上げることができます。さらに、各属性には独自の利点があります。

Native では、プレイヤーが主人公の背景を選択し、その主人公の外観を作成すると、属性に使える 4 ポイントを獲得し、レベルアップするたびに追加の 1 ポイントを獲得します。プレイヤーが それらの使い道を一度 確定すると変更できません。属性の最大レベルは 63 です [3]

  • 体力(Strength, STR) これが +1 されるごとにヒット・ポイントが +1 される。(副効果として、近接武器、弓、投擲武器の与ダメージが増す。)
  • 敏捷性(Agility, AGI) これが +1 されるごとに武器熟練度ポイントが +5 され、戦場での移動速度が少し増す。
  • 知性(Intelligence, INT) これが +1 されるたびに、スキルに使えるポイントが +1 される。
  • 魅力(カリスマ性)(Charisma, CHA) これが +1 されるたびに隊の人員容量が +1 される。

ランダムな属性とスキルのポイント

モジュール・システムで兵種を追加または編集した後、コンパイルした時点でゲーム内で兵種の属性(体力、敏捷性など)やスキルが、実際に割り当てた値と違ってくることがあります。これは、新規ゲームごとに発生するレベルとスキルの再配分に関係しています。ゲーム・エンジンは、各兵種が、割り当てられたレベルに応じて持つべき属性ポイントとスキル・ポイントの合計量を持っているかどうかをチェックします。分配されるポイントが少ない場合、ゲームは自動的に属性とスキル・ポイントをランダムに割り当てます。これを避けたい場合は、レベルを下げるか、「魅力/カリスマ性」や「説得」などのような影響のない値を上げます [4] (訳注: この脚注のリンク先で「魅力」には触れていますが、「説得」は見当たりません)。

もちろん、そうせずにレベルに応じてそれらを配分することもできます [5]。属性の場合、その計算式は次のようになります。

Native では、
属性ポイント総量 = 17 + 16 + 兵種のレベル * 1
一般には、
属性ポイント総量 = 17 + 定義済み属性ポイント初期値 + 兵種のレベル * レベル当たり属性ポイント (module.ini の設定)

(訳注: module.ini は各 MOD のフォルダに置くファイル。この web 文書のトップページ、下のほうに説明ページへの見出しあり。)

定義済み属性ポイント初期値は、module_troops.py 内で最初のハード・コーディングされたレコード、つまり主人公(プレイヤー・キャラクタ)に書かれていて、Native なら下記のように その総量(合計値)は 16 です。

["player","Player","Player",tf_hero|tf_unmoveable_in_party_window,no_scene,reserved,fac_player_faction,
  [],
  str_4|agi_4|int_4|cha_4,wp(15),0,0x000000018000000136db6db6db6db6db00000000001db6db0000000000000000],

スキル・ポイントについては、「知性」属性が 1 ポイント与えられるとスキル・ポイントが 1 つ追加されることに注意して下さい。つまり、計算式は次のようになります。

Native:
スキル・ポイント総量 = 13 + 兵種のレベル * 1 + 知性
In general:
スキル・ポイント総量 = 13 + 兵種のレベル * レベル当たりスキル・ポイント (module.ini の設定) + 知性

主人公のキャラクタを作る時に上限を念頭に置くこともあるかもしれません。Native では、スキルは特定の属性の 3 分の 1 までしか上げられません(切り捨て)。これは、MOD の module.ini ファイルの設定 attribute_required_per_skill_level で変更できます。 Native ではデフォルト値が 3 です。

To do: より高いレベルの兵種ではスキル・ポイントが余る(TheCaitularity (credit), Modding Q&A

ヘルス/ヒット・ポイント

ヘルス・ポイントの計算式: 35 + 強靭さスキル * 2 + 体力

熟練度/武器ポイント

(訳注: スキル画面で右側に表示されるポイント群)

武器熟練度は、個々の武器を扱う能力です。片手武器、両手武器、長柄武器、弓、クロスボウ、投擲の計 6 つです。With Fire & Sword (WFaS) では、クロスボウが銃器に置き換えられています。お気づきかもしれませんが、銃器熟練度がデフォルトでは隠れていて、MOD によっては それを有効化することでクロスボウと銃器の両方を備えたものもあります。module.ini の当該行の 0 か 1 かを組み合わせることで、置換も併存もできます。

use_crossbow_as_firearm = 0
display_wp_firearms = 0

3 つの近接武器(片手、両手、長柄)の熟練度には、剣、斧、槍、メイスなどを扱う能力が含まれます。長柄武器は片手でも両手でも使えますが、片手武器や両手武器とは独立した武器クラスと見なされます。武器を使っているうちに、対応する熟練度が増します。人物が熟練度を上げると、近接武器なら より多くのダメージを与え、遠隔武器なら より正確に使えるようになります。攻撃速度も向上します。

プレイヤーは 2 つの方法で熟練度を高めることができます。1 つは戦闘で武器を使うこと、もう 1 つはレベルアップ時に獲得した熟練度ポイントを各熟練度に消費することです。武器熟練度の最大レベルは 699 であり、それ以上は武器熟練度にポイントを消費できなくなります。「武器熟練スキル」(スキル画面中央)のレベルは全ての熟練度(同画面右)の上限を 699 よりも下げ、それらのポイントの消費可能数を制限します。ただし、武器を使うことで、「武器熟練スキル」に制限されることなく、熟練度を上げることができます。武器熟練スキルの上昇により標準熟練度の上限が段階的に上がり、武器熟練スキルが 0 の時の上限 60 から始まり、武器熟練スキルが 1 つ上がるごとに上限が 40 ポイントずつ増します。

熟練度が高いほど、それをさらに上げるためにより多くの熟練度ポイントが必要になります。例えば、片手武器の熟練度が 20 のときに 1 ポイント増加するには、熟練度ポイントが 1 つで済みます。しかし、熟練度 380 を 1 ポイント上げるためには 10 ~ 16 の熟練度ポイントを費やす必要がある可能性があります。この量は武器熟練スキルの高さによって異なります。武器熟練スキルが高いほど、より多くのポイントを消費する必要があります。

武器の熟練度を高めると、(近接武器や、弓術スキルや豪投スキルが奏功する遠隔武器を使用する場合)与ダメージが増し、(遠隔武器を使う場合)攻撃速度は速くなって精度は向上します。全ての結果は段階的で、目立った違いが出るには熟練度 50 ポイントほどが必要な場合があり、熟練度がまだ低いうちは結果はより顕著になります。武器熟練スキルは熟練度ポイントの獲得率を高め、全ての熟練度の消費上限を引き上げます。

熟練度は、他の要因もあるものの、より高いレベルの敵と戦うほど速く増加し、より低いレベルの敵と戦うと遅くなります。攻撃が特に困難であったり与えたダメージが大きかったりする場合には、1 回の攻撃でより多くの利益を得る可能性もあります。武器で与えられるダメージが大きいほど、プレイヤーはその武器の熟練度に応じてより多くの経験値を獲得できます。遠隔武器の熟練度は、ダメージを与えたときよりも、より困難なショットが命中したときのほうが速く増加します(与ダメージ量は考慮されますが、比重はそれほど大きくありません)。騎乗で移動中の射撃だけでなく、ヘッド・ショットは、難易度の倍率にボーナスをもたらします。更に、使用される遠隔武器の種類も射撃の難易度に影響します。投擲武器は他の遠隔武器タイプよりも射程が一般的に短いので、射撃難易度が適度に上昇しますが、クロスボウは使い易さの点でペナルティを受けます。弓は射撃難易度の計算に関してボーナスもペナルティも受けません。

武器の熟練度は、前述の属性ポイントやスキル ポイントの場合のように、ランダムなポイント再配分の影響を受けません。およそ適合する設定を探すこともできるでしょう。「敏捷性」属性に与えられる各ポイントが、更なる 5 つの武器熟練度ポイントを与えることに注意して下さい。まとめると、次のような計算式になるでしょう。

一般に、
武器熟練度ポイントの総量 = 兵種のレベル * レベルあたり武器ポイント (module.ini で設定。Native では 10) + 5 * 敏捷性属性 + 定義済み武器熟練度初期値 + 10~60 の間の任意のパラメータを選択

Todo:ハード・コーディングされた能力名を編集する方法について少し文章を要追加 (Specialist, Modding Q&A)。(モジュール・ファイルを拡張するような新しい環境に限定の話でないなら)このようなメモが役立つ場所を他に探す必要がある。

ゲーム・エンジンが未割り当ての武器熟練度ポイントを武器熟練度ポイントに変換する計算式は次のとおりです [6](訳注: 実際のゲーム・エンジンは c++ などで書かれているので、このコードは それを Python で表現したもの。いずれにせよ、「60 から始まって 40 ずつ増える」のがハード・コーディングされていて MOD システムでは変更できないことを裏付けている。)

weaponMasterTable = [60, 100, 140, 180, 220, 260, 300, 340, 380, 420, 460]

def clamp(x, min, max):
    return min if x < min else max if x > max else x

def raise_wpf_nonlinear(currentWpf, addedWpf, weaponMaster):
    factor = 70.0 / (currentWpf + 70.0)
    threshold = float(weaponMasterTable[clamp(weaponMaster, 0, 10)])

    if threshold < currentWpf:
        factor *= 20.0 / (currentWpf - threshold + 20.0)

    for i in xrange(1, 15):
        if currentWpf > 50 * i:
            factor *= 0.65

    return currentWpf + factor * 2 * addedWpf

def get_wpp_required_to_upgrade(currentWpf, weaponMaster):
    newWpf = currentWpf
    oldWpf = int(newWpf)
    required = 0

    while (True):
        newWpf = raise_wpf_nonlinear(newWpf, 1.0, weaponMaster)
        required += 1

        if int(newWpf) > oldWpf:
            break

    return required;

print 'WPF:',
currentWpf = clamp(float(raw_input()), 0.0, 1000.0)
print 'WM:',
weaponMaster = clamp(int(raw_input()), 0, 10)
wppToUpgrade = get_wpp_required_to_upgrade(currentWpf, weaponMaster)
print 'WM skill: %d' % weaponMaster
print 'WPF before raise: %d' % int(currentWpf)
print 'WPP needed for raise: %d' % wppToUpgrade
print 'WPF after raise: %d' % int(raise_wpf_nonlinear(currentWpf, wppToUpgrade, weaponMaster))

顔コードの作成

顔コードを割り当てたい兵種がヒーロー(候伯、商人など、一人しか いない兵種)の場合は、レコードの(1 から数えて)12 番目の項目を設定するだけで済みます(いくつか例外があります。主に女性)。他の人物には、ゲーム・エンジンが 12 番目と 13 番目にある 2 つの顔コードの中からランダムな顔を選択します。「ランダム化」といっても かなり おかしなもので、ダミーのスキン/種族を指定しないと、ほとんどの場合、指定したことが無視されます [7]。 スキンが 2 つでない場合にその機能を完全に無効にするハード・コーディングされたスイッチがあるので、顔が再利用される問題を取り除くには、ダミーのスキンを module_skins.py に追加する必要があります。同じ顔がいくつも現れる理由は、恐らくパフォーマンス向上です [8]。ゲーム・エンジンによる このランダム化の方法は次のようなものです [9]

(訳注: こちらは Python でなく C++)

void mbFaceGenerator::loadKeysPair(const mbFaceKeys &keys1, const mbFaceKeys &keys2, int dna)
{
    g_dnaRng.seed(dna);
    loadKeys(keys1);

    int startHairNo = m_hairNo;
    int startBeardNo = m_beardNo;
    int startFaceTextureNo = m_faceTextureNo;
    float startAge = m_age;
    float startHairColor = m_hairColor;
    float startDeformValues[MB_MAX_NUM_DEFORM_KEYS];

    for (int i = 0; i < MB_MAX_NUM_DEFORM_KEYS; ++i)
    {
        startDeformValues = m_deformValues;
    }

    loadKeys(keys2);
    m_hairNo = startHairNo + rglRound((m_hairNo - startHairNo) * g_dnaRng.getFloat());
    m_beardNo = startBeardNo + rglRound((m_beardNo - startBeardNo) * g_dnaRng.getFloat());
    m_faceTextureNo = startFaceTextureNo + rglRound((m_faceTextureNo - startFaceTextureNo) * g_dnaRng.getFloat());
    m_age = startAge + (m_age - startAge) * g_dnaRng.getFloat();
    m_hairColor = startHairColor + (m_hairColor - startHairColor) * g_dnaRng.getFloat();

    for (int i = 0; i < MB_MAX_NUM_DEFORM_KEYS; ++i)
    {
        m_deformValues = startDeformValues + (m_deformValues - startDeformValues) * ((g_dnaRng.getFloat() + g_dnaRng.getFloat()) * 0.5f);
    }

#if !defined DEDICATED_SERVER
    getSkin()->applyConstraintsToDeformValues(-1, m_deformValues);
#endif
}

顔コードを得るには、次のようにします。事前に Warband 起動時の小メニューで「編集モード」を有効にしておき、ゲーム中に c キーで主人公のスキル画面を表示、左上に 3D で表示された顔をクリックします。(編集モードかどうかにかかわらず)顔編集画面になるので、変更したければ各スライダーと設定を希望の顔になるまで変更します。ここで Ctrl e をクリックすると、上に現在の顔に相当する顔コードが(ボタンとして)表示されます。それをクリックすると、顔コードがクリップ・ボードにコピーされます。それを module_troops.py に貼り付けます [10]

(訳注: 元に戻す機能は無いので注意。ただしゲームをセーブしなければ保存されません。下図は訳者が追加。)

また、module_tropps.py の先頭から少し下がったところにある、swadian_face_younger_1 で始まる行の一群で顔コードを事前定義していて、その中の下記を利用することもできます。

woman_face_1 = 0x0000000000000001000000000000000000000000001c00000000000000000000

例えば この woman_face_1 を tropps のレコードに指定してもよいし、独自の顔コードの定義を自由に追加することもできます。この一群の顔コードの下には、次のような二次的な定義(「女性1」を基に「難民1」を作っている)もあります。

refugee_face1 = woman_face_1

「単に women_face_1 のほうを使えばいいのでは?」と思われるかもしれません。このようにしている理由は簡単で、後で「難民」に別の顔コードを与える可能性があるかもしれないが、今のところは別の定義済みの顔コードで間に合わせる、つまり一種のプレース・ホルダーにしている、ということです (訳注: つまり、左辺の識別子は「難民」用に仮予約しているが、右辺は仮決めなので自由に変えてよい、ということ)

新しい種族を加える場合は、module_troops.py の先頭近く(ただし、ハード・コーディングされたものより後)に兵種として追加します。次に、上述のように編集モードで Warband を起動し、ゲーム内の主人公のスキル画面を表示します。編集モードでは左上のプレイヤーの画像の上に人物選択用の左右のボタンが現れるので、追加した兵種が現れるまで どちらかのボタンを押して探します。見つかったらその画像をクリックすると、上と同様に顔コードにアクセスできます。事前にそのような新しい兵種の顔コードの初期値をゼロにしておくと、このようなテンプレート兵種の顔コードを直接設定する時間を節約できます [11]

(訳注: 上記「ゼロにしておく」は不可解。脚注 [11] の一連の投稿では、質問者が「troll_face1 と troll_face2 をともに 0 にする、ということか」と確認したのに対し、回答者は「I mean an actual zero」と答えている。ともに 0 でなく何なのかが不明。更に質問者は「それを試したが やはりクラッシュする」と言っている。)

マルチ・プレイヤーでは、プレイヤーの兵種テンプレートとボットの兵種のレコードを区別する必要があります(Native では、例えば兵種 ID に _ai を追加することで区別します)。マルチ・プレイヤーの兵種の顔は、プレイヤーのプロフィール・エンジン側の顔によってオーバライドされるので、割り当てた顔コードはボットに対してのみ機能します。ただし、できることは、別のエージェントを生成し、player_spawn_new_agent の代わりに player_control_agent 命令を使うことです [12]

Todo:

顔コードのシード(たね)が兵種の装備を決定する(Somebody, Modding Q&A and Modding Q&A)。たぶん、この投稿に関する後の項も参照。

顔キーの命令(kalarhan, Modding Q&A

顔コード関連(SupaNinjaMan, WSE problem

マルチ・プレイヤーのヒーローの兵種が正常に動作するには、常に同じ顔コードが 2 回必要(TortenSkjold (credit), Mount & Blade Modding Discord

顔コードに 0 を指定(jacobhinds, Modding Q&A

DNA

兵種の DNA は、ヒーロー以外の兵種のエージェントの顔と装備の選択を決定する乱数のシード(たね)です。2 人のエージェントが同じ DNA を持っている場合、彼らは毎回同じ顔と装備を持っているはずです [13]。兵種の DNA を扱う命令は ごく僅かですが、その中で最もよく知られているのは set_visitor 命令です。

前の項で説明したように、兵種の DNA の設定は、module_skins.py に 3 つ以上のスキンのレコードがある場合にのみ機能します。ゲーム・エンジンは、恐らくパフォーマンス上の理由から、32 種類の異なる顔(男性 16 人、女性 16 人)のみを生成するシステムを使用しています。このシステムの欠点は、DNA に関係なく 16 人の顔が毎回ランダム化されるので、DNA が期待どおりに機能しない場合があることです。2 つ以外の数のスキンがあると、この「最適化」が無効になります。DNA を指定しない場合、ゲーム・エンジンは下記の計算式を使って DNA を生成します。[14]

37 * 入場点番号 + 79 * シーン番号 + 19 * 入場点に出現する増援の人数 + 147 * ゲーム固有のシード;

ダミーのスキン・レコードを追加しないと、ウェーブ(訳注: 戦場で第 2 波、第 3 波と追加で現れる敵や味方の増援のことらしい)で出現する兵種の顔のランダム化が崩れるのではないか、と疑問に思うかもしれません。ウェーブ内の全ての兵種は、同じシーン、同じ入場点、同じ数の援軍、および同じ固有シードを持ちます。これによってクローンが大量に発生するようであれば、それは全く良いことではありません。ただし、増援の数は同じではなく、最初の兵種は 0、2 番目の兵種は 1 のようになります。このカウンターは戦闘全体にわたって継続し、それにより兵種の顔がランダム化されます [15]

ボット/AI 兵の装備選択

前述のように、兵種の DNA はエージェントによる疑似ランダムな装備の選択に使われています (訳注: プログラムが作り出す乱数は 通常は どれも疑似乱数なので、ここでだけ敢えて「疑似」を付けるのは、逆に不自然?)。ゲーム・エンジンは下記のコードを使います [16]

(訳注: Python でなく C++)

void mbTroop::equipItems(int dna, bool requireCivilian, bool prohibitFullhelm)
{
    if (m_flags & tf_inactive)
        return;

    bool bowEquipped = false;
    bool bowAmmoEquipped = false;
    bool crossbowEquipped = false;
    bool crossbowAmmoEquipped = false;
    bool gunEquipped = false;
    bool gunAmmoEquipped = false;
    bool oneHanderEquipped = false;
    bool shieldEquipped = false;
    bool polearmEquipped = false;
    bool swordEquipped = false;

    unequipItems();

    unsigned int requireFlags = 0;
    unsigned int ignoreFlags = 0;

    if (isHero() && requireCivilian)
        requireFlags = itp_civilian;

    if (isHero() && prohibitFullhelm)
        ignoreFlags = itp_covers_head;

    equipItemSlot(dna, ek_head_armor, requireFlags, ignoreFlags, -1, -1, -1, (m_flags & tf_guarantee_helmet) != 0);
    equipItemSlot(dna, ek_body_armor, requireFlags, ignoreFlags, -1, -1, -1, (m_flags & tf_guarantee_armor) != 0);
    equipItemSlot(dna, ek_leg_armor, requireFlags, ignoreFlags, -1, -1, -1, (m_flags & tf_guarantee_boots) != 0);
    equipItemSlot(dna, ek_hand_armor, requireFlags, ignoreFlags, -1, -1, -1, (m_flags & tf_guarantee_gloves) != 0);
    equipItemSlot(dna, ek_horse, 0, ignoreFlags, -1, -1, -1, (m_flags & tf_guarantee_horse) != 0);

    for (int i = ek_weapon_0; i <= ek_weapon_3; ++i)
    {
        bool equipped = false;

        if (i == ek_weapon_0)
        {
            bool guaranteeRanged = (m_flags & tf_guarantee_ranged) != 0;

            equipped = equipItemSlot(dna, 0, 0, ignoreFlags, itp_type_pistol, itp_type_musket, -1, guaranteeRanged);

            if (!equipped)
                equipped = equipItemSlot(dna, 0, 0, ignoreFlags, itp_type_bow, itp_type_crossbow, itp_type_thrown, guaranteeRanged);

            if (equipped)
            {
                switch (m_equipment.getItemKind()->getType())
                {
                case itp_type_bow:
                    bowEquipped = true;
                    break;
                case itp_type_crossbow:
                    crossbowEquipped = true;
                    break;
                case itp_type_thrown:
                    oneHanderEquipped = true;
                    break;
                case itp_type_pistol:
                case itp_type_musket:
                gunEquipped = true;
                break;
                }
            }
        }

        if (!equipped && bowEquipped && !bowAmmoEquipped)
        {
            equipped = equipItemSlot(dna, i, 0, ignoreFlags, itp_type_arrows, -1, -1, true);

            if (equipped)
                bowAmmoEquipped = true;
        }

        if (!equipped && crossbowEquipped && !crossbowAmmoEquipped)
        {
            equipped = equipItemSlot(dna, i, 0, ignoreFlags, itp_type_bolts, -1, -1, true);

            if (equipped)
                crossbowAmmoEquipped = true;
        }

        if (!equipped && gunEquipped && !gunAmmoEquipped)
        {
            equipped = equipItemSlot(dna, i, 0, ignoreFlags, itp_type_bullets, -1, -1, true);

            if (equipped)
                gunAmmoEquipped = true;
        }

        if (!equipped && oneHanderEquipped && !shieldEquipped)
        {
            equipped = equipItemSlot(dna, i, 0, ignoreFlags, itp_type_shield, -1, -1, (m_flags & tf_guarantee_shield) != 0);

            if (equipped)
                shieldEquipped = true;
        }

        if (!equipped && !polearmEquipped)
        {
            equipped = equipItemSlot(dna, i, 0, ignoreFlags, itp_type_polearm, -1, -1, (m_flags & tf_guarantee_polearm) != 0);

            if (equipped)
            {
                polearmEquipped = true;

                if (!(m_equipment.getItemKind()->m_properties & itp_two_handed))
                    oneHanderEquipped = true;
            }
        }

        if (!equipped && !swordEquipped)
        {
            equipped = equipItemSlot(dna, i, 0, ignoreFlags, itp_type_one_handed, itp_type_two_handed, -1, true);

            if (equipped)
            {
                swordEquipped = true;

                if (!(m_equipment.getItemKind()->m_properties & itp_two_handed))
                    oneHanderEquipped = true;
            }

            if (!equipped && !polearmEquipped)
            {
                equipped = equipItemSlot(dna, i, 0, ignoreFlags, itp_type_polearm, -1, -1, true);

                if (equipped)
                {
                    polearmEquipped = true;

                    if (!(m_equipment.getItemKind()->m_properties & itp_two_handed))
                        oneHanderEquipped = true;
                }
            }
        }

        if (!equipped)
        {
            equipped = equipItemSlot(dna, i, 0, ignoreFlags, itp_type_thrown, -1, -1, false);

            if (equipped)
                oneHanderEquipped = true;
        }

        if (!equipped)
        {
            equipped = equipItemSlot(dna, i, 0, ignoreFlags, itp_type_one_handed, itp_type_two_handed, -1, false);

            if (equipped)
            {
                if (!(m_equipment.getItemKind()->m_properties & itp_two_handed))
                    oneHanderEquipped = true;
            }
        }

        if (!equipped && bowEquipped)
            equipped = equipItemSlot(dna, i, 0, ignoreFlags, itp_type_arrows, -1, -1, false);

        if (!equipped && crossbowEquipped)
            equipped = equipItemSlot(dna, i, 0, ignoreFlags, itp_type_bolts, -1, -1, false);

        if (!equipped && gunEquipped)
            equipped = equipItemSlot(dna, i, 0, ignoreFlags, itp_type_bullets, -1, -1, false);

        if (isHero() && !equipped)
        {
            if (!equipItemSlot(dna, i, 0, ignoreFlags, itp_type_thrown, -1, -1, true))
            {
                if (equipItemSlot(dna, i, 0, ignoreFlags, itp_type_one_handed, itp_type_two_handed, itp_type_polearm, true))
                {
                    if (!(m_equipment.getItemKind()->m_properties & itp_two_handed))
                        oneHanderEquipped = true;
                }
            }
        }
    }
}

bool mbTroop::equipItemSlot(int dna, int itemSlotNo, unsigned int requireFlags, unsigned int ignoreFlags, int requiredItemType1, int requiredItemType2, int requiredItemType3, bool guarantee)
{
    int inventorySlotNo = findSuitableItem(dna, itemSlotNo, requireFlags, ignoreFlags, requiredItemType1, requiredItemType2, requiredItemType3, guarantee);

    if (inventorySlotNo < 0)
        return false;

    unequipItem(itemSlotNo);
    m_equipment[itemSlotNo] = m_inventory[inventorySlotNo];
    m_inventory[inventorySlotNo].m_itemKindNo = -1;
    return true;
}

int mbTroop::findSuitableItem(int dna, int itemSlotNo, unsigned int requireFlags, unsigned int ignoreFlags, int requiredItemType1, int requiredItemType2, int requiredItemType3, bool guarantee)
{
    int numInventorySlots = getNumInventorySlots();
    int numSuitableItems = 0;

    for (int i = 0; i < numInventorySlots; ++i)
    {
        if (m_inventory.isValid() && isInventoryItemSuitable(i, itemSlotNo, requireFlags, ignoreFlags, requiredItemType1, requiredItemType2, requiredItemType3))
            numSuitableItems++;
    }

    if (rglConfig::Battle::bRandomTroopEquipping)
    {
        if (numSuitableItems <= 0)
            return -1;

        if (!guarantee)
            numSuitableItems++;

        g_dnaRng.setValue(157 * dna + 7);

        int randomIndex = g_dnaRng.getInt(numSuitableItems);
        int index = 0;

        for (int i = 0; i < numInventorySlots; ++i)
        {
            if (m_inventory.isValid() && isInventoryItemSuitable(i, itemSlotNo, requireFlags, ignoreFlags, requiredItemType1, requiredItemType2, requiredItemType3) && index++ == randomIndex)
                return i;
        }

        return -1;
    }
    else
    {
        float bestValue = -1.0f;
        float worstValue = 1000000000.0f;
        int bestCase = -1;
        int worstCase = -1;

        if (numSuitableItems && !guarantee)
            numSuitableItems += 3;

        float threshold = numSuitableItems > 0 ? (1.7f / numSuitableItems) : 0.0f;

        for (int i = 0; i < numInventorySlots; ++i)
        {
            if (m_inventory.m_itemKindNo >= 0 && isInventoryItemSuitable(i, itemSlotNo, requireFlags, ignoreFlags, requiredItemType1, requiredItemType2, requiredItemType3))
            {
                float value = (float)m_inventory.getItemKind()->m_value;

                g_dnaRng.setValue(3 * m_inventory.m_itemKindNo + 157 * dna + 7);

                if (isHero() || threshold > g_dnaRng.getFloat())
                {
                    if (bestValue < value)
                    {
                        bestValue = value;
                        bestCase = i;
                    }
                }

                if (guarantee)
                {
                    if (worstValue > value)
                    {
                        worstValue = value;
                        worstCase = i;
                    }
                }
            }
        }

        if (bestCase != -1)
            return bestCase;
        else
            return worstCase;
    }
}

bool mbTroop::isInventoryItemSuitable(int inventorySlotNo, int itemSlotNo, unsigned int requireFlags, unsigned int ignoreFlags, int requiredItemType1, int requiredItemType2, int requiredItemType3)
{
    mbItemKind *itemKind = m_inventory[inventorySlotNo].getItemKind();
    int type = itemKind->getType();

    return canEquipItemInSlot(m_inventory[inventorySlotNo], itemSlotNo) && (itemKind->m_properties & requireFlags) == requireFlags && (itemKind->m_properties & ignoreFlags) == 0 && (requiredItemType1 == -1 || requiredItemType1 == type || requiredItemType2 == type || requiredItemType3 == type);
}

従って、エージェントがヒーローの兵種で、ミッション・テンプレートでの発生点のレコードに不用装備群フラグ(Alter Flags)の af_require_civilian がある場合、そのエージェントはアイテム・プロパティ・フラグ itp_civilian を持つ装備を選択するだけ、ということがわかります。また、発生点レコードに不用装備群フラグ af_override_fullhelm がある場合、そのエージェントはフラグ itp_covers_head の付いたヘルメットを無視します。 tf_inactive フラグが付いた兵種は何も装備しません。

(訳注: 「兵種」が定義上の区分であるのに対し、「エージェント」はそれが具現化されてシーン(戦場や宿屋など)に現れたもの。不用装備群フラグについて詳細はミッション・テンプレートのページの「不用装備群フラグ」の項。)

武器の選択は、下記のようになります。

  1. フラグ tf_guarantee_ranged を兵種に指定すると、そのエージェントは装備品スロット 0(このスロットは 0~3)に遠隔武器を持つ。入手可能なら常に銃器(itp_type_pistol または itp_type_musket)を持ち物から優先して使う。そうでなければ、別の遠隔武器(itp_type_bowitp_type_crossbow、または itp_type_thrown)を使う。
    保証フラグが指定されていない場合、(可能な時に)遠隔武器を装備するかどうかはランダムになる。持ち物に銃器がある場合でも、弓、クロスボウ、または投擲武器を装備する可能性がある。
    弓、クロスボウ、マスケット銃、またはピストルは常にエージェントの装備品スロット 0 に装備される。投擲武器以外の 2 つ目の遠隔武器を装備することはない。
  2. エージェントが矢、ボルト、弾薬を必要とする遠隔武器を装備した場合、2 番目の装備品スロット(訳注: つまりスロット 1)に各弾(itp_type_arrowsitp_type_bolts、または itp_type_bullets)を持つ。
  3. エージェントがカテゴリ oneHanderEquiped の武器(つまり片手武器)を装備している場合、フラグ tf_guarantee_shield (盾を保証)が兵種に指定されていれば、そのエージェントは常に持ち物から盾を選択する。それ以外の場合は、エージェントが(可能な時に)盾を装備するかどうかはランダムになる。
    この oneHanderEquipedカテゴリに属するのは、投擲武器、全ての長柄武器、それとitp_two_handed フラグの無い片手または両手武器。カテゴリ oneHanderEquiped の武器を装備していないエージェントは、決して盾を装備しない。
  4. フラグ tf_guarantee_polearm を兵種に指定すると、そのエージェントは持ち物にある長柄武器が利用可能であれば、装備品スロットに長柄武器を持つ。装備された長柄武器にアイテム・プロパティ・フラグ itp_two_handed が指定されていない場合、カテゴリ oneHanderEquiped の武器(つまり片手武器)になる。
    保証フラグが指定されていなければ、エージェントが(可能な時に)長柄武器を装備するかどうかはランダムになる。
  5. 装備品スロットに まだ空きがある場合、エージェントは持ち物から片手武器または両手武器(itp_type_one_handed または itp_type_two_handed)を装備する。
    兵種の持ち物に片手武器も両手武器も無ければ、そのエージェントは、たとえフラグ tf_guarantee_polearm (長柄武器)が指定されていなくても、持ち物に長柄武器があれば それを装備する。
  6. 装備品スロットに まだ空きがあるなら、そのエージェントは持ち物から投擲武器を装備する可能性がランダムにある。
  7. 装備品スロットに まだ空きがあるなら、そのエージェントは持ち物から片手武器または両手武器を装備する可能性がランダムにある。
  8. 装備品スロットに まだ空きがあり、エージェントが弓を装備している場合、持ち物からさらに多くの矢を装備する可能性がランダムにある。
  9. 装備品スロットに まだ空きがあり、エージェントがクロスボウを装備している場合、持ち物から更に多くのボルトを装備する可能性がランダムにある。
  10. 装備品スロットに まだ空きがあり、エージェントが銃器を装備している場合、持ち物から更に多くの弾丸を装備する可能性がランダムにある。
  11. 装備品スロットに まだ空きがあり、兵種がヒーローである場合、そのエージェントは投擲武器を装備する。それを持っていなければ、代わりに長柄武器、片手武器、または両手武器を持つ。

同じ種類の複数の武器が持ち物にある場合の半ランダム的な選択プロセスと、価格などが それにどのように影響するかの説明が未記入。

複数の武器を装備したボット(jacobhinds, Modding Q&A

ボットの装備選択(kalarhan, Modding Q&A

ボットによる、価格に基づく装備選択。(K700 氏に再質問する前に検索すること) いくつかの投稿は後で別の投稿にリンクされていて、これにもマークする必要があった(kalarhan, Modding Q&A

兵種の持ち物に複数の装備を置く(kalarhan redirect, Modding Q&A

装備は正規曲線分布に基づいて選択される。例えば、安価、普通、高価の剣が 1 つずつあると、ほとんどの兵が普通のを使い、ごく一部が安価と高価なものを使う。普通のを 2 個増やすなどして比率を変えることで、分布を調整できる。(kalarhan, Modding Q&A

AI の武器選択(InVain, kalarhan と _Sebastian_, Modding Q&A, Modding Q&A, Modding Q&AModding Q&A

AI の防具選択(jacobhinds, Modding Q&A, と kraggrim, Modding Q&A

ボットの装備の分布(kalarhan, Modding Q&A

武器の保障(Somebody, Modding Q&A

兵の静止画 [17]

会話中は通常、会話シーン内で現在 話しているグループが表示され、発言者ごとにカメラが向きます。ただし、兵の静止画 を指定すると、代わりに静止画が表示されます。2D 平面メッシュを使うことが推奨されますが、module.ini 内にロードされた BRF ファイルからのメッシュも可です。メニューやプレゼンテーションの場合の同様の機能とは異なり、module_meshes.py 内にメッシュを登録する必要はありません。BRF にあるメッシュ名を文字列として使うだけで済みます。

このメッシュは、+Y 方向が上、+X 方向が右で、カメラが +Z から -Z 方向を見下ろすように正投影的に(パース無しに)レンダリングされます。メッシュは自動的にセンタリングされるので、位置を指定する必要はありません。メッシュは会話ウィンドウの境界に合わせて拡大縮小されます。この強制的な拡縮により、ウィンドウの縦横比に合わせてメッシュが潰れたり伸びたりするといった問題が発生する可能性があります。更に、非長方形メッシュの場合、メッシュの背後の空間は各フレームで再描画されず、関係ポップアップが画面上に残るなど、視覚的な異常が起こります。

半透明の領域は、フレームごとにメッシュの以前の画像の上に重ねるにつれ、どんどん透明度が下がるので、要注意です。完全な透明なら、この問題は発生しません。

また、2 つの兵種の会話で、一方が兵の静止画を持ち、もう一方が持たない場合、2 番目の兵は映らず、一方の兵の静止画のみが表示されることにも注意して下さい。静止画を持つ者同士が会話をする場合の挙動を確認するには更なるテストが必要ですが、最初の発言者が優先され、その画像が表示されるようです。

兵種ツリー

(訳注: ご存じのように、一部の既製 MOD ではこのツリーを「ビジュアル化」する機能を内包しています。Native、つまり MOD システムのデフォルト状態では その機能はありません。ビジュアル化機能を汎用的に分離しようとした発端は恐らく右の投稿と思われ、そこに表示(presentation)その他のためのコードがあります。Dynamic Kingdom Troop Tree Presentation, Open Source Code and Kits)

兵種ツリーの構造を決めるリストは module_troops.py の一番下にあります。そこでは、経験値の条件が満たされた時に、どの兵種を昇格(アップグレード)できるか、何に昇格できるのか、を定義できます。

見ての通り、各兵種の昇格の選択肢は、upgrade() または upgrade2() という関数を使って定義します。最初の文字列は昇格前の兵種 ID で、2 番目の文字列は昇格後の兵種 ID です。例えば、upgrade(troops,"farmer", "watchman") なら、「農民」が必要な経験を積むと「見張り」に昇格できます。

2 つの違いは次の通りです。

  • upgrade(troops,"昇格前兵種", "昇格後兵種") には選択肢が無く、昇格後の兵種は 1 種類だけです。
  • upgrade2(troops,"昇格前兵種", "昇格後兵種1", "昇格後兵種2") のほうは 2 択です。現状、最大で 2 択までです。

この昇格を指定する一連の行は、兵種のメイン・リストの外側(「]」で閉じた後)であることに注意して下さい。コンパイラは決まった順序でレコードを認識するので、MOD 開発者は それに合わせて各要素を正しく配置する必要があります。また、昇格の各行は、リストではなく関数呼び出しなので行末にカンマ「,」が無い、ということにも注意して下さい。ソース・コードを読み書きする際に、個々のブロックの内外など、どこを読んでいるかとか、どんなコード規約が使われているかといったことを、常に念頭に置くようにして下さい。

昇格イベントが生じると、ゲーム・エンジンによって 2 つのスクリプトが呼び出されます。隊員一覧画面で兵種を選択すると、エンジンは昇格可能な各経路についてスクリプト「game_get_upgrade_cost」を 1 回ずつ、つまり計 1~2 回呼び出します。1 回なのか 2 回なのかは、その兵種が昇格可能な 2 番目の経路(つまり分岐)があるかどうかで決まります。この兵種が昇格する時、スクリプト「game_get_upgrade_cost」が 1 回目に呼び出された時、選択した昇格費用を取得し、更に、全ての昇格費用を取得するために 2 回呼び出されます。エンジンが そうする理由は わかりません。結果的に、昇格経路が分岐しない兵種の場合、このスクリプトは昇格中に 3 回呼び出され、分岐する兵種の場合は 5 回呼び出されます [18]

(デフォルトでは)兵種ツリーには 1 つの兵種から 3 つ以上に分岐して昇格するような経路を設定することはできないことに注意して下さい。これはハード・コーディングされた制限であり、隊員一覧画面だけでなく、ゲーム・エンジンの C++ クラス「Troop」でも 2 つまでです [19]ただし、これは あくまで通常のシステムでの話です。独自コードや UI などを使用して独自のシステムを作ることを妨げるものではありません。2 つまでしか分岐しないシステムは既に完成していて すぐに使えるので利用が容易、というだけのことです [20]

兵科(クラス)と班(所属)と副班(サブクラス)

兵科(class)は元々の M&B からあるものです。コードに手を加えない限り、設定した保証フラグに応じて常に、歩兵、弓兵、騎兵(順にクラス番号 0、1、2)のいずれかになります(デフォルトでは歩兵となり、tf_guarantee_ranged なら弓兵、tf_guarantee_horse なら騎兵になり、騎兵の指定が弓兵よりも優先されます)。このように兵科は兵種に適用されますが、班(division)は戦場などにいるエージェントにのみ適用されます。Warband では班が導入されていて、戦闘中に数字キー(1 ~ 9 または班番号 0 ~ 8)を使って班ごとに指示を出すことができます。ほとんどの場合、Warband の兵科は無視しても大きな影響はありません。副班(sub-class)は少し複雑で、複数班にまたがる人員を命令対象にできます [21]

訳注:

下記は A World of Ice and Fire(AWoIaF)という MOD の副班設定画面です。この時点で隊には兵種「従者」が 22 名いて、本来はその全員の兵科が「歩兵」なので、シーン(戦場など)では 1 キーに続いて出した命令に従います。ここでは そのうちの 6 名を「槍兵」へ出向させているので、戦場では その 6 名が槍兵とともに行動し、4 キーに続く命令に従います。残る 16 名が主班として表示されています。同様に、兵科が「弓兵」の「リーチ長弓兵」17 名のうち 8 名を歩兵へ出向させ、その 8 名は歩兵と共に 1 キーで指令を受けます。リヴァーランド弓兵は 10 名中 3 名を槍兵に出向させています。

このような出向者を sub-division または sub-class と呼び、ここでは「副班」と訳しています。

班/クラスの指定

フラグ群についての注意

班(division)の設定(Lav, Modding Q&A

経験値の計算式

経験値を獲得すると、「X の経験値を獲得しました」というメッセージが表示されます。計算式はハード・コーディングされているので、編集できません。また、経験値が減ることもありません。

(lvl+10)*(lvl+10)/5-1

ただし、敵を倒した時に発生するミッション・トリガーを作成することで元のメッセージを無効にし、その先頭に経験値の計算処理を追加しておき、獲得した合計経験値を新しい独自メッセージで報告することならできます。次のようにします [22]

(ti_on_agent_killed_or_wounded, 0, 0,[
    (store_trigger_param_1, ":victim"),
    (store_trigger_param_2, ":killer"),
    (store_trigger_param_3, ":is_wounded"),
    (agent_get_troop_id, ":victim_troop", ":victim"),
    (agent_get_troop_id, ":killer_troop", ":killer"),
    (try_begin),
        (eq, ":killer_troop", "trp_player"),
        (set_show_messages, 0),
        (store_character_level, ":victim_lvl", ":victim_troop"),
        (store_add, ":old_xp", ":victim_lvl", 10),
        (val_mul, ":old_xp", ":old_xp"),
        (val_div, ":old_xp", 5),
        (val_sub, ":old_xp", 1),
        (assign, ":new_xp", ":old_xp"),
        #[then increase new_xp any way you want as long as you are using val_xxx ops instead of store_xxx]
        (assign, reg3, ":old_xp"),
        (assign, reg2, ":new_xp"),
        (str_clear, s0),
        (str_clear, s1),
        (str_clear, s2),
        (str_store_troop_name, s0, ":victim_troop"),
        (str_store_troop_name, s1, ":killer_troop"),
        (try_begin),
            (eq, ":is_wounded", 0),
            (str_store_string, s2, "@killed"),
        (else_try),
            (eq, ":is_wounded", 1),
            (str_store_string, s2, "@knocked unconscious"),
        (try_end),
        (store_sub, ":diff", reg2, reg3),
        (add_xp_to_troop, ":diff", "trp_player"),
        (assign, "$player_has_recently_made_a_kill", 1),
    (try_end),
],
  [],
  ),
(0, 0, 0, [],
[(eq, "$player_has_recently_made_a_kill", 1),
(set_show_messages, 1),
(assign, "$player_has_recently_made_a_kill", 0),
(display_message, "@{s0} {s2} by {s1}", 0x008080), # a bit darker than the normal lovely teal text, but still
(display_message, "@You got {reg2} experience"),
]),

ゲーム・エンジンはレベルを上げるために必要となる経験値の固定テーブルを持っています。昇格に必要な経験値は、(昇格後ではなく)昇格前の兵種によって決まります。その計算式は下記です [23](訳注: Python でなく C++)

int needed_upgrade_xp = 2 * (30 + 0.006f * レベル境界[troops[troop_id].level + 3]);

ただし、ゲーム・エンジンは 上の計算式を直接使うわけではなく、四則演算を済ませて単純化した下記テーブルを使います [24]

  1. 0
  2. 600
  3. 1360
  4. 2296
  5. 3426
  6. 4768
  7. 6345
  8. 8179
  9. 10297
  10. 13010
  11. 16161
  12. 19806
  13. 24007
  14. 28832
  15. 34362
  16. 40682
  17. 47892
  18. 56103
  19. 65441
  20. 77233
  21. 90809
  22. 106425
  23. 124371
  24. 144981
  25. 168636
  26. 195769
  27. 226879
  28. 262533
  29. 303381
  30. 350164
  31. 412091
  32. 484440
  33. 568947
  34. 667638
  35. 782877
  36. 917424
  37. 1074494
  38. 1257843
  39. 1471851
  40. 1721626
  41. 2070551
  42. 2489361
  43. 2992033
  44. 3595340
  45. 4319408
  46. 5188389
  47. 6231267
  48. 7482821
  49. 8984785
  50. 11236531
  51. 14051314
  52. 17569892
  53. 21968216
  54. 27466220
  55. 34338824
  56. 42929680
  57. 53668348
  58. 67091784
  59. 83871184
  60. 160204608
  61. 320304608
  62. 644046016
  63. 2050460032

訳注: 上表について、訳者が Warband v1.174 で get_level_boundary 命令をループして試した限りでは、最後の 11 個が原文と合わないので、訂正してあります。脚注 [24] の先の投稿の内容が古いか誤りと思われます。

編集可能な一般設定については、この web 文書の module.ini のページにある レベル関連パラメータ という項を参照して下さい。 追加のルール、例えば騎兵になるのに必要な経験値を増やすとか、盗賊になるのに必要な経験値を減らす、といったもの実装するには、スクリプト game_get_upgrade_xp を編集する必要があります [25]

一般兵が誰かを倒して経験値を得る際のゲーム・エンジンの計算式は下記です [26](訳注: Python でなく C++)

int mbTroop::getKillExperience()
{
    return (int)(((m_level + 10) * (m_level + 10)) * 0.1f);
}

mbTroop *troop = getTroop();
        float experience = (float)troop->getKillExperience();

        if (g_mission->isTeamFight())
            experience *= 0.1f;

        if (experience != 0.0f)
        {
            mbTroop *damagerTroop = g_game->getTroop(troopNo);

            if (!damagerTroop->isHero())
                agent->m_experience += (int)(experience * rglConfig::Campaign::fRegularExperienceMultiplier);
            else if (troopNo == g_game->m_playerTroopNo)
                g_game->addExperienceToTroop(troopNo, (int)(experience * rglConfig::Campaign::fPlayerExperienceMultiplier));
            else
                g_game->addExperienceToTroop(troopNo, (int)(experience * rglConfig::Campaign::fHeroExperienceMultiplier));
        }

その他の注意点

フラグを準復活させる方法についての簡単な説明は? キャプチャ・アライブは時代遅れではない(NPC99, Modding Q&AModding Q&A)。tf_no_capture_alive と tf_allways_fall_dead という兵種フラグは互いに打ち消し合っている? 機能していないように見える(議論, Modding Q&A

これらの設定がどれも最新の Native MOD システムに既に存在していることを確認する必要がある。銃器の熟練度(Somebody, Modding Q&A); module_troops の前半部分(Somebody, Modding Q&A

agent_set_x_modifier(Somebody, Modding Q&A