diff options
Diffstat (limited to 'library/modules/Units.cpp')
| -rw-r--r-- | library/modules/Units.cpp | 518 |
1 files changed, 514 insertions, 4 deletions
diff --git a/library/modules/Units.cpp b/library/modules/Units.cpp index ee383cc0..01b7b50f 100644 --- a/library/modules/Units.cpp +++ b/library/modules/Units.cpp @@ -63,11 +63,15 @@ using namespace std; #include "df/burrow.h" #include "df/creature_raw.h" #include "df/caste_raw.h" +#include "df/game_mode.h" +#include "df/unit_misc_trait.h" +#include "df/unit_skill.h" using namespace DFHack; using namespace df::enums; using df::global::world; using df::global::ui; +using df::global::gamemode; bool Units::isValid() { @@ -613,6 +617,58 @@ df::nemesis_record *Units::getNemesis(df::unit *unit) return NULL; } + +bool Units::isHidingCurse(df::unit *unit) +{ + if (!unit->job.hunt_target) + { + auto identity = Units::getIdentity(unit); + if (identity && identity->unk_4c == 0) + return true; + } + + return false; +} + +int Units::getPhysicalAttrValue(df::unit *unit, df::physical_attribute_type attr) +{ + auto &aobj = unit->body.physical_attrs[attr]; + int value = std::max(0, aobj.value - aobj.soft_demotion); + + if (auto mod = unit->curse.attr_change) + { + int mvalue = (value * mod->phys_att_perc[attr] / 100) + mod->phys_att_add[attr]; + + if (isHidingCurse(unit)) + value = std::min(value, mvalue); + else + value = mvalue; + } + + return std::max(0, value); +} + +int Units::getMentalAttrValue(df::unit *unit, df::mental_attribute_type attr) +{ + auto soul = unit->status.current_soul; + if (!soul) return 0; + + auto &aobj = soul->mental_attrs[attr]; + int value = std::max(0, aobj.value - aobj.soft_demotion); + + if (auto mod = unit->curse.attr_change) + { + int mvalue = (value * mod->ment_att_perc[attr] / 100) + mod->ment_att_add[attr]; + + if (isHidingCurse(unit)) + value = std::min(value, mvalue); + else + value = mvalue; + } + + return std::max(0, value); +} + static bool casteFlagSet(int race, int caste, df::caste_raw_flags flag) { auto creature = df::creature_raw::find(race); @@ -626,8 +682,9 @@ static bool casteFlagSet(int race, int caste, df::caste_raw_flags flag) return craw->flags.is_set(flag); } -static bool isCrazed(df::unit *unit) +bool Units::isCrazed(df::unit *unit) { + CHECK_NULL_POINTER(unit); if (unit->flags3.bits.scuttle) return false; if (unit->curse.rem_tags1.bits.CRAZED) @@ -637,13 +694,64 @@ static bool isCrazed(df::unit *unit) return casteFlagSet(unit->race, unit->caste, caste_raw_flags::CRAZED); } -static bool isOpposedToLife(df::unit *unit) +bool Units::isOpposedToLife(df::unit *unit) { + CHECK_NULL_POINTER(unit); if (unit->curse.rem_tags1.bits.OPPOSED_TO_LIFE) return false; if (unit->curse.add_tags1.bits.OPPOSED_TO_LIFE) return true; - return casteFlagSet(unit->race, unit->caste, caste_raw_flags::CANNOT_UNDEAD); + return casteFlagSet(unit->race, unit->caste, caste_raw_flags::OPPOSED_TO_LIFE); +} + +bool Units::hasExtravision(df::unit *unit) +{ + CHECK_NULL_POINTER(unit); + if (unit->curse.rem_tags1.bits.EXTRAVISION) + return false; + if (unit->curse.add_tags1.bits.EXTRAVISION) + return true; + return casteFlagSet(unit->race, unit->caste, caste_raw_flags::EXTRAVISION); +} + +bool Units::isBloodsucker(df::unit *unit) +{ + CHECK_NULL_POINTER(unit); + if (unit->curse.rem_tags1.bits.BLOODSUCKER) + return false; + if (unit->curse.add_tags1.bits.BLOODSUCKER) + return true; + return casteFlagSet(unit->race, unit->caste, caste_raw_flags::BLOODSUCKER); +} + +bool Units::isMischievous(df::unit *unit) +{ + CHECK_NULL_POINTER(unit); + if (unit->curse.rem_tags1.bits.MISCHIEVOUS) + return false; + if (unit->curse.add_tags1.bits.MISCHIEVOUS) + return true; + return casteFlagSet(unit->race, unit->caste, caste_raw_flags::MISCHIEVOUS); +} + +df::unit_misc_trait *Units::getMiscTrait(df::unit *unit, df::misc_trait_type type, bool create) +{ + CHECK_NULL_POINTER(unit); + + auto &vec = unit->status.misc_traits; + for (size_t i = 0; i < vec.size(); i++) + if (vec[i]->id == type) + return vec[i]; + + if (create) + { + auto obj = new df::unit_misc_trait(); + obj->id = type; + vec.push_back(obj); + return obj; + } + + return NULL; } bool DFHack::Units::isDead(df::unit *unit) @@ -753,6 +861,371 @@ double DFHack::Units::getAge(df::unit *unit, bool true_age) return cur_time - birth_time; } +inline void adjust_skill_rating(int &rating, bool is_adventure, int value, int dwarf3_4, int dwarf1_2, int adv9_10, int adv3_4, int adv1_2) +{ + if (is_adventure) + { + if (value >= adv1_2) rating >>= 1; + else if (value >= adv3_4) rating = rating*3/4; + else if (value >= adv9_10) rating = rating*9/10; + } + else + { + if (value >= dwarf1_2) rating >>= 1; + else if (value >= dwarf3_4) rating = rating*3/4; + } +} + +int Units::getEffectiveSkill(df::unit *unit, df::job_skill skill_id) +{ + CHECK_NULL_POINTER(unit); + + /* + * This is 100% reverse-engineered from DF code. + */ + + if (!unit->status.current_soul) + return 0; + + // Retrieve skill from unit soul: + + df::enum_field<df::job_skill,int16_t> key(skill_id); + auto skill = binsearch_in_vector(unit->status.current_soul->skills, &df::unit_skill::id, key); + + int rating = 0; + if (skill) + rating = std::max(0, int(skill->rating) - skill->rusty); + + // Apply special states + + if (unit->counters.soldier_mood == df::unit::T_counters::None) + { + if (unit->counters.nausea > 0) rating >>= 1; + if (unit->counters.winded > 0) rating >>= 1; + if (unit->counters.stunned > 0) rating >>= 1; + if (unit->counters.dizziness > 0) rating >>= 1; + if (unit->counters2.fever > 0) rating >>= 1; + } + + if (unit->counters.soldier_mood != df::unit::T_counters::MartialTrance) + { + if (!unit->flags3.bits.ghostly && !unit->flags3.bits.scuttle && + !unit->flags2.bits.vision_good && !unit->flags2.bits.vision_damaged && + !hasExtravision(unit)) + { + rating >>= 2; + } + if (unit->counters.pain >= 100 && unit->mood == -1) + { + rating >>= 1; + } + if (unit->counters2.exhaustion >= 2000) + { + rating = rating*3/4; + if (unit->counters2.exhaustion >= 4000) + { + rating = rating*3/4; + if (unit->counters2.exhaustion >= 6000) + rating = rating*3/4; + } + } + } + + // Hunger etc timers + + bool is_adventure = (gamemode && *gamemode == game_mode::ADVENTURE); + + if (!unit->flags3.bits.scuttle && isBloodsucker(unit)) + { + using namespace df::enums::misc_trait_type; + + if (auto trait = getMiscTrait(unit, TimeSinceSuckedBlood)) + { + adjust_skill_rating( + rating, is_adventure, trait->value, + 302400, 403200, // dwf 3/4; 1/2 + 1209600, 1209600, 2419200 // adv 9/10; 3/4; 1/2 + ); + } + } + + adjust_skill_rating( + rating, is_adventure, unit->counters2.thirst_timer, + 50000, 50000, 115200, 172800, 345600 + ); + adjust_skill_rating( + rating, is_adventure, unit->counters2.hunger_timer, + 75000, 75000, 172800, 1209600, 2592000 + ); + if (is_adventure && unit->counters2.sleepiness_timer >= 846000) + rating >>= 2; + else + adjust_skill_rating( + rating, is_adventure, unit->counters2.sleepiness_timer, + 150000, 150000, 172800, 259200, 345600 + ); + + return rating; +} + +inline void adjust_speed_rating(int &rating, bool is_adventure, int value, int dwarf100, int dwarf200, int adv50, int adv75, int adv100, int adv200) +{ + if (is_adventure) + { + if (value >= adv200) rating += 200; + else if (value >= adv100) rating += 100; + else if (value >= adv75) rating += 75; + else if (value >= adv50) rating += 50; + } + else + { + if (value >= dwarf200) rating += 200; + else if (value >= dwarf100) rating += 100; + } +} + +static int calcInventoryWeight(df::unit *unit) +{ + int armor_skill = Units::getEffectiveSkill(unit, job_skill::ARMOR); + int armor_mul = 15 - std::min(15, armor_skill); + + int inv_weight = 0, inv_weight_fraction = 0; + + for (size_t i = 0; i < unit->inventory.size(); i++) + { + auto item = unit->inventory[i]->item; + if (!item->flags.bits.weight_computed) + continue; + + int wval = item->weight; + int wfval = item->weight_fraction; + auto mode = unit->inventory[i]->mode; + + if ((mode == df::unit_inventory_item::Worn || + mode == df::unit_inventory_item::WrappedAround) && + item->isArmor() && armor_skill > 1) + { + wval = wval * armor_mul / 16; + wfval = wfval * armor_mul / 16; + } + + inv_weight += wval; + inv_weight_fraction += wfval; + } + + return inv_weight*100 + inv_weight_fraction/10000; +} + +int Units::computeMovementSpeed(df::unit *unit) +{ + using namespace df::enums::physical_attribute_type; + + /* + * Pure reverse-engineered computation of unit _slowness_, + * i.e. number of ticks to move * 100. + */ + + // Base speed + + auto creature = df::creature_raw::find(unit->race); + if (!creature) + return 0; + + auto craw = vector_get(creature->caste, unit->caste); + if (!craw) + return 0; + + int speed = craw->misc.speed; + + if (unit->flags3.bits.ghostly) + return speed; + + // Curse multiplier + + if (unit->curse.speed_mul_percent != 100) + { + speed *= 100; + if (unit->curse.speed_mul_percent != 0) + speed /= unit->curse.speed_mul_percent; + } + + speed += unit->curse.speed_add; + + // Swimming + + auto cur_liquid = unit->status2.liquid_type.bits.liquid_type; + bool in_magma = (cur_liquid == tile_liquid::Magma); + + if (unit->flags2.bits.swimming) + { + speed = craw->misc.swim_speed; + if (in_magma) + speed *= 2; + + if (craw->flags.is_set(caste_raw_flags::SWIMS_LEARNED)) + { + int skill = Units::getEffectiveSkill(unit, job_skill::SWIMMING); + + // Originally a switch: + if (skill > 1) + speed = speed * std::max(6, 21-skill) / 20; + } + } + else + { + int delta = 150*unit->status2.liquid_depth; + if (in_magma) + delta *= 2; + speed += delta; + } + + // General counters and flags + + if (unit->profession == profession::BABY) + speed += 3000; + + if (unit->flags3.bits.unk15) + speed /= 20; + + if (unit->counters2.exhaustion >= 2000) + { + speed += 200; + if (unit->counters2.exhaustion >= 4000) + { + speed += 200; + if (unit->counters2.exhaustion >= 6000) + speed += 200; + } + } + + if (unit->flags2.bits.gutted) speed += 2000; + + if (unit->counters.soldier_mood == df::unit::T_counters::None) + { + if (unit->counters.nausea > 0) speed += 1000; + if (unit->counters.winded > 0) speed += 1000; + if (unit->counters.stunned > 0) speed += 1000; + if (unit->counters.dizziness > 0) speed += 1000; + if (unit->counters2.fever > 0) speed += 1000; + } + + if (unit->counters.soldier_mood != df::unit::T_counters::MartialTrance) + { + if (unit->counters.pain >= 100 && unit->mood == -1) + speed += 1000; + } + + // Hunger etc timers + + bool is_adventure = (gamemode && *gamemode == game_mode::ADVENTURE); + + if (!unit->flags3.bits.scuttle && Units::isBloodsucker(unit)) + { + using namespace df::enums::misc_trait_type; + + if (auto trait = Units::getMiscTrait(unit, TimeSinceSuckedBlood)) + { + adjust_speed_rating( + speed, is_adventure, trait->value, + 302400, 403200, // dwf 100; 200 + 1209600, 1209600, 1209600, 2419200 // adv 50; 75; 100; 200 + ); + } + } + + adjust_speed_rating( + speed, is_adventure, unit->counters2.thirst_timer, + 50000, 0x7fffffff, 172800, 172800, 172800, 345600 + ); + adjust_speed_rating( + speed, is_adventure, unit->counters2.hunger_timer, + 75000, 0x7fffffff, 1209600, 1209600, 1209600, 2592000 + ); + adjust_speed_rating( + speed, is_adventure, unit->counters2.sleepiness_timer, + 57600, 150000, 172800, 259200, 345600, 864000 + ); + + // Activity state + + if (unit->relations.draggee_id != -1) speed += 1000; + + if (unit->flags1.bits.on_ground) + speed += 2000; + else if (unit->flags3.bits.on_crutch) + { + int skill = Units::getEffectiveSkill(unit, job_skill::CRUTCH_WALK); + speed += 2000 - 100*std::min(20, skill); + } + + if (unit->flags1.bits.hidden_in_ambush && !Units::isMischievous(unit)) + { + int skill = Units::getEffectiveSkill(unit, job_skill::SNEAK); + speed += 2000 - 100*std::min(20, skill); + } + + if (unsigned(unit->counters2.paralysis-1) <= 98) + speed += unit->counters2.paralysis*10; + if (unsigned(unit->counters.webbed-1) <= 8) + speed += unit->counters.webbed*100; + + // Muscle weight vs vascular tissue (?) + + auto &attr_tissue = unit->body.physical_attr_tissues; + int muscle = attr_tissue[STRENGTH]; + int blood = attr_tissue[AGILITY]; + speed = std::max(speed*3/4, std::min(speed*3/2, int(int64_t(speed)*muscle/blood))); + + // Attributes + + int strength_attr = Units::getPhysicalAttrValue(unit, STRENGTH); + int agility_attr = Units::getPhysicalAttrValue(unit, AGILITY); + + int total_attr = std::max(200, std::min(3800, strength_attr + agility_attr)); + speed = ((total_attr-200)*(speed/2) + (3800-total_attr)*(speed*3/2))/3600; + + // Stance + + if (!unit->flags1.bits.on_ground && unit->status2.able_stand > 2) + { + // WTF + int as = unit->status2.able_stand; + int x = (as-1) - (as>>1); + int y = as - unit->status2.able_stand_impair; + if (unit->flags3.bits.on_crutch) y--; + y = y * 500 / x; + if (y > 0) speed += y; + } + + // Mood + + if (unit->mood == mood_type::Melancholy) speed += 8000; + + // Inventory encumberance + + int total_weight = calcInventoryWeight(unit); + int free_weight = std::max(1, muscle/10 + strength_attr*3); + + if (free_weight < total_weight) + { + int delta = (total_weight - free_weight)/10 + 1; + if (!is_adventure) + delta = std::min(5000, delta); + speed += delta; + } + + // skipped: unknown loop on inventory items that amounts to 0 change + + if (is_adventure) + { + auto player = vector_get(world->units.active, 0); + if (player && player->id == unit->relations.group_leader_id) + speed = std::min(speed, computeMovementSpeed(player)); + } + + return std::min(10000, std::max(0, speed)); +} + static bool noble_pos_compare(const Units::NoblePosition &a, const Units::NoblePosition &b) { if (a.position->precedence < b.position->precedence) @@ -838,7 +1311,7 @@ std::string DFHack::Units::getCasteProfessionName(int race, int casteid, df::pro { std::string prof, race_prefix; - if (pid < 0 || !is_valid_enum_item(pid)) + if (pid < (df::profession)0 || !is_valid_enum_item(pid)) return ""; bool use_race_prefix = (race >= 0 && race != df::global::ui->race_id); @@ -948,3 +1421,40 @@ std::string DFHack::Units::getCasteProfessionName(int race, int casteid, df::pro return Translation::capitalize(prof, true); } + +int8_t DFHack::Units::getProfessionColor(df::unit *unit, bool ignore_noble) +{ + std::vector<NoblePosition> np; + + if (!ignore_noble && getNoblePositions(&np, unit)) + { + if (np[0].position->flags.is_set(entity_position_flags::COLOR)) + return np[0].position->color[0] + np[0].position->color[2] * 8; + } + + return getCasteProfessionColor(unit->race, unit->caste, unit->profession); +} + +int8_t DFHack::Units::getCasteProfessionColor(int race, int casteid, df::profession pid) +{ + // make sure it's an actual profession + if (pid < 0 || !is_valid_enum_item(pid)) + return 3; + + // If it's not a Peasant, it's hardcoded + if (pid != profession::STANDARD) + return ENUM_ATTR(profession, color, pid); + + if (auto creature = df::creature_raw::find(race)) + { + if (auto caste = vector_get(creature->caste, casteid)) + { + if (caste->flags.is_set(caste_raw_flags::CASTE_COLOR)) + return caste->caste_color[0] + caste->caste_color[2] * 8; + } + return creature->color[0] + creature->color[2] * 8; + } + + // default to dwarven peasant color + return 3; +} |
