等级系统
Difficulty scaling(等级系统)是旷野之息用来控制敌人跟武器等级的一个机制。你会注意到,在游玩的过程中,同一个地点的敌人可能在若干次血月后变成了更强的敌人,且其手上的武器也有所变化。
经验值
等级系统是根据存档里的一个“经验值”系统决定的。只有怪物死掉可以获得经验值。(意即解神庙、解任务之类的事情不算。)
每当有敌人死掉,游戏就会想增加 Defeated_{SameGroupActorName}_Num 这个计数器。 花括号的意思是,如果是波克布林死掉,就会更新波克布林专属的计数器。 不过,只有下列条件满足的时候,计数器才会真的增加。
- 计数器本身还不到 10(意思是大部分的怪物只有杀死前十只才算)
- 死掉的那个敌人没有 NotCountDefeatedNum flag(意思是某些特殊情况下不算)
- 导师米兹·乔希亚专用的计数器 'Defeated_Priest_Boss_Normal_Num' 是 0(意思是只有杀第一次才算)
- 魔兽加侬也是只有第一次才算
- 厄咒加侬只有在神兽里打败的才算,原始游戏打一次,DLC 跟幻影打一次,总共两次(在城堡里打的不算)
需要注意的是只要有任何敌人死掉,不管死法,也不管是不是林克亲手杀的,游戏都会试图增加计数器。下面会讲到哪些情况下不会算。
由于主线剧情会要求玩家杀怪,所以玩家或多或少都会累积经验值,并且相应地导致等级上升。
游戏的存档里会储存死了几只怪的计数器,有了计数之后 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)
This means that scaling is always disabled in the 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 那里拿到的武器(阿卡莱古代研究所、英杰武器)不在上面的列表里,从而不升级。
武器加成
武器的加成有三个要素。 一是武器本身会被更新成更好的武器(骑士之剑 -> 王族之剑); 二是武器的攻击、防御、耐久等数值择一增加; 三是增加多少。
There are two bonus tiers in the game: one for low-level types (which appear in blue/white in the game UI) and another for high-level types (yellow). Those that belong to the Yellow tier are usually superior to the other ones. For instance, Attack Up + is the superior variant of Attack Up and it typically grants a higher attack power boost compared to Attack Up.
加成的方向
要往哪个方向加成(加攻击还是加耐久?还是别的?)是随机的, 每个可能的方向有同样的机率被选到。
加成的方向 | Available in bonus 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 之间的随机数字 |
Durability up 耐久度提升 | 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
。
呼叫时给定一个武器的名字、预设的加成(至少多少)、跟目前玩家的经验值。然后这个函数会回传一个武器名称(可能跟给定的不一样),其中包括应当使用何种加成。
如果函数找半天找不到满足给定的条件武器、加成,游戏就会用预设的武器、预设的加成。(程式码里最后的 return false 会处理所有例外、失败的情形。)
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
// 不同的武器系列儲存在不同的 table 裡,要枚舉 table 再枚舉 table 裡的細項
i = -1; // 應該是 index,但是 -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; // ???這個 table 裡的武器不能升級,或是 entry 是 null (boundary check)
// 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
处理敌人的升级。方法跟上面 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 // 沒找到
}
大数据
精确的数值可以在这里找到:
This makes it possible to see both the required points for enemy/weapon upgrades, as well as all of the special cases extremely easily.
厄咒加侬
厄咒加侬也有升级版,不过是依据的是另一个完全独立的系统。 具体升级的内容是:神兽里的厄咒加侬的血量是由玩家打死过多少别的加侬决定的。
__int64 SiteBoss::getInitialHP(SiteBoss *this) // 0x71002D01F4
{
const int baseHp = Enemy::getInitialHP(this); // 基礎血量(底)
const int halfBaseHp = baseHp >> 1; // 除以 2(台)
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;
}
换句换说,玩家第一个击倒的厄咒加侬的血量是 800,下一个 1200,再下一个 1600,最后一个 2000。
Special case 1: 城堡里的厄咒加侬
Castle blights have IsRemainBoss
set to false in their root AI parameters (see AIDef:AI/SiteBossSpearRoot for example), which sets flag 4.
城堡里的厄咒加侬,不管是第几只,血量都是 800+3×400 = 2000。
打死城堡里的厄咒加侬并不计入经验值。(相应的计数器不会增加。)
Special case 2: 幻影空间里的厄咒加侬
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.
补充:幻影里的厄咒加侬基础血量是 500,其他情况是 800。
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
.