add method to override enemy EXP in quests
This commit is contained in:
@@ -744,6 +744,10 @@ QuestIndex::QuestIndex(
|
||||
vq->meta.lock_status_register = metadata_json.get_int("LockStatusRegister");
|
||||
} catch (const out_of_range&) {
|
||||
}
|
||||
try {
|
||||
vq->meta.enemy_exp_overrides = QuestMetadata::parse_enemy_exp_overrides(metadata_json.at("EnemyEXPOverrides"));
|
||||
} catch (const out_of_range&) {
|
||||
}
|
||||
try {
|
||||
vq->meta.common_item_set_name = metadata_json.at("CommonItemSetName").as_string();
|
||||
} catch (const out_of_range&) {
|
||||
|
||||
+50
-1
@@ -44,6 +44,9 @@ void QuestMetadata::assert_compatible(const QuestMetadata& other) const {
|
||||
"quest version has a different lock status register (existing: {:04X}, new: {:04X})",
|
||||
this->lock_status_register, other.lock_status_register));
|
||||
}
|
||||
if (this->enemy_exp_overrides != other.enemy_exp_overrides) {
|
||||
throw runtime_error("quest version has different enemy EXP overrides");
|
||||
}
|
||||
if (!this->battle_rules != !other.battle_rules) {
|
||||
throw runtime_error(std::format(
|
||||
"quest version has a different battle rules presence state (existing: {}, new: {})",
|
||||
@@ -140,11 +143,19 @@ phosg::JSON QuestMetadata::json() const {
|
||||
for (const auto& fa : this->area_for_floor) {
|
||||
floors_json.emplace_back(fa);
|
||||
}
|
||||
auto enemy_exp_overrides_json = phosg::JSON::dict();
|
||||
for (const auto& [key, exp_override] : this->enemy_exp_overrides) {
|
||||
auto difficulty = static_cast<Difficulty>((key >> 24) & 3);
|
||||
auto floor = static_cast<uint8_t>((key >> 16) & 0xFF);
|
||||
auto enemy_type = static_cast<EnemyType>(key & 0xFFFF);
|
||||
auto key_str = std::format("{}:0x{:02X}:{}", name_for_difficulty(difficulty), floor, phosg::name_for_enum(enemy_type));
|
||||
enemy_exp_overrides_json.emplace(key_str, exp_override);
|
||||
}
|
||||
return phosg::JSON::dict({
|
||||
{"CategoryID", this->category_id},
|
||||
{"Number", this->quest_number},
|
||||
{"Episode", name_for_episode(this->episode)},
|
||||
{"FloorAssignments", floors_json},
|
||||
{"FloorAssignments", std::move(floors_json)},
|
||||
{"Joinable", this->joinable},
|
||||
{"MaxPlayers", this->max_players},
|
||||
{"BattleRules", this->battle_rules ? this->battle_rules->json() : phosg::JSON(nullptr)},
|
||||
@@ -160,5 +171,43 @@ phosg::JSON QuestMetadata::json() const {
|
||||
{"DefaultDropMode", phosg::name_for_enum(this->default_drop_mode)},
|
||||
{"AllowStartFromChatCommand", this->allow_start_from_chat_command},
|
||||
{"LockStatusRegister", (this->lock_status_register >= 0) ? this->lock_status_register : phosg::JSON(nullptr)},
|
||||
{"EnemyEXPOverrides", std::move(enemy_exp_overrides_json)},
|
||||
});
|
||||
}
|
||||
|
||||
std::unordered_map<uint32_t, uint32_t> QuestMetadata::parse_enemy_exp_overrides(const phosg::JSON& json) {
|
||||
try {
|
||||
std::unordered_map<uint32_t, uint32_t> ret;
|
||||
for (const auto& [key, exp_value_json] : json.as_dict()) {
|
||||
// Key is like "Difficulty:Floor:EnemyType" or "Difficulty:EnemyType"
|
||||
auto key_tokens = phosg::split(key, ':');
|
||||
|
||||
static const unordered_map<string, Difficulty> difficulty_keys(
|
||||
{{"Normal", Difficulty::NORMAL}, {"Hard", Difficulty::HARD}, {"VeryHard", Difficulty::VERY_HARD}, {"Ultimate", Difficulty::ULTIMATE}});
|
||||
|
||||
Difficulty difficulty = Difficulty::NORMAL;
|
||||
EnemyType enemy_type = EnemyType::UNKNOWN;
|
||||
uint8_t floor = 0xFF;
|
||||
if (key_tokens.size() == 2) {
|
||||
enemy_type = phosg::enum_for_name<EnemyType>(key_tokens[1]);
|
||||
} else if (key_tokens.size() == 3) {
|
||||
floor = stoul(key_tokens[1], nullptr, 0);
|
||||
enemy_type = phosg::enum_for_name<EnemyType>(key_tokens[2]);
|
||||
} else {
|
||||
throw runtime_error("malformatted key: " + key);
|
||||
}
|
||||
difficulty = difficulty_keys.at(key_tokens[0]);
|
||||
if (floor == 0xFF) {
|
||||
for (size_t floor = 0; floor < 0x12; floor++) {
|
||||
ret.emplace(QuestMetadata::exp_override_key(difficulty, floor, enemy_type), exp_value_json->as_int());
|
||||
}
|
||||
} else {
|
||||
ret.emplace(QuestMetadata::exp_override_key(difficulty, floor, enemy_type), exp_value_json->as_int());
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
|
||||
} catch (const exception& e) {
|
||||
throw std::runtime_error(std::format("invalid enemy EXP overrides: ", e.what()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,10 +9,12 @@
|
||||
#include <vector>
|
||||
|
||||
#include "CommonItemSet.hh"
|
||||
#include "EnemyType.hh"
|
||||
#include "IntegralExpression.hh"
|
||||
#include "Map.hh"
|
||||
#include "PlayerSubordinates.hh"
|
||||
#include "RareItemSet.hh"
|
||||
#include "StaticGameData.hh"
|
||||
|
||||
struct QuestMetadata {
|
||||
// This structure contains configuration that should be the same across all
|
||||
@@ -40,11 +42,17 @@ struct QuestMetadata {
|
||||
ServerDropMode default_drop_mode = ServerDropMode::CLIENT; // Ignored if allowed_drop_modes == 0
|
||||
bool allow_start_from_chat_command = false;
|
||||
int16_t lock_status_register = -1;
|
||||
std::unordered_map<uint32_t, uint32_t> enemy_exp_overrides;
|
||||
|
||||
std::string name;
|
||||
std::string short_description;
|
||||
std::string long_description;
|
||||
|
||||
static std::unordered_map<uint32_t, uint32_t> parse_enemy_exp_overrides(const phosg::JSON& json);
|
||||
static inline uint32_t exp_override_key(Difficulty difficulty, uint8_t floor, EnemyType enemy_type) {
|
||||
return (static_cast<uint32_t>(difficulty) << 24) | (static_cast<uint32_t>(floor) << 16) | static_cast<uint32_t>(enemy_type);
|
||||
}
|
||||
|
||||
void assign_default_areas(Version version, Episode episode);
|
||||
void assert_compatible(const QuestMetadata& other) const;
|
||||
phosg::JSON json() const;
|
||||
|
||||
+5
-5
@@ -1904,15 +1904,15 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = {
|
||||
// Specifies which enemy should be affected by subsequent get_*_data
|
||||
// opcodes (the following 4 definitions). valueA is the battle parameter
|
||||
// index for the desired enemy.
|
||||
{0xF891, "load_enemy_data", nullptr, {I32}, F_V2_V4 | F_ARGS},
|
||||
{0xF891, "set_override_enemy_bp_index", "load_enemy_data", {I32}, F_V2_V4 | F_ARGS},
|
||||
|
||||
// Replaces enemy stats with the given structures (PlayerStats, AttackData,
|
||||
// ResistData, or MovementData) for the enemy previously specified with
|
||||
// load_enemy_data.
|
||||
{0xF892, "get_physical_data", nullptr, {{LABEL16, Arg::DataType::PLAYER_STATS, "stats"}}, F_V2_V4},
|
||||
{0xF893, "get_attack_data", nullptr, {{LABEL16, Arg::DataType::ATTACK_DATA, "attack_data"}}, F_V2_V4},
|
||||
{0xF894, "get_resist_data", nullptr, {{LABEL16, Arg::DataType::RESIST_DATA, "resist_data"}}, F_V2_V4},
|
||||
{0xF895, "get_movement_data", nullptr, {{LABEL16, Arg::DataType::MOVEMENT_DATA, "movement_data"}}, F_V2_V4},
|
||||
{0xF892, "set_enemy_physical_data", "get_physical_data", {{LABEL16, Arg::DataType::PLAYER_STATS, "stats"}}, F_V2_V4},
|
||||
{0xF893, "set_enemy_attack_data", "get_attack_data", {{LABEL16, Arg::DataType::ATTACK_DATA, "attack_data"}}, F_V2_V4},
|
||||
{0xF894, "set_enemy_resist_data", "get_resist_data", {{LABEL16, Arg::DataType::RESIST_DATA, "resist_data"}}, F_V2_V4},
|
||||
{0xF895, "set_enemy_movement_data", "get_movement_data", {{LABEL16, Arg::DataType::MOVEMENT_DATA, "movement_data"}}, F_V2_V4},
|
||||
|
||||
// Reads 2 bytes or 4 bytes from the event flags in the system file.
|
||||
// regA = event flag index
|
||||
|
||||
@@ -3919,11 +3919,19 @@ static void add_player_exp(shared_ptr<Client> c, uint32_t exp, uint16_t from_ene
|
||||
|
||||
static uint32_t base_exp_for_enemy_type(
|
||||
shared_ptr<const BattleParamsIndex> bp_index,
|
||||
shared_ptr<const Quest> quest, // Null in free play
|
||||
EnemyType enemy_type,
|
||||
Episode current_episode,
|
||||
Difficulty difficulty,
|
||||
uint8_t area,
|
||||
uint8_t floor,
|
||||
bool is_solo) {
|
||||
if (quest) {
|
||||
try {
|
||||
return quest->meta.enemy_exp_overrides.at(QuestMetadata::exp_override_key(difficulty, floor, enemy_type));
|
||||
} catch (const out_of_range&) {
|
||||
}
|
||||
}
|
||||
|
||||
// Always try the current episode first. If the current episode is Ep4, try
|
||||
// Ep1 next if in Crater and Ep2 next if in Desert (this mirrors the logic in
|
||||
// BB Patch Project's omnispawn patch).
|
||||
@@ -3936,10 +3944,11 @@ static uint32_t base_exp_for_enemy_type(
|
||||
episode_order[1] = Episode::EP1;
|
||||
episode_order[2] = Episode::EP4;
|
||||
} else if (current_episode == Episode::EP4) {
|
||||
if (area <= 0x05) {
|
||||
uint8_t area = quest->meta.area_for_floor.at(floor);
|
||||
if (area <= 0x28) { // Crater
|
||||
episode_order[1] = Episode::EP1;
|
||||
episode_order[2] = Episode::EP2;
|
||||
} else {
|
||||
} else { // Desert
|
||||
episode_order[1] = Episode::EP2;
|
||||
episode_order[2] = Episode::EP1;
|
||||
}
|
||||
@@ -3956,11 +3965,11 @@ static uint32_t base_exp_for_enemy_type(
|
||||
}
|
||||
}
|
||||
throw runtime_error(std::format(
|
||||
"no base exp is available (type={}, episode={}, difficulty={}, area={:02X}, solo={})",
|
||||
"no base exp is available (type={}, episode={}, difficulty={}, floor={:02X}, solo={})",
|
||||
phosg::name_for_enum(enemy_type),
|
||||
name_for_episode(current_episode),
|
||||
name_for_difficulty(difficulty),
|
||||
area,
|
||||
floor,
|
||||
is_solo ? "true" : "false"));
|
||||
}
|
||||
|
||||
@@ -4006,7 +4015,7 @@ static asio::awaitable<void> on_steal_exp_bb(shared_ptr<Client> c, SubcommandMes
|
||||
|
||||
auto type = ene_st->type(c->version(), l->episode, l->difficulty, l->event);
|
||||
uint32_t enemy_exp = base_exp_for_enemy_type(
|
||||
s->battle_params, type, l->episode, l->difficulty, ene_st->super_ene->floor, l->mode == GameMode::SOLO);
|
||||
s->battle_params, l->quest, type, l->episode, l->difficulty, ene_st->super_ene->floor, l->mode == GameMode::SOLO);
|
||||
|
||||
// Note: The original code checks if special.type is 9, 10, or 11, and skips
|
||||
// applying the android bonus if so. We don't do anything for those special
|
||||
@@ -4052,7 +4061,7 @@ static asio::awaitable<void> on_enemy_exp_request_bb(shared_ptr<Client> c, Subco
|
||||
|
||||
auto type = ene_st->type(c->version(), l->episode, l->difficulty, l->event);
|
||||
double base_exp = base_exp_for_enemy_type(
|
||||
s->battle_params, type, l->episode, l->difficulty, ene_st->super_ene->floor, l->mode == GameMode::SOLO);
|
||||
s->battle_params, l->quest, type, l->episode, l->difficulty, ene_st->super_ene->floor, l->mode == GameMode::SOLO);
|
||||
l->log.info_f("Base EXP for this enemy ({}) is {:g}", phosg::name_for_enum(type), base_exp);
|
||||
|
||||
for (size_t client_id = 0; client_id < 4; client_id++) {
|
||||
|
||||
@@ -68,4 +68,19 @@
|
||||
// clearing the 0x02 bit of AllowedDropModes).
|
||||
// "AllowedDropModes": 0x1D,
|
||||
// "DefaultDropMode": "SERVER_PRIVATE",
|
||||
|
||||
// Quests may override enemies' stats, which may result in a different amount
|
||||
// of experience than the enemy would give by default. If the quest uses the
|
||||
// set_enemy_physical_data opcode (or get_physical_data in qedit), then the
|
||||
// EXP values should be set in this dictionary. Each entry here applies to a
|
||||
// specific enemy type in a specific difficulty level on a specific floor (or
|
||||
// on all floors).
|
||||
// "EnemyEXPOverrides": {
|
||||
// // Key is like "Difficulty:Floor:EnemyType" or "Difficulty:EnemyType",
|
||||
// // where floor is a decimal or hexadecimal integer and EnemyType matches
|
||||
// // one of the values in the EnemyType enum in EnemyType.hh. For example:
|
||||
// "Normal:1:GOBOOMA": 100, // Normal difficulty, floor 1 (Forest 1) only
|
||||
// "VeryHard:0x0B:DRAGON": 10000, // Very Hard difficulty, floor 11 (Dragon)
|
||||
// "Ultimate:DELSABER": 200, // Ultimate difficulty, all floors
|
||||
// },
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user