Difficulty scaling/zh: Difference between revisions

From ZeldaMods (Breath of the Wild)
Jump to navigation Jump to search
(Created page with "它負責處理武器的升級。有三個東西會呼叫它:寶箱、敵人{{Check}}、 <code>Ecosystem::LevelSensor::scaleActor</code>。")
(Created page with "呼叫時給定一個武器的名字、預設的加成(至少多少)、跟目前玩家的經驗值。然後這個函數會回傳一個武器名稱(可能跟給定的不...")
Line 228: Line 228:
<code>Ecosystem::LevelSensor::scaleActor</code>。
<code>Ecosystem::LevelSensor::scaleActor</code>。


Given a weapon name, its modifier and current point status, this function returns the weapon to actually spawn and the modifier to use (if possible).
呼叫時給定一個武器的名字、預設的加成(至少多少)、跟目前玩家的經驗值。然後這個函數會回傳一個武器名稱(可能跟給定的不一樣),其中包括應當使用何種加成。


If the algorithm fails to find an appropriate weapon that satisfies all conditions (point requirements, weapon series, modifier), the originally specified weapon and modifier will be used directly.
If the algorithm fails to find an appropriate weapon that satisfies all conditions (point requirements, weapon series, modifier), the originally specified weapon and modifier will be used directly.

Revision as of 12:40, 11 May 2020

Other languages:

Multi-shot burst (bows) 多發弓發數增加

經驗值

等級系統是根據存檔裏的一個「經驗值」系統決定的。只有怪物死掉可以獲得經驗值。(意即解神廟、解任務之類的事情不算。)

升級的計算過程

由於主線劇情會要求玩家殺怪,所以玩家或多或少都會累積經驗值,並且相應地導致等級上升。

遊戲的存檔裏會儲存死了幾隻怪的計數器,有了計數之後 Ecosystem::LevelSensor 這個子系統會用 #Ecosystem::LevelSensor::calculatePoints 計算你的經驗值。不同的怪會給你不同的經驗,表格存在這裏 #Ecosystem::LevelSensor::loadByml.

算出經驗值之後,這個子系統會用這兩個函數 #Ecosystem::LevelSensor::scaleWeapon#Ecosystem::LevelSensor::scaleActor 來決定敵人跟武器應該分別是什麼等級。

阻止升級的情況

下面這個情況會導致敵人跟武器維持原樣(總而言之就是林克在劍之考驗裏)。

  • WorldMgr::sInstance->stageType == 1 (Open World stage)
  • 且 WorldMgr::sInstance->isAocField (current map is Trial of the Sword)
  • 且 WorldMgr::sInstance->disableScaling (set to true when entering Trial of the Sword)

另一種會跳過升級的情況是 map area 等於 28, 也就是林克位在 "HateruSea",野外的考驗的塞哈特諾島。

武器 

下面這些武器在生成的時候會呼叫它的 'scaleWeapon' 函數

  • 單獨存在的武器: The actor property 'LevelSensorMode' is higher than 1 and it wasn't already picked up. 設定上可以升級而且還沒被撿走
  • 寶箱裏的武器: If SharpWeaponJudgeType is not 4, when AIDef:AI/TreasureBox initialises the drop actor. 同樣是設定上允許升級
  • 西諾克斯脖子上的武器: The flag {MapName}_Necklace_{i}_{HinoxName}_{ID} is false.
  • 敵人手上的武器: The flag {MapName}_WeaponDrop_{ID} is false, and [the actor property 'LevelSensorMode' is higher than 1 or the enemy is a Guardian Scout ('Enemy_Guardian_Mini' 廟裏的守護者)].

註記:從 NPC 那裏拿到的武器(阿卡萊古代研究所、英傑武器)不在上面的列表裏,從而不升級。

武器加成

武器的加成有三個要素。 一是武器本身會被更新成更好的武器(騎士之劍 -> 王族之劍); 二是武器的攻擊、防禦、耐久等數值擇一增加; 三是增加多少。

加成的方向

要往哪個方向加成(加攻擊還是加耐久?還是別的?)是隨機的, 每個可能的方向有同樣的機率被選到。

加成的方向 Available in modifier tiers
Attack up 攻擊力 Blue/White and Yellow
Durability up 耐久度提升 Blue/White and Yellow
Long throw 遠距離投擲 Yellow
Multi-shot burst (bows) 多發弓發數增加 Yellow
Quick shot (bows) 速射 Yellow
AddSurfMaster Yellow
Shield guard up 防禦力 Blue/White and Yellow
Critical Hit 最後爆擊 Blue/White
ZoomRapid Yellow

藍標、白標(Blue/White)是比較低階的升級,標記成「某某某提升」。黃標(Yellow)是高階的升級,標記成「某某某大提升」。以攻擊力為例,藍標可能會增加 10~20 的攻擊力(攻擊力提升 +14),而黃標會增加 20~30(攻擊力大提升 +24)。

需注意的是:

  • 最後爆擊 "Critical Hit" 只可能在 weaponCommonSharpWeaponAddCrit 是 true 且 加成的 tier 程度是 White/Blue (白色或藍色的「提升」而不是黃色的「大提升」)時出現。 但是隨着經驗值的提升,所有武器的加成都會漸漸變成 Yellow (黃色的「大提升」),所以「最後爆擊」在遊戲後期就不會再出現。
  • AddSurfMaster is a bonus that only applies to shields and gives them a lower friction for shield surfing. That bonus type is unused in the game.

加成的程度

武器加成多少(如攻擊力應當提升多少)的數值來源於這個表 bgparamlist (with a copy of the information in ActorInfoData). Valid ranges and bonuses for each weapon are configured in the WeaponCommon section.

amiibo

加成的方向 加成的程度
None 不加成 -
Attack up 攻擊力 addAtkMax
Durability up 耐久度提升 addLifeMax
Long throw 遠距離投擲 addThrowMax
Multi-shot burst (bows) 多發弓發數增加 5-shot burst 五連發
Quick shot (bows) 速射 addRapidFireMin
AddSurfMaster GlobalParameter::shieldSurfMasterFrictionRatio
Shield guard up 防禦力 addGuardMax

這段的意思是 amiibo 武器的加成都會是固定的值。譬如說,如果遊戲決定增加攻擊力,而且這個武器的攻擊力最多 +20,那就會 +20。

非 amiibo

加成的方向 加成的程度
None 不加成 -
Attack up 攻擊力 addAtkMin 到 addAtkMax 之間的隨機數字
不過西諾克斯的 LevelSensorMode 屬性並不控制他脖子上的武器如何升級(到頭來是脖子上而不是手上)。總之,西諾克斯用的是另一個機制來決定脖子上的武器如何升級,而且這個機制可以繞過 'LevelSensorMode'。 addLifeMin 到 addLifeMax 之間的隨機數字
Long throw 遠距離投擲 addThrowMin 到 addThrowMax 之間的隨機數字
Multi-shot burst (bows) 多發弓發數增加 5-shot burst 五連發
Quick shot (bows) 速射 addRapidFireMin 到 addRapidFireMax 之間的隨機數字
AddSurfMaster GlobalParameter::shieldSurfMasterFrictionRatio
Shield guard up 防禦力 addGuardMin 到 addGuardMax 之間的隨機數字

這段的意思是非 amiibo 武器的加成都是隨機的值。譬如說,如果遊戲決定增加攻擊力,而且這個武器的攻擊力從 +10 到 +20 都有可能,那最後生成的武器就會是 10 到 20 之間的隨機數字(應該是均勻分佈)。

敵人

遊戲在載入敵人時會根據一定的規則升級敵人。

不過 'LevelSensorMode' < 1 的時候所有敵人(跟武器)都不會升級。

註記:有些敵人本身是無法升級的(例如帶冰、火、電的蜥蜴), 這僅僅是代表他們自身就已經是最高級了。 而他們手上的武器只要滿足下列條件就可以升級:

  • 'LevelSensorMode' 非零且經驗值足夠高
  • 或是某個 flag 被強制寫入應該升級 the modifier tier is overridden using 'SharpWeaponJudgeType'.

[1.3.0] 版本加入了大師模式。大師模式裏所有敵人都會先驗地提高一級。這個部分跟 'LevelSensorMode' 造成的升級互不衝突而且可以疊加。更精確的控制是由下面這些參數決定的

參數 預設值 Description
IsHardModeActor false 是否只在大師模式出現
DisableRankUpForHardMode false 在大師模式裏是否避免升級

在大師模式裏,某些敵人的 IsHardModeActor、DisableRankUpForHardMode、LevelSensorMode這三個參數會同時被遊戲設計師手動調整。調整的目的是讓大師模式的後期還是會存在低等級的怪物(例如台地南邊有個紅色小波)。

屬性

LevelSensorMode

這個 actor 屬性決定了一個武器或敵人會不會升級,一個敵人的 LevelSensorMode 屬性同時控制了它手上的武器升不升級。

不過西諾克斯的 LevelSensorMode 屬性並不控制他脖子上的武器如何升級(到頭來是脖子上而不是手上)。總之,西諾克斯用的是另一個機制來決定脖子上的武器如何升級,而且這個機制可以繞過 'LevelSensorMode'。

SharpWeaponJudgeType

這個屬性決定某個敵人或武器「至少」會被加成多少。 (意即:就算經驗值不夠還是會加成。)

map units 這類的地方,下面這些值記載了遊戲打算如何加成武器。

Description
0 None: 不加成
1 RandomBlue: 可能是藍標也可能是黃標,隨機決定。 (with weaponCommonSharpWeaponPer being the probability).
2 Blue: 至少藍/白標,按經驗值往上加。
3 Yellow: 至少黃標。
4 NoneForced (限於寶箱): Weapon will never spawn with any modifiers. 寶箱的這個屬性會抹去其他一切升級。

如果 scaling 處在打開狀態的話,武器有可能因為經驗值足夠的關係被升到更高級。

經驗值不夠則維持原升級(不會降級)。

換句話說,就算是設成 0 ('None') 的武器也不是永遠不升級了, 而是遊戲開發者有意給玩家最低等級的武器。 只要玩家的經驗值高過某個門檻,就會得到 Blue 藍標加成,再高一點 Yellow 黃標加成。

升級的計算過程

Ecosystem::LevelSensor::loadByml

這個函數會被 Ecosystem::init 呼叫, 後者是被 ksys::InitializeApp 呼叫。

它的功用是載入 byml 檔案 Ecosystem/LevelSensor.byml

Ecosystem::LevelSensor::calculatePoints

每當遊戲生成 actors 時,PlacementMgr 會呼叫這個函數。(actor 可以是武器或是敵人)

它會用各種表格跟各種經驗值計算武器或敵人要升級多少。

遊戲到目前為止都只用殺敵數 Defeated_%s_Num 來計算經驗值並以此推測升級程度,但是從程式設計的角度來說可以用別的東西來計算經驗值。

有趣的是,遊戲利用殺敵數計算經驗值時,會把一個「原始經驗值」轉化成「用在武器升級的經驗值」跟「用在敵人升級的經驗值」。這樣設計師就可以在後期微調「原始」轉化到「武器」的匯率;「敵人」亦同。

float points = 0.0;
for (kill_flag : this->byml["flag"]) // 枚舉所有敵人
    int kill_count = GameData::getIntegerFlag(kill_flag["name"]);
    points += kill_count * kill_flag["point"]; // 殺敵數 * 此敵人的分數

this->points = points; // 原始經驗值
this->weapon_points = points * this->byml["setting"].Level2WeaponPower; // 換算成武器用分數
this->enemy_points = points * this->byml["setting"].Level2EnemyPower; // 換算成敵人用分數

就我們能看到的部分,任天堂從來沒有調整過這兩個數值。

Ecosystem::LevelSensor::scaleWeapon

它負責處理武器的升級。有三個東西會呼叫它:寶箱、敵人[check]Ecosystem::LevelSensor::scaleActor

呼叫時給定一個武器的名字、預設的加成(至少多少)、跟目前玩家的經驗值。然後這個函數會回傳一個武器名稱(可能跟給定的不一樣),其中包括應當使用何種加成。

If the algorithm fails to find an appropriate weapon that satisfies all conditions (point requirements, weapon series, modifier), the originally specified weapon and modifier will be used directly.

Pseudocode (1.0.0):

bool Ecosystem::LevelSensor::scaleWeapon(const sead::SafeString& weapon_to_look_up,
                                         WeaponModifier required_modifier,
                                         const char** weapon_to_use_name,
                                         WeaponModifier* modifier_to_use,
                                         void* unknown)
{
  // some checks using 'unknown' here which seems to be a pointer to the actor
  //
  for (weapon_table : this->byml["weapon"]) {
    // find the first weapon entry for which the player has enough points
    // with the specified name and modifier
    i = -1;
    for (j = 0; j < weapon_table["actors"].size; ++j) {
      entry = weapon_table["actors"][j];
      float points_for_next_transition = entry["value"];
      //
      if (this->weapon_points > points_for_next_transition &&
          weapon_to_look_up == entry["name"] &&
          convert_to_modifier(entry["plus"]) == required_modifier) {
        i = j;
        break;
      }
    }
    //
    if (i == -1)
      continue;
    //
    do {
      entry = weapon_table["actors"][i];
      //
      // not_rank_up means there is no link between weapons;
      // this table is just used to look up modifiers.
      // so go down the list until there are no more entries for the requested weapon
      // or until we reach a modifier that requires more points.
      if (weapon_table["not_rank_up"] && entry["name"] != weapon_to_look_up)
        break;
      //
      // otherwise, just go down the list until we reach the end or a weapon which
      // requires more points. this will possibly upgrade the weapon (e.g. Knight -> Royal).
      if (this->weapon_points <= entry["value"])
        break;
      //
      ++i;
    } while (i < weapon_table["actors"].size);
    //
    *weapon_to_use_name = entry["name"];
    *modifier_to_use = convert_to_modifier(entry["plus"]);
    return true;
  }
  return false;  // cannot scale up
}

Ecosystem::LevelSensor::scaleActor

Analogous to LevelSensor::scaleWeapon.

Pseudocode (1.0.0):

if (actor->params["LevelSensorMode"] < 1)
  return false;
//
if (actor_name.contains("Enemy")) {
  for (enemy_table : this->byml["enemy"]) {
    i = -1;
    for (j = 0; j < enemy_table["actors"].size; ++j) {
      entry = enemy_table["actors"][j];
      if (entry["name"] == actor_name && this->enemy_points > entry["value"]) {
        i = j;
        break;
      }
    }
    //
    if (i == -1)
      continue;
    //
    do {
      entry = enemy_table["actors"][i];
      if (this->enemy_points <= entry["value"])
        break;
      ++i;
    } while (i < enemy_table["actors"].size);
    //
    *actor_to_use = entry["name"];
    return true;
  }
  return false;  // cannot scale up
}
//
if (actor_name.contains("Weapon")) {
  weapon_name = actor->getWeaponName();
  modifier = actor->params["SharpWeaponJudgeType"];
  if (modifier == WeaponModifier::RandomBlue)
    modifier = get_random_blue_modifier(actor->getWeaponName());
  //
  if (scaleWeapon(weapon_name, &weapon_to_use, &modifier_to_use)) {
    actor->setProperty("SharpWeaponJudgeType", modifier_to_use);
    *actor_to_use = weapon_to_use;
    return true;
  }
  return false;  // cannot scale up
}

The Data

To make things easier to understand, here are links to:

This makes it possible to see both the required points for enemy/weapon upgrades, as well as all of the special cases extremely easily.

Ganon Blights

Ganon blights also have varying difficulty but follow a different system. Their health is determined by the base HP (set in bgparamlist) and blight defeat flags.

__int64 SiteBoss::getInitialHP(SiteBoss *this) // 0x71002D01F4
{
  const int baseHp = Enemy::getInitialHP(this);
  const int halfBaseHp = baseHp >> 1;
  const bool dieGanonWind = hasFlag_Die_PGanonWind(0);
  const bool dieGanonWater = hasFlag_Die_PGanonWater(0);
  const bool dieGanonFire = hasFlag_Die_PGanonFire(0);
  const bool dieGanonElectric = hasFlag_Die_PGanonElectric(0);
  const int flags = this->siteBossFlags & 0xFFFFFFFC;
  int multiplier;
  if ( flags == 4 )
    multiplier = 3;
  else if ( flags == 8 )
    multiplier = 4;
  else
    multiplier = dieGanonFire + dieGanonWind + dieGanonWater + dieGanonElectric;
  return baseHp + multiplier * halfBaseHp;
}

Effectively, this means that the first blight Link fights will have 800+0×400 = 800 HP, the second will have 800+1×400 = 1200 HP, the third 800+2×400 = 1600 HP and the last one 800+3×400 = 2000 HP.

Special case 1: Castle Blights

Castle blights have IsRemainBoss set to false in their root AI parameters (see AIDef:AI/SiteBossSpearRoot for example), which sets flag 4.

Thus, blights that are fought in the Castle always have 800+3×400 = 2000 HP regardless of story progression.

If flag 4 is set, the AIDef:Action/SiteBossDie code will NOT increment the "defeated" counter. This means castle blights do not give any scaling points.

Special case 2: DLC2 Blights

Illusory Realm blights possess the EnemySiteBoss_R actor tag. This causes flag 8 to be set. So they will always have 500+4×250 = 1500 HP.

Interestingly, the Windblight AI function relies doesn't check the actor tag but the actor name instead. For flag 8 to be set, the actor name must be Enemy_SiteBoss_Bow_R.