CreatureLib/src/Battling/Models/Creature.cpp

510 lines
20 KiB
C++

#include "Creature.hpp"
#include <utility>
#include "../EventHooks/EventDataClasses.hpp"
#include "../History/HistoryElements/DamageHistory.hpp"
#include "../Models/Battle.hpp"
#include "../ScriptHandling/ScriptMacros.hpp"
#include "DamageSource.hpp"
using namespace CreatureLib;
namespace CreatureLib::Battling {
Creature::Creature(const ArbUt::BorrowedPtr<const BattleLibrary>& library,
const ArbUt::BorrowedPtr<const Library::CreatureSpecies>& species,
const ArbUt::BorrowedPtr<const Library::SpeciesVariant>& variant, level_int_t level,
u32 experience, u32 uid, Library::Gender gender, u8 coloring,
ArbUt::OptionalBorrowedPtr<const Library::Item> heldItem, std::optional<std::string> nickname,
const Library::TalentIndex& talent, const std::vector<LearnedAttack*>& attacks,
bool allowedExperienceGain)
: _library(library), _species(species), _variant(variant), _level(level), _experience(experience),
_uniqueIdentifier(uid), _gender(gender), _coloring(coloring), _weight(variant->GetWeight()),
_height(variant->GetHeight()), _nickname(std::move(nickname)), _talentIndex(talent),
_hasOverridenTalent(false), _attacks(attacks), _allowedExperienceGain(allowedExperienceGain),
_heldItem(heldItem) {
_activeTalent =
_library->LoadScript(this, ScriptCategory::Talent, GetActiveTalent()->GetEffect()).TakeOwnership();
if (_activeTalent.HasValue()) {
_activeTalent.GetValue()->OnInitialize(_library.GetRaw(), GetActiveTalent()->GetParameters());
}
if (_heldItem.HasValue() && _heldItem.GetValue()->GetBattleTriggerEffect().HasValue()) {
_heldItemTriggerScript =
_library
->LoadScript(this, ScriptCategory::ItemBattleTrigger,
_heldItem.GetValue()->GetBattleTriggerEffect().GetValue()->GetEffectName())
.TakeOwnership();
}
for (auto t : _variant->GetTypes()) {
_types.push_back(t);
}
}
void Creature::ChangeSpecies(const ArbUt::BorrowedPtr<const Library::CreatureSpecies>& species,
const ArbUt::BorrowedPtr<const Library::SpeciesVariant>& variant) {
_species = species;
// If the creature is genderless, but it's new species is not, we want to set its gender
if (_gender != CreatureLib::Library::Gender::Genderless && _species->GetGenderRate() != -1) {
// If we are currently in battle, use the battle random so we can get predictable events.
if (_battleData.Battle.HasValue()) {
_gender = _species->GetRandomGender(_battleData.Battle.GetValue()->GetRandom()->GetRNG());
}
// Else create a new random.
else {
ArbUt::Random rand;
_gender = _species->GetRandomGender(rand);
}
} // Else if the new species is genderless, but the creature has a gender, make the creature genderless.
else if (_species->GetGenderRate() == -1 && _gender != CreatureLib::Library::Gender::Genderless) {
_gender = CreatureLib::Library::Gender::Genderless;
}
if (_battleData.Battle.HasValue()) {
_battleData.Battle.GetValue()->TriggerEventListener<ChangeSpeciesEvent>(this, _species);
}
ChangeVariant(variant);
}
void Battling::Creature::ChangeVariant(const ArbUt::StringView& variantName) {
auto opt = _species->TryGetVariant(variantName);
if (!opt.has_value()) {
THROW("Unknown variant: ", variantName);
}
ChangeVariant(opt.value());
}
void Creature::ChangeVariant(const ArbUt::BorrowedPtr<const Library::SpeciesVariant>& variant) {
_variant = variant;
// Set the types to the new variant.
_types.clear();
for (auto t : variant->GetTypes()) {
_types.push_back(t);
}
_weight = variant->GetWeight();
_height = variant->GetHeight();
// Grab the new active talent.
_activeTalent =
_library->LoadScript(this, ScriptCategory::Talent, GetActiveTalent()->GetEffect()).TakeOwnership();
if (_activeTalent.HasValue()) {
_activeTalent.GetValue()->OnInitialize(_library.GetRaw(), GetActiveTalent()->GetParameters());
}
// We modify the health of the creature by the change in its max health.
auto prevHealth = GetBoostedStat(CreatureLib::Library::Statistic::Health);
RecalculateFlatStats();
i32 diffHealth = GetBoostedStat(CreatureLib::Library::Statistic::Health) - prevHealth;
if (_currentHealth < static_cast<u32>(INT32_MAX) && static_cast<i32>(_currentHealth) < -diffHealth) {
_currentHealth = 0;
} else {
_currentHealth += diffHealth;
}
// TODO: consider variant specific attacks?
if (_battleData.Battle.HasValue()) {
_battleData.Battle.GetValue()->TriggerEventListener<ChangeVariantEvent>(this, _variant);
}
}
void Creature::ChangeLevelBy(i8 amount) {
auto level = _level + amount;
if (level > _library->GetSettings()->GetMaximalLevel())
level = _library->GetSettings()->GetMaximalLevel();
if (level < 1)
level = 1;
_level = level;
_experience = _library->GetGrowthRateLibrary()->CalculateExperience(_species->GetGrowthRate(), _level);
RecalculateFlatStats();
}
ArbUt::BorrowedPtr<const Library::Talent> Creature::GetActiveTalent() const {
if (_hasOverridenTalent && _overridenTalent.HasValue()) {
return _overridenTalent.GetValue();
}
return _variant->GetTalent(_talentIndex);
}
void Creature::SetBattleData(const ArbUt::BorrowedPtr<Battle>& battle, const ArbUt::BorrowedPtr<BattleSide>& side) {
_battleData.Battle = battle.GetRaw();
_battleData.Side = side.GetRaw();
this->ResetActiveScripts();
}
void Creature::ClearBattleData() noexcept {
_battleData = {};
_battleData.SeenOpponents = {};
ResetActiveScripts();
_weight = _variant->GetWeight();
_height = _variant->GetHeight();
_types.clear();
for (auto t : _variant->GetTypes()) {
_types.push_back(t);
}
}
void Battling::Creature::SetOnBattleField(bool value) {
_battleData.OnBattleField = value;
if (!value) {
ResetActiveScripts();
_weight = _variant->GetWeight();
_height = _variant->GetHeight();
}
}
bool Creature::ChangeStatBoost(Library::Statistic stat, i8 diffAmount, bool selfInflicted) {
bool preventStatChange = false;
HOOK(PreventStatBoostChange, this, this, stat, diffAmount, selfInflicted, &preventStatChange);
if (preventStatChange) {
return false;
}
HOOK(ModifyStatBoostChange, this, this, stat, &diffAmount);
bool changed = false;
auto oldValue = this->_statBoost.GetStat(stat);
if (diffAmount > 0) {
changed = this->_statBoost.IncreaseStatBy(stat, diffAmount);
} else if (diffAmount < 0) {
changed = this->_statBoost.DecreaseStatBy(stat, -diffAmount);
}
if (changed) {
if (this->GetBattle().HasValue()) {
auto newValue = this->_statBoost.GetStat(stat);
this->GetBattle().GetValue()->TriggerEventListener<ChangeStatBoostEvent>(this, stat, oldValue,
newValue);
}
this->RecalculateBoostedStat(stat);
}
return changed;
}
void Creature::RecalculateFlatStats() {
const auto& statCalc = this->_library->GetStatCalculator();
this->_flatStats = statCalc->CalculateFlatStats(this);
RecalculateBoostedStats();
}
void Creature::RecalculateBoostedStats() {
this->_boostedStats = this->_library->GetStatCalculator()->CalculateFlatStats(this);
}
void Creature::RecalculateFlatStat(Library::Statistic stat) {
auto s = this->_library->GetStatCalculator()->CalculateFlatStat(this, stat);
this->_flatStats.SetStat(stat, s);
RecalculateBoostedStat(stat);
}
void Creature::RecalculateBoostedStat(Library::Statistic stat) {
auto s = this->_library->GetStatCalculator()->CalculateBoostedStat(this, stat);
this->_boostedStats.SetStat(stat, s);
}
bool Creature::IsUsable() const noexcept { return !this->IsFainted(); }
bool Creature::IsFainted() const noexcept { return this->_currentHealth == 0; }
void Creature::OnFaint(DamageSource damageSource) {
EnsureNotNull(_battleData.Battle)
EnsureNotNull(_battleData.Side)
if (_battleData.Battle.HasValue()) {
_battleData.Battle.GetValue()->TriggerEventListener<FaintEvent>(this);
HOOK(OnFaint, this, this, damageSource);
HOOK(OnRemove, this);
}
_library->GetExperienceLibrary()->HandleExperienceGain(this, _battleData.SeenOpponents);
if (_battleData.Battle.HasValue() && _battleData.Side.HasValue()) {
auto sideIndex = _battleData.Side.GetValue()->GetCreatureIndex(this);
if (!_battleData.Battle.GetValue()->CanSlotBeFilled(_battleData.Side.GetValue()->GetSideIndex(),
sideIndex)) {
_battleData.Side.GetValue()->MarkSlotAsUnfillable(this);
}
_battleData.Battle.GetValue()->ValidateBattleState();
}
}
void Creature::Damage(u32 damage, DamageSource source) {
if (damage > _currentHealth) {
damage = _currentHealth;
}
if (damage == 0)
return;
auto newHealth = _currentHealth - damage;
auto battle = this->GetBattle();
if (battle.HasValue()) {
battle.GetValue()->TriggerEventListener<DamageEvent>(this, source, _currentHealth, newHealth);
battle.GetValue()->RegisterHistoryElement<DamageHistory>(this, damage, source);
HOOK(OnDamage, this, this, source, _currentHealth, newHealth);
}
_currentHealth = newHealth;
if (IsFainted() && damage > 0 && battle != nullptr) {
OnFaint(source);
}
}
void Creature::Heal(u32 amount, bool canRevive) {
if (_currentHealth == 0 && !canRevive) {
return;
}
if (amount > GetMaxHealth() - _currentHealth) {
amount = GetMaxHealth() - _currentHealth;
}
// HOOK: On Heal
auto newHealth = _currentHealth + amount;
auto battle = this->GetBattle();
if (battle.HasValue()) {
battle.GetValue()->TriggerEventListener<HealEvent>(this, _currentHealth, newHealth);
}
_currentHealth = newHealth;
}
void Creature::RestoreAllAttackUses() noexcept {
for (auto& a : _attacks) {
if (a != nullptr) {
a->RestoreAllUses();
}
}
}
void Creature::OverrideActiveTalent(const ArbUt::StringView& talent) {
_hasOverridenTalent = true;
if (_activeTalent.HasValue()) {
_activeTalent.GetValue()->OnRemove();
_activeTalent = this->_library->LoadScript(this, ScriptCategory::Talent, talent).TakeOwnership();
}
_overridenTalent = _library->GetStaticLib()->GetTalentLibrary()->Get(talent);
}
const std::vector<u8>& Creature::GetTypes() const noexcept { return _types; }
bool Creature::HasType(u8 type) const noexcept {
return std::find(_types.begin(), _types.end(), type) != _types.end();
}
void Creature::SetType(u8 index, u8 type) noexcept {
if (_types.size() > index) {
_types[index] = type;
} else {
_types.push_back(type);
}
}
size_t Creature::ScriptCount() const {
auto c = 3;
if (_battleData.Side.HasValue()) {
c += _battleData.Side.GetValue()->ScriptCount();
}
return c;
}
void Creature::GetActiveScripts(ArbUt::List<ScriptWrapper>& scripts) {
GetOwnScripts(scripts);
if (_battleData.Side.HasValue()) {
_battleData.Side.GetValue()->GetActiveScripts(scripts);
}
}
void Creature::GetOwnScripts(ArbUt::List<ScriptWrapper>& scripts) {
scripts.Append(ScriptWrapper::FromScript(&_heldItemTriggerScript));
scripts.Append(ScriptWrapper::FromScript(&_activeTalent));
scripts.Append(ScriptWrapper::FromScript(&_status));
scripts.Append(ScriptWrapper::FromSet(&_volatile));
}
void Creature::ClearVolatileScripts() { _volatile.Clear(); }
void Creature::AddExperience(u32 amount) {
auto maxLevel = _library->GetSettings()->GetMaximalLevel();
if (_level >= maxLevel) {
return;
}
auto exp = _experience + amount;
auto level = _library->GetGrowthRateLibrary()->CalculateLevel(this->GetSpecies()->GetGrowthRate(), exp);
if (level >= maxLevel) {
exp = _library->GetGrowthRateLibrary()->CalculateExperience(this->GetSpecies()->GetGrowthRate(), maxLevel);
}
if (_battleData.Battle.HasValue()) {
_battleData.Battle.GetValue()->TriggerEventListener<ExperienceGainEvent>(this, _experience, exp);
}
_experience = exp;
_level = level;
}
ArbUt::OptionalBorrowedPtr<const Library::CreatureSpecies> Creature::GetDisplaySpecies() const noexcept {
auto species = _displaySpecies;
if (!species.HasValue())
species = _species.GetRaw();
return species;
}
ArbUt::OptionalBorrowedPtr<const Library::SpeciesVariant> Creature::GetDisplayVariant() const noexcept {
auto variant = _displayVariant;
if (!variant.HasValue())
variant = _variant.GetRaw();
return variant;
}
void Creature::SetHeldItem(const ArbUt::StringView& itemName) {
if (itemName == ""_cnc) {
_heldItem = {};
_heldItemTriggerScript = {};
} else {
auto v = _library->GetItemLibrary()->TryGet(itemName);
if (!v.has_value()) {
THROW("Item not found '", itemName.c_str(), "'.");
}
_heldItem = v.value();
}
}
void Creature::SetHeldItemByHash(u32 itemNameHash) {
if (itemNameHash == ArbUt::StringView::CalculateHash("")) {
_heldItem = {};
_heldItemTriggerScript = {};
} else {
auto v = _library->GetItemLibrary()->TryGetByHash(itemNameHash);
if (!v.has_value()) {
THROW("Item not found.");
}
_heldItem = v.value();
}
}
bool Creature::ConsumeHeldItem() {
if (!_heldItem.HasValue()) {
return false;
}
auto scriptOpt = _library->GetScriptResolver()->LoadItemScript(_heldItem.GetValue());
if (!scriptOpt.HasValue()) {
return false;
}
auto script = scriptOpt.GetValue();
auto isCreatureUseItem = script->IsCreatureUseItem();
if (isCreatureUseItem) {
if (!script->IsUseValidForCreature(this)) {
return false;
}
script->OnCreatureUse(this, true);
} else {
script->OnUse(_battleData.Battle.GetValue());
}
auto item = _heldItem;
SetHeldItem(""_cnc);
if (_battleData.Battle.HasValue()) {
HOOK(OnAfterHeldItemConsume, this, this, item.GetValue());
}
return true;
}
BattleScript* Creature::AddVolatileScript(const ArbUt::StringView& name) {
auto script = _volatile.Get(name);
if (script.HasValue()) {
script.GetValue()->Stack();
return script.GetValue();
}
script = this->_library->LoadScript(this, ScriptCategory::Creature, name).TakeOwnership();
if (!script.HasValue()) {
THROW("Invalid volatile script requested for creature: '", name.c_str(), "'.");
}
return _volatile.Add(script.GetValue());
}
BattleScript* Creature::AddVolatileScript(BattleScript* script) { return _volatile.Add(script); }
void Creature::RemoveVolatileScript(const ArbUt::BasicStringView& name) { _volatile.Remove(name); }
void Creature::RemoveVolatileScript(BattleScript* script) { _volatile.Remove(script->GetName()); }
bool Creature::HasVolatileScript(const ArbUt::BasicStringView& name) const { return _volatile.Has(name); }
void Creature::AddAttack(LearnedAttack* attack) {
for (size_t i = 0; i < _attacks.Count(); i++) {
if (!_attacks[i].HasValue()) {
_attacks.Set(i, attack);
return;
}
}
if (_attacks.Count() < _library->GetStaticLib()->GetSettings()->GetMaximalAttacks()) {
_attacks.Append(attack);
return;
}
THROW("Can't add attack. The creature already has the maximum amount of attacks.");
}
u8 Creature::GetAvailableAttackSlot() const noexcept {
for (u8 i = 0; i < (u8)_attacks.Count(); i++) {
if (_attacks[i] == nullptr) {
return i;
}
}
if (_attacks.Count() < _library->GetStaticLib()->GetSettings()->GetMaximalAttacks()) {
return _attacks.Count();
}
return -1;
}
void Creature::ReplaceAttack(size_t index, LearnedAttack* attack) {
if (_attacks.Count() <= index) {
if (_attacks.Count() < _library->GetStaticLib()->GetSettings()->GetMaximalAttacks()) {
_attacks.Append(attack);
}
THROW("Can't replace attack at index ", index, ". Number of attacks is ", _attacks.Count(), ".");
}
_attacks.Set(index, attack);
}
Creature* Creature::Clone() const {
auto attacks = std::vector<LearnedAttack*>(_attacks.Count());
auto i = 0;
for (auto* attack : _attacks) {
if (attack == nullptr) {
attacks[i++] = nullptr;
} else {
attacks[i++] = attack->Clone();
}
}
auto* c = new Creature(_library, _species, _variant, _level, _experience, _uniqueIdentifier, _gender, _coloring,
_heldItem, _nickname, _talentIndex, attacks, _allowedExperienceGain);
c->_displaySpecies = _displaySpecies;
c->_displayVariant = _displayVariant;
c->_currentHealth = _currentHealth;
c->_statBoost = _statBoost;
c->_flatStats = _flatStats;
c->_boostedStats = _boostedStats;
c->_battleData.Battle = _battleData.Battle;
c->_battleData.Side = _battleData.Side;
c->_battleData.OnBattleField = _battleData.OnBattleField;
c->_battleData.Index = _battleData.Index;
if (_activeTalent.HasValue()) {
c->_activeTalent = _activeTalent.GetValue()->Clone(c);
}
c->_hasOverridenTalent = _hasOverridenTalent;
c->_overridenTalent = _overridenTalent;
if (_status.HasValue()) {
c->_status = _status.GetValue()->Clone(c);
}
_volatile.Clone(c, c->_volatile);
c->_types = std::vector<u8>(_types);
c->RecalculateFlatStats();
return c;
}
void Creature::SetStatus(const ArbUt::StringView& name) {
if (_status.HasValue()) {
_status.GetValue()->OnRemove();
}
_status = _library->LoadScript(this, ScriptCategory::Status, name).TakeOwnership();
if (_battleData.Battle.HasValue()) {
_battleData.Battle.GetValue()->TriggerEventListener<StatusChangeEvent>(this, name);
}
}
void Creature::ClearStatus() {
if (!_status.HasValue()) {
return;
}
_status.GetValue()->OnRemove();
_status = nullptr;
if (_battleData.Battle.HasValue()) {
_battleData.Battle.GetValue()->TriggerEventListener<StatusChangeEvent>(this, ""_cnc);
}
}
Creature::~Creature() {
if (_battleData.OnBattleField && _battleData.Side.HasValue()) {
_battleData.Side.GetValue()->ForceClearCreature(_battleData.Index.GetCreatureIndex());
}
}
}