1686 lines
59 KiB
C++
1686 lines
59 KiB
C++
#include "ItemCreator.hh"
|
|
|
|
#include <algorithm>
|
|
#include <array>
|
|
|
|
#include "EnemyType.hh"
|
|
#include "Loggers.hh"
|
|
|
|
// Note: There are clearly better ways of doing this, but this implementation closely follows what the original code in
|
|
// the client does.
|
|
template <typename ItemT, size_t MaxCount>
|
|
struct ProbabilityTable {
|
|
ItemT items[MaxCount];
|
|
size_t count;
|
|
|
|
ProbabilityTable() : count(0) {}
|
|
|
|
ProbabilityTable(const std::vector<ShopRandomSetBase::IntPairT<ItemT>>& table) : ProbabilityTable() {
|
|
for (const auto& entry : table) {
|
|
for (size_t y = 0; y < entry.weight; y++) {
|
|
this->push(entry.value);
|
|
}
|
|
}
|
|
}
|
|
|
|
template <size_t Count>
|
|
ProbabilityTable(const std::array<ShopRandomSetBase::IntPairT<ItemT>, Count>& table) : ProbabilityTable() {
|
|
for (const auto& entry : table) {
|
|
for (size_t y = 0; y < entry.weight; y++) {
|
|
this->push(entry.value);
|
|
}
|
|
}
|
|
}
|
|
|
|
void push(ItemT item) {
|
|
if (this->count == MaxCount) {
|
|
throw std::runtime_error("push to full probability table");
|
|
}
|
|
this->items[this->count++] = item;
|
|
}
|
|
|
|
ItemT pop() {
|
|
if (this->count == 0) {
|
|
throw std::runtime_error("pop from empty probability table");
|
|
}
|
|
return this->items[--this->count];
|
|
}
|
|
|
|
void shuffle(std::shared_ptr<RandomGenerator> rand_crypt) {
|
|
for (size_t z = 1; z < this->count; z++) {
|
|
size_t other_z = rand_crypt->next() % (z + 1);
|
|
ItemT t = this->items[z];
|
|
this->items[z] = this->items[other_z];
|
|
this->items[other_z] = t;
|
|
}
|
|
}
|
|
|
|
ItemT sample(std::shared_ptr<RandomGenerator> rand_crypt) const {
|
|
if (this->count == 0) {
|
|
throw std::runtime_error("sample from empty probability table");
|
|
} else if (this->count == 1) {
|
|
return this->items[0];
|
|
} else {
|
|
return this->items[rand_crypt->next() % this->count];
|
|
}
|
|
}
|
|
};
|
|
|
|
ItemCreator::ItemCreator(
|
|
std::shared_ptr<const CommonItemSet> common_item_set,
|
|
std::shared_ptr<const RareItemSet> rare_item_set,
|
|
std::shared_ptr<const ArmorShopRandomSet> armor_random_set,
|
|
std::shared_ptr<const ToolShopRandomSet> tool_random_set,
|
|
std::shared_ptr<const WeaponShopRandomSet> weapon_random_set,
|
|
std::shared_ptr<const TekkerAdjustmentSet> tekker_adjustment_set,
|
|
std::shared_ptr<const ItemParameterTable> item_parameter_table,
|
|
std::shared_ptr<const ItemData::StackLimits> stack_limits,
|
|
GameMode mode,
|
|
Difficulty difficulty,
|
|
uint8_t section_id,
|
|
std::shared_ptr<RandomGenerator> rand_crypt,
|
|
std::shared_ptr<const BattleRules> restrictions)
|
|
: log(std::format("[ItemCreator:{}/{}/{}/{}] ", phosg::name_for_enum(stack_limits->version), abbreviation_for_mode(mode), abbreviation_for_difficulty(difficulty), section_id), lobby_log.min_level),
|
|
logic_version(stack_limits->version),
|
|
is_legacy_replay(false),
|
|
stack_limits(stack_limits),
|
|
mode(mode),
|
|
difficulty(difficulty),
|
|
section_id(section_id),
|
|
rare_item_set(rare_item_set),
|
|
armor_random_set(armor_random_set),
|
|
tool_random_set(tool_random_set),
|
|
weapon_random_set(weapon_random_set),
|
|
tekker_adjustment_set(tekker_adjustment_set),
|
|
item_parameter_table(item_parameter_table),
|
|
common_item_set(common_item_set),
|
|
restrictions(restrictions),
|
|
rand_crypt(rand_crypt) {
|
|
this->generate_unit_stars_tables();
|
|
}
|
|
|
|
void ItemCreator::set_section_id(uint8_t new_section_id) {
|
|
if (this->section_id != new_section_id) {
|
|
this->section_id = new_section_id;
|
|
this->log.prefix = std::format("[ItemCreator:{}/{}/{}/{}] ",
|
|
phosg::name_for_enum(stack_limits->version),
|
|
abbreviation_for_mode(mode),
|
|
abbreviation_for_difficulty(difficulty),
|
|
this->section_id);
|
|
}
|
|
}
|
|
|
|
bool ItemCreator::are_rare_drops_allowed() const {
|
|
// Note: The client has an additional check here, which appears to be a subtle anti-cheating measure. There is a flag
|
|
// on the client, initially zero, which is set to 1 when certain unexpected item-related things happen (for example,
|
|
// a player possessing a mag with a level above 200, or a stack of consumables with an amount above the stack size
|
|
// limit). When the flag is set, this function returns false, which prevents all rare item drops. newserv
|
|
// intentionally does not implement this flag.
|
|
return (this->mode != GameMode::CHALLENGE);
|
|
}
|
|
|
|
uint8_t ItemCreator::table_index_for_area(uint8_t area) const {
|
|
if (this->restrictions && (this->restrictions->box_drop_area != 0)) {
|
|
return this->restrictions->box_drop_area - 1;
|
|
}
|
|
static constexpr std::array<uint8_t, 0x2F> data = {
|
|
// Episode 1
|
|
0xFF, // 00 => Pioneer 2 (no drops)
|
|
0x00, // 01 => Forest 1
|
|
0x01, // 02 => Forest 2
|
|
0x02, // 03 => Cave 1
|
|
0x03, // 04 => Cave 2
|
|
0x04, // 05 => Cave 3
|
|
0x05, // 06 => Mine 1
|
|
0x06, // 07 => Mine 2
|
|
0x07, // 08 => Ruins 1
|
|
0x08, // 09 => Ruins 2
|
|
0x09, // 0A => Ruins 3
|
|
0x02, // 0B => Dragon -> Cave 1
|
|
0x05, // 0C => De Rol Le -> Mine 1
|
|
0x07, // 0D => Vol Opt -> Ruins 1
|
|
0x09, // 0E => Dark Falz -> Ruins 3
|
|
0xFF, // 0F => Lobby (no drops)
|
|
0x09, // 10 => Palace -> Ruins 3
|
|
0x09, // 11 => Spaceship -> Ruins 3
|
|
// Episode 2
|
|
0xFF, // 12 => Lab (no drops)
|
|
0x00, // 13 => VR Temple Alpha
|
|
0x01, // 14 => VR Temple Beta
|
|
0x02, // 15 => VR Spaceship Alpha
|
|
0x03, // 16 => VR Spaceship Beta
|
|
0x07, // 17 => Central Control Area -> Seaside
|
|
0x04, // 18 => Jungle North
|
|
0x05, // 19 => Jungle South
|
|
0x06, // 1A => Mountain
|
|
0x07, // 1B => Seaside
|
|
0x08, // 1C => Seabed Upper
|
|
0x09, // 1D => Seabed Lower
|
|
0x08, // 1E => Gal Gryphon -> Seabed Upper
|
|
0x09, // 1F => Olga Flow -> Seabed Lower
|
|
0x02, // 20 => Barba Ray -> VR Spaceship Alpha
|
|
0x04, // 21 => Gol Dragon -> Jungle North
|
|
0x07, // 22 => Seaside Night -> Seaside
|
|
0x09, // 23 => Tower -> Seabed Lower
|
|
// Episode 4
|
|
0x01, // 24 => Crater East
|
|
0x02, // 25 => Crater West
|
|
0x03, // 26 => Crater South
|
|
0x04, // 27 => Crater North
|
|
0x05, // 28 => Crater Interior
|
|
0x06, // 29 => Subterranean Desert 1
|
|
0x07, // 2A => Subterranean Desert 2
|
|
0x08, // 2B => Subterranean Desert 3
|
|
0x09, // 2C => Saint-Milion
|
|
0xFF, // 2D => Pioneer 2 (no drops)
|
|
0xFF, // 2E => Test area (no drops)
|
|
};
|
|
if (area >= data.size() || data[area] == 0xFF) {
|
|
throw std::runtime_error("invalid area number");
|
|
}
|
|
return data[area];
|
|
}
|
|
|
|
ItemCreator::DropResult ItemCreator::on_box_item_drop(uint8_t area, bool force_rare) {
|
|
try {
|
|
uint8_t table_index = this->table_index_for_area(area);
|
|
this->log.info_f("Box drop checks for area {:02X} (table index {:02X})", area, table_index);
|
|
|
|
DropResult res;
|
|
res.item = this->check_rare_specs_and_create_rare_box_item(area, force_rare);
|
|
if (!res.item.empty()) {
|
|
res.is_from_rare_table = true;
|
|
} else {
|
|
uint8_t item_class = this->get_rand_from_weighted_tables_2d_vertical(
|
|
this->pt(area)->box_item_class_prob_table, table_index);
|
|
this->log.info_f("Item class is {:02X}", item_class);
|
|
switch (item_class) {
|
|
case 0: // Weapon
|
|
res.item.data1[0] = 0;
|
|
break;
|
|
case 1: // Armor
|
|
res.item.data1[0] = 1;
|
|
res.item.data1[1] = 1;
|
|
break;
|
|
case 2: // Shield
|
|
res.item.data1[0] = 1;
|
|
res.item.data1[1] = 2;
|
|
break;
|
|
case 3: // Unit
|
|
res.item.data1[0] = 1;
|
|
res.item.data1[1] = 3;
|
|
break;
|
|
case 4: // Tool
|
|
res.item.data1[0] = 3;
|
|
break;
|
|
case 5: // Meseta
|
|
res.item.data1[0] = 4;
|
|
break;
|
|
case 6: // Nothing
|
|
break;
|
|
default:
|
|
throw std::logic_error("this should be impossible");
|
|
}
|
|
if (item_class < 6) {
|
|
this->generate_common_item_variances(res.item, area);
|
|
}
|
|
}
|
|
return res;
|
|
|
|
} catch (const std::exception& e) {
|
|
this->log.error_f("Exception in item creation: {}", e.what());
|
|
return DropResult();
|
|
}
|
|
}
|
|
|
|
ItemCreator::DropResult ItemCreator::on_monster_item_drop(EnemyType enemy_type, uint8_t area, bool force_rare) {
|
|
try {
|
|
// Note: The original implementation has a bounds check for enemy_type here, because it uses rt_index instead
|
|
// if (enemy_type >= NUM_RT_INDEXES_V4) {
|
|
// this->log.warning_f("Invalid enemy type: {:X}", enemy_type);
|
|
// return DropResult();
|
|
// }
|
|
this->log.info_f("Enemy type: {}", phosg::name_for_enum(enemy_type));
|
|
|
|
auto pt = this->pt(area);
|
|
uint8_t type_drop_prob = 0;
|
|
try {
|
|
type_drop_prob = pt->enemy_type_drop_probs.at(enemy_type);
|
|
} catch (const std::out_of_range&) {
|
|
this->log.info_f("No drop probability is set for this enemy type");
|
|
return DropResult();
|
|
}
|
|
if (!force_rare) {
|
|
uint8_t drop_sample = this->rand_int(100);
|
|
if (drop_sample >= type_drop_prob) {
|
|
this->log.info_f("Drop not chosen ({} >= {})", drop_sample, type_drop_prob);
|
|
return DropResult();
|
|
} else {
|
|
this->log.info_f("Drop chosen ({} < {})", drop_sample, type_drop_prob);
|
|
}
|
|
}
|
|
|
|
DropResult res;
|
|
res.item = this->check_rare_spec_and_create_rare_enemy_item(enemy_type, area, force_rare);
|
|
if (!res.item.empty()) {
|
|
res.is_from_rare_table = true;
|
|
} else {
|
|
uint8_t item_class_determinant = this->should_allow_meseta_drops() ? this->rand_int(3) : (this->rand_int(2) + 1);
|
|
uint8_t item_class;
|
|
switch (item_class_determinant) {
|
|
case 0:
|
|
item_class = 5;
|
|
break;
|
|
case 1:
|
|
item_class = 4;
|
|
break;
|
|
case 2:
|
|
try {
|
|
item_class = pt->enemy_type_item_classes.at(enemy_type);
|
|
} catch (const std::out_of_range&) {
|
|
this->log.info_f("Item class is not set for this enemy type");
|
|
item_class = 0xFF;
|
|
}
|
|
break;
|
|
default:
|
|
throw std::logic_error("invalid item class determinant");
|
|
}
|
|
|
|
this->log.info_f(
|
|
"Rare drop not chosen; item class determinant is {}; item class is {}", item_class_determinant, item_class);
|
|
|
|
switch (item_class) {
|
|
case 0: // Weapon
|
|
res.item.data1[0] = 0x00;
|
|
break;
|
|
case 1: // Armor
|
|
res.item.data1w[0] = 0x0101;
|
|
break;
|
|
case 2: // Shield
|
|
res.item.data1w[0] = 0x0201;
|
|
break;
|
|
case 3: // Unit
|
|
res.item.data1w[0] = 0x0301;
|
|
break;
|
|
case 4: // Tool
|
|
res.item.data1[0] = 0x03;
|
|
break;
|
|
case 5: // Meseta
|
|
res.item.data1[0] = 0x04;
|
|
try {
|
|
res.item.data2d = this->choose_meseta_amount(pt->enemy_type_meseta_ranges.at(enemy_type)) & 0xFFFF;
|
|
} catch (const std::out_of_range&) {
|
|
this->log.info_f("Meseta range is not set for this enemy type");
|
|
return DropResult();
|
|
}
|
|
break;
|
|
default:
|
|
return res;
|
|
}
|
|
|
|
if (res.item.data1[0] != 0x04) {
|
|
this->generate_common_item_variances(res.item, area);
|
|
}
|
|
}
|
|
|
|
return res;
|
|
|
|
} catch (const std::exception& e) {
|
|
this->log.error_f("Exception in item creation: {}", e.what());
|
|
return DropResult();
|
|
}
|
|
}
|
|
|
|
ItemData ItemCreator::check_rare_specs_and_create_rare_item(
|
|
const std::vector<RareItemSet::ExpandedDrop>& specs, uint8_t area, bool force_rare) {
|
|
if (specs.empty()) {
|
|
return ItemData();
|
|
}
|
|
|
|
// This logic differs from the original client logic. This logic "stacks" all rare rates into a single probability
|
|
// space, whereas the original client logic chooses a new random number for each rare spec that it checks. The
|
|
// stacking logic makes the order of specs irrelevant, whereas the original client logic means that later specs are
|
|
// actually more rare than they should be. In the original client, this only matters for boxes, because enemies could
|
|
// not have multiple specs. Also, the original code uses 0xFFFFFFFF as the maximum here; we use 0x100000000 instead,
|
|
// which makes all rare items SLIGHTLY more rare.
|
|
int64_t det = force_rare ? 0 : this->rand_int(0x100000000);
|
|
if (this->is_legacy_replay) {
|
|
// For some old tests, we waste a few replay values because they used the old (non-stacked) logic. New tests should
|
|
// not use this codepath.
|
|
for (size_t z = 1; z < specs.size(); z++) {
|
|
this->rand_int(0x100000000);
|
|
}
|
|
}
|
|
this->log.info_f("{} specs to check with det={:08X}", specs.size(), det);
|
|
for (const auto& spec : specs) {
|
|
if (this->log.should_log(phosg::LogLevel::L_INFO)) {
|
|
this->log.info_f("Checking spec {:08X} => {} with det={:08X}", spec.probability, spec.data.hex(), det);
|
|
}
|
|
det -= spec.probability;
|
|
if (det < 0) {
|
|
return this->create_rare_item(spec.data, area);
|
|
}
|
|
}
|
|
return ItemData();
|
|
}
|
|
|
|
ItemData ItemCreator::check_rare_specs_and_create_rare_box_item(uint8_t area, bool force_rare) {
|
|
if (!this->are_rare_drops_allowed()) {
|
|
return ItemData();
|
|
}
|
|
|
|
uint8_t table_index = this->table_index_for_area(area);
|
|
Episode episode = episode_for_area(area);
|
|
auto specs = this->rare_item_set->get_box_specs(this->mode, episode, this->difficulty, this->section_id, table_index);
|
|
return this->check_rare_specs_and_create_rare_item(specs, area, force_rare);
|
|
}
|
|
|
|
uint32_t ItemCreator::rand_int(uint64_t max) {
|
|
return this->rand_crypt->next() % max;
|
|
}
|
|
|
|
float ItemCreator::rand_float_0_1_from_crypt() {
|
|
// This lacks some precision, but matches the original implementation.
|
|
return (static_cast<double>(this->rand_crypt->next() >> 16) / 65536.0);
|
|
}
|
|
|
|
uint32_t ItemCreator::choose_meseta_amount(const CommonItemSet::Table::Range<uint16_t>& range) {
|
|
// Note: The original code returns 0xFF here if either limit is equal to 0xFF (despite them being 16-bit integers!)
|
|
uint16_t ret;
|
|
if (range.min == range.max) {
|
|
ret = range.min;
|
|
} else if (range.max < range.min) {
|
|
ret = this->rand_int((range.min - range.max) + 1) + range.max;
|
|
} else {
|
|
ret = this->rand_int((range.max - range.min) + 1) + range.min;
|
|
}
|
|
|
|
this->log.info_f("Chose {} Meseta from range [{}, {}]", ret, range.min, range.max);
|
|
return ret;
|
|
}
|
|
|
|
bool ItemCreator::should_allow_meseta_drops() const {
|
|
return (this->mode != GameMode::CHALLENGE);
|
|
}
|
|
|
|
ItemData ItemCreator::check_rare_spec_and_create_rare_enemy_item(EnemyType enemy_type, uint8_t area, bool force_rare) {
|
|
// Note: The original implementation has a bounds check for enemy_type here, since it uses rt_index instead.
|
|
// if ((enemy_type <= 0) || (enemy_type >= NUM_RT_INDEXES_V4)) return ItemData{};
|
|
if (!this->are_rare_drops_allowed()) {
|
|
return ItemData{};
|
|
}
|
|
|
|
// Note: In the original implementation, enemies can only have one possible rare drop. In our implementation, they
|
|
// can have multiple rare drops if JSONRareItemSet is used (the other RareItemSet implementations never return
|
|
// multiple drops for an enemy type).
|
|
Episode episode = episode_for_area(area);
|
|
auto specs = this->rare_item_set->get_enemy_specs(
|
|
this->mode, episode, this->difficulty, this->section_id, enemy_type);
|
|
return this->check_rare_specs_and_create_rare_item(specs, area, force_rare);
|
|
}
|
|
|
|
ItemData ItemCreator::create_rare_item(const ItemData& drop_item, uint8_t area) {
|
|
ItemData item = drop_item;
|
|
if (item.can_be_encoded_in_rel_rare_table()) {
|
|
switch (item.data1[0]) {
|
|
case 0:
|
|
if (this->pt(area)->has_rare_bonus_value_prob_table) {
|
|
this->generate_rare_weapon_bonuses(item, episode_for_area(area), this->rand_int(10));
|
|
} else {
|
|
this->generate_common_weapon_bonuses(item, area);
|
|
}
|
|
this->set_item_unidentified_flag_if_not_challenge(item);
|
|
break;
|
|
case 1:
|
|
this->generate_common_armor_slots_and_bonuses(item, episode_for_area(area));
|
|
break;
|
|
case 2:
|
|
this->generate_common_mag_variances(item);
|
|
break;
|
|
case 3:
|
|
this->clear_tool_item_if_invalid(item);
|
|
this->set_tool_item_amount_to_1(item);
|
|
break;
|
|
case 4:
|
|
break;
|
|
default:
|
|
throw std::logic_error("invalid item class");
|
|
}
|
|
this->set_item_kill_count_if_unsealable(item);
|
|
}
|
|
|
|
this->clear_item_if_restricted(item);
|
|
return item;
|
|
}
|
|
|
|
void ItemCreator::generate_rare_weapon_bonuses(ItemData& item, Episode episode, uint32_t table_index) {
|
|
if (item.data1[0] != 0) {
|
|
return;
|
|
}
|
|
|
|
auto pt = this->pt(episode);
|
|
if (!pt->has_rare_bonus_value_prob_table) {
|
|
throw std::logic_error("generate_rare_weapon_bonuses called for common item table without rare bonus value probability table");
|
|
}
|
|
|
|
for (size_t z = 0; z < 6; z += 2) {
|
|
uint8_t bonus_type = this->get_rand_from_weighted_tables_2d_vertical(pt->bonus_type_prob_table, table_index);
|
|
int16_t bonus_value = this->get_rand_from_weighted_tables_2d_vertical(pt->bonus_value_prob_table, 5);
|
|
item.data1[z + 6] = bonus_type;
|
|
item.data1[z + 7] = bonus_value * 5 - 10;
|
|
// Note: The original code has a special case here, which divides item.data1[z + 7] by 5 and multiplies it by 5
|
|
// again if bonus_type is 5 (Hit). Why this is done is unclear, because item.data1[z + 7] must already be a
|
|
// multiple of 5.
|
|
}
|
|
|
|
this->deduplicate_weapon_bonuses(item);
|
|
}
|
|
|
|
void ItemCreator::generate_common_weapon_bonuses(ItemData& item, uint8_t area) {
|
|
if (item.data1[0] != 0) {
|
|
return;
|
|
}
|
|
|
|
auto pt = this->pt(area);
|
|
uint8_t table_index = this->table_index_for_area(area);
|
|
for (size_t row = 0; row < 3; row++) {
|
|
uint8_t spec = pt->nonrare_bonus_prob_spec.at(row).at(table_index);
|
|
if (spec == 0xFF) {
|
|
this->log.info_f("Bonus {} is forbidden", row);
|
|
} else {
|
|
item.data1[(row * 2) + 6] = this->get_rand_from_weighted_tables_2d_vertical(pt->bonus_type_prob_table, table_index);
|
|
int16_t amount = this->get_rand_from_weighted_tables_2d_vertical(pt->bonus_value_prob_table, spec);
|
|
item.data1[(row * 2) + 7] = amount * 5 - 10;
|
|
this->log.info_f("Bonus {} generated as {:02X} {:02X} from table index {:02X} and spec {:02X}", row, item.data1[(row * 2) + 6], item.data1[(row * 2) + 7], table_index, spec);
|
|
}
|
|
// Note: The original code has a special case here, which divides item.data1[z + 7] by 5 and multiplies it by 5
|
|
// again if bonus_type is 5 (Hit). Why this is done is unclear, because item.data1[z + 7] must already be a
|
|
// multiple of 5.
|
|
}
|
|
|
|
this->deduplicate_weapon_bonuses(item);
|
|
}
|
|
|
|
void ItemCreator::deduplicate_weapon_bonuses(ItemData& item) const {
|
|
for (size_t x = 0; x < 6; x += 2) {
|
|
for (size_t y = 0; y < x; y += 2) {
|
|
if (item.data1[y + 6] == 0x00) {
|
|
item.data1[x + 6] = 0x00;
|
|
} else if (item.data1[x + 6] == item.data1[y + 6]) {
|
|
item.data1[x + 6] = 0x00;
|
|
}
|
|
}
|
|
if (item.data1[x + 6] == 0x00) {
|
|
item.data1[x + 7] = 0x00;
|
|
}
|
|
}
|
|
}
|
|
|
|
void ItemCreator::set_item_kill_count_if_unsealable(ItemData& item) const {
|
|
if (this->item_parameter_table->is_unsealable_item(item)) {
|
|
this->log.info_f("Item is unsealable; setting kill count to zero");
|
|
item.set_kill_count(0);
|
|
}
|
|
}
|
|
|
|
void ItemCreator::set_item_unidentified_flag_if_not_challenge(ItemData& item) const {
|
|
if (this->mode == GameMode::CHALLENGE) {
|
|
return;
|
|
}
|
|
if (item.data1[0] != 0x00) {
|
|
return;
|
|
}
|
|
// On V1, V3, and V4, all rare weapons and weapons with specials are untekked when created; on V2, only rares that
|
|
// are not in the standard item classes are untekked when created.
|
|
bool is_rare = this->item_parameter_table->is_item_rare(item);
|
|
bool use_v2_logic = is_v2(this->logic_version) && (this->logic_version != Version::GC_NTE);
|
|
if (use_v2_logic ? (is_rare ? (item.data1[1] > 0x0C) : (item.data1[4] != 0)) : (is_rare || (item.data1[4] != 0))) {
|
|
item.data1[4] |= 0x80;
|
|
}
|
|
}
|
|
|
|
void ItemCreator::set_tool_item_amount_to_1(ItemData& item) const {
|
|
if (item.data1[0] == 0x03) {
|
|
item.set_tool_item_amount(*this->stack_limits, 1);
|
|
}
|
|
}
|
|
|
|
void ItemCreator::clear_tool_item_if_invalid(ItemData& item) {
|
|
if ((item.data1[1] == 0x02) && ((item.data1[2] > 0x1D) || (item.data1[4] > 0x12))) {
|
|
item.clear();
|
|
}
|
|
}
|
|
|
|
void ItemCreator::clear_item_if_restricted(ItemData& item) const {
|
|
if (this->item_parameter_table->is_item_rare(item) && !this->are_rare_drops_allowed()) {
|
|
this->log.info_f("Restricted: item is rare, but rares not allowed");
|
|
item.clear();
|
|
return;
|
|
}
|
|
|
|
if (this->mode == GameMode::CHALLENGE) {
|
|
// Forbid HP/TP-restoring units and meseta in challenge mode. PSO GC doesn't check for 0x61 or 0x62 here since
|
|
// those items (HP/Resurrection and TP/Resurrection) only exist on BB.
|
|
if (item.data1[0] == 1) {
|
|
if ((item.data1[1] == 3) && (((item.data1[2] >= 0x33) && (item.data1[2] <= 0x38)) || (item.data1[2] == 0x61) || (item.data1[2] == 0x62))) {
|
|
this->log.info_f("Restricted: restore units not allowed in Challenge mode");
|
|
item.clear();
|
|
return;
|
|
}
|
|
} else if (item.data1[0] == 4) {
|
|
this->log.info_f("Restricted: meseta not allowed in Challenge mode");
|
|
item.clear();
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (this->restrictions) {
|
|
switch (item.data1[0]) {
|
|
case 0:
|
|
case 1:
|
|
switch (this->restrictions->weapon_and_armor_mode) {
|
|
case BattleRules::WeaponAndArmorMode::ALLOW:
|
|
case BattleRules::WeaponAndArmorMode::CLEAR_AND_ALLOW:
|
|
break;
|
|
case BattleRules::WeaponAndArmorMode::FORBID_RARES:
|
|
if (this->item_parameter_table->is_item_rare(item)) {
|
|
this->log.info_f("Restricted: rare weapons and armors not allowed");
|
|
item.clear();
|
|
}
|
|
break;
|
|
case BattleRules::WeaponAndArmorMode::FORBID_ALL:
|
|
this->log.info_f("Restricted: weapons and armors not allowed");
|
|
item.clear();
|
|
break;
|
|
default:
|
|
throw std::logic_error("invalid weapon and armor mode");
|
|
}
|
|
break;
|
|
case 2:
|
|
if (this->restrictions->mag_mode == BattleRules::MagMode::FORBID_ALL) {
|
|
this->log.info_f("Restricted: mags not allowed");
|
|
item.clear();
|
|
}
|
|
break;
|
|
case 3:
|
|
if (this->restrictions->tool_mode == BattleRules::ToolMode::FORBID_ALL) {
|
|
this->log.info_f("Restricted: tools not allowed");
|
|
item.clear();
|
|
} else if (item.data1[1] == 2) {
|
|
switch (this->restrictions->tech_disk_mode) {
|
|
case BattleRules::TechDiskMode::ALLOW:
|
|
break;
|
|
case BattleRules::TechDiskMode::FORBID_ALL:
|
|
this->log.info_f("Restricted: tech disks not allowed");
|
|
item.clear();
|
|
break;
|
|
case BattleRules::TechDiskMode::LIMIT_LEVEL:
|
|
this->log.info_f("Restricted: tech disk level limited to {}",
|
|
static_cast<uint8_t>(this->restrictions->max_tech_level + 1));
|
|
if (this->restrictions->max_tech_level == 0) {
|
|
item.data1[2] = 0;
|
|
} else {
|
|
item.data1[2] %= this->restrictions->max_tech_level;
|
|
}
|
|
break;
|
|
default:
|
|
throw std::logic_error("invalid tech disk mode");
|
|
}
|
|
} else if ((item.data1[1] == 9) && this->restrictions->forbid_scape_dolls) {
|
|
this->log.info_f("Restricted: scape dolls not allowed");
|
|
item.clear();
|
|
}
|
|
break;
|
|
case 4:
|
|
if (this->restrictions->meseta_mode == BattleRules::MesetaMode::FORBID_ALL) {
|
|
this->log.info_f("Restricted: meseta not allowed");
|
|
item.clear();
|
|
}
|
|
break;
|
|
default:
|
|
throw std::logic_error("invalid item");
|
|
}
|
|
}
|
|
}
|
|
|
|
void ItemCreator::generate_common_item_variances(ItemData& item, uint8_t area) {
|
|
switch (item.data1[0]) {
|
|
case 0:
|
|
this->generate_common_weapon_variances(item, area);
|
|
break;
|
|
case 1:
|
|
if (item.data1[1] == 3) {
|
|
float f1 = 1.0 + this->pt(area)->unit_max_stars_table.at(this->table_index_for_area(area));
|
|
float f2 = this->rand_float_0_1_from_crypt();
|
|
uint8_t stars = static_cast<uint32_t>(f1 * f2) & 0xFF;
|
|
this->log.info_f("Unit stars: {:g} * {:g} = {}", f1, f2, stars);
|
|
this->generate_common_unit_variances(stars, item);
|
|
if (item.data1[2] == 0xFF) {
|
|
this->log.info_f("Unit subtype not valid; clearing item");
|
|
item.clear();
|
|
}
|
|
} else {
|
|
this->generate_common_armor_or_shield_type_and_variances(item, area);
|
|
}
|
|
break;
|
|
case 2:
|
|
this->generate_common_mag_variances(item);
|
|
break;
|
|
case 3:
|
|
this->generate_common_tool_variances(item, area);
|
|
break;
|
|
case 4: {
|
|
const auto& range = this->pt(area)->box_meseta_ranges.at(this->table_index_for_area(area));
|
|
item.data2d = this->choose_meseta_amount(range) & 0xFFFF;
|
|
break;
|
|
}
|
|
default:
|
|
// Note: The original code does the following here:
|
|
// item.clear();
|
|
// item.data1[0] = 0x05;
|
|
throw std::logic_error("invalid item class");
|
|
}
|
|
|
|
this->clear_item_if_restricted(item);
|
|
this->set_item_kill_count_if_unsealable(item);
|
|
}
|
|
|
|
void ItemCreator::generate_common_armor_or_shield_type_and_variances(ItemData& item, uint8_t area) {
|
|
this->generate_common_armor_slots_and_bonuses(item, episode_for_area(area));
|
|
|
|
auto pt = this->pt(area);
|
|
uint8_t table_index = this->table_index_for_area(area);
|
|
|
|
uint8_t type = this->get_rand_from_weighted_tables_1d(pt->armor_shield_type_index_prob_table);
|
|
item.data1[2] = table_index + type + pt->armor_or_shield_type_bias;
|
|
if (item.data1[2] < 3) {
|
|
item.data1[2] = 0;
|
|
} else {
|
|
item.data1[2] -= 3;
|
|
}
|
|
this->log.info_f("Armor/shield type: max({:02X} + {:02X} + {:02X} - 3, 0) = {:02X}",
|
|
table_index, type, pt->armor_or_shield_type_bias, item.data1[2]);
|
|
}
|
|
|
|
void ItemCreator::generate_common_armor_slots_and_bonuses(ItemData& item, Episode episode) {
|
|
if ((item.data1[0] != 0x01) || (item.data1[1] < 1) || (item.data1[1] > 2)) {
|
|
return;
|
|
}
|
|
|
|
if (item.data1[1] == 1) {
|
|
this->generate_common_armor_slot_count(item, episode);
|
|
}
|
|
|
|
const auto& def = this->item_parameter_table->get_armor_or_shield(item.data1[1], item.data1[2]);
|
|
item.set_armor_or_shield_defense_bonus(def.dfp_range * this->rand_float_0_1_from_crypt());
|
|
item.set_common_armor_evasion_bonus(def.evp_range * this->rand_float_0_1_from_crypt());
|
|
}
|
|
|
|
void ItemCreator::generate_common_armor_slot_count(ItemData& item, Episode episode) {
|
|
item.data1[5] = this->get_rand_from_weighted_tables_1d(this->pt(episode)->armor_slot_count_prob_table);
|
|
}
|
|
|
|
void ItemCreator::generate_common_tool_variances(ItemData& item, uint8_t area) {
|
|
item.clear();
|
|
|
|
auto pt = this->pt(area);
|
|
uint8_t table_index = this->table_index_for_area(area);
|
|
|
|
uint8_t tool_class = this->get_rand_from_weighted_tables_2d_vertical(pt->tool_class_prob_table, table_index);
|
|
if ((!is_v1_or_v2(this->logic_version) || (this->logic_version == Version::GC_NTE)) && (tool_class == 0x1A)) {
|
|
tool_class = 0x73;
|
|
}
|
|
this->log.info_f("Generating tool with class {:02X}", tool_class);
|
|
|
|
// Note: This block was originally a separate function called generate_common_tool_type
|
|
{
|
|
// It appears that when Sega deleted Hit Material in v3, they never deleted it from the ItemPT entries, so
|
|
// sometimes ItemCreator tries to generate it. The original implementation just generates no item when that
|
|
// happens, so we do the same here.
|
|
try {
|
|
auto data = this->item_parameter_table->find_tool_by_id(tool_class);
|
|
item.data1[0] = 0x03;
|
|
item.data1[1] = data.first;
|
|
item.data1[2] = data.second;
|
|
} catch (const std::out_of_range&) {
|
|
this->log.info_f("Tool class is missing; skipping item generation");
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (item.data1[1] == 0x02) { // Tech disk
|
|
item.data1[4] = this->get_rand_from_weighted_tables_2d_vertical(pt->technique_index_prob_table, table_index);
|
|
item.data1[2] = this->generate_tech_disk_level(item.data1[4], area);
|
|
this->clear_tool_item_if_invalid(item);
|
|
}
|
|
this->set_tool_item_amount_to_1(item);
|
|
}
|
|
|
|
uint8_t ItemCreator::generate_tech_disk_level(uint32_t tech_num, uint8_t area) {
|
|
const auto& range = this->pt(area)->technique_level_ranges.at(tech_num).at(this->table_index_for_area(area));
|
|
if (((range.min == 0xFF) || (range.max == 0xFF)) || (range.max < range.min)) {
|
|
return 0xFF;
|
|
} else if (range.min != range.max) {
|
|
return this->rand_int((range.max - range.min) + 1) + range.min;
|
|
}
|
|
return range.min;
|
|
}
|
|
|
|
void ItemCreator::generate_common_mag_variances(ItemData& item) {
|
|
if (item.data1[0] == 0x02) {
|
|
item.data1[1] = 0x00;
|
|
item.assign_mag_stats(ItemMagStats());
|
|
|
|
// The original code (on PSO GC) assigns the mag color as 0x0E. We assign a random color instead.
|
|
if (is_pre_v1(this->logic_version)) {
|
|
item.data2[3] = 0x00;
|
|
} else if (is_v1_or_v2(this->logic_version)) {
|
|
item.data2[3] = this->rand_crypt->next() % 0x0E;
|
|
} else {
|
|
item.data2[3] = this->rand_crypt->next() % 0x12;
|
|
}
|
|
}
|
|
}
|
|
|
|
void ItemCreator::generate_common_weapon_variances(ItemData& item, uint8_t area) {
|
|
item.clear();
|
|
item.data1[0] = 0x00;
|
|
|
|
auto pt = this->pt(area);
|
|
uint8_t table_index = this->table_index_for_area(area);
|
|
|
|
parray<uint8_t, 0x0D> weapon_type_prob_table;
|
|
weapon_type_prob_table[0] = 0;
|
|
memmove(weapon_type_prob_table.data() + 1, pt->base_weapon_type_prob_table.data(), 0x0C);
|
|
|
|
for (size_t z = 1; z < 13; z++) {
|
|
// Technically this should be `if (... < 0)`, but whatever
|
|
if ((table_index + pt->subtype_base_table.at(z - 1)) & 0x80) {
|
|
weapon_type_prob_table[z] = 0;
|
|
}
|
|
}
|
|
|
|
this->log.info_f("Subtype table: {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X}",
|
|
weapon_type_prob_table[0], weapon_type_prob_table[1], weapon_type_prob_table[2], weapon_type_prob_table[3],
|
|
weapon_type_prob_table[4], weapon_type_prob_table[5], weapon_type_prob_table[6], weapon_type_prob_table[7],
|
|
weapon_type_prob_table[8], weapon_type_prob_table[9], weapon_type_prob_table[10], weapon_type_prob_table[11],
|
|
weapon_type_prob_table[12]);
|
|
|
|
item.data1[1] = this->get_rand_from_weighted_tables_1d(weapon_type_prob_table);
|
|
if (item.data1[1] == 0) {
|
|
this->log.info_f("00 chosen from subtype table; skipping item");
|
|
item.clear();
|
|
} else {
|
|
int8_t subtype_base = pt->subtype_base_table.at(item.data1[1] - 1);
|
|
uint8_t area_length = pt->subtype_area_length_table.at(item.data1[1] - 1);
|
|
this->log.info_f("Subtype table yielded {:02X}; subtype base is {} with area length {}", item.data1[1], subtype_base, area_length);
|
|
if (subtype_base < 0) {
|
|
item.data1[2] = (table_index + subtype_base) / area_length;
|
|
this->log.info_f("Resulting subtype: ({:02X} + {:02X}) / {:02X} = {:02X}", table_index, subtype_base, area_length, item.data1[2]);
|
|
this->generate_common_weapon_grind(item, area, (table_index + subtype_base) - (item.data1[2] * area_length));
|
|
} else {
|
|
item.data1[2] = subtype_base + (table_index / area_length);
|
|
this->log.info_f("Resulting subtype: {:02X} + ({:02X} / {:02X}) = {:02X}", subtype_base, table_index, area_length, item.data1[2]);
|
|
this->generate_common_weapon_grind(item, area, table_index - (table_index / area_length) * area_length);
|
|
}
|
|
this->generate_common_weapon_bonuses(item, area);
|
|
this->generate_common_weapon_special(item, area);
|
|
this->set_item_unidentified_flag_if_not_challenge(item);
|
|
}
|
|
}
|
|
|
|
void ItemCreator::generate_common_weapon_grind(ItemData& item, uint8_t area, uint8_t offset_within_subtype_range) {
|
|
if (item.data1[0] == 0) {
|
|
uint8_t offset = std::clamp<uint8_t>(offset_within_subtype_range, 0, 3);
|
|
item.data1[3] = this->get_rand_from_weighted_tables_2d_vertical(this->pt(area)->grind_prob_table, offset);
|
|
this->log.info_f("Generated grind {:02X} from offset within subtype range {:02X}", item.data1[3], offset_within_subtype_range);
|
|
}
|
|
}
|
|
|
|
void ItemCreator::generate_common_weapon_special(ItemData& item, uint8_t area) {
|
|
auto pt = this->pt(area);
|
|
uint8_t table_index = this->table_index_for_area(area);
|
|
if (item.data1[0] != 0) {
|
|
return;
|
|
}
|
|
if (this->item_parameter_table->is_item_rare(item)) {
|
|
this->log.info_f("Item is rare; skipping special generation");
|
|
return;
|
|
}
|
|
uint8_t special_mult = pt->special_mult.at(table_index);
|
|
if (special_mult == 0) {
|
|
this->log.info_f("Special multiplier is zero for table index {:02X}; skipping special generation", table_index);
|
|
return;
|
|
}
|
|
uint8_t det = this->rand_int(100);
|
|
uint8_t prob = pt->special_percent.at(table_index);
|
|
if (det >= prob) {
|
|
this->log.info_f("Special not chosen ({:02X} > {:02X})", det, prob);
|
|
return;
|
|
}
|
|
item.data1[4] = this->choose_weapon_special(special_mult * this->rand_float_0_1_from_crypt());
|
|
}
|
|
|
|
uint8_t ItemCreator::choose_weapon_special(uint8_t det) {
|
|
if (det >= 4) {
|
|
this->log.info_f("Special not chosen (det {:02X} >= 4)", det);
|
|
return 0;
|
|
}
|
|
|
|
static const uint8_t maxes[4] = {8, 10, 11, 11};
|
|
uint8_t det2 = this->rand_int(maxes[det]);
|
|
this->log.info_f("Choosing special with det {:02X} and det2 {:02X}", det, det2);
|
|
size_t index = 0;
|
|
for (size_t z = 1; z < this->item_parameter_table->num_specials(); z++) {
|
|
if (det + 1 == this->item_parameter_table->get_special_stars(z)) {
|
|
if (index == det2) {
|
|
this->log.info_f("Chose special {:02X}", z);
|
|
return z;
|
|
} else {
|
|
index++;
|
|
}
|
|
}
|
|
}
|
|
this->log.info_f("No special was eligible");
|
|
return 0;
|
|
}
|
|
|
|
void ItemCreator::generate_unit_stars_tables() {
|
|
// Note: This part of the function was originally in a different function, since it had another callsite. Unlike the
|
|
// original code, we generate these tables only once at construction time, so we've inlined the function here.
|
|
|
|
size_t star_base_index;
|
|
uint8_t num_units;
|
|
switch (this->logic_version) {
|
|
case Version::PC_PATCH:
|
|
case Version::BB_PATCH:
|
|
case Version::GC_EP3_NTE:
|
|
case Version::GC_EP3:
|
|
throw std::logic_error("ItemCreator cannot be created for Episode 3 games");
|
|
case Version::DC_NTE:
|
|
star_base_index = 0x124;
|
|
num_units = 0x43;
|
|
break;
|
|
case Version::DC_11_2000:
|
|
case Version::DC_V1:
|
|
star_base_index = 0x128;
|
|
num_units = 0x44;
|
|
break;
|
|
case Version::DC_V2:
|
|
case Version::PC_NTE:
|
|
case Version::PC_V2:
|
|
star_base_index = 0x1D1;
|
|
num_units = 0x44;
|
|
break;
|
|
case Version::GC_NTE:
|
|
star_base_index = 0x251;
|
|
num_units = 0x47;
|
|
break;
|
|
case Version::GC_V3:
|
|
case Version::XB_V3:
|
|
star_base_index = 0x2AF;
|
|
num_units = 0x48;
|
|
break;
|
|
case Version::BB_V4:
|
|
star_base_index = 0x37D;
|
|
num_units = 0x64;
|
|
break;
|
|
default:
|
|
throw std::logic_error("invalid game version");
|
|
}
|
|
|
|
for (auto& vec : this->unit_results_by_star_count) {
|
|
vec.clear();
|
|
}
|
|
|
|
for (uint8_t z = 0; z < num_units; z++) {
|
|
uint8_t stars = this->item_parameter_table->get_item_stars(z + star_base_index);
|
|
if (z < 0x10) {
|
|
// Units 00-0F can have modifiers; others can't
|
|
this->unit_results_by_star_count.at(stars - 1).emplace_back(UnitResult{z, -2});
|
|
this->unit_results_by_star_count.at(stars - 1).emplace_back(UnitResult{z, -1});
|
|
this->unit_results_by_star_count.at(stars + 1).emplace_back(UnitResult{z, 1});
|
|
this->unit_results_by_star_count.at(stars + 1).emplace_back(UnitResult{z, 2});
|
|
}
|
|
this->unit_results_by_star_count.at(stars).emplace_back(UnitResult{z, 0});
|
|
}
|
|
}
|
|
|
|
void ItemCreator::generate_common_unit_variances(uint8_t stars, ItemData& item) {
|
|
if (stars >= 0x0D) {
|
|
return;
|
|
}
|
|
item.clear();
|
|
|
|
const auto& results = this->unit_results_by_star_count.at(stars);
|
|
if (results.empty()) {
|
|
this->log.info_f("There are no available units with {} stars", stars);
|
|
return;
|
|
}
|
|
|
|
const auto& result = (results.size() == 1) ? results[0] : results[this->rand_int(results.size())];
|
|
item.data1[0] = 0x01;
|
|
item.data1[1] = 0x03;
|
|
item.data1[2] = result.unit;
|
|
if (result.modifier) {
|
|
const auto& def = this->item_parameter_table->get_unit(result.unit);
|
|
item.set_unit_bonus(def.modifier_amount * result.modifier);
|
|
}
|
|
this->log.info_f("Generated unit {:02X} with modifier {}, from {} choices with {} stars",
|
|
result.unit, result.modifier, results.size(), stars);
|
|
}
|
|
|
|
// Returns a weighted random result, indicating the chosen position in the weighted table. For example, an input table
|
|
// of 40 40 40 40 would be equally likely to return 0, 1, 2, or 3. An input table of 40 40 80 would return 2 50% of the
|
|
// time, and 0 or 1 each 25% of the time.
|
|
template <typename IntT>
|
|
IntT ItemCreator::get_rand_from_weighted_tables(const IntT* tables, size_t offset, size_t num_values, size_t stride) {
|
|
uint64_t rand_max = 0;
|
|
for (size_t x = 0; x != num_values; x++) {
|
|
rand_max += tables[x * stride + offset];
|
|
}
|
|
if (rand_max == 0) {
|
|
throw std::runtime_error("weighted table is empty");
|
|
}
|
|
|
|
uint32_t x = this->rand_int(rand_max);
|
|
for (size_t z = 0; z < num_values; z++) {
|
|
IntT table_value = tables[z * stride + offset];
|
|
if (x < table_value) {
|
|
return z;
|
|
}
|
|
x -= table_value;
|
|
}
|
|
throw std::logic_error("selector was not less than rand_max");
|
|
}
|
|
|
|
template <typename IntT, size_t X>
|
|
IntT ItemCreator::get_rand_from_weighted_tables_1d(const parray<IntT, X>& tables) {
|
|
return ItemCreator::get_rand_from_weighted_tables<IntT>(tables.data(), 0, X, 1);
|
|
}
|
|
|
|
template <typename IntT, size_t X, size_t Y>
|
|
IntT ItemCreator::get_rand_from_weighted_tables_2d_vertical(const parray<parray<IntT, X>, Y>& tables, size_t offset) {
|
|
return ItemCreator::get_rand_from_weighted_tables<IntT>(tables[0].data(), offset, Y, X);
|
|
}
|
|
|
|
std::vector<ItemData> ItemCreator::generate_armor_shop_contents(Episode episode, size_t player_level) {
|
|
std::vector<ItemData> shop;
|
|
this->generate_armor_shop_armors(shop, episode, player_level);
|
|
this->generate_armor_shop_shields(shop, player_level);
|
|
this->generate_armor_shop_units(shop, player_level);
|
|
return shop;
|
|
}
|
|
|
|
size_t ItemCreator::get_table_index_for_armor_shop(
|
|
size_t player_level) {
|
|
if (player_level < 11) {
|
|
return 0;
|
|
} else if (player_level < 26) {
|
|
return 1;
|
|
} else if (player_level < 43) {
|
|
return 2;
|
|
} else if (player_level < 61) {
|
|
return 3;
|
|
} else {
|
|
return 4;
|
|
}
|
|
}
|
|
|
|
bool ItemCreator::shop_does_not_contain_duplicate_armor(
|
|
const std::vector<ItemData>& shop, const ItemData& item) {
|
|
for (const auto& shop_item : shop) {
|
|
if ((shop_item.data1[0] == item.data1[0]) &&
|
|
(shop_item.data1[1] == item.data1[1]) &&
|
|
(shop_item.data1[2] == item.data1[2]) &&
|
|
(shop_item.data1[5] == item.data1[5])) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool ItemCreator::shop_does_not_contain_duplicate_tech_disk(
|
|
const std::vector<ItemData>& shop, const ItemData& item) {
|
|
for (const auto& shop_item : shop) {
|
|
if ((shop_item.data1[0] == item.data1[0]) &&
|
|
(shop_item.data1[1] == item.data1[1]) &&
|
|
(shop_item.data1[2] == item.data1[2]) &&
|
|
(shop_item.data1[4] == item.data1[4])) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool ItemCreator::shop_does_not_contain_duplicate_or_too_many_similar_weapons(
|
|
const std::vector<ItemData>& shop, const ItemData& item) {
|
|
size_t similar_items = 0;
|
|
for (const auto& shop_item : shop) {
|
|
// Disallow exact matches
|
|
if (shop_item == item) {
|
|
return false;
|
|
}
|
|
|
|
if ((shop_item.data1[0] == item.data1[0]) && (shop_item.data1[1] == item.data1[1])) {
|
|
similar_items++;
|
|
if (similar_items >= 2) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool ItemCreator::shop_does_not_contain_duplicate_item_by_data1_0_1_2(
|
|
const std::vector<ItemData>& shop, const ItemData& item) {
|
|
for (const auto& shop_item : shop) {
|
|
if ((shop_item.data1[0] == item.data1[0]) &&
|
|
(shop_item.data1[1] == item.data1[1]) &&
|
|
(shop_item.data1[2] == item.data1[2])) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void ItemCreator::generate_armor_shop_armors(std::vector<ItemData>& shop, Episode episode, size_t player_level) {
|
|
size_t num_items;
|
|
if (player_level < 11) {
|
|
num_items = 4;
|
|
} else if (player_level < 26) {
|
|
num_items = 6;
|
|
} else {
|
|
// Note: The original code has another case here that can result in 8 items, but that overflows BB's shop item
|
|
// list command, so we omit it here.
|
|
num_items = 7;
|
|
}
|
|
size_t table_index = this->get_table_index_for_armor_shop(player_level);
|
|
|
|
ProbabilityTable<uint8_t, 100> pt{this->armor_random_set->armor_table.at(table_index)};
|
|
pt.shuffle(this->rand_crypt);
|
|
|
|
for (size_t items_generated = 0; items_generated < num_items;) {
|
|
ItemData item;
|
|
item.data1[0] = 1;
|
|
item.data1[1] = 1;
|
|
item.data1[2] = pt.pop();
|
|
|
|
if ((this->difficulty == Difficulty::ULTIMATE) && (player_level > 99)) {
|
|
if (player_level > 150) {
|
|
item.data1[2] += 3;
|
|
} else if (player_level >= 100) {
|
|
item.data1[2] += 2;
|
|
}
|
|
}
|
|
|
|
this->generate_common_armor_slot_count(item, episode);
|
|
if (this->shop_does_not_contain_duplicate_armor(shop, item)) {
|
|
shop.emplace_back(std::move(item));
|
|
items_generated++;
|
|
}
|
|
}
|
|
}
|
|
|
|
void ItemCreator::generate_armor_shop_shields(std::vector<ItemData>& shop, size_t player_level) {
|
|
size_t num_items;
|
|
if (player_level < 11) {
|
|
num_items = 4;
|
|
} else if (player_level < 26) {
|
|
num_items = 5;
|
|
} else if (player_level < 42) {
|
|
num_items = 6;
|
|
} else {
|
|
num_items = 7;
|
|
}
|
|
size_t table_index = this->get_table_index_for_armor_shop(player_level);
|
|
|
|
ProbabilityTable<uint8_t, 100> pt{this->armor_random_set->shield_table.at(table_index)};
|
|
pt.shuffle(this->rand_crypt);
|
|
|
|
for (size_t items_generated = 0; items_generated < num_items;) {
|
|
ItemData item;
|
|
item.data1[0] = 1;
|
|
item.data1[1] = 2;
|
|
item.data1[2] = pt.pop();
|
|
|
|
if ((this->difficulty == Difficulty::ULTIMATE) && (player_level > 99)) {
|
|
if (player_level > 150) {
|
|
item.data1[2] += 3;
|
|
} else if (player_level >= 100) {
|
|
item.data1[2] += 2;
|
|
}
|
|
}
|
|
|
|
if (this->shop_does_not_contain_duplicate_item_by_data1_0_1_2(shop, item)) {
|
|
shop.emplace_back(std::move(item));
|
|
items_generated++;
|
|
}
|
|
}
|
|
}
|
|
|
|
void ItemCreator::generate_armor_shop_units(std::vector<ItemData>& shop, size_t player_level) {
|
|
size_t num_items;
|
|
if (player_level < 11) {
|
|
return; // num_items = 0
|
|
} else if (player_level < 26) {
|
|
num_items = 3;
|
|
} else if (player_level < 43) {
|
|
num_items = 5;
|
|
} else {
|
|
num_items = 6;
|
|
}
|
|
size_t table_index = this->get_table_index_for_armor_shop(player_level);
|
|
|
|
ProbabilityTable<uint8_t, 100> pt{this->armor_random_set->unit_table.at(table_index)};
|
|
pt.shuffle(this->rand_crypt);
|
|
|
|
for (size_t items_generated = 0; items_generated < num_items;) {
|
|
ItemData item;
|
|
item.data1[0] = 1;
|
|
item.data1[1] = 3;
|
|
item.data1[2] = pt.pop();
|
|
if (this->shop_does_not_contain_duplicate_item_by_data1_0_1_2(shop, item)) {
|
|
shop.emplace_back(std::move(item));
|
|
items_generated++;
|
|
}
|
|
}
|
|
}
|
|
|
|
std::vector<ItemData> ItemCreator::generate_tool_shop_contents(size_t player_level) {
|
|
std::vector<ItemData> shop;
|
|
this->generate_common_tool_shop_recovery_items(shop, player_level);
|
|
this->generate_rare_tool_shop_recovery_items(shop, player_level);
|
|
this->generate_tool_shop_tech_disks(shop, player_level);
|
|
sort(shop.begin(), shop.end(), ItemData::compare_for_sort);
|
|
return shop;
|
|
}
|
|
|
|
size_t ItemCreator::get_table_index_for_tool_shop(size_t player_level) {
|
|
if (player_level < 11) {
|
|
return 0;
|
|
} else if (player_level < 26) {
|
|
return 1;
|
|
} else if (player_level < 43) {
|
|
return 2;
|
|
} else if (player_level < 61) {
|
|
return 3;
|
|
} else {
|
|
return 4;
|
|
}
|
|
}
|
|
|
|
void ItemCreator::generate_common_tool_shop_recovery_items(std::vector<ItemData>& shop, size_t player_level) {
|
|
size_t table_index;
|
|
if (player_level < 11) {
|
|
table_index = 0;
|
|
} else if (player_level < 26) {
|
|
table_index = 1;
|
|
} else if (player_level < 45) {
|
|
table_index = 2;
|
|
} else if (player_level < 61) {
|
|
table_index = 3;
|
|
} else if (player_level < 100) {
|
|
table_index = 4;
|
|
} else {
|
|
table_index = 5;
|
|
}
|
|
|
|
for (const auto& entry : this->tool_random_set->common_recovery_table.at(table_index)) {
|
|
if (entry == 0x0F) {
|
|
continue;
|
|
}
|
|
|
|
auto& item = shop.emplace_back();
|
|
item.data1[0] = 3;
|
|
item.data1[1] = ToolShopRandomSet::item_defs[entry].first;
|
|
item.data1[2] = ToolShopRandomSet::item_defs[entry].second;
|
|
}
|
|
}
|
|
|
|
void ItemCreator::generate_rare_tool_shop_recovery_items(std::vector<ItemData>& shop, size_t player_level) {
|
|
if (player_level < 11) {
|
|
return;
|
|
}
|
|
static constexpr size_t num_items = 2;
|
|
|
|
size_t table_index = this->get_table_index_for_tool_shop(player_level);
|
|
ProbabilityTable<uint8_t, 100> pt{this->tool_random_set->rare_recovery_table.at(table_index)};
|
|
pt.shuffle(this->rand_crypt);
|
|
|
|
size_t effective_num_items = num_items;
|
|
size_t items_generated = 0;
|
|
while (items_generated < effective_num_items) {
|
|
uint8_t type = pt.pop();
|
|
if (type == 0x0F) {
|
|
if (effective_num_items == num_items) {
|
|
effective_num_items--;
|
|
}
|
|
} else {
|
|
ItemData item;
|
|
item.data1[0] = 3;
|
|
item.data1[1] = ToolShopRandomSet::item_defs[type].first;
|
|
item.data1[2] = ToolShopRandomSet::item_defs[type].second;
|
|
if (this->shop_does_not_contain_duplicate_item_by_data1_0_1_2(shop, item)) {
|
|
shop.emplace_back(std::move(item));
|
|
items_generated++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void ItemCreator::generate_tool_shop_tech_disks(std::vector<ItemData>& shop, size_t player_level) {
|
|
size_t num_items;
|
|
if (player_level < 11) {
|
|
num_items = 4;
|
|
} else if (player_level < 43) {
|
|
num_items = 5;
|
|
} else {
|
|
num_items = 7;
|
|
}
|
|
|
|
size_t table_index = this->get_table_index_for_tool_shop(player_level);
|
|
ProbabilityTable<uint8_t, 100> pt{this->tool_random_set->tech_disk_table.at(table_index)};
|
|
pt.shuffle(this->rand_crypt);
|
|
|
|
size_t items_generated = 0;
|
|
while (items_generated < num_items) {
|
|
uint8_t tech_num_index = pt.pop();
|
|
ItemData item;
|
|
item.data1[0] = 3;
|
|
item.data1[1] = 2;
|
|
item.data1[4] = ToolShopRandomSet::tech_num_map.at(tech_num_index);
|
|
this->choose_tech_disk_level_for_tool_shop(item, player_level, tech_num_index);
|
|
if (this->shop_does_not_contain_duplicate_tech_disk(shop, item)) {
|
|
shop.emplace_back(std::move(item));
|
|
items_generated++;
|
|
}
|
|
}
|
|
}
|
|
|
|
void ItemCreator::choose_tech_disk_level_for_tool_shop(ItemData& item, size_t player_level, uint8_t tech_num_index) {
|
|
size_t table_index = this->get_table_index_for_tool_shop(player_level);
|
|
auto table = this->tool_random_set->tech_disk_level_table.at(table_index);
|
|
if (tech_num_index >= table.size()) {
|
|
throw std::runtime_error("technique number out of range");
|
|
}
|
|
const auto& e = table[tech_num_index];
|
|
|
|
switch (e.mode) {
|
|
case ToolShopRandomSet::TechDiskLevelEntry::Mode::LEVEL_1:
|
|
item.data1[2] = 0;
|
|
break;
|
|
case ToolShopRandomSet::TechDiskLevelEntry::Mode::PLAYER_LEVEL_DIVISOR:
|
|
item.data1[2] = std::clamp<ssize_t>(
|
|
(std::min<size_t>(player_level, 99) / e.player_level_divisor_or_min_level) - 1, 0, 14);
|
|
break;
|
|
case ToolShopRandomSet::TechDiskLevelEntry::Mode::RANDOM_IN_RANGE: {
|
|
// Note: This logic does not give a uniform distribution - if the minimum level is not zero (level 1), then the
|
|
// minimum level is more likely than all the other levels. This behavior matches the client's logic, though it's
|
|
// unclear if this nonuniformity was intentional.
|
|
int16_t min_level = std::max<int16_t>(e.player_level_divisor_or_min_level - 1, 0);
|
|
item.data1[2] = std::clamp<int16_t>(this->rand_int(e.max_level), min_level, 14);
|
|
break;
|
|
}
|
|
default:
|
|
throw std::logic_error("invalid tech disk level mode");
|
|
}
|
|
}
|
|
|
|
std::vector<ItemData> ItemCreator::generate_weapon_shop_contents(size_t player_level) {
|
|
size_t num_items;
|
|
if (player_level < 11) {
|
|
num_items = 10;
|
|
} else if (player_level < 43) {
|
|
num_items = 12;
|
|
} else {
|
|
num_items = 16;
|
|
}
|
|
|
|
size_t table_index;
|
|
if (this->difficulty == Difficulty::ULTIMATE) {
|
|
if (player_level < 11) {
|
|
table_index = 0;
|
|
} else if (player_level < 26) {
|
|
table_index = 1;
|
|
} else if (player_level < 43) {
|
|
table_index = 2;
|
|
} else if (player_level < 61) {
|
|
table_index = 3;
|
|
} else if (player_level < 100) {
|
|
table_index = 4;
|
|
} else if (player_level < 151) {
|
|
table_index = 5;
|
|
} else {
|
|
table_index = 6;
|
|
}
|
|
} else {
|
|
if (player_level < 11) {
|
|
table_index = 0;
|
|
} else if (player_level < 26) {
|
|
table_index = 1;
|
|
} else if (player_level < 43) {
|
|
table_index = 2;
|
|
} else if (player_level < 61) {
|
|
table_index = 3;
|
|
} else {
|
|
table_index = 4;
|
|
}
|
|
}
|
|
|
|
ProbabilityTable<uint8_t, 100> pt{this->weapon_random_set->weapon_type_weight_tables.at(table_index).at(section_id)};
|
|
pt.shuffle(this->rand_crypt);
|
|
|
|
std::vector<ItemData> shop;
|
|
while (shop.size() < num_items) {
|
|
ItemData item;
|
|
|
|
const std::pair<uint8_t, uint8_t>* def;
|
|
uint8_t which = pt.pop();
|
|
if (which == 0x39) {
|
|
def = &WeaponShopRandomSet::type_defs_39.at(this->section_id);
|
|
} else if (which == 0x3A) {
|
|
def = &WeaponShopRandomSet::type_defs_3A.at(this->section_id);
|
|
} else {
|
|
def = &WeaponShopRandomSet::type_defs.at(which);
|
|
}
|
|
item.data1[0] = 0;
|
|
item.data1[1] = def->first;
|
|
item.data1[2] = def->second;
|
|
|
|
this->generate_weapon_shop_item_grind(item, player_level);
|
|
this->generate_weapon_shop_item_special(item, player_level);
|
|
this->generate_weapon_shop_item_bonus1(item, player_level);
|
|
this->generate_weapon_shop_item_bonus2(item, player_level);
|
|
item.data1[10] = 0;
|
|
item.data1[11] = 0;
|
|
|
|
if (this->shop_does_not_contain_duplicate_or_too_many_similar_weapons(shop, item)) {
|
|
shop.emplace_back(std::move(item));
|
|
}
|
|
}
|
|
|
|
sort(shop.begin(), shop.end(), ItemData::compare_for_sort);
|
|
return shop;
|
|
}
|
|
|
|
void ItemCreator::generate_weapon_shop_item_grind(ItemData& item, size_t player_level) {
|
|
size_t table_index;
|
|
if (player_level < 4) {
|
|
table_index = 0;
|
|
} else if (player_level < 11) {
|
|
table_index = 1;
|
|
} else if (player_level < 26) {
|
|
table_index = 2;
|
|
} else if (player_level < 41) {
|
|
table_index = 3;
|
|
} else if (player_level < 56) {
|
|
table_index = 4;
|
|
} else {
|
|
table_index = 5;
|
|
}
|
|
|
|
uint8_t favored_weapon = TekkerAdjustmentSet::favored_weapon_type_for_section_id(this->section_id);
|
|
bool is_favored = (favored_weapon != 0xFF) && (item.data1[1] == favored_weapon);
|
|
const auto& range = is_favored
|
|
? this->weapon_random_set->favored_grind_range_table.at(table_index)
|
|
: this->weapon_random_set->default_grind_range_table.at(table_index);
|
|
|
|
const auto& weapon_def = this->item_parameter_table->get_weapon(item.data1[1], item.data1[2]);
|
|
item.data1[3] = std::clamp<uint8_t>(this->rand_int(range.max + 1), range.min, weapon_def.max_grind);
|
|
}
|
|
|
|
void ItemCreator::generate_weapon_shop_item_special(ItemData& item, size_t player_level) {
|
|
|
|
size_t table_index;
|
|
if (player_level < 11) {
|
|
table_index = 0;
|
|
} else if (player_level < 18) {
|
|
table_index = 1;
|
|
} else if (player_level < 26) {
|
|
table_index = 2;
|
|
} else if (player_level < 36) {
|
|
table_index = 3;
|
|
} else if (player_level < 46) {
|
|
table_index = 4;
|
|
} else if (player_level < 61) {
|
|
table_index = 5;
|
|
} else if (player_level < 76) {
|
|
table_index = 6;
|
|
} else {
|
|
table_index = 7;
|
|
}
|
|
|
|
ProbabilityTable<uint32_t, 100> pt{this->weapon_random_set->special_mode_table.at(table_index)};
|
|
pt.shuffle(this->rand_crypt);
|
|
|
|
// Note: The original code shuffles pt and then pops a single value from it. For simplicity, we just sample a single
|
|
// value instead.
|
|
switch (pt.sample(this->rand_crypt)) {
|
|
case 0:
|
|
item.data1[4] = 0;
|
|
break;
|
|
case 1:
|
|
item.data1[4] = this->choose_weapon_special(0);
|
|
break;
|
|
case 2:
|
|
item.data1[4] = this->choose_weapon_special(1);
|
|
break;
|
|
default:
|
|
throw std::runtime_error("invalid special mode");
|
|
}
|
|
}
|
|
|
|
void ItemCreator::generate_weapon_shop_item_bonus1(ItemData& item, size_t player_level) {
|
|
size_t table_index;
|
|
if (player_level < 4) {
|
|
table_index = 0;
|
|
} else if (player_level < 11) {
|
|
table_index = 1;
|
|
} else if (player_level < 18) {
|
|
table_index = 2;
|
|
} else if (player_level < 26) {
|
|
table_index = 3;
|
|
} else if (player_level < 36) {
|
|
table_index = 4;
|
|
} else if (player_level < 46) {
|
|
table_index = 5;
|
|
} else if (player_level < 61) {
|
|
table_index = 6;
|
|
} else if (player_level < 76) {
|
|
table_index = 7;
|
|
} else {
|
|
table_index = 8;
|
|
}
|
|
|
|
ProbabilityTable<uint32_t, 100> pt{this->weapon_random_set->bonus_type_table1.at(table_index)};
|
|
pt.shuffle(this->rand_crypt);
|
|
|
|
// Note: The original code shuffles pt and then pops a single value from it. For simplicity, we just sample a single
|
|
// value instead.
|
|
item.data1[6] = pt.sample(this->rand_crypt);
|
|
if (item.data1[6] == 0) {
|
|
item.data1[7] = 0;
|
|
} else {
|
|
const auto& range = this->weapon_random_set->bonus_range_table1.at(table_index);
|
|
item.data1[7] = WeaponShopRandomSet::bonus_values.at(std::max<size_t>(this->rand_int(range.max + 1), range.min));
|
|
}
|
|
}
|
|
|
|
void ItemCreator::generate_weapon_shop_item_bonus2(ItemData& item, size_t player_level) {
|
|
size_t table_index;
|
|
if (player_level < 6) {
|
|
table_index = 0;
|
|
} else if (player_level < 11) {
|
|
table_index = 1;
|
|
} else if (player_level < 18) {
|
|
table_index = 2;
|
|
} else if (player_level < 26) {
|
|
table_index = 3;
|
|
} else if (player_level < 36) {
|
|
table_index = 4;
|
|
} else if (player_level < 46) {
|
|
table_index = 5;
|
|
} else if (player_level < 61) {
|
|
table_index = 6;
|
|
} else if (player_level < 76) {
|
|
table_index = 7;
|
|
} else {
|
|
table_index = 8;
|
|
}
|
|
|
|
ProbabilityTable<uint32_t, 100> pt{this->weapon_random_set->bonus_type_table2.at(table_index)};
|
|
pt.shuffle(this->rand_crypt);
|
|
|
|
do {
|
|
item.data1[8] = pt.pop();
|
|
} while ((item.data1[8] != 0) && (item.data1[8] == item.data1[6]));
|
|
|
|
if (item.data1[8] == 0) {
|
|
item.data1[9] = 0;
|
|
} else {
|
|
const auto& range = this->weapon_random_set->bonus_range_table2.at(table_index);
|
|
item.data1[9] = WeaponShopRandomSet::bonus_values.at(std::max<size_t>(this->rand_int(range.max + 1), range.min));
|
|
}
|
|
}
|
|
|
|
ItemCreator::DropResult ItemCreator::on_specialized_box_item_drop(
|
|
uint8_t area, float param3, uint32_t param4, uint32_t param5, uint32_t param6) {
|
|
DropResult res;
|
|
res.item = this->base_item_for_specialized_box(param4, param5, param6);
|
|
if (param3 == 0.0f) {
|
|
uint16_t type = res.item.data1w[0];
|
|
res.item.clear();
|
|
res.item.data1w[0] = type;
|
|
this->generate_common_item_variances(res.item, area);
|
|
}
|
|
return res;
|
|
}
|
|
|
|
ItemData ItemCreator::base_item_for_specialized_box(uint32_t param4, uint32_t param5, uint32_t param6) const {
|
|
ItemData item;
|
|
item.data1[0] = (param4 >> 0x18) & 0x0F;
|
|
item.data1[1] = (param4 >> 0x10) + ((item.data1[0] == 0x00) || (item.data1[0] == 0x01));
|
|
item.data1[2] = param4 >> 8;
|
|
|
|
switch (item.data1[0]) {
|
|
case 0x00:
|
|
item.data1[3] = (param5 >> 0x18) & 0xFF;
|
|
item.data1[4] = param4 & 0xFF;
|
|
item.data1[6] = (param5 >> 8) & 0xFF;
|
|
item.data1[7] = param5 & 0xFF;
|
|
item.data1[8] = (param6 >> 0x18) & 0xFF;
|
|
item.data1[9] = (param6 >> 0x10) & 0xFF;
|
|
item.data1[10] = (param6 >> 8) & 0xFF;
|
|
item.data1[11] = param6 & 0xFF;
|
|
break;
|
|
case 0x01:
|
|
item.data1[3] = (param5 >> 0x18) & 0xFF;
|
|
item.data1[4] = (param5 >> 0x10) & 0xFF;
|
|
item.data1[5] = param4 & 0xFF;
|
|
break;
|
|
case 0x02:
|
|
item.assign_mag_stats(ItemMagStats());
|
|
break;
|
|
case 0x03:
|
|
if (item.data1[1] == 0x02) {
|
|
item.data1[4] = param4 & 0xFF;
|
|
}
|
|
item.set_tool_item_amount(*this->stack_limits, 1);
|
|
break;
|
|
case 0x04:
|
|
item.data2d = ((param5 >> 0x10) & 0xFFFF) * 10;
|
|
break;
|
|
default:
|
|
throw std::runtime_error("invalid item class");
|
|
}
|
|
|
|
return item;
|
|
}
|
|
|
|
ssize_t ItemCreator::apply_tekker_deltas(ItemData& item, uint8_t section_id) {
|
|
if (item.data1[0] != 0) {
|
|
throw std::runtime_error("tekker deltas can only be applied to weapons");
|
|
}
|
|
|
|
bool favored = (item.data1[1] == TekkerAdjustmentSet::favored_weapon_type_for_section_id(section_id));
|
|
ssize_t luck = 0;
|
|
|
|
this->log.info_f("Applying tekker deltas for {} weapon", favored ? "favored" : "non-favored");
|
|
|
|
auto sample_prob_table = [this](const TekkerAdjustmentSet::Table& table) -> int8_t {
|
|
size_t sample = this->rand_crypt->next() % table.total;
|
|
for (const auto& [k, v] : table.probs) {
|
|
if (sample < v) {
|
|
return k;
|
|
}
|
|
sample -= v;
|
|
}
|
|
throw std::logic_error("Table total is incorrect");
|
|
};
|
|
|
|
// Adjust the weapon's special
|
|
{
|
|
int8_t delta = sample_prob_table(favored
|
|
? this->tekker_adjustment_set->favored_special_delta_table[section_id]
|
|
: this->tekker_adjustment_set->default_special_delta_table[section_id]);
|
|
this->log.info_f("(Special) Delta {} chosen", delta);
|
|
for (; delta != 0; delta += (delta < 0) - (0 < delta)) {
|
|
try {
|
|
// Note: The original code checks specifically for -1 and +1 here and only increments or decrements the special
|
|
// by 1, and the data files only include delta_indexes 4, 5, and 6 (which correspond to -1, 0, and 1). But we
|
|
// want to support other levels of delta indexes, so we simply add delta instead. When using the original
|
|
// JudgeItem.rel file, the behavior should be the same, but this logic feels more correct.
|
|
uint8_t new_special = item.data1[4] + delta;
|
|
if (this->item_parameter_table->get_special(item.data1[4]).type ==
|
|
this->item_parameter_table->get_special(new_special).type) {
|
|
item.data1[4] = new_special;
|
|
this->log.info_f("(Special) Delta {} applied", delta);
|
|
break;
|
|
} else {
|
|
this->log.info_f("(Special) Delta {} canceled because it would change special category", delta);
|
|
}
|
|
} catch (const std::out_of_range&) {
|
|
// Invalid special number passed to get_special; treat it as if delta == 0
|
|
}
|
|
}
|
|
luck += this->tekker_adjustment_set->special_luck_table.at(delta);
|
|
this->log.info_f("(Special) Luck is now {}", luck);
|
|
}
|
|
|
|
// Adjust the weapon's grind if it's not rare
|
|
if (!this->item_parameter_table->is_item_rare(item)) {
|
|
const auto& weapon_def = this->item_parameter_table->get_weapon(item.data1[1], item.data1[2]);
|
|
int8_t delta = sample_prob_table(favored
|
|
? this->tekker_adjustment_set->favored_grind_delta_table[section_id]
|
|
: this->tekker_adjustment_set->default_grind_delta_table[section_id]);
|
|
this->log.info_f("(Grind) Delta {} chosen", delta);
|
|
int16_t new_grind = static_cast<int16_t>(item.data1[3]) + static_cast<int16_t>(delta);
|
|
item.data1[3] = std::clamp<int16_t>(new_grind, 0, weapon_def.max_grind);
|
|
luck += this->tekker_adjustment_set->grind_luck_table.at(delta);
|
|
this->log.info_f("(Grind) Luck is now {}", luck);
|
|
} else {
|
|
this->log.info_f("(Grind) Item is rare; skipping grind adjustment");
|
|
}
|
|
|
|
// Adjust the weapon's bonuses
|
|
{
|
|
int8_t delta = sample_prob_table(favored
|
|
? this->tekker_adjustment_set->favored_bonus_delta_table[section_id]
|
|
: this->tekker_adjustment_set->default_bonus_delta_table[section_id]);
|
|
this->log.info_f("(Bonuses) Delta {} chosen", delta);
|
|
// Note: The original code doesn't check if there's actually a bonus in each slot before incrementing the values.
|
|
// Presumably there's a check later that will clear any invalid bonuses, but we don't have such a check, so we need
|
|
// to check here if each bonus is actually present.
|
|
for (size_t z = 6; z <= 10; z += 2) {
|
|
if (item.data1[z] >= 1 && item.data1[z] <= 5) {
|
|
item.data1[z + 1] = std::min<int8_t>(item.data1[z + 1] + delta, 100);
|
|
}
|
|
}
|
|
luck += this->tekker_adjustment_set->bonus_luck_table.at(delta);
|
|
this->log.info_f("(Bonuses) Luck is now {}", luck);
|
|
}
|
|
|
|
return luck;
|
|
}
|