2035 lines
52 KiB
C++
2035 lines
52 KiB
C++
// Copyright (c) rAthena Dev Teams - Licensed under GNU GPL
|
|
// For more information, see LICENCE in the main folder
|
|
|
|
#include "homunculus.hpp"
|
|
|
|
#include <cstdlib>
|
|
|
|
#include <common/cbasetypes.hpp>
|
|
#include <common/malloc.hpp>
|
|
#include <common/mmo.hpp>
|
|
#include <common/nullpo.hpp>
|
|
#include <common/random.hpp>
|
|
#include <common/showmsg.hpp>
|
|
#include <common/strlib.hpp>
|
|
#include <common/timer.hpp>
|
|
#include <common/utils.hpp>
|
|
|
|
#include "battle.hpp"
|
|
#include "clif.hpp"
|
|
#include "intif.hpp"
|
|
#include "itemdb.hpp"
|
|
#include "log.hpp"
|
|
#include "npc.hpp"
|
|
#include "party.hpp"
|
|
#include "pc.hpp"
|
|
#include "trade.hpp"
|
|
|
|
using namespace rathena;
|
|
|
|
static TIMER_FUNC(hom_hungry);
|
|
|
|
//For holding the view data of npc classes. [Skotlex]
|
|
static struct view_data hom_viewdb[MAX_HOMUNCULUS_CLASS];
|
|
|
|
struct s_homun_intimacy_grade {
|
|
//const char *grade;
|
|
uint32 min_value;
|
|
};
|
|
|
|
/// Intimacy grade, order based on enum e_homun_grade
|
|
static struct s_homun_intimacy_grade intimacy_grades[] = {
|
|
{ /*"Hate with passion",*/ 100 },
|
|
{ /*"Hate", */ 400 },
|
|
{ /*"Awkward", */ 1100 },
|
|
{ /*"Shy", */ 10100 },
|
|
{ /*"Neutral", */ 25100 },
|
|
{ /*"Cordial", */ 75100 },
|
|
{ /*"Loyal", */ 91100 },
|
|
};
|
|
|
|
const std::string HomExpDatabase::getDefaultLocation() {
|
|
return std::string(db_path) + "/exp_homun.yml";
|
|
}
|
|
|
|
uint64 HomExpDatabase::parseBodyNode(const ryml::NodeRef& node) {
|
|
if (!this->nodesExist(node, { "Level", "Exp" })) {
|
|
return 0;
|
|
}
|
|
|
|
uint16 level;
|
|
|
|
if (!this->asUInt16(node, "Level", level))
|
|
return 0;
|
|
|
|
uint64 exp;
|
|
|
|
if (!this->asUInt64(node, "Exp", exp))
|
|
return 0;
|
|
|
|
if (level == 0) {
|
|
this->invalidWarning(node["Level"], "The minimum level is 1.\n");
|
|
return 0;
|
|
}
|
|
if (level >= MAX_LEVEL) {
|
|
this->invalidWarning(node["Level"], "Homunculus level %d exceeds maximum level %d, skipping.\n", level, MAX_LEVEL);
|
|
return 0;
|
|
}
|
|
|
|
std::shared_ptr<s_homun_exp_db> homun_exp = this->find(level);
|
|
bool exists = homun_exp != nullptr;
|
|
|
|
if (!exists) {
|
|
homun_exp = std::make_shared<s_homun_exp_db>();
|
|
homun_exp->level = level;
|
|
}
|
|
|
|
homun_exp->exp = static_cast<t_exp>(exp);
|
|
|
|
if (!exists)
|
|
this->put(level, homun_exp);
|
|
|
|
return 1;
|
|
}
|
|
|
|
HomExpDatabase homun_exp_db;
|
|
|
|
/**
|
|
* Returns the experience required to level up according to the table.
|
|
* @param level: Homunculus level
|
|
* @return Experience
|
|
*/
|
|
t_exp HomExpDatabase::get_nextexp(uint16 level) {
|
|
auto next_exp = this->find(level);
|
|
if (next_exp)
|
|
return next_exp->exp;
|
|
else
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Check if the skill is a valid homunculus skill based skill range or availablity in skill db
|
|
* @param skill_id
|
|
* @return -1 if invalid skill or skill index for homunculus skill in s_homunculus::hskill
|
|
*/
|
|
short hom_skill_get_index(uint16 skill_id) {
|
|
if (!SKILL_CHK_HOMUN(skill_id))
|
|
return -1;
|
|
skill_id -= HM_SKILLBASE;
|
|
if (skill_id >= MAX_HOMUNSKILL)
|
|
return -1;
|
|
return skill_id;
|
|
}
|
|
|
|
/**
|
|
* Get homunculus view data
|
|
* @param class_ Homunculus class
|
|
* @return vd
|
|
*/
|
|
struct view_data* hom_get_viewdata(int class_)
|
|
{ //Returns the viewdata for homunculus
|
|
if (homdb_checkid(class_))
|
|
return &hom_viewdb[class_-HM_CLASS_BASE];
|
|
return nullptr;
|
|
}
|
|
|
|
/**
|
|
* Get homunculus type of specified class_
|
|
* @param class_
|
|
* @return enum homun_type
|
|
*/
|
|
enum homun_type hom_class2type(int class_) {
|
|
int mid = hom_class2mapid(class_);
|
|
if((mid&(HOM_REG|HOM_EVO)) == (HOM_REG|HOM_EVO))
|
|
return HT_EVO;
|
|
else if(mid&(HOM_REG))
|
|
return HT_REG;
|
|
else if(mid&(HOM_S))
|
|
return HT_S;
|
|
else
|
|
return HT_INVALID;
|
|
}
|
|
|
|
/**
|
|
* Get homunculus MAPID from specified class
|
|
* @param hom_class
|
|
* @return Homunculus MAPID (see enum hom_mapid)
|
|
*/
|
|
int hom_class2mapid(int hom_class)
|
|
{
|
|
switch(hom_class)
|
|
{
|
|
// Normal Homunculus
|
|
case 6001: case 6005: return MAPID_LIF;
|
|
case 6002: case 6006: return MAPID_AMISTR;
|
|
case 6003: case 6007: return MAPID_FILIR;
|
|
case 6004: case 6008: return MAPID_VANILMIRTH;
|
|
// Evolved Homunculus
|
|
case 6009: case 6013: return MAPID_LIF_E;
|
|
case 6010: case 6014: return MAPID_AMISTR_E;
|
|
case 6011: case 6015: return MAPID_FILIR_E;
|
|
case 6012: case 6016: return MAPID_VANILMIRTH_E;
|
|
// Homunculus S
|
|
case 6048: return MAPID_EIRA;
|
|
case 6049: return MAPID_BAYERI;
|
|
case 6050: return MAPID_SERA;
|
|
case 6051: return MAPID_DIETER;
|
|
case 6052: return MAPID_ELANOR;
|
|
|
|
default: return -1;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add homunculus spirit ball
|
|
* @param hd
|
|
* @param max Maximum number of spirit ball
|
|
*/
|
|
void hom_addspiritball(TBL_HOM *hd, int max) {
|
|
nullpo_retv(hd);
|
|
|
|
if (max > MAX_SPIRITBALL)
|
|
max = MAX_SPIRITBALL;
|
|
if (hd->homunculus.spiritball < 0)
|
|
hd->homunculus.spiritball = 0;
|
|
|
|
if (hd->homunculus.spiritball && hd->homunculus.spiritball >= max)
|
|
hd->homunculus.spiritball = max;
|
|
else
|
|
hd->homunculus.spiritball++;
|
|
|
|
clif_spiritball(&hd->bl);
|
|
}
|
|
|
|
/**
|
|
* Delete homunculus spirit ball
|
|
* @param hd
|
|
* @param count Number spirit ball will be deleted
|
|
* @param type 1 - Update client
|
|
*/
|
|
void hom_delspiritball(TBL_HOM *hd, int count, int type) {
|
|
nullpo_retv(hd);
|
|
|
|
if (hd->homunculus.spiritball <= 0) {
|
|
hd->homunculus.spiritball = 0;
|
|
return;
|
|
}
|
|
if (count <= 0)
|
|
return;
|
|
if (count > MAX_SPIRITBALL)
|
|
count = MAX_SPIRITBALL;
|
|
if (count > hd->homunculus.spiritball)
|
|
count = hd->homunculus.spiritball;
|
|
|
|
hd->homunculus.spiritball -= count;
|
|
if (!type)
|
|
clif_spiritball(&hd->bl);
|
|
}
|
|
|
|
/**
|
|
* Update homunculus info to its master after receiving damage
|
|
* @param hd
|
|
*/
|
|
void hom_damage(struct homun_data *hd) {
|
|
if (hd->master)
|
|
clif_hominfo(hd->master,hd,0);
|
|
}
|
|
|
|
/**
|
|
* Set homunculus's dead status
|
|
* @param hd
|
|
* @return flag &1 - Standard dead, &2 - Remove object from map, &4 - Delete object from memory
|
|
*/
|
|
int hom_dead(struct homun_data *hd)
|
|
{
|
|
//There's no intimacy penalties on death (from Tharis)
|
|
map_session_data *sd = hd->master;
|
|
|
|
clif_emotion(&hd->bl, ET_KEK);
|
|
|
|
//Delete timers when dead.
|
|
hom_hungry_timer_delete(hd);
|
|
hd->homunculus.hp = 0;
|
|
|
|
if (!sd) //unit remove map will invoke unit free
|
|
return 3;
|
|
|
|
clif_emotion(&sd->bl, ET_CRY);
|
|
|
|
#ifdef RENEWAL
|
|
status_change_end(&sd->bl, SC_HOMUN_TIME);
|
|
#endif
|
|
|
|
//Remove from map (if it has no intimacy, it is auto-removed from memory)
|
|
return 3;
|
|
}
|
|
|
|
/**
|
|
* Vaporize a character's homunculus
|
|
* @param sd
|
|
* @param flag 1: then HP needs to be 80% or above. 2: then set to morph state.
|
|
*/
|
|
int hom_vaporize(map_session_data *sd, int flag)
|
|
{
|
|
struct homun_data *hd;
|
|
|
|
nullpo_ret(sd);
|
|
|
|
hd = sd->hd;
|
|
if (!hd || hd->homunculus.vaporize)
|
|
return 0;
|
|
|
|
if (status_isdead(&hd->bl))
|
|
return 0; //Can't vaporize a dead homun.
|
|
|
|
if (flag == HOM_ST_REST && get_percentage(hd->battle_status.hp, hd->battle_status.max_hp) < 80)
|
|
return 0;
|
|
|
|
hd->regen.state.block = 3; //Block regen while vaporized.
|
|
//Delete timers when vaporized.
|
|
hom_hungry_timer_delete(hd);
|
|
hd->homunculus.vaporize = flag ? flag : HOM_ST_REST;
|
|
if (battle_config.hom_setting&HOMSET_RESET_REUSESKILL_VAPORIZED) {
|
|
hd->blockskill.clear();
|
|
hd->blockskill.shrink_to_fit();
|
|
}
|
|
clif_hominfo(sd, sd->hd, 0);
|
|
hom_save(hd);
|
|
|
|
#ifdef RENEWAL
|
|
status_change_end(&sd->bl, SC_HOMUN_TIME);
|
|
#endif
|
|
|
|
return unit_remove_map(&hd->bl, CLR_OUTSIGHT);
|
|
}
|
|
|
|
/**
|
|
* Delete a homunculus, completely "killing it".
|
|
* Emote is the emotion the master should use, send negative to disable.
|
|
* @param hd
|
|
* @param emote
|
|
*/
|
|
int hom_delete(struct homun_data *hd, int emote)
|
|
{
|
|
map_session_data *sd;
|
|
nullpo_ret(hd);
|
|
sd = hd->master;
|
|
|
|
if (!sd)
|
|
return unit_free(&hd->bl,CLR_DEAD);
|
|
|
|
if (emote >= 0)
|
|
clif_emotion(&sd->bl, emote);
|
|
|
|
//This makes it be deleted right away.
|
|
hd->homunculus.intimacy = 0;
|
|
// Send homunculus_dead to client
|
|
hd->homunculus.hp = 0;
|
|
clif_hominfo(sd, hd, 0);
|
|
return unit_remove_map(&hd->bl,CLR_OUTSIGHT);
|
|
}
|
|
|
|
/**
|
|
* Calculates homunculus skill tree for specific evolve/class.
|
|
* @param hd: Homunculus data
|
|
* @param skill_tree: Homunculus db skill tree
|
|
*/
|
|
void hom_calc_skilltree_sub(homun_data &hd, std::vector<s_homun_skill_tree_entry> &skill_tree) {
|
|
bool evolved = false;
|
|
|
|
if (hd.homunculus.class_ == hd.homunculusDB->evo_class)
|
|
evolved = true;
|
|
|
|
for (const auto &skit : skill_tree) {
|
|
uint16 skill_id = skit.id;
|
|
short idx = hom_skill_get_index(skill_id);
|
|
|
|
if (skill_id == 0 || idx == -1)
|
|
continue;
|
|
if (hd.homunculus.hskill[idx].id)
|
|
continue; //Skill already known.
|
|
|
|
bool fail = false;
|
|
|
|
if (!battle_config.skillfree) {
|
|
if (skit.intimacy > 0 && hd.homunculus.intimacy < skit.intimacy) {
|
|
continue;
|
|
}
|
|
if (skit.evolution && !evolved) {
|
|
continue;
|
|
}
|
|
if (skit.need_level > hd.homunculus.level)
|
|
continue;
|
|
for (const auto &needit : skit.need) {
|
|
if (needit.first > 0 && hom_checkskill(&hd, needit.first) < needit.second) {
|
|
fail = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (!fail)
|
|
hd.homunculus.hskill[idx].id = skill_id;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Calculates homunculus skill tree
|
|
* @param hd: Homunculus data
|
|
*/
|
|
void hom_calc_skilltree(homun_data *hd) {
|
|
nullpo_retv(hd);
|
|
|
|
std::shared_ptr<s_homunculus_db> homun_current = homunculus_db.homun_search(hd->homunculus.class_);
|
|
|
|
// If the current class can't be loaded, then for sure there's no prev_class!
|
|
if (homun_current == nullptr)
|
|
return;
|
|
|
|
std::shared_ptr<s_homunculus_db> homun = homunculus_db.homun_search(hd->homunculus.prev_class);
|
|
|
|
/* load previous homunculus form skills first. */
|
|
if (homun != nullptr) {
|
|
hom_calc_skilltree_sub(*hd, homun->skill_tree);
|
|
}
|
|
|
|
hom_calc_skilltree_sub(*hd, homun_current->skill_tree);
|
|
|
|
clif_homskillinfoblock( *hd );
|
|
}
|
|
|
|
/**
|
|
* Check skill from homunculus
|
|
* @param hd
|
|
* @param skill_id
|
|
* @return Skill Level or 0 if invalid or unlearned skill
|
|
*/
|
|
short hom_checkskill(struct homun_data *hd,uint16 skill_id)
|
|
{
|
|
short idx = hom_skill_get_index(skill_id);
|
|
if (idx < 0) // Invalid skill
|
|
return 0;
|
|
|
|
if (!hd)
|
|
return 0;
|
|
|
|
if (hd->homunculus.hskill[idx].id == skill_id)
|
|
return (hd->homunculus.hskill[idx].lv);
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Get max level for homunculus skill
|
|
* @param id Skill ID
|
|
* @param b_class
|
|
* @return Skill Level
|
|
*/
|
|
int hom_skill_tree_get_max(int skill_id, int b_class){
|
|
std::shared_ptr<s_homunculus_db> homun = homunculus_db.homun_search(b_class);
|
|
|
|
if (homun == nullptr)
|
|
return 0;
|
|
|
|
for (const auto &skit : homun->skill_tree) {
|
|
if (skit.id == skill_id)
|
|
return skit.max;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Get required minimum level to learn the skill
|
|
* @param class_ Homunculus class
|
|
* @param skill_id Homunculus skill ID
|
|
* @return Level required or 0 if invalid
|
|
**/
|
|
uint16 hom_skill_get_min_level(int class_, uint16 skill_id) {
|
|
std::shared_ptr<s_homunculus_db> homun = homunculus_db.homun_search(class_);
|
|
|
|
if (homun == nullptr)
|
|
return 0;
|
|
|
|
for (const auto &skit : homun->skill_tree) {
|
|
if (skit.id == skill_id)
|
|
return skit.need_level;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Level up an homunculus skill
|
|
* @param hd
|
|
* @param skill_id
|
|
*/
|
|
void hom_skillup(struct homun_data *hd, uint16 skill_id)
|
|
{
|
|
short idx = 0;
|
|
nullpo_retv(hd);
|
|
|
|
if (hd->homunculus.vaporize)
|
|
return;
|
|
|
|
if ((idx = hom_skill_get_index(skill_id)) < 0)
|
|
return;
|
|
if (hd->homunculus.skillpts > 0 &&
|
|
hd->homunculus.hskill[idx].id &&
|
|
hd->homunculus.hskill[idx].flag == SKILL_FLAG_PERMANENT && //Don't allow raising while you have granted skills. [Skotlex]
|
|
hd->homunculus.level >= hom_skill_get_min_level(hd->homunculus.class_, skill_id) &&
|
|
hd->homunculus.hskill[idx].lv < hom_skill_tree_get_max(skill_id, hd->homunculus.class_)
|
|
)
|
|
{
|
|
hd->homunculus.hskill[idx].lv++;
|
|
hd->homunculus.skillpts-- ;
|
|
status_calc_homunculus(hd, SCO_NONE);
|
|
|
|
clif_homskillup( *hd, skill_id );
|
|
|
|
if (hd->master) {
|
|
clif_hominfo(hd->master,hd,0);
|
|
}
|
|
|
|
clif_homskillinfoblock( *hd );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Homunculus leveled up
|
|
* @param hd
|
|
*/
|
|
int hom_levelup(struct homun_data *hd)
|
|
{
|
|
int m_class;
|
|
|
|
if ((m_class = hom_class2mapid(hd->homunculus.class_)) == -1) {
|
|
ShowError("hom_levelup: Invalid class %d.\n", hd->homunculus.class_);
|
|
return 0;
|
|
}
|
|
|
|
struct s_hom_stats *min = nullptr, *max = nullptr;
|
|
|
|
/// When homunculus is homunculus S, we check to see if we need to apply previous class stats
|
|
if(m_class&HOM_S && hd->homunculus.level < battle_config.hom_S_growth_level) {
|
|
std::shared_ptr<s_homunculus_db> homun_s_db = homunculus_db.homun_search(hd->homunculus.prev_class);
|
|
|
|
if (homun_s_db == nullptr) {
|
|
ShowError("hom_levelup: Failed to find database entry for %d.\n", hd->homunculus.prev_class);
|
|
return 0;
|
|
}
|
|
|
|
max = &homun_s_db->gmax;
|
|
min = &homun_s_db->gmin;
|
|
}
|
|
|
|
if (((m_class&HOM_REG) && hd->homunculus.level >= battle_config.hom_max_level)
|
|
|| ((m_class&HOM_S) && hd->homunculus.level >= battle_config.hom_S_max_level)
|
|
|| !hd->exp_next || hd->homunculus.exp < hd->exp_next)
|
|
return 0;
|
|
|
|
s_homunculus &hom = hd->homunculus;
|
|
|
|
hom.level++;
|
|
if (!(hom.level % 3))
|
|
hom.skillpts++; //1 skillpoint each 3 base level
|
|
|
|
hom.exp -= hd->exp_next;
|
|
hd->exp_next = homun_exp_db.get_nextexp(hom.level);
|
|
|
|
if (!max) {
|
|
max = &hd->homunculusDB->gmax;
|
|
min = &hd->homunculusDB->gmin;
|
|
}
|
|
|
|
int growth_max_hp = rnd_value(min->HP, max->HP);
|
|
int growth_max_sp = rnd_value(min->SP, max->SP);
|
|
int growth_str = rnd_value(min->str, max->str);
|
|
int growth_agi = rnd_value(min->agi, max->agi);
|
|
int growth_vit = rnd_value(min->vit, max->vit);
|
|
int growth_dex = rnd_value(min->dex, max->dex);
|
|
int growth_int = rnd_value(min->int_,max->int_);
|
|
int growth_luk = rnd_value(min->luk, max->luk);
|
|
|
|
//Aegis discards the decimals in the stat growth values!
|
|
growth_str-=growth_str%10;
|
|
growth_agi-=growth_agi%10;
|
|
growth_vit-=growth_vit%10;
|
|
growth_dex-=growth_dex%10;
|
|
growth_int-=growth_int%10;
|
|
growth_luk-=growth_luk%10;
|
|
|
|
hom.max_hp += growth_max_hp;
|
|
hom.max_sp += growth_max_sp;
|
|
hom.str += growth_str;
|
|
hom.agi += growth_agi;
|
|
hom.vit += growth_vit;
|
|
hom.dex += growth_dex;
|
|
hom.int_+= growth_int;
|
|
hom.luk += growth_luk;
|
|
|
|
APPLY_HOMUN_LEVEL_STATWEIGHT();
|
|
|
|
// Needed to update skill list for mutated homunculus so unlocked skills will appear when the needed level is reached.
|
|
status_calc_homunculus(hd,SCO_NONE);
|
|
clif_hominfo(hd->master,hd,0);
|
|
clif_homskillinfoblock( *hd );
|
|
|
|
if ( hd->master && battle_config.homunculus_show_growth ) {
|
|
char output[256] ;
|
|
sprintf(output,
|
|
"Growth: hp:%d sp:%d str(%.2f) agi(%.2f) vit(%.2f) int(%.2f) dex(%.2f) luk(%.2f) ",
|
|
growth_max_hp, growth_max_sp,
|
|
growth_str/10.0, growth_agi/10.0, growth_vit/10.0,
|
|
growth_int/10.0, growth_dex/10.0, growth_luk/10.0);
|
|
clif_messagecolor(&hd->master->bl, color_table[COLOR_LIGHT_GREEN], output, false, SELF);
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
/**
|
|
* Changes homunculus class
|
|
* @param hd: Homunculus data
|
|
* @param class_: New class
|
|
* @reutrn Fails if the class cannot be changed, otherwise true
|
|
*/
|
|
static bool hom_change_class(struct homun_data *hd, int32 class_) {
|
|
std::shared_ptr<s_homunculus_db> homun = homunculus_db.homun_search(class_);
|
|
|
|
if (homun == nullptr)
|
|
return false;
|
|
|
|
hd->homunculusDB = homun;
|
|
hd->homunculus.class_ = class_;
|
|
status_set_viewdata(&hd->bl, class_);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Make an homonculus evolve, (changing in evolution class and apply bonus)
|
|
* @param hd : homonculus datas
|
|
* @return 0:failure, 1:success
|
|
*/
|
|
int hom_evolution(struct homun_data *hd)
|
|
{
|
|
nullpo_ret(hd);
|
|
|
|
if(!hd->homunculusDB->evo_class || hd->homunculus.class_ == hd->homunculusDB->evo_class) {
|
|
clif_emotion(&hd->bl, ET_SWEAT);
|
|
return 0 ;
|
|
}
|
|
|
|
map_session_data *sd = hd->master;
|
|
|
|
if (!sd)
|
|
return 0;
|
|
|
|
if (!hom_change_class(hd, hd->homunculusDB->evo_class)) {
|
|
ShowError("hom_evolution: Can't evolve homunc from %d to %d\n", hd->homunculus.class_, hd->homunculusDB->evo_class);
|
|
return 0;
|
|
}
|
|
|
|
//Apply evolution bonuses
|
|
s_homunculus *hom = &hd->homunculus;
|
|
s_hom_stats *max = &hd->homunculusDB->emax;
|
|
s_hom_stats *min = &hd->homunculusDB->emin;
|
|
|
|
hom->max_hp += rnd_value(min->HP, max->HP);
|
|
hom->max_sp += rnd_value(min->SP, max->SP);
|
|
hom->str += 10*rnd_value(min->str, max->str);
|
|
hom->agi += 10*rnd_value(min->agi, max->agi);
|
|
hom->vit += 10*rnd_value(min->vit, max->vit);
|
|
hom->int_+= 10*rnd_value(min->int_,max->int_);
|
|
hom->dex += 10*rnd_value(min->dex, max->dex);
|
|
hom->luk += 10*rnd_value(min->luk, max->luk);
|
|
hom->intimacy = battle_config.homunculus_evo_intimacy_reset;
|
|
|
|
hom_calc_skilltree(hd);
|
|
|
|
unit_remove_map(&hd->bl, CLR_OUTSIGHT);
|
|
if (map_addblock(&hd->bl))
|
|
return 0;
|
|
|
|
clif_spawn(&hd->bl);
|
|
clif_emotion(&sd->bl, ET_BEST);
|
|
clif_specialeffect(&hd->bl,EF_HO_UP,AREA);
|
|
|
|
//status_Calc flag&1 will make current HP/SP be reloaded from hom structure
|
|
hom->hp = hd->battle_status.hp;
|
|
hom->sp = hd->battle_status.sp;
|
|
status_calc_homunculus(hd, SCO_FIRST);
|
|
|
|
if (!(battle_config.hom_setting&HOMSET_NO_INSTANT_LAND_SKILL))
|
|
skill_unit_move(&sd->hd->bl,gettick(),1); // apply land skills immediately
|
|
|
|
return 1 ;
|
|
}
|
|
|
|
/**
|
|
* Make an homonculus mutate in renewal homon
|
|
* @param hd : homonculus datas
|
|
* @param homun_id : id to make it transform into (must be a valid homon class)
|
|
* @return 0:failure, 1:sucess
|
|
*/
|
|
int hom_mutate(struct homun_data *hd, int homun_id)
|
|
{
|
|
struct s_homunculus *hom;
|
|
map_session_data *sd;
|
|
int m_class, m_id, prev_class = 0;
|
|
nullpo_ret(hd);
|
|
|
|
m_class = hom_class2mapid(hd->homunculus.class_);
|
|
m_id = hom_class2mapid(homun_id);
|
|
|
|
if( m_class == -1 || m_id == -1 || !(m_class&HOM_EVO) || !(m_id&HOM_S) ) {
|
|
clif_emotion(&hd->bl, ET_SWEAT);
|
|
return 0;
|
|
}
|
|
|
|
sd = hd->master;
|
|
if (!sd)
|
|
return 0;
|
|
|
|
prev_class = hd->homunculus.class_;
|
|
|
|
if (!hom_change_class(hd, homun_id)) {
|
|
ShowError("hom_mutate: Can't evolve homunc from %d to %d\n", hd->homunculus.class_, homun_id);
|
|
return 0;
|
|
}
|
|
|
|
hom_calc_skilltree(hd);
|
|
|
|
unit_remove_map(&hd->bl, CLR_OUTSIGHT);
|
|
if(map_addblock(&hd->bl))
|
|
return 0;
|
|
|
|
clif_spawn(&hd->bl);
|
|
clif_emotion(&sd->bl, ET_BEST);
|
|
clif_specialeffect(&hd->bl,EF_HO_UP,AREA);
|
|
|
|
//status_Calc flag&1 will make current HP/SP be reloaded from hom structure
|
|
hom = &hd->homunculus;
|
|
hom->hp = hd->battle_status.hp;
|
|
hom->sp = hd->battle_status.sp;
|
|
hom->prev_class = prev_class;
|
|
status_calc_homunculus(hd, SCO_FIRST);
|
|
|
|
if (!(battle_config.hom_setting&HOMSET_NO_INSTANT_LAND_SKILL))
|
|
skill_unit_move(&sd->hd->bl,gettick(),1); // apply land skills immediately
|
|
|
|
return 1;
|
|
}
|
|
|
|
/**
|
|
* Add homunculus exp
|
|
* @param hd
|
|
* @param exp Added EXP
|
|
*/
|
|
void hom_gainexp(struct homun_data *hd,t_exp exp)
|
|
{
|
|
int m_class;
|
|
|
|
nullpo_retv(hd);
|
|
|
|
if(hd->homunculus.vaporize)
|
|
return;
|
|
|
|
if((m_class = hom_class2mapid(hd->homunculus.class_)) == -1) {
|
|
ShowError("hom_gainexp: Invalid class %d. \n", hd->homunculus.class_);
|
|
return;
|
|
}
|
|
|
|
if( hd->exp_next == 0 ||
|
|
((m_class&HOM_REG) && hd->homunculus.level >= battle_config.hom_max_level) ||
|
|
((m_class&HOM_S) && hd->homunculus.level >= battle_config.hom_S_max_level) )
|
|
{
|
|
hd->homunculus.exp = 0;
|
|
return;
|
|
}
|
|
|
|
hd->homunculus.exp += exp;
|
|
|
|
if (hd->master && hd->homunculus.exp < hd->exp_next) {
|
|
clif_hominfo(hd->master,hd,0);
|
|
return;
|
|
}
|
|
|
|
// Do the levelup(s)
|
|
while( hd->homunculus.exp > hd->exp_next ){
|
|
// Max level reached or error
|
|
if( !hom_levelup(hd) ){
|
|
break;
|
|
}
|
|
}
|
|
|
|
if( hd->exp_next == 0 )
|
|
hd->homunculus.exp = 0 ;
|
|
|
|
clif_specialeffect(&hd->bl,EF_HO_UP,AREA);
|
|
status_calc_homunculus(hd, SCO_NONE);
|
|
status_percent_heal(&hd->bl, 100, 100);
|
|
}
|
|
|
|
/**
|
|
* Increase homunculus intimacy
|
|
* @param hd
|
|
* @param value Added intimacy
|
|
* @return New intimacy value
|
|
*/
|
|
int hom_increase_intimacy(struct homun_data * hd, unsigned int value)
|
|
{
|
|
nullpo_ret(hd);
|
|
if (battle_config.homunculus_friendly_rate != 100)
|
|
value = (value * battle_config.homunculus_friendly_rate) / 100;
|
|
|
|
if (hd->homunculus.intimacy + value <= 100000)
|
|
hd->homunculus.intimacy += value;
|
|
else
|
|
hd->homunculus.intimacy = 100000;
|
|
return hd->homunculus.intimacy;
|
|
}
|
|
|
|
/**
|
|
* Decrease homunculus intimacy
|
|
* @param hd
|
|
* @param value Reduced intimacy
|
|
* @return New intimacy value
|
|
*/
|
|
int hom_decrease_intimacy(struct homun_data * hd, unsigned int value)
|
|
{
|
|
nullpo_ret(hd);
|
|
if (hd->homunculus.intimacy >= value)
|
|
hd->homunculus.intimacy -= value;
|
|
else
|
|
hd->homunculus.intimacy = 0;
|
|
|
|
return hd->homunculus.intimacy;
|
|
}
|
|
|
|
/**
|
|
* Update homunculus info to master after healing
|
|
* @param hd
|
|
*/
|
|
void hom_heal(struct homun_data *hd) {
|
|
if (hd->master)
|
|
clif_hominfo(hd->master,hd,0);
|
|
}
|
|
|
|
/**
|
|
* Save homunculus data
|
|
* @param hd
|
|
*/
|
|
void hom_save(struct homun_data *hd)
|
|
{
|
|
// copy data that must be saved in homunculus struct ( hp / sp )
|
|
TBL_PC *sd;
|
|
|
|
nullpo_retv(hd);
|
|
|
|
sd = hd->master;
|
|
//Do not check for max_hp/max_sp caps as current could be higher to max due
|
|
//to status changes/skills (they will be capped as needed upon stat
|
|
//calculation on login)
|
|
hd->homunculus.hp = hd->battle_status.hp;
|
|
hd->homunculus.sp = hd->battle_status.sp;
|
|
intif_homunculus_requestsave(sd->status.account_id, &hd->homunculus);
|
|
}
|
|
|
|
/**
|
|
* Perform requested action from selected homunculus menu
|
|
* @param sd
|
|
* @param type
|
|
*/
|
|
void hom_menu(map_session_data *sd, int type)
|
|
{
|
|
nullpo_retv(sd);
|
|
if (sd->hd == nullptr)
|
|
return;
|
|
|
|
switch(type) {
|
|
case 0:
|
|
break;
|
|
case 1:
|
|
hom_food(sd, sd->hd);
|
|
break;
|
|
case 2:
|
|
hom_delete(sd->hd, -1);
|
|
break;
|
|
default:
|
|
ShowError("hom_menu : unknown menu choice : %d\n", type);
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Feed homunculus
|
|
* @param sd
|
|
* @param hd
|
|
*/
|
|
int hom_food(map_session_data *sd, struct homun_data *hd)
|
|
{
|
|
int i, foodID, emotion;
|
|
|
|
nullpo_retr(1,sd);
|
|
nullpo_retr(1,hd);
|
|
|
|
if (hd->homunculus.vaporize)
|
|
return 1;
|
|
|
|
foodID = hd->homunculusDB->foodID;
|
|
i = pc_search_inventory(sd,foodID);
|
|
if (i < 0) {
|
|
clif_hom_food( *sd, foodID, 0 );
|
|
return 1;
|
|
}
|
|
pc_delitem(sd,i,1,0,0,LOG_TYPE_CONSUME);
|
|
|
|
if ( hd->homunculus.hunger >= 91 ) {
|
|
hom_decrease_intimacy(hd, 50);
|
|
emotion = ET_KEK;
|
|
} else if ( hd->homunculus.hunger >= 76 ) {
|
|
hom_decrease_intimacy(hd, 5);
|
|
emotion = ET_PROFUSELY_SWEAT;
|
|
} else if ( hd->homunculus.hunger >= 26 ) {
|
|
hom_increase_intimacy(hd, 75);
|
|
emotion = ET_DELIGHT;
|
|
} else if ( hd->homunculus.hunger >= 11 ) {
|
|
hom_increase_intimacy(hd, 100);
|
|
emotion = ET_DELIGHT;
|
|
} else {
|
|
hom_increase_intimacy(hd, 50);
|
|
emotion = ET_DELIGHT;
|
|
}
|
|
|
|
hd->homunculus.hunger += 10; //dunno increase value for each food
|
|
if(hd->homunculus.hunger > 100)
|
|
hd->homunculus.hunger = 100;
|
|
|
|
log_feeding(sd, LOG_FEED_HOMUNCULUS, foodID);
|
|
|
|
clif_emotion(&hd->bl,emotion);
|
|
clif_send_homdata( *hd, SP_HUNGRY );
|
|
clif_send_homdata( *hd, SP_INTIMATE );
|
|
clif_hom_food( *sd, foodID, 1 );
|
|
|
|
// Too much food :/
|
|
if(hd->homunculus.intimacy == 0)
|
|
return hom_delete(sd->hd, ET_HUK);
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Timer to reduce hunger level
|
|
*/
|
|
static TIMER_FUNC(hom_hungry){
|
|
map_session_data *sd;
|
|
struct homun_data *hd;
|
|
|
|
sd = map_id2sd(id);
|
|
if (!sd)
|
|
return 1;
|
|
|
|
if (!sd->status.hom_id || !(hd=sd->hd))
|
|
return 1;
|
|
|
|
if (hd->hungry_timer != tid) {
|
|
ShowError("hom_hungry_timer %d != %d\n",hd->hungry_timer,tid);
|
|
return 0;
|
|
}
|
|
|
|
hd->hungry_timer = INVALID_TIMER;
|
|
|
|
hd->homunculus.hunger--;
|
|
if(hd->homunculus.hunger <= 10) {
|
|
clif_emotion(&hd->bl, ET_FRET);
|
|
} else if(hd->homunculus.hunger == 25) {
|
|
clif_emotion(&hd->bl, ET_SCRATCH);
|
|
} else if(hd->homunculus.hunger == 75) {
|
|
clif_emotion(&hd->bl, ET_OK);
|
|
}
|
|
|
|
if( battle_config.feature_homunculus_autofeed && hd->homunculus.autofeed && hd->homunculus.hunger <= battle_config.feature_homunculus_autofeed_rate ){
|
|
hom_food( sd, hd );
|
|
}
|
|
|
|
if (hd->homunculus.hunger < 0) {
|
|
hd->homunculus.hunger = 0;
|
|
// Delete the homunculus if intimacy <= 100
|
|
if (!hom_decrease_intimacy(hd, 100))
|
|
return hom_delete(hd, ET_HUK);
|
|
clif_send_homdata( *hd, SP_INTIMATE );
|
|
}
|
|
|
|
clif_send_homdata( *hd, SP_HUNGRY );
|
|
|
|
int hunger_delay = (battle_config.homunculus_starving_rate > 0 && hd->homunculus.hunger <= battle_config.homunculus_starving_rate) ? battle_config.homunculus_starving_delay : hd->homunculusDB->hungryDelay; // Every 20 seconds if hunger <= 10
|
|
|
|
hd->hungry_timer = add_timer(tick+hunger_delay,hom_hungry,sd->bl.id,0); //simple Fix albator
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Remove hungry timer from homunculus
|
|
* @param hd
|
|
*/
|
|
int hom_hungry_timer_delete(struct homun_data *hd)
|
|
{
|
|
nullpo_ret(hd);
|
|
if (hd->hungry_timer != INVALID_TIMER) {
|
|
delete_timer(hd->hungry_timer,hom_hungry);
|
|
hd->hungry_timer = INVALID_TIMER;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
/**
|
|
* Change homunculus name
|
|
*/
|
|
int hom_change_name(map_session_data *sd,char *name)
|
|
{
|
|
int i;
|
|
struct homun_data *hd;
|
|
nullpo_retr(1, sd);
|
|
|
|
hd = sd->hd;
|
|
if (!hom_is_active(hd))
|
|
return 1;
|
|
if (hd->homunculus.rename_flag && !battle_config.hom_rename)
|
|
return 1;
|
|
|
|
for (i = 0; i < NAME_LENGTH && name[i];i++) {
|
|
if (!(name[i]&0xe0) || name[i] == 0x7f)
|
|
return 1;
|
|
}
|
|
|
|
return intif_rename_hom(sd, name);
|
|
}
|
|
|
|
/**
|
|
* Acknowledge change name request from inter-server
|
|
* @param sd
|
|
* @param name
|
|
* @param flag
|
|
*/
|
|
void hom_change_name_ack(map_session_data *sd, char* name, int flag)
|
|
{
|
|
struct homun_data *hd = sd->hd;
|
|
if (!hom_is_active(hd))
|
|
return;
|
|
|
|
normalize_name(name," ");//bugreport:3032
|
|
|
|
if (!flag || name[0] == '\0') {
|
|
clif_displaymessage(sd->fd, msg_txt(sd,280)); // You cannot use this name
|
|
return;
|
|
}
|
|
safestrncpy(hd->homunculus.name,name,NAME_LENGTH);
|
|
clif_name_area(&hd->bl);
|
|
hd->homunculus.rename_flag = 1;
|
|
clif_hominfo(sd,hd,0);
|
|
}
|
|
|
|
/**
|
|
* Create homunc structure
|
|
* @param sd
|
|
* @param hom
|
|
*/
|
|
void hom_alloc(map_session_data *sd, struct s_homunculus *hom)
|
|
{
|
|
nullpo_retv(sd);
|
|
|
|
Assert((sd->status.hom_id == 0 || sd->hd == 0) || sd->hd->master == sd);
|
|
|
|
std::shared_ptr<s_homunculus_db> homun_db = homunculus_db.homun_search(hom->class_);
|
|
|
|
if (homun_db == nullptr) {
|
|
ShowError("hom_alloc: unknown class [%d] for homunculus '%s', requesting deletion.\n", hom->class_, hom->name);
|
|
sd->status.hom_id = 0;
|
|
intif_homunculus_requestdelete(hom->hom_id);
|
|
return;
|
|
}
|
|
|
|
struct homun_data *hd;
|
|
t_tick tick = gettick();
|
|
|
|
sd->hd = hd = (struct homun_data*)aCalloc(1,sizeof(struct homun_data));
|
|
hd->bl.type = BL_HOM;
|
|
hd->bl.id = npc_get_new_npc_id();
|
|
|
|
hd->master = sd;
|
|
hd->homunculusDB = homun_db;
|
|
memcpy(&hd->homunculus, hom, sizeof(struct s_homunculus));
|
|
hd->exp_next = homun_exp_db.get_nextexp(hd->homunculus.level);
|
|
|
|
status_set_viewdata(&hd->bl, hd->homunculus.class_);
|
|
status_change_init(&hd->bl);
|
|
unit_dataset(&hd->bl);
|
|
hd->ud.dir = sd->ud.dir;
|
|
|
|
// Find a random valid pos around the player
|
|
hd->bl.m = sd->bl.m;
|
|
hd->bl.x = sd->bl.x;
|
|
hd->bl.y = sd->bl.y;
|
|
unit_calc_pos(&hd->bl, sd->bl.x, sd->bl.y, sd->ud.dir);
|
|
hd->bl.x = hd->ud.to_x;
|
|
hd->bl.y = hd->ud.to_y;
|
|
|
|
// Ticks need to be initialized before adding bl to map_addiddb
|
|
hd->regen.tick.hp = tick;
|
|
hd->regen.tick.sp = tick;
|
|
|
|
map_addiddb(&hd->bl);
|
|
status_calc_homunculus(hd, SCO_FIRST);
|
|
|
|
hd->hungry_timer = INVALID_TIMER;
|
|
hd->masterteleport_timer = INVALID_TIMER;
|
|
}
|
|
|
|
/**
|
|
* Init homunculus timers
|
|
* @param hd
|
|
*/
|
|
void hom_init_timers(struct homun_data * hd)
|
|
{
|
|
if (hd->hungry_timer == INVALID_TIMER) {
|
|
int hunger_delay = (battle_config.homunculus_starving_rate > 0 && hd->homunculus.hunger <= battle_config.homunculus_starving_rate) ? battle_config.homunculus_starving_delay : hd->homunculusDB->hungryDelay; // Every 20 seconds if hunger <= 10
|
|
|
|
hd->hungry_timer = add_timer(gettick()+hunger_delay,hom_hungry,hd->master->bl.id,0);
|
|
}
|
|
hd->regen.state.block = 0; //Restore HP/SP block.
|
|
hd->masterteleport_timer = INVALID_TIMER;
|
|
}
|
|
|
|
/**
|
|
* Make a player spawn a homonculus (call)
|
|
* @param sd
|
|
* @return False:failure, True:sucess
|
|
*/
|
|
bool hom_call(map_session_data *sd)
|
|
{
|
|
struct homun_data *hd;
|
|
|
|
if (!sd->status.hom_id) //Create a new homun.
|
|
return hom_create_request(sd, HM_CLASS_BASE + rnd_value(0, 7)) ;
|
|
|
|
// If homunc not yet loaded, load it
|
|
if (!sd->hd)
|
|
return intif_homunculus_requestload(sd->status.account_id, sd->status.hom_id) > 0;
|
|
|
|
hd = sd->hd;
|
|
|
|
if (!hd->homunculus.vaporize)
|
|
return false; //Can't use this if homun wasn't vaporized.
|
|
|
|
if (hd->homunculus.vaporize == HOM_ST_MORPH)
|
|
return false; // Can't call homunculus (morph state).
|
|
|
|
hom_init_timers(hd);
|
|
hd->homunculus.vaporize = HOM_ST_ACTIVE;
|
|
if (hd->bl.prev == nullptr)
|
|
{ //Spawn him
|
|
hd->bl.x = sd->bl.x;
|
|
hd->bl.y = sd->bl.y;
|
|
hd->bl.m = sd->bl.m;
|
|
if(map_addblock(&hd->bl))
|
|
return false;
|
|
clif_spawn(&hd->bl);
|
|
clif_send_homdata( *hd, SP_ACK );
|
|
clif_hominfo(sd,hd,1);
|
|
clif_hominfo(sd,hd,0); // send this x2. dunno why, but kRO does that [blackhole89]
|
|
clif_homskillinfoblock( *hd );
|
|
if (battle_config.hom_setting&HOMSET_COPY_SPEED)
|
|
status_calc_bl(&hd->bl, { SCB_SPEED });
|
|
hom_save(hd);
|
|
} else
|
|
//Warp him to master.
|
|
unit_warp(&hd->bl,sd->bl.m, sd->bl.x, sd->bl.y,CLR_OUTSIGHT);
|
|
|
|
#ifdef RENEWAL
|
|
sc_start(&sd->bl, &sd->bl, SC_HOMUN_TIME, 100, 1, skill_get_time(AM_CALLHOMUN, 1));
|
|
#endif
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Receive homunculus data from char server
|
|
* @param account_id : owner account_id of the homon
|
|
* @param sh : homonculus data from char-serv
|
|
* @param flag : does the creation in inter-serv was a success (0:no,1:yes)
|
|
* @return 0:failure, 1:sucess
|
|
*/
|
|
int hom_recv_data(uint32 account_id, struct s_homunculus *sh, int flag)
|
|
{
|
|
map_session_data *sd;
|
|
struct homun_data *hd;
|
|
bool created = false;
|
|
|
|
sd = map_id2sd(account_id);
|
|
if(!sd)
|
|
return 0;
|
|
if (sd->status.char_id != sh->char_id)
|
|
{
|
|
if (sd->status.hom_id == sh->hom_id)
|
|
sh->char_id = sd->status.char_id; //Correct char id.
|
|
else
|
|
return 0;
|
|
}
|
|
if(!flag) { // Failed to load
|
|
sd->status.hom_id = 0;
|
|
return 0;
|
|
}
|
|
|
|
if (!sd->status.hom_id) { //Hom just created.
|
|
sd->status.hom_id = sh->hom_id;
|
|
created = true;
|
|
}
|
|
if (sd->hd) //uh? Overwrite the data.
|
|
memcpy(&sd->hd->homunculus, sh, sizeof(struct s_homunculus));
|
|
else
|
|
hom_alloc(sd, sh);
|
|
|
|
hd = sd->hd;
|
|
if (created)
|
|
status_percent_heal(&hd->bl, 100, 100);
|
|
|
|
if(hd && hd->homunculus.hp && !hd->homunculus.vaporize && hd->bl.prev == nullptr && sd->bl.prev != nullptr)
|
|
{
|
|
if(map_addblock(&hd->bl))
|
|
return 0;
|
|
clif_spawn(&hd->bl);
|
|
clif_send_homdata( *hd, SP_ACK );
|
|
clif_hominfo(sd,hd,1);
|
|
clif_hominfo(sd,hd,0); // send this x2. dunno why, but kRO does that [blackhole89]
|
|
clif_homskillinfoblock( *hd );
|
|
hom_init_timers(hd);
|
|
|
|
#ifdef RENEWAL
|
|
sc_start(&sd->bl, &sd->bl, SC_HOMUN_TIME, 100, 1, skill_get_time(AM_CALLHOMUN, 1));
|
|
#endif
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
/**
|
|
* Ask homunculus creation to char-server
|
|
* @param sd
|
|
* @param class_
|
|
* @return True:Success; False:Failed
|
|
*/
|
|
bool hom_create_request(map_session_data *sd, int class_)
|
|
{
|
|
nullpo_ret(sd);
|
|
|
|
std::shared_ptr<s_homunculus_db> homun_db = homunculus_db.homun_search(class_);
|
|
|
|
if (homun_db == nullptr)
|
|
return false;
|
|
|
|
struct s_homunculus homun;
|
|
|
|
memset(&homun, 0, sizeof(struct s_homunculus));
|
|
//Initial data
|
|
safestrncpy(homun.name, homun_db->name, NAME_LENGTH-1);
|
|
homun.class_ = class_;
|
|
homun.level = 1;
|
|
homun.hunger = 32; //32%
|
|
homun.intimacy = 2100; //21/1000
|
|
homun.char_id = sd->status.char_id;
|
|
|
|
homun.hp = 10;
|
|
|
|
s_hom_stats base = homun_db->base;
|
|
|
|
homun.max_hp = base.HP;
|
|
homun.max_sp = base.SP;
|
|
homun.str = base.str *10;
|
|
homun.agi = base.agi *10;
|
|
homun.vit = base.vit *10;
|
|
homun.int_= base.int_*10;
|
|
homun.dex = base.dex *10;
|
|
homun.luk = base.luk *10;
|
|
|
|
// Request homunculus creation
|
|
intif_homunculus_create(sd->status.account_id, &homun);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Make a player resurect an homon (player must have one)
|
|
* @param sd : player pointer
|
|
* @param per : hp percentage to revive homon
|
|
* @param x : x map coordinate
|
|
* @param y : Y map coordinate
|
|
* @return 0:failure, 1:success
|
|
*/
|
|
int hom_ressurect(map_session_data* sd, unsigned char per, short x, short y)
|
|
{
|
|
struct homun_data* hd;
|
|
nullpo_ret(sd);
|
|
|
|
if (!sd->status.hom_id)
|
|
return 0; // no homunculus
|
|
|
|
if (!sd->hd) //Load homun data;
|
|
return intif_homunculus_requestload(sd->status.account_id, sd->status.hom_id);
|
|
|
|
hd = sd->hd;
|
|
|
|
if (hd->homunculus.vaporize == HOM_ST_REST)
|
|
return 0; // vaporized homunculi need to be 'called'
|
|
|
|
if (!status_isdead(&hd->bl))
|
|
return 0; // already alive
|
|
|
|
hom_init_timers(hd);
|
|
|
|
if (!hd->bl.prev)
|
|
{ //Add it back to the map.
|
|
hd->bl.m = sd->bl.m;
|
|
hd->bl.x = x;
|
|
hd->bl.y = y;
|
|
if(map_addblock(&hd->bl))
|
|
return 0;
|
|
clif_spawn(&hd->bl);
|
|
}
|
|
|
|
hd->ud.state.blockedmove = false;
|
|
|
|
#ifdef RENEWAL
|
|
sc_start(&sd->bl, &sd->bl, SC_HOMUN_TIME, 100, 1, skill_get_time(AM_CALLHOMUN, 1));
|
|
#endif
|
|
|
|
return status_revive(&hd->bl, per, 0);
|
|
}
|
|
|
|
/**
|
|
* Revive homunculus
|
|
* @param hd
|
|
* @param hp
|
|
* @param sp
|
|
*/
|
|
void hom_revive(struct homun_data *hd, unsigned int hp, unsigned int sp)
|
|
{
|
|
map_session_data *sd = hd->master;
|
|
hd->homunculus.hp = hd->battle_status.hp;
|
|
if (!sd)
|
|
return;
|
|
clif_send_homdata( *hd, SP_ACK );
|
|
clif_hominfo(sd,hd,1);
|
|
clif_hominfo(sd,hd,0);
|
|
clif_homskillinfoblock( *hd );
|
|
|
|
if( hd->homunculus.class_ == MER_ELEANOR ){
|
|
sc_start(&hd->bl,&hd->bl, SC_STYLE_CHANGE, 100, MH_MD_FIGHTING, INFINITE_TICK);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Reset homunculus status
|
|
* @param hd
|
|
*/
|
|
void hom_reset_stats(struct homun_data *hd)
|
|
{ //Resets a homunc stats back to zero (but doesn't touches hunger or intimacy)
|
|
struct s_homunculus *hom = &hd->homunculus;
|
|
struct s_hom_stats *base = &hd->homunculusDB->base;
|
|
|
|
hom->level = 1;
|
|
hom->hp = 10;
|
|
hom->max_hp = base->HP;
|
|
hom->max_sp = base->SP;
|
|
hom->str = base->str *10;
|
|
hom->agi = base->agi *10;
|
|
hom->vit = base->vit *10;
|
|
hom->int_= base->int_*10;
|
|
hom->dex = base->dex *10;
|
|
hom->luk = base->luk *10;
|
|
hom->exp = 0;
|
|
hd->exp_next = homun_exp_db.get_nextexp(hom->level);
|
|
memset(&hd->homunculus.hskill, 0, sizeof hd->homunculus.hskill);
|
|
hd->homunculus.skillpts = 0;
|
|
}
|
|
|
|
/**
|
|
* Shuffle homunculus status
|
|
* @param hd
|
|
*/
|
|
int hom_shuffle(struct homun_data *hd)
|
|
{
|
|
map_session_data *sd;
|
|
int lv, i, skillpts;
|
|
struct s_skill b_skill[MAX_HOMUNSKILL];
|
|
|
|
if (!hom_is_active(hd))
|
|
return 0;
|
|
|
|
sd = hd->master;
|
|
lv = hd->homunculus.level;
|
|
t_exp exp = hd->homunculus.exp;
|
|
memcpy(&b_skill, &hd->homunculus.hskill, sizeof(b_skill));
|
|
skillpts = hd->homunculus.skillpts;
|
|
//Reset values to level 1.
|
|
hom_reset_stats(hd);
|
|
//Level it back up
|
|
for (i = 1; i < lv && hd->exp_next; i++){
|
|
hd->homunculus.exp += hd->exp_next;
|
|
// Should never happen, but who knows
|
|
if( !hom_levelup(hd) ) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if(hd->homunculus.class_ == hd->homunculusDB->evo_class) {
|
|
//Evolved bonuses
|
|
struct s_homunculus *hom = &hd->homunculus;
|
|
struct s_hom_stats *max = &hd->homunculusDB->emax, *min = &hd->homunculusDB->emin;
|
|
|
|
hom->max_hp += rnd_value(min->HP, max->HP);
|
|
hom->max_sp += rnd_value(min->SP, max->SP);
|
|
hom->str += 10*rnd_value(min->str, max->str);
|
|
hom->agi += 10*rnd_value(min->agi, max->agi);
|
|
hom->vit += 10*rnd_value(min->vit, max->vit);
|
|
hom->int_+= 10*rnd_value(min->int_,max->int_);
|
|
hom->dex += 10*rnd_value(min->dex, max->dex);
|
|
hom->luk += 10*rnd_value(min->luk, max->luk);
|
|
}
|
|
|
|
hd->homunculus.exp = exp;
|
|
memcpy(&hd->homunculus.hskill, &b_skill, sizeof(b_skill));
|
|
hd->homunculus.skillpts = skillpts;
|
|
clif_homskillinfoblock( *hd );
|
|
status_calc_homunculus(hd, SCO_NONE);
|
|
status_percent_heal(&hd->bl, 100, 100);
|
|
clif_specialeffect(&hd->bl,EF_HO_UP,AREA);
|
|
|
|
return 1;
|
|
}
|
|
|
|
/**
|
|
* Get minimum intimacy value of specified grade
|
|
* @param grade see enum e_homun_grade
|
|
* @return Intimacy value
|
|
**/
|
|
uint32 hom_intimacy_grade2intimacy(enum e_homun_grade grade) {
|
|
if (grade < HOMGRADE_HATE_WITH_PASSION || grade > HOMGRADE_LOYAL)
|
|
return 0;
|
|
return intimacy_grades[grade].min_value;
|
|
}
|
|
|
|
/**
|
|
* Get grade of given intimacy value
|
|
* @param intimacy
|
|
* @return Grade, see enum e_homun_grade
|
|
**/
|
|
enum e_homun_grade hom_intimacy_intimacy2grade(uint32 intimacy) {
|
|
#define CHK_HOMINTIMACY(grade) { if (intimacy >= intimacy_grades[(grade)].min_value) return (grade); }
|
|
CHK_HOMINTIMACY(HOMGRADE_LOYAL)
|
|
CHK_HOMINTIMACY(HOMGRADE_CORDIAL)
|
|
CHK_HOMINTIMACY(HOMGRADE_NEUTRAL)
|
|
CHK_HOMINTIMACY(HOMGRADE_SHY)
|
|
CHK_HOMINTIMACY(HOMGRADE_AWKWARD)
|
|
CHK_HOMINTIMACY(HOMGRADE_HATE)
|
|
#undef CHK_HOMINTIMACY
|
|
return HOMGRADE_HATE_WITH_PASSION;
|
|
}
|
|
|
|
/**
|
|
* Get initmacy grade
|
|
* @param hd
|
|
*/
|
|
uint8 hom_get_intimacy_grade(struct homun_data *hd) {
|
|
return hom_intimacy_intimacy2grade(hd->homunculus.intimacy);
|
|
}
|
|
|
|
const std::string HomunculusDatabase::getDefaultLocation() {
|
|
return std::string(db_path) + "/homunculus_db.yml";
|
|
}
|
|
|
|
bool HomunculusDatabase::parseStatusNode(const std::string &nodeName, const std::string &subNodeName, const ryml::NodeRef &node, s_hom_stats &bonus) {
|
|
uint32 value;
|
|
|
|
if (!this->asUInt32(node, nodeName, value))
|
|
return false;
|
|
|
|
if (subNodeName.compare("Hp") == 0)
|
|
bonus.HP = value;
|
|
else if (subNodeName.compare("Sp") == 0)
|
|
bonus.SP = value;
|
|
else if (subNodeName.compare("Str") == 0)
|
|
bonus.str = static_cast<uint16>(value);
|
|
else if (subNodeName.compare("Agi") == 0)
|
|
bonus.agi = static_cast<uint16>(value);
|
|
else if (subNodeName.compare("Vit") == 0)
|
|
bonus.vit = static_cast<uint16>(value);
|
|
else if (subNodeName.compare("Int") == 0)
|
|
bonus.int_ = static_cast<uint16>(value);
|
|
else if (subNodeName.compare("Dex") == 0)
|
|
bonus.dex = static_cast<uint16>(value);
|
|
else if (subNodeName.compare("Luk") == 0)
|
|
bonus.luk = static_cast<uint16>(value);
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Reads and parses an entry from the homunculus_db.
|
|
* @param node: YAML node containing the entry.
|
|
* @return count of successfully parsed rows
|
|
*/
|
|
uint64 HomunculusDatabase::parseBodyNode(const ryml::NodeRef &node) {
|
|
std::string class_name;
|
|
|
|
if (!this->asString(node, "Class", class_name))
|
|
return 0;
|
|
|
|
std::string class_name_constant = "MER_" + class_name;
|
|
int64 class_tmp;
|
|
|
|
if (!script_get_constant(class_name_constant.c_str(), &class_tmp)) {
|
|
this->invalidWarning(node["Class"], "Invalid homunculus Class \"%s\", skipping.\n", class_name.c_str());
|
|
return 0;
|
|
}
|
|
|
|
int32 class_id = static_cast<int32>(class_tmp);
|
|
std::shared_ptr<s_homunculus_db> hom = this->find(class_id);
|
|
bool exists = hom != nullptr;
|
|
|
|
if (!exists) {
|
|
if (!this->nodesExist(node, { "Name", "Status", "SkillTree" }))
|
|
return 0;
|
|
|
|
hom = std::make_shared<s_homunculus_db>();
|
|
hom->base_class = class_id;
|
|
hom->base = { 1 };
|
|
hom->gmin = {};
|
|
hom->gmax = {};
|
|
hom->emin = {};
|
|
hom->emax = {};
|
|
}
|
|
|
|
if (this->nodeExists(node, "Name")) {
|
|
std::string name;
|
|
|
|
if (!this->asString(node, "Name", name))
|
|
return 0;
|
|
|
|
safestrncpy(hom->name, name.c_str(), sizeof(hom->name));
|
|
}
|
|
|
|
if (this->nodeExists(node, "EvolutionClass")) {
|
|
std::string evo_class_name;
|
|
|
|
if (!this->asString(node, "EvolutionClass", evo_class_name))
|
|
return 0;
|
|
|
|
std::string evo_class_name_constant = "MER_" + evo_class_name;
|
|
int64 constant;
|
|
|
|
if (!script_get_constant(evo_class_name_constant.c_str(), &constant)) {
|
|
this->invalidWarning(node["EvolutionClass"], "Invalid homunculus Evolution Class %s, skipping.\n", evo_class_name.c_str());
|
|
return 0;
|
|
}
|
|
|
|
hom->evo_class = static_cast<int32>(constant);
|
|
} else {
|
|
if (!exists)
|
|
hom->evo_class = class_id;
|
|
}
|
|
|
|
if (this->nodeExists(node, "Food")) {
|
|
std::string food;
|
|
|
|
if (!this->asString(node, "Food", food))
|
|
return 0;
|
|
|
|
std::shared_ptr<item_data> item = item_db.search_aegisname(food.c_str());
|
|
|
|
if (item == nullptr) {
|
|
this->invalidWarning(node["Food"], "Invalid homunculus Food %s, skipping.\n", food.c_str());
|
|
return 0;
|
|
}
|
|
|
|
hom->foodID = item->nameid;
|
|
} else {
|
|
if (!exists)
|
|
hom->foodID = ITEMID_PET_FOOD;
|
|
}
|
|
|
|
if (this->nodeExists(node, "HungryDelay")) {
|
|
int32 delay;
|
|
|
|
if (!this->asInt32(node, "HungryDelay", delay))
|
|
return 0;
|
|
|
|
hom->hungryDelay = delay;
|
|
} else {
|
|
if (!exists)
|
|
hom->hungryDelay = 60000;
|
|
}
|
|
|
|
if (this->nodeExists(node, "Race")) {
|
|
std::string race;
|
|
|
|
if (!this->asString(node, "Race", race))
|
|
return 0;
|
|
|
|
std::string race_constant = "RC_" + race;
|
|
int64 constant;
|
|
|
|
if (!script_get_constant(race_constant.c_str(), &constant)) {
|
|
this->invalidWarning(node["Race"], "Invalid homunculus Race %s, skipping.\n", race.c_str());
|
|
return 0;
|
|
}
|
|
|
|
hom->race = static_cast<e_race>(constant);
|
|
} else {
|
|
if (!exists)
|
|
hom->race = RC_DEMIHUMAN;
|
|
}
|
|
|
|
if (this->nodeExists(node, "Element")) {
|
|
std::string element;
|
|
|
|
if (!this->asString(node, "Element", element))
|
|
return 0;
|
|
|
|
std::string element_constant = "ELE_" + element;
|
|
int64 constant;
|
|
|
|
if (!script_get_constant(element_constant.c_str(), &constant)) {
|
|
this->invalidWarning(node["Element"], "Invalid homunculus Element %s, skipping.\n", element.c_str());
|
|
return 0;
|
|
}
|
|
|
|
hom->element = static_cast<e_element>(constant);
|
|
} else {
|
|
if (!exists)
|
|
hom->element = ELE_NEUTRAL;
|
|
}
|
|
|
|
if (this->nodeExists(node, "Size")) {
|
|
std::string size;
|
|
|
|
if (!this->asString(node, "Size", size))
|
|
return 0;
|
|
|
|
std::string size_constant = "SIZE_" + size;
|
|
int64 constant;
|
|
|
|
if (!script_get_constant(size_constant.c_str(), &constant)) {
|
|
this->invalidWarning(node["Size"], "Invalid homunculus Size %s, skipping.\n", size.c_str());
|
|
return 0;
|
|
}
|
|
|
|
hom->base_size = static_cast<e_size>(constant);
|
|
} else {
|
|
if (!exists)
|
|
hom->base_size = SZ_SMALL;
|
|
}
|
|
|
|
if (this->nodeExists(node, "EvolutionSize")) {
|
|
std::string size;
|
|
|
|
if (!this->asString(node, "EvolutionSize", size))
|
|
return 0;
|
|
|
|
std::string size_constant = "SIZE_" + size;
|
|
int64 constant;
|
|
|
|
if (!script_get_constant(size_constant.c_str(), &constant)) {
|
|
this->invalidWarning(node["EvolutionSize"], "Invalid homunculus EvolutionSize %s, skipping.\n", size.c_str());
|
|
return 0;
|
|
}
|
|
|
|
hom->base_size = static_cast<e_size>(constant);
|
|
} else {
|
|
if (!exists)
|
|
hom->base_size = SZ_MEDIUM;
|
|
}
|
|
|
|
if (this->nodeExists(node, "AttackDelay")) {
|
|
uint16 aspd;
|
|
|
|
if (!this->asUInt16(node, "AttackDelay", aspd))
|
|
return 0;
|
|
|
|
if (aspd > 2000) {
|
|
this->invalidWarning(node["AttackDelay"], "Homunculus AttackDelay %hu exceeds 2000, capping.\n", aspd);
|
|
aspd = 2000;
|
|
}
|
|
|
|
hom->baseASPD = aspd;
|
|
} else {
|
|
if (!exists)
|
|
hom->baseASPD = 700;
|
|
}
|
|
|
|
if (this->nodeExists(node, "Status")) {
|
|
std::vector<std::string> stat_list = { "Hp", "Sp", "Str", "Agi", "Vit", "Int", "Dex", "Luk" };
|
|
|
|
for (const auto &statusNode : node["Status"]) {
|
|
if (!this->nodeExists(statusNode, "Type"))
|
|
return 0;
|
|
|
|
std::string stat_name;
|
|
|
|
if (!this->asString(statusNode, "Type", stat_name))
|
|
return 0;
|
|
|
|
if (!util::vector_exists(stat_list, stat_name)) {
|
|
this->invalidWarning(statusNode["Type"], "Invalid Status Type %s, skipping.\n", stat_name.c_str());
|
|
return 0;
|
|
}
|
|
|
|
if (this->nodeExists(statusNode, "Base")) {
|
|
if (!this->parseStatusNode("Base", stat_name, statusNode, hom->base)) {
|
|
return 0;
|
|
}
|
|
} else {
|
|
if (!exists) {
|
|
hom->base = { 1 };
|
|
}
|
|
}
|
|
|
|
if (this->nodeExists(statusNode, "GrowthMinimum")) {
|
|
if (!this->parseStatusNode("GrowthMinimum", stat_name, statusNode, hom->gmin)) {
|
|
return 0;
|
|
}
|
|
} else {
|
|
if (!exists) {
|
|
hom->gmin = {};
|
|
}
|
|
}
|
|
|
|
if (this->nodeExists(statusNode, "GrowthMaximum")) {
|
|
if (!this->parseStatusNode("GrowthMaximum", stat_name, statusNode, hom->gmax)) {
|
|
return 0;
|
|
}
|
|
} else {
|
|
if (!exists) {
|
|
hom->gmax = {};
|
|
}
|
|
}
|
|
|
|
if (this->nodeExists(statusNode, "EvolutionMinimum")) {
|
|
if (!this->parseStatusNode("EvolutionMinimum", stat_name, statusNode, hom->emin)) {
|
|
return 0;
|
|
}
|
|
} else {
|
|
if (!exists) {
|
|
hom->emin = {};
|
|
}
|
|
}
|
|
|
|
if (this->nodeExists(statusNode, "EvolutionMaximum")) {
|
|
if (!this->parseStatusNode("EvolutionMaximum", stat_name, statusNode, hom->emax)) {
|
|
return 0;
|
|
}
|
|
} else {
|
|
if (!exists) {
|
|
hom->emax = {};
|
|
}
|
|
}
|
|
}
|
|
|
|
// Cap values
|
|
if (hom->gmin.HP > hom->gmax.HP) {
|
|
hom->gmin.HP = hom->gmax.HP;
|
|
this->invalidWarning(node, "GrowthMinimum HP %d is greater than GrowthMaximum HP %d for homunculus %s, capping minimum to maximum.\n", hom->gmin.HP, hom->gmax.HP, class_name.c_str());
|
|
}
|
|
if (hom->gmin.SP > hom->gmax.SP) {
|
|
hom->gmin.SP = hom->gmax.SP;
|
|
this->invalidWarning(node, "GrowthMinimum SP %d is greater than GrowthMaximum SP %d for homunculus %s, capping minimum to maximum.\n", hom->gmin.SP, hom->gmax.SP, class_name.c_str());
|
|
}
|
|
if (hom->gmin.str > hom->gmax.str) {
|
|
hom->gmin.str = hom->gmax.str;
|
|
this->invalidWarning(node, "GrowthMinimum STR %d is greater than GrowthMaximum STR %d for homunculus %s, capping minimum to maximum.\n", hom->gmin.str, hom->gmax.str, class_name.c_str());
|
|
}
|
|
if (hom->gmin.agi > hom->gmax.agi) {
|
|
hom->gmin.agi = hom->gmax.agi;
|
|
this->invalidWarning(node, "GrowthMinimum AGI %d is greater than GrowthMaximum AGI %d for homunculus %s, capping minimum to maximum.\n", hom->gmin.agi, hom->gmax.agi, class_name.c_str());
|
|
}
|
|
if (hom->gmin.vit > hom->gmax.vit) {
|
|
hom->gmin.vit = hom->gmax.vit;
|
|
this->invalidWarning(node, "GrowthMinimum VIT %d is greater than GrowthMaximum VIT %d for homunculus %s, capping minimum to maximum.\n", hom->gmin.vit, hom->gmax.vit, class_name.c_str());
|
|
}
|
|
if (hom->gmin.int_ > hom->gmax.int_) {
|
|
hom->gmin.int_ = hom->gmax.int_;
|
|
this->invalidWarning(node, "GrowthMinimum INT %d is greater than GrowthMaximum INT %d for homunculus %s, capping minimum to maximum.\n", hom->gmin.int_, hom->gmax.int_, class_name.c_str());
|
|
}
|
|
if (hom->gmin.dex > hom->gmax.dex) {
|
|
hom->gmin.dex = hom->gmax.dex;
|
|
this->invalidWarning(node, "GrowthMinimum DEX %d is greater than GrowthMaximum DEX %d for homunculus %s, capping minimum to maximum.\n", hom->gmin.dex, hom->gmax.dex, class_name.c_str());
|
|
}
|
|
if (hom->gmin.luk > hom->gmax.luk) {
|
|
hom->gmin.luk = hom->gmax.luk;
|
|
this->invalidWarning(node, "GrowthMinimum LUK %d is greater than GrowthMaximum LUK %d for homunculus %s, capping minimum to maximum.\n", hom->gmin.luk, hom->gmax.luk, class_name.c_str());
|
|
}
|
|
if (hom->emin.HP > hom->emax.HP) {
|
|
hom->emin.HP = hom->emax.HP;
|
|
this->invalidWarning(node, "EvolutionMinimum HP %d is greater than EvolutionMaximum HP %d for homunculus %s, capping minimum to maximum.\n", hom->emin.HP, hom->emax.HP, class_name.c_str());
|
|
}
|
|
if (hom->emin.SP > hom->emax.SP) {
|
|
hom->emin.SP = hom->emax.SP;
|
|
this->invalidWarning(node, "EvolutionMinimum SP %d is greater than EvolutionMaximum SP %d for homunculus %s, capping minimum to maximum.\n", hom->emin.SP, hom->emax.SP, class_name.c_str());
|
|
}
|
|
if (hom->emin.str > hom->emax.str) {
|
|
hom->emin.str = hom->emax.str;
|
|
this->invalidWarning(node, "EvolutionMinimum STR %d is greater than EvolutionMaximum STR %d for homunculus %s, capping minimum to maximum.\n", hom->emin.str, hom->emax.str, class_name.c_str());
|
|
}
|
|
if (hom->emin.agi > hom->emax.agi) {
|
|
hom->emin.agi = hom->emax.agi;
|
|
this->invalidWarning(node, "EvolutionMinimum AGI %d is greater than EvolutionMaximum AGI %d for homunculus %s, capping minimum to maximum.\n", hom->emin.agi, hom->emax.agi, class_name.c_str());
|
|
}
|
|
if (hom->emin.vit > hom->emax.vit) {
|
|
hom->emin.vit = hom->emax.vit;
|
|
this->invalidWarning(node, "EvolutionMinimum VIT %d is greater than EvolutionMaximum VIT %d for homunculus %s, capping minimum to maximum.\n", hom->emin.vit, hom->emax.vit, class_name.c_str());
|
|
}
|
|
if (hom->emin.int_ > hom->emax.int_) {
|
|
hom->emin.int_ = hom->emax.int_;
|
|
this->invalidWarning(node, "EvolutionMinimum INT %d is greater than EvolutionMaximum INT %d for homunculus %s, capping minimum to maximum.\n", hom->emin.int_, hom->emax.int_, class_name.c_str());
|
|
}
|
|
if (hom->emin.dex > hom->emax.dex) {
|
|
hom->emin.dex = hom->emax.dex;
|
|
this->invalidWarning(node, "EvolutionMinimum DEX %d is greater than EvolutionMaximum DEX %d for homunculus %s, capping minimum to maximum.\n", hom->emin.dex, hom->emax.dex, class_name.c_str());
|
|
}
|
|
if (hom->emin.luk > hom->emax.luk) {
|
|
hom->emin.luk = hom->emax.luk;
|
|
this->invalidWarning(node, "EvolutionMinimum LUK %d is greater than EvolutionMaximum LUK %d for homunculus %s, capping minimum to maximum.\n", hom->emin.luk, hom->emax.luk, class_name.c_str());
|
|
}
|
|
}
|
|
|
|
if (this->nodeExists(node, "SkillTree")) {
|
|
const ryml::NodeRef &skillsNode = node["SkillTree"];
|
|
|
|
for (const ryml::NodeRef &skill : skillsNode) {
|
|
s_homun_skill_tree_entry entry;
|
|
|
|
if (this->nodeExists(skill, "Skill")) {
|
|
std::string skill_name;
|
|
|
|
if (!this->asString(skill, "Skill", skill_name))
|
|
return 0;
|
|
|
|
uint16 skill_id = skill_name2id(skill_name.c_str());
|
|
|
|
if (skill_id == 0) {
|
|
this->invalidWarning(skill["Skill"], "Invalid homunculus skill %s, skipping.\n", skill_name.c_str());
|
|
return 0;
|
|
}
|
|
|
|
if (!SKILL_CHK_HOMUN(skill_id)) {
|
|
this->invalidWarning(skill["Skill"], "Homunculus skill %s (%u) is out of the homunculus skill range [%u-%u], skipping.\n", skill_name.c_str(), skill_id, HM_SKILLBASE, HM_SKILLBASE + MAX_HOMUNSKILL - 1);
|
|
return 0;
|
|
}
|
|
|
|
entry.id = skill_id;
|
|
}
|
|
|
|
if (this->nodeExists(skill, "Clear")) {
|
|
std::vector<s_homun_skill_tree_entry>::iterator it = hom->skill_tree.begin();
|
|
bool found = false;
|
|
|
|
while (it != hom->skill_tree.end()) {
|
|
if (it->id == entry.id) { // Skill found, remove it from the skill tree.
|
|
it = hom->skill_tree.erase(it);
|
|
found = true;
|
|
} else {
|
|
it++;
|
|
}
|
|
}
|
|
|
|
if (!found)
|
|
this->invalidWarning(skill["Clear"], "Failed to remove nonexistent skill %s from homunuculus %s.\n", skill_db.find(entry.id)->name, class_name.c_str());
|
|
continue;
|
|
}
|
|
|
|
if (this->nodeExists(skill, "MaxLevel")) {
|
|
uint16 level;
|
|
|
|
if (!this->asUInt16(skill, "MaxLevel", level))
|
|
return 0;
|
|
|
|
uint16 db_level = skill_get_max(entry.id);
|
|
|
|
if (level > db_level) {
|
|
this->invalidWarning(skill["MaxLevel"], "Skill %s exceeds maximum defined skill level %d from homunuculus %s, capping.\n", skill_db.find(entry.id)->name, db_level, class_name.c_str());
|
|
level = db_level;
|
|
}
|
|
|
|
entry.max = level;
|
|
}
|
|
|
|
if (this->nodeExists(skill, "RequiredLevel")) {
|
|
uint16 level;
|
|
|
|
if (!this->asUInt16(skill, "RequiredLevel", level))
|
|
return 0;
|
|
|
|
uint16 config_max = battle_config.hom_max_level;
|
|
|
|
if ((hom_class2type(class_id) == HT_S))
|
|
config_max = battle_config.hom_S_max_level;
|
|
|
|
if (level > config_max) {
|
|
this->invalidWarning(skill["RequiredLevel"], "Homunculus Required Skill level %u exceeds maximum level %u, capping.\n", level, config_max);
|
|
level = config_max;
|
|
}
|
|
|
|
entry.need_level = level;
|
|
} else {
|
|
if (!exists)
|
|
entry.need_level = 0;
|
|
}
|
|
|
|
if (this->nodeExists(skill, "RequiredIntimacy")) {
|
|
uint16 intimacy;
|
|
|
|
if (!this->asUInt16(skill, "RequiredIntimacy", intimacy))
|
|
return 0;
|
|
|
|
if (intimacy > 1000) {
|
|
this->invalidWarning(skill["RequiredIntimacy"], "Homunculus Required Intimacy %u exceeds maximum intimacy 1000, capping.\n", intimacy);
|
|
intimacy = 1000;
|
|
}
|
|
|
|
entry.intimacy = intimacy * 100;
|
|
} else {
|
|
if (!exists)
|
|
entry.intimacy = 0;
|
|
}
|
|
|
|
if (this->nodeExists(skill, "RequireEvolution")) {
|
|
bool evo;
|
|
|
|
if (!this->asBool(skill, "RequireEvolution", evo))
|
|
return 0;
|
|
|
|
if (evo && hom->base_class == hom->evo_class) {
|
|
this->invalidWarning(skill["RequireEvolution"], "Homunculus %s does not have any evolution making skill %s unobtainable, skipping.\n", class_name.c_str(), skill_db.find(entry.id)->name);
|
|
return 0;
|
|
}
|
|
|
|
entry.evolution = evo;
|
|
} else {
|
|
if (!exists)
|
|
entry.evolution = false;
|
|
}
|
|
|
|
if (this->nodeExists(skill, "Required")) {
|
|
const ryml::NodeRef &required = skill["Required"];
|
|
|
|
for (const ryml::NodeRef &prereqskill : required) {
|
|
uint16 skill_id = 0, skill_lv = 0;
|
|
|
|
if (this->nodeExists(prereqskill, "Skill")) {
|
|
std::string skill_name;
|
|
|
|
if (!this->asString(prereqskill, "Skill", skill_name))
|
|
return 0;
|
|
|
|
skill_id = skill_name2id(skill_name.c_str());
|
|
|
|
if (skill_id == 0) {
|
|
this->invalidWarning(prereqskill["Skill"], "Invalid homunculus skill %s, skipping.\n", skill_name.c_str());
|
|
return 0;
|
|
}
|
|
|
|
if (!SKILL_CHK_HOMUN(skill_id)) {
|
|
this->invalidWarning(prereqskill["Skill"], "Homunculus skill %s (%u) is out of the homunculus skill range [%u-%u], skipping.\n", skill_name.c_str(), skill_id, HM_SKILLBASE, HM_SKILLBASE + MAX_HOMUNSKILL - 1);
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
if (this->nodeExists(prereqskill, "Clear")) {
|
|
bool found = false;
|
|
|
|
for (auto &skit : hom->skill_tree) {
|
|
std::unordered_map<uint16, uint16>::iterator it = skit.need.begin();
|
|
|
|
while (it != skit.need.end()) {
|
|
if (it->first == skill_id) { // Skill found, remove it from the skill tree.
|
|
it = skit.need.erase(it);
|
|
found = true;
|
|
} else {
|
|
it++;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!found)
|
|
this->invalidWarning(prereqskill["Clear"], "Failed to remove nonexistent prerequisite skill %s from homunuculus %s.\n", skill_db.find(skill_id)->name, class_name.c_str());
|
|
continue;
|
|
}
|
|
|
|
if (this->nodeExists(prereqskill, "Level")) {
|
|
if (!this->asUInt16(prereqskill, "Level", skill_lv))
|
|
return 0;
|
|
}
|
|
|
|
if (skill_id > 0 && skill_lv > 0)
|
|
entry.need.emplace(skill_id, skill_lv);
|
|
}
|
|
}
|
|
|
|
hom->skill_tree.push_back(entry);
|
|
}
|
|
}
|
|
|
|
if (!exists)
|
|
this->put(class_id, hom);
|
|
|
|
return 1;
|
|
}
|
|
|
|
/**
|
|
* Since evolved homunculus share a database entry, use this search.
|
|
* !TODO: Clean this up so evolved homunculus have their own entry
|
|
* @param class_: Homun class to look up
|
|
* @return Shared pointer of homunculus on success, otherwise nullptr
|
|
*/
|
|
std::shared_ptr<s_homunculus_db> HomunculusDatabase::homun_search(int32 class_) {
|
|
std::shared_ptr<s_homunculus_db> hom = homunculus_db.find(class_);
|
|
|
|
if (hom != nullptr) {
|
|
return hom;
|
|
}
|
|
|
|
for (const auto &homit : homunculus_db) {
|
|
hom = homit.second;
|
|
|
|
if (hom->evo_class == class_) {
|
|
return hom;
|
|
}
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
HomunculusDatabase homunculus_db;
|
|
|
|
void hom_reload(void){
|
|
homunculus_db.load();
|
|
homun_exp_db.reload();
|
|
}
|
|
|
|
void do_init_homunculus(void){
|
|
int class_;
|
|
|
|
homunculus_db.load();
|
|
homun_exp_db.load();
|
|
|
|
// Add homunc timer function to timer func list [Toms]
|
|
add_timer_func_list(hom_hungry, "hom_hungry");
|
|
|
|
//Stock view data for homuncs
|
|
memset(&hom_viewdb, 0, sizeof(hom_viewdb));
|
|
for (class_ = 0; class_ < ARRAYLENGTH(hom_viewdb); class_++)
|
|
hom_viewdb[class_].class_ = HM_CLASS_BASE+class_;
|
|
}
|
|
|
|
void do_final_homunculus(void) {
|
|
//Nothing todo yet
|
|
}
|