// MOBClass.cpp #include "Net7.h" #include "ObjectClass.h" #include "MOBClass.h" #include "PlayerClass.h" #include "ObjectManager.h" #include "opcodes.h" #include "ServerManager.h" #include "MOBDatabase.h" #include "PlayerManager.h" #define SHIELD 1 #define HULL 2 //MOB DPS average for each level step, 1.1f = level 0, 14933 = level 200 (CL 66). float MobDPS[] = { 1.1f, 3.2f, 7.2f, 14.7f, 24.0f, 41.0f, 74.0f, 131.0f, 198.0f, 310.0f, 448.0f, 648.0f, 915.0f, 1436.0f, 2080.0f, 2912.0f, 3965.0f, 5017.0f, 6347.0f, 8028.0f, 14933.0f }; //Damage Absorbtion Capacity - average for MOBs float MobDAC[] = { 29, 57, 160, 377, 864, 1365, 2845, 5203, 10146, 14171, 27030, 36864, 62797, 87172, 164296, 220191, 350057, 466263, 619402, 820852, 1085421 }; MOB::MOB(long object_id) : Object (object_id) { m_Type = OT_MOB; m_CreateInfo.Type = 0; m_MOBVisibleList.clear(); m_BehaviourList.clear(); m_Respawn_tick = 0; m_MovementID = 0; m_Velocity = 0; m_LastUpdate = GetNet7TickCount(); m_YInput = 0; m_ZInput = 0; m_LastAttackTime = 0; m_Position_info.Orientation[0] = 0.0f; m_Position_info.Orientation[1] = 0.0f; m_Position_info.Orientation[2] = 0.0f; m_Position_info.Orientation[3] = 1.0f; m_ObjectRadius = 250.0f; //default ship size m_Signature = 3000.0f; m_ScanSkill = 0; m_SpawnGroup = 0; m_Attacking = false; m_MOB_Data = (0); //TODO: set this up properly somewhere m_DamageType = 1; // Set up gameID m_ShipIndex.SetGameID(object_id); m_ArrivalTime = 0; m_Behaviour = DRIFT; m_Destination = (0); m_ShieldFXSent = 0; m_WeaponFX = 0; m_WeaponDamageDelay = 0; m_WeaponReloadTime = 0; memset (&m_HuntPosition, 0, sizeof(m_HuntPosition)); memset (&m_DestinationPos, 0, sizeof(m_DestinationPos)); memset (&m_RangeList, 0, sizeof(m_RangeList)); memset (&m_PlayerVisibleList, 0, sizeof(m_PlayerVisibleList)); memset (&m_ClickedList, 0, sizeof(m_ClickedList)); memset (&m_HateList, 0, sizeof(m_HateList)); m_DamageTime = 0; } MOB::~MOB() { // TODO: destroy everything } void MOB::SetMOBType(short type) { MOBData *mob_data = g_ServerMgr->MOBList()->GetMOBData(type); m_MOB_Data = mob_data; if (type == 0) { //crawl MOB DB until we find a matching entry mob_data = g_ServerMgr->MOBList()->GetMOBDataFromBasset(this->BaseAsset()); if (mob_data) type = mob_data->m_Type; } if (!mob_data) { return; } m_MOB_Data = mob_data; // Set mob info SetName(mob_data->m_Name); SetBasset(mob_data->m_Basset); SetLevel(type); SetHSV(mob_data->m_HSV[0], mob_data->m_HSV[1], mob_data->m_HSV[2]); SetScale(mob_data->m_Scale); //LogMessage("Adding MOB: %s\n", Name()); m_MOB_Type = type; // Set aux data m_ShipIndex.SetName(m_Name); m_ShipIndex.SetCombatLevel(mob_data->m_Level); SetFactionID(mob_data->m_FactionID); } void MOB::SetLevel(short type) { if (!m_MOB_Data) { return; } short overall_level = m_MOB_Data->m_Level * 3; short index = (overall_level / 10); float fraction = (float)(overall_level) / 10.0f - (float) index; //TODO: calculate this from MOB items when in place float lower_bound = MobDPS[index]; float upper_bound = MobDPS[index+1]; m_WeaponDPS = lower_bound + (upper_bound - lower_bound)*fraction; //work out MOB Damage absorbtion Capacity from table lower_bound = MobDAC[index]; upper_bound = MobDAC[index+1]; m_StartShieldLevel = lower_bound + (upper_bound - lower_bound)*fraction; if (m_MOB_Data->m_Type == STRUCTURAL || m_MOB_Data->m_Type == CYBERNETIC) { //split the DAC between hull and shield m_StartHullLevel = m_StartShieldLevel * 0.3f; m_StartShieldLevel -= m_StartHullLevel * 0.9f; } else { m_StartHullLevel = 0; } m_ShieldLevel = m_StartShieldLevel; m_HullLevel = m_StartHullLevel; m_Level = m_MOB_Data->m_Level; // Set up hull points m_ShipIndex.SetMaxHullPoints(m_HullLevel); m_ShipIndex.SetHullPoints(m_HullLevel); //Setup Shields m_ShipIndex.Shield.SetStartValue(m_StartShieldLevel); m_ShipIndex.Shield.SetChangePerTick(0); m_ShipIndex.Shield.SetEndTime(GetNet7TickCount()); m_ShipIndex.SetMaxShield(m_ShieldLevel); m_ScanRange = 2500.0f + (150.0f*m_MOB_Data->m_Level); //TODO: Add this entry into MOB database, and get it from there. m_WeaponFX = 0; m_WeaponDamageDelay = 0; m_WeaponReloadTime = 0; //does this mob have any weapons? //find weapon. bool found = false; for (u32 i = 0; i < m_MOB_Data->m_Loot.size(); i++) { ItemBase * equip = g_ItemBaseMgr->GetItem(m_MOB_Data->m_Loot[i]->item_base_id); if (equip) { _Item mob_item; memset(&mob_item,0,sizeof(_Item)); mob_item.ItemTemplateID = equip->ItemTemplateID(); mob_item.Quality = 1.0f; //we could adjust quality of MOB items here mob_item.Structure = 1.0f; mob_item.StackCount = 1; ItemInstance instance = equip->GetItemInstance(mob_item.InstanceInfo); g_ItemBaseMgr->QualityCalculator(&mob_item); switch (equip->SubCategory()) { case 101: case 102: //todo - use ammo FX if available m_WeaponDamageDelay = 1; m_WeaponFX = equip->UseEffect(); m_WeaponReloadTime = 2; found = true; break; case 100: m_WeaponFX = equip->UseEffect(); m_WeaponDamageDelay = 0; m_WeaponReloadTime = instance.WeaponReload; m_WeaponDPS = (float)instance.WeaponDamage / m_WeaponReloadTime; //LogMessage("Weapon DPS = %.2f. Reload time = %.2f\n", m_WeaponDPS, m_WeaponReloadTime); found = true; break; }; if (found) break; } } if (!found) { m_WeaponReloadTime = 4; switch (m_MOB_Data->m_Type) { case STRUCTURAL: m_WeaponFX = 33; m_WeaponDamageDelay = 0; break; case CYBERNETIC: m_WeaponFX = 34; m_WeaponDamageDelay = 0; break; case ENERGY: m_WeaponFX = 35; m_WeaponDamageDelay = 0; break; case ROCK_BASED: m_WeaponFX = 38; m_WeaponDamageDelay = 1; break; case ORGANIC_GREEN: m_WeaponFX = 41; m_WeaponDamageDelay = 1; break; default: m_WeaponFX = 43; m_WeaponDamageDelay = 1; break; }; } } /////////////////////////////////////////////////// // Range List Handling // // These methods handle adding to and removing from // the player range lists. // m_RangeList functions in exactly the same way as the Player list, it's a list of players who can see this MOB. // Used only for updating the MOB status to players // // m_MOBVisibleList on the other hand is a list of players and MOBs that this MOB can see // Used for deciding which target to attack, and target interactions. void MOB::UpdateObjectVisibilityList() { Player *p = (0); u32 * sector_list = m_ObjectMgr->GetSectorList(); ObjectList * mob_list = m_ObjectMgr->GetMOBList(); bool in_range; bool in_mob_range; float range; if (RespawnTick() != 0) { return; } //TODO: // - Add grid system to further reduce checking while (g_PlayerMgr->GetNextPlayerOnList(p, sector_list)) { if (p && p->Active()) { //get range and find out if this player is already on the list range = RangeFrom(p->Position()); in_range = g_PlayerMgr->GetIndex(p, m_RangeList); in_mob_range = g_PlayerMgr->GetIndex(p, m_PlayerVisibleList); if (in_range) { //see if this MOB has gone out of player 'p' scan range if (range > (float)(p->ShipIndex()->CurrentStats.GetScanRange() + m_Signature + 200.0f)) { g_PlayerMgr->UnSetIndex(p, m_RangeList); p->RemoveObject(GameID()); } } else { //see if this MOB has come into player 'p' scan range if (range < (float)(p->ShipIndex()->CurrentStats.GetScanRange() + m_Signature) && p->InSpace()) { g_PlayerMgr->SetIndex(p, m_RangeList); g_PlayerMgr->UnSetIndex(p, m_ClickedList); SendMOBData(p); //send this MOB to 'p' } } //this checks to see who is visible to the MOB if (in_mob_range) { //see if 'p' has gone out of MOB scan range if (range > (float)(m_ScanRange + p->ShipIndex()->CurrentStats.GetVisibility() + 200.0f)) { g_PlayerMgr->UnSetIndex(p, m_PlayerVisibleList); if (m_HostilityTargetID == p->GameID()) { LostTarget(p); //LogMessage("%s no longer hostile to %s\n", Name(), p->Name()); } } else if (range < 1000.0f) //you might be too close to a mob { //see if MOB fires a warning shot off CheckWarningShots(p); } } else { //see if 'p' has come into MOB scan range if (range < (float)(m_ScanRange + p->ShipIndex()->CurrentStats.GetVisibility()) && p->InSpace()) { g_PlayerMgr->SetIndex(p, m_PlayerVisibleList); } } } } //now scan other MOBs in sector, to see if any have come into our range //Only for turrets for now ObjectList::iterator itrOList; Object *o; if (m_DefaultBehaviour == TURRET && m_ObjectMgr->GetSectorManager()->GetSectorID() == 1052) { for (itrOList = mob_list->begin(); itrOList < mob_list->end(); ++itrOList) { o = (*itrOList); if ( o != this && o->Active() && o->ObjectType() == OT_MOB) { range = o->RangeFrom(Position()); in_mob_range = ObjectInRangeList(&m_MOBVisibleList, o); if (in_mob_range) { //see if MOB 'o' has gone out of this MOB weapon range if (range > (float)(7500.0f + 200.0f) ) { RemoveObjectFromRangeList(&m_MOBVisibleList, o); if (o->GameID() == HostilityTarget()) SetHostilityTarget(0); } } else { //see if MOB 'o' has come into this MOB weapon range if (range < (float)(7500.0f) ) { AddObjectToRangeList(&m_MOBVisibleList, o); //now lock onto this target if we don't already have one LockMOBTarget(o->GameID()); } } } } } } /* aggro: 0 - never 1 - only if shot 2 - if inside 3 - if visible 4 - always */ void MOB::CheckWarningShots(Player *p) { if (p->GetFactionStanding(this) <= 0.0f) //MOB only spontaneously attacks if faction standing 0 or less { if (m_MOB_Data->m_Agressiveness > 0 && m_MOB_Data->m_Agressiveness < 5) { long hate = 0; long roll = rand()%100; switch (m_MOB_Data->m_Agressiveness) { case 1: //this MOB not likely to fire warning shot, if it does then not too many, just a 'back off'. if (roll > 97) hate = 1; break; case 2: if (roll > 95) hate = 5; break; case 3: if (roll > 93) hate = 20; break; case 4: if (roll > 90) hate = 100; break; default: break; } if (hate != 0) { AddHate(p->GameID(), hate); } } } } void MOB::UpdateMOB(u32 current_tick, bool handle_attacks) { if (RespawnTick() > 0 && RespawnTick() < current_tick) { LogMessage("Re-Spawning %s\n", Name()); SetRespawnTick(0); //re-spawn mob SetActive(true); SetLastUpdateNow(); OnRespawn(); } else if (RespawnTick() == 0) { CalcNewPosition(current_tick); UpdateObjectVisibilityList(); UpdateObject(); IncrementMovementID(5); if (MovementID() % UpdateRate() == 0) { SendLocationAndSpeed(false); } if (MovementID() % 100 == 0) { HandleMovementChanges(); } if (handle_attacks) { if (HostilityTarget() == 0) { ChooseTarget(); } else { HandleAttack(); } } } } bool MOB::ObjectInRangeList(ObjectList *object_list, Object *obj) { bool in_range = false; ObjectList::iterator itrOList; m_Mutex.Lock(); for (itrOList = object_list->begin(); itrOList < object_list->end(); ++itrOList) { if (obj == (*itrOList)) { in_range = true; break; } } m_Mutex.Unlock(); return in_range; } void MOB::RemoveObjectFromRangeList(ObjectList *object_list, Object *obj) { ObjectList::iterator itrOList; m_Mutex.Lock(); for (itrOList = object_list->begin(); itrOList < object_list->end(); ++itrOList) { if (obj == (*itrOList)) { object_list->erase(itrOList); //LogMessage("%s no longer sees %s\n", Name(), obj->Name()); break; } } m_Mutex.Unlock(); } void MOB::AddObjectToRangeList(ObjectList *object_list, Object *obj) { m_Mutex.Lock(); object_list->push_back(obj); //LogMessage("%s now sees %s\n", Name(), obj->Name()); m_Mutex.Unlock(); } void MOB::SetClickedBy(Player *p) { g_PlayerMgr->SetIndex(p, m_ClickedList); } void MOB::RemovePlayerFromRangeLists(Player *p) { //RemovePlayerFromRangeList(p); //RemoveObjectFromRangeList(&m_MOBVisibleList, p); if (p) { long game_id = p->GameID(); if (game_id > 0) { g_PlayerMgr->UnSetIndex(p, m_RangeList); g_PlayerMgr->UnSetIndex(p, m_PlayerVisibleList); RemoveHate(game_id); } } } void MOB::AddHate(int GameID, int Hate) { // Add to hate list if required m_Mutex.Lock(); long lowest_hate = 0; long lowest_hate_val = m_HateList[0].hate; bool inserted = false; //find spot on hate list for (int i = 0; i < HATE_SIZE; i++) { if (m_HateList[i].hate < lowest_hate_val) //track lowest hate value { lowest_hate = i; lowest_hate_val = m_HateList[i].hate; } if (m_HateList[i].GameID == GameID || m_HateList[i].hate == 0) { m_HateList[i].hate += Hate; m_HateList[i].GameID = GameID; inserted = true; break; } } if (!inserted && Hate > lowest_hate_val) //no empty slots, but player qualifies for shit list { m_HateList[lowest_hate].GameID = GameID; m_HateList[lowest_hate].hate = Hate; } m_Mutex.Unlock(); } void MOB::SubtractHate(int GameID, int Hate) { for (int i = 0; i < HATE_SIZE; i++) { if (m_HateList[i].GameID == GameID) { m_HateList[i].hate -= Hate; break; } } } void MOB::RemoveHate(int GameID) { for (int i = 0; i < HATE_SIZE; i++) { if (m_HateList[i].GameID == GameID) { m_HateList[i].GameID = 0; m_HateList[i].hate = 0; break; } } } long MOB::GetMaxHateID() { long max_hate = 0; long game_id = 0; m_Mutex.Lock(); for (int i = 0; i < HATE_SIZE; i++) { if (m_HateList[i].hate > max_hate) { max_hate = m_HateList[i].hate; game_id = m_HateList[i].GameID; } } m_Mutex.Unlock(); return game_id; } float MOB::GetStealthDetectionLevel() { return (m_ScanSkill * 5.0f) + m_MOB_Data->m_Level * 3; } //TODO: expand so MOBs can target other MOBs void MOB::ChooseTarget() { Player *p = (0); short player_count = 0; if (m_DefaultBehaviour == TURRET) { if (m_MOB_Type) { LockTurretTarget(); } return; } while (g_PlayerMgr->GetNextPlayerOnList(p, m_PlayerVisibleList)) { //do not allow players who are cloaked at a lvl insufficient for the mob's targeting to be added to the target list if(!IsDetectable(p)) { continue; } if (!p->ShipIndex()->GetIsIncapacitated() && !p->WarpDrive() && !p->Hijackee()) { player_count++; break; } } if (player_count > 0) { LockTarget(); } } void MOB::LockMOBTarget(long target_id) { Object *target = m_ObjectMgr->GetObjectFromID(target_id); //is this another turret? Don't attack other turrets if (!target || !m_MOB_Type || (target->ObjectType() == OT_MOB && ((MOB*)target)->m_DefaultBehaviour == TURRET)) { return; } //are we already attacking? ... if not ... if (HostilityTarget() == 0 && target->ObjectType() == OT_MOB) { //see if this target is to be attacked based on faction float faction_standing = GetFactionStanding(target); if (faction_standing < 1.0f) { //attack this target SetHostilityTarget(target_id); } } } void MOB::LockTarget() { // Find target based on hate int AttackGameID = 0; int MaxHate = -1; //see if we hate someone the most AttackGameID = GetMaxHateID(); Player *p = g_PlayerMgr->GetPlayer(AttackGameID); // if the MOB doesn't hate anyone, but can see people, it might want to lock on anyway! //TODO: this should be influenced somehow by factions too maybe? // (unless it sees dead people!) if (!p) { if (m_MOB_Data->m_Agressiveness >= 3) // is this an attack-on-sight MOB? { while (AttackGameID == 0 && g_PlayerMgr->GetNextPlayerOnList(p, m_PlayerVisibleList)) { if (p && p->GetFactionStanding(this) <= 0.0f) { AttackGameID = p->GameID(); } } ; } } if (p && (p->ShipIndex()->GetIsIncapacitated() || p->HasCombatImmunity())) { RemoveHate(p->GameID()); AttackGameID = 0; } if (p && !g_PlayerMgr->GetIndex(p, m_PlayerVisibleList)) { RemoveHate(p->GameID()); AttackGameID = 0; } //do not lock players that have cloaked if (!IsDetectable(p)) { RemoveHate(p->GameID()); AttackGameID = 0; } if (HostilityTarget() != 0 && HostilityTarget() != AttackGameID) { //ok we switched targets, so we stopped locking onto the last target. Player *p2 = g_PlayerMgr->GetPlayer(HostilityTarget()); if (p2) { SendRelationship(p2); LostTarget(p2); } } if (AttackGameID != 0 && p) { if (HostilityTarget() != AttackGameID) { SendAggroRelationship(p); } SetHostilityTarget(AttackGameID); m_Mutex.Lock(); m_Behaviour = PURSUE; SetUpdateRate(10); m_ArrivalTime = 0; m_Mutex.Unlock(); } } void MOB::HandleAttack() { unsigned long current_tick = GetNet7TickCount(); Player *p = g_PlayerMgr->GetPlayer(m_HostilityTargetID); Object *o = 0; if (!m_MOB_Data) { return; } if (!p) { o = m_ObjectMgr->GetObjectFromID(m_HostilityTargetID); if (o && o->ObjectType() != OT_MOB) { LostTarget(p); return; } if (o && !o->Active()) { RemoveObjectFromRangeList(&m_MOBVisibleList, o); LostTarget(p); return; } } if(!IsDetectable(p)) { LostTarget(p); return; } if (p && !p->ShipIndex()->GetIsIncapacitated() && p->InSpace() && !p->Hijackee()) { if ( current_tick > (m_LastAttackTime + (m_WeaponReloadTime * 1000)) && RangeFrom(p->Position()) < 1500.0f) { float range = RangeFrom(p->Position()); if (range > 5000.0f) { LostTarget(p); return; } if (m_Attacking == false) { m_Attacking = true; p->AttackMusicUpdate(true, GameID()); } FireWeapon((Object *)p); // Look for a new target with the most hate LockTarget(); } } else if (o && current_tick > (m_LastAttackTime + (m_WeaponReloadTime * 1000)) ) { //attack a non-player target FireWeapon(o); LockTarget(); m_DamageNode.func = B_MOB_DAMAGE; m_DamageNode.obj = o; } else { m_HostilityTargetID = 0; } } void MOB::LockTurretTarget() { //choose target for turret ObjectList::iterator itrOList; Object *o; for (itrOList = m_MOBVisibleList.begin(); itrOList != m_MOBVisibleList.end(); ++itrOList) { o = (*itrOList); //choose a new target LockMOBTarget(o->GameID()); } } void MOB::FireWeapon(Object *obj) { unsigned long current_tick = GetNet7TickCount(); short weapon_fx = 0; long weapon_time = 0; float range = RangeFrom(obj->Position()); weapon_time = m_WeaponDamageDelay ? (long)(range*3.0f) : 1000; SendFXToVisibilityList(obj->GameID(), GameID(), weapon_time, m_WeaponFX); float damage_fraction = (float)((rand() % 13) + (rand() % 13) + (rand() % 13) + 3)/30.0f; float damage = m_WeaponDPS * damage_fraction * m_WeaponReloadTime; // Remove hate for every hit you take SubtractHate(obj->GameID(), (int)(damage/10.0f)); //this will also send the client-damage packet GetSectorManager()->AddTimedCallPNode(&m_DamageNode, B_SHIP_DAMAGE, weapon_time, 0, GameID(), m_DamageType, 0, 0, 0, damage); m_DamageNode.player_id = obj->GameID(); m_LastAttackTime = current_tick; } void MOB::SendFXToVisibilityList(long target_id, long source_id, long time_delay, short effect_id) { Player *p = (0); ObjectToObjectEffect object_fx; object_fx.Bitmask = 0x07; object_fx.GameID = source_id; object_fx.TargetID = target_id; object_fx.EffectDescID = effect_id; object_fx.Message = 0; object_fx.EffectID = m_ObjectMgr->GetAvailableSectorID(); object_fx.TimeStamp = GetNet7TickCount(); object_fx.Duration = short(time_delay); while (g_PlayerMgr->GetNextPlayerOnList(p, m_RangeList)) { p->SendObjectToObjectEffect(&object_fx); } } //This sends to all players who can see this MOB void MOB::SendToVisibilityList(bool force_update) { Player *p = (0); PositionInformation pos; if ((m_Destination || m_DestinationFlag) && !force_update) return; //don't send regular impluses if we have a target m_Mutex.Lock(); UpdatePositionInfo(&pos); m_Mutex.Unlock(); while (g_PlayerMgr->GetNextPlayerOnList(p, m_RangeList)) { p->SendAdvancedPositionalUpdate(GameID(), &pos); } } void MOB::SendMOBData(Player *p) { if (p->DebugPlayer()) return; //TODO: remove this once MOB data is done, it's only here for live MOB basset refresh //SetMOBType(m_MOB_Type); SendObject(p); } //tentative aggro settings: /* 0 - green 1 - yellow - MOB may fire a warning shot or two if you get too close. 2 - yellow - as above, but doing more damage. 3 - yellow - as above, but may call in mates if you attack first. 4 - yellow - as above but with stronger chance of summoning help if you attack first. 5 - red - will attack when it spots you + same rules as 3. above. 6 - red - patrols area for players/enemy mobs. 7 - red - hunt for players, will summon help if it's getting in trouble. 8 - red - hunt for players, will summon all other MOBs in spawn within 5k as soon as it attacks. group_attack: 0 - never 1 - paired 2 - recruit all nearby aggro: 0 - never 1 - only if shot 2 - if inside 3 - if visible 4 - always */ void MOB::SendRelationship(Player *p) { long reaction = RELATIONSHIP_ATTACK; //MOBs are hostile by default if they have no faction if (m_MOB_Data && m_MOB_Data->m_FactionID) { //OK, MOB has faction. Let's get the MOB's standing with this player. float standing = p->GetFactionStanding(this); if (standing > -2000.0f) reaction = RELATIONSHIP_SHUN; if (standing >= 2000.0f) reaction = RELATIONSHIP_FRIENDLY; } p->SendRelationship(GameID(), reaction, 0); } void MOB::SendAggroRelationship(Player *p) { p->SendRelationship(GameID(), RELATIONSHIP_ATTACK, 0); } void MOB::Remove() { Object *o = (0); Player *p = (0); SetActive(false); while (g_PlayerMgr->GetNextPlayerOnList(p, m_RangeList)) { p->RemoveObject(GameID()); } if (m_HostilityTargetID) { o = m_ObjectMgr->GetObjectFromID(m_HostilityTargetID); if (o) { if (o->ObjectType() == OT_PLAYER) { p = (Player *)o; p = g_PlayerMgr->GetPlayer(m_HostilityTargetID); LostTarget(p); } else { RemoveObjectFromRangeList(&m_MOBVisibleList, o); LostTarget(p); } } } m_HostilityTargetID = 0; memset (&m_RangeList, 0, sizeof(m_RangeList)); memset (&m_PlayerVisibleList, 0, sizeof(m_PlayerVisibleList)); memset (&m_ClickedList, 0, sizeof(m_ClickedList)); memset (&m_HateList, 0, sizeof(m_HateList)); } void MOB::DamageMOB(long game_id, long damage_type, float damage, long inflicted) { //TO-DO: allow mobs to have deflects, and calculate deflects properly /*float redux_percent; float damage; switch(damage_type) { case DAMAGE_IMPACT: //is this needed? redux_percent = m_Stats.GetStat(STAT_IMPACT_DEFLECT); damage = damage_orig * (1.0f - (redux_percent > 0.5f ? 0.5f : redux_percent)); break; case DAMAGE_EXPLOSIVE: redux_percent = m_Stats.GetStat(STAT_EXPLOSIVE_DEFLECT); damage = damage_orig * (1.0f - (redux_percent > 0.5f ? 0.5f : redux_percent)); break; case DAMAGE_PLASMA: redux_percent = m_Stats.GetStat(STAT_PLASMA_DEFLECT); damage = damage_orig * (1.0f - (redux_percent > 0.5f ? 0.5f : redux_percent)); break; case DAMAGE_ENERGY: redux_percent = m_Stats.GetStat(STAT_ENERGY_DEFLECT); damage = damage_orig * (1.0f - (redux_percent > 0.5f ? 0.5f : redux_percent)); break; case DAMAGE_EMP: redux_percent = m_Stats.GetStat(STAT_EMP_DEFLECT); damage = damage_orig * (1.0f - (redux_percent > 0.5f ? 0.5f : redux_percent)); break; case DAMAGE_CHEMICAL: redux_percent = m_Stats.GetStat(STAT_CHEM_DEFLECT); damage = damage_orig * (1.0f - (redux_percent > 0.5f ? 0.5f : redux_percent)); break; }*/ //first take away the points m_ShieldLevel -= damage; if (m_ShieldLevel < 0.0f) { m_HullLevel += m_ShieldLevel; m_ShieldLevel = 0.0f; } AddHate(game_id, (int)damage); u32 tick = GetNet7TickCount(); //issue mob hit FX - limit this to 1 per half second, pointless sending more if (tick > (m_DamageTime + 500)) { SendMobDamage(damage, damage_type, game_id, inflicted); m_DamageTime = tick; } //is the MOB dead now? if (m_ShieldLevel <= 0.0f && m_HullLevel <= 0.0f && Active()) { DestroyMOB(game_id); } } short husk_bassets[] = { 1963, 1960, 1961, 1966, 1962, 1964, 1965 }; void MOB::DestroyMOB(long game_id) { //send explosion to everyone on rangelist SendExplosion(); //remove MOB Remove(); LostTarget(); RemoveHate(game_id); CalcOrientation(1.0f, 0.0f); SetHeading(); SetRespawnTick(GetNet7TickCount() + 120000); //create & populate Husk Husk *husk = (Husk*) m_ObjectMgr->AddNewObject(OT_HUSK, false); husk->SetActive(false); if (!m_MOB_Data) { return; } if (husk) { husk->SetBasset(husk_bassets[m_MOB_Data->m_Type]); husk->SetHuskName(Name()); husk->SetPosition(Position()); husk->SetLevel((Level() / 8) + 1); husk->SetScale(20.0f); husk->ResetResource(); husk->PopulateHusk(m_MOB_Type); husk->SendObjectReset(); //ensure contents are re-sent husk->SetCreateType(25); husk->SetDestroyTimer(300000, -1); //corpse timer set to 5 minutes //temporary credit loot for mobs TODO: get this from Database when data ready switch (m_MOB_Data->m_Type) { case STRUCTURAL: husk->SetCreditLoot(Level() * 20 + rand()%(Level()+1)); break; case CYBERNETIC: husk->SetCreditLoot(Level() * 10 + rand()%(Level()+1)); break; default: husk->SetCreditLoot(0); break; } } //set target of destroying player to husk and give XP Object *o = m_ObjectMgr->GetObjectFromID(game_id); Player *p = (0); if (o && o->ObjectType() == OT_PLAYER) { p = (Player*)o; } if (p) { p->SendClientSound("On_Destroy",0,0); p->AddMOBDestroyExperience(Level(), Name()); p->CheckMissionMOBKill(m_MOB_Type); husk->SetPlayerLootLock(p->GameID()); husk->SetLootTimer(1000*150); //2 1/2 mins until public loot } else if (o->ObjectType() == OT_MOB) { MOB *m = (MOB*)o; LogMessage("Mob %s killed by Turret %s\n", Name(), o->Name()); m->RemoveObjectFromRangeList(&m->m_MOBVisibleList, this); m->LostTarget(p); } ResetMOBSpawnPosition(); husk->SetActive(true); } void MOB::OnRespawn() { m_ShieldLevel = m_StartShieldLevel; m_HullLevel = m_StartHullLevel; } void MOB::SendExplosion() { Player * p = (0); short explosion_basset; short explosion_fx; short explosion_sfx = 0; if (!m_MOB_Data) { return; } switch (m_MOB_Data->m_Type) { case STRUCTURAL: explosion_basset = 1976; explosion_fx = 1013; explosion_sfx = 0x2755; //TODO: bots only break; case ORGANIC_RED: explosion_basset = 1977; explosion_fx = 1014; break; case CRYSTALLINE: explosion_basset = 1978; explosion_fx = 1015; break; case CYBERNETIC: explosion_basset = 1979; explosion_fx = 1016; break; case ENERGY: explosion_basset = 1980; explosion_fx = 1017; if (m_MOB_Data->m_Name && strstr(m_MOB_Data->m_Name, "Manes") != 0) { LogMessage("exploding manes\n"); explosion_sfx = 10167; } break; case ROCK_BASED: explosion_basset = 1981; explosion_fx = 1018; break; case ORGANIC_GREEN: explosion_basset = 1982; explosion_fx = 1019; break; default: explosion_basset = 1976; explosion_fx = 1013; break; } ObjectEffect ObjExplosion1; ObjExplosion1.Bitmask = 0x07; ObjExplosion1.GameID = GameID(); ObjExplosion1.EffectDescID = explosion_sfx; ObjExplosion1.EffectID = m_ObjectMgr->GetAvailableSectorID(); ObjExplosion1.TimeStamp = GetNet7TickCount(); ObjExplosion1.Duration = 4000; ObjectEffect ObjExplosion2; ObjExplosion2.Bitmask = 0x07; ObjExplosion2.GameID = GameID(); ObjExplosion2.EffectDescID = 0x018d; //red shockwave ObjExplosion2.EffectID = m_ObjectMgr->GetAvailableSectorID(); ObjExplosion2.TimeStamp = GetNet7TickCount(); ObjExplosion2.Duration = 4000; //PointEffect(Position(), explosion_fx); while (g_PlayerMgr->GetNextPlayerOnList(p, m_RangeList)) { p->PointEffect(Position(), explosion_fx); p->UnSetTarget(GameID()); if (explosion_sfx != 0) { p->SendObjectEffect(&ObjExplosion1); } p->SendObjectEffect(&ObjExplosion2); } } void MOB::UpdateObject() { RangeListVec::iterator itrRList; Player * p = (0); //unsigned long tick = GetNet7TickCount(); //do the MOB shield level if (m_LastTickShieldLevel != m_ShieldLevel || m_LastTickHullLevel != m_HullLevel) { bool shields = false; bool hull = false; if (m_LastTickShieldLevel != m_ShieldLevel) { m_ShipIndex.Shield.SetStartValue(m_ShieldLevel/m_StartShieldLevel); shields = true; } if (m_LastTickHullLevel != m_HullLevel) { m_ShipIndex.SetHullPoints(m_HullLevel); hull = true; } m_ShipIndex.Buffer()->Lock(); m_ShipIndex.BuildDiffPacket(shields, hull); //shield level has changed, update clients in range ... maybe only do this for clients who are targetting this MOB? while (g_PlayerMgr->GetNextPlayerOnList(p, m_RangeList)) { p->SendOpcode(ENB_OPCODE_001B_AUX_DATA, m_ShipIndex.m_DiffPacket, m_ShipIndex.m_DiffSize); } /* Unblock buffer */ m_ShipIndex.Buffer()->Unlock(); m_LastTickShieldLevel = m_ShieldLevel; m_LastTickHullLevel = m_HullLevel; m_ShieldFXSent = 0; //reset the shield FX send } } void MOB::SendMobDamage(float damage, long damage_type, long game_id, long inflicted) { RangeListVec::iterator itrRList; Player * p = (0); ObjectEffect ObjExplosion; ObjExplosion.Bitmask = 0x07; ObjExplosion.GameID = GameID(); ObjExplosion.EffectDescID = 566; ObjExplosion.EffectID = m_ObjectMgr->GetAvailableSectorID(); ObjExplosion.TimeStamp = GetNet7TickCount(); ObjExplosion.Duration = 1000; while (g_PlayerMgr->GetNextPlayerOnList(p, m_RangeList)) { //if this is first couple of explosions, send everything if (m_ShieldFXSent < 2) { p->SendObjectToObjectLinkedEffect(this, p, 0x0021, 0x0003, 2.0f); p->SendObjectEffect(&ObjExplosion); m_ShieldFXSent++; } else if (p->GameID() == game_id && p->AttacksThisTick() == 0) //always send at least one effect for the attacking player { p->SendObjectToObjectLinkedEffect(this, p, 0x0021, 0x0003, 2.0f); p->IncAttacksThisTick(); } //always send client damage p->SendClientDamage(GameID(), p->GameID(), damage, 0, damage_type, inflicted); } } void MOB::SendShieldLevel(Player *p) { // TODO: Use shield regen when needed m_ShipIndex.Shield.SetStartValue(m_ShieldLevel/m_StartShieldLevel); SendAuxShip(p); } //defunct now void MOB::SendShieldUpdate(Player *p) { SendAuxDiff(p, SHIELD); } void MOB::SendAuxDiff(Player * p, long type) { m_ShipIndex.Buffer()->Lock(); bool shields = (type & SHIELD) ? true : false; bool hull = (type & HULL) ? true : false; m_ShipIndex.BuildDiffPacket(shields, hull); if (p) { p->SendOpcode(ENB_OPCODE_001B_AUX_DATA, m_ShipIndex.m_DiffPacket, m_ShipIndex.m_DiffSize); } /* Unblock buffer */ m_ShipIndex.Buffer()->Unlock(); } void MOB::SendAuxShip(Player * other) { /* Block anyone else from using the buffer */ m_ShipIndex.Buffer()->Lock(); m_ShipIndex.BuildClickPacket(); if (other) { other->SendOpcode(ENB_OPCODE_001B_AUX_DATA, m_ShipIndex.m_ClickPacket, m_ShipIndex.m_ClickSize); } /* Unblock buffer */ m_ShipIndex.Buffer()->Unlock(); } //also defunct void MOB::SendHullUpdate(Player *p) { // Update hull SendAuxDiff(p, HULL); } void MOB::UpdatePositionInfo(PositionInformation *pos) { memcpy(pos, &m_Position_info, sizeof(m_Position_info)); if (m_DestinationFlag) { pos->Bitmask |= 0x0100; pos->UpdatePeriod = m_ArrivalTime - GetNet7TickCount(); pos->Position[0] = m_DestinationPos[0]; pos->Position[1] = m_DestinationPos[1]; pos->Position[2] = m_DestinationPos[2]; } } void MOB::SendPosition(Player *player) { PositionInformation pos; UpdatePositionInfo(&pos); player->SendAdvancedPositionalUpdate( GameID(), &pos); } void MOB::SendAuxDataPacket(Player *player) { // Build and send aux packet m_ShipIndex.Buffer()->Lock(); m_ShipIndex.BuildCreatePacket(); if (m_ShipIndex.m_CreateSize > 0) { player->SendOpcode(ENB_OPCODE_001B_AUX_DATA, m_ShipIndex.m_CreatePacket, (u16)m_ShipIndex.m_CreateSize); } m_ShipIndex.Buffer()->Unlock(); } //called on creation of MOB graphic object for Player void MOB::OnCreate(Player *player) { } //Called when this MOB is targeted by player void MOB::OnTargeted(Player *player) { if (!IsClickedBy(player)) { SendShieldLevel(player); SetClickedBy(player); } player->BlankVerbs(); if (player->ShipIndex()->GetIsIncapacitated()) { player->AddVerb(VERBID_FOLLOW, -1.0f); } else { player->AddVerb(VERBID_FOLLOW, 1500.0f); } } //this returns true if Player 'player' has clicked on the MOB bool MOB::IsClickedBy(Player *player) { return g_PlayerMgr->GetIndex(player, m_ClickedList); } void MOB::SetBehaviour(short new_behaviour) { m_Behaviour = new_behaviour; } void MOB::AddBehaviourObject(Object *obj) { m_Mutex.Lock(); m_BehaviourList.push_back(obj); m_Mutex.Unlock(); } void MOB::AddBehaviourPosition(float *pos) { m_HuntPosition[0] = pos[0]; m_HuntPosition[1] = pos[1]; m_HuntPosition[2] = pos[2]; } void MOB::LostTarget(Player *p) { if (p && m_HostilityTargetID == p->GameID()) { if (m_Attacking == true) p->AttackMusicUpdate(false, GameID()); //RemoveHate(p->GameID()); m_Attacking = false; } m_HostilityTargetID = 0; Turn(m_DefaultTurn); m_YInput = 0; m_Position_info.RotY = 0; m_ZInput = 0; m_Position_info.RotZ = 0; m_Behaviour = m_DefaultBehaviour; m_Velocity = m_DefaultVelocity; m_Destination = (0); SetUpdateRate(m_DefaultUpdateRate); } void MOB::CalcNewHeading(float tdiff) { float rot_Z[4] = { 0.0f, 0.0f, 0.0f, 1.0f }; float rot_Y[4] = { 0.0f, 0.0f, 0.0f, 1.0f }; float result[4]; bool change = false; if (m_ZInput != 0.0f) { rot_Z[2] = -m_ZInput*tdiff*0.42f/883.0f*1000.0f; change = true; } if (m_YInput != 0.0f) { rot_Y[1] = -m_YInput*tdiff*0.42f/883.0f*1000.0f; change = true; } if (change) { Quat4fMul(rot_Z, rot_Y, result); Quat4fMul(Orientation(), result, Orientation()); Quat4fNormalize(Orientation()); SetHeading(); } //work out behaviour MOBBehaviour(); } void MOB::MOBBehaviour() { m_DestinationFlag = false; switch(m_Behaviour) { case INVALID_BEHAVIOUR: { //see if this MOB is part of a field if (m_SpawnGroup) { Object *obj = m_SpawnGroup; if (obj->ObjectType() == OT_FIELD) { if (m_BehaviourList.size() == 0) { m_BehaviourList.push_back(obj); } m_Behaviour = PATROL_FIELD; m_DefaultBehaviour = PATROL_FIELD; } } else { m_Behaviour = DRIFT; m_DefaultBehaviour = DRIFT; } } break; case PATROL_FIELD: { //select a target to check out. TODO: If the MOB sees gaps in the field he'll sometimes check them out Field *field = (0); if (m_BehaviourList.size() > 0) { Object *obj = m_BehaviourList[0]; if (obj->ObjectType() == OT_FIELD) { field = (Field*)m_BehaviourList[0]; } } if (!field) return; //have we reached the current destination? if (m_Destination) { if (GetNet7TickCount() > m_ArrivalTime) { m_Destination = field->SetDestination(m_Destination); TravelToDestination(); } } else { m_Destination = field->SetDestination(0); TravelToDestination(); } } break; case PURSUE: //set off after Hostility target PursueTarget(); break; case CURIOUS: //set off after destination ExamineTarget(); break; default: break; } } void MOB::PursueTarget() { Object *obj = m_ObjectMgr->GetObjectFromID(HostilityTarget()); if (obj && GetNet7TickCount() > m_ArrivalTime) { Face(obj); m_Velocity = m_DefaultVelocity; // work out position to target, with distance to travel before next update float *_heading = Heading(); float range = RangeFrom(obj->Position()); float travel_distance = m_Velocity*((float)UpdateRate()*0.1f); if (range < travel_distance || range < 750.0f) { travel_distance = range - 650.0f; //aim for within a good fair distance m_Velocity = (travel_distance)/((float)UpdateRate()*0.1f); if (range < 400.0f) { m_Velocity = -50.0f; travel_distance = m_Velocity*((float)UpdateRate()*0.1f); } else if (range < 750.0f) { m_Velocity = 0.0f; travel_distance = 0.0f; } else if (m_Velocity > m_DefaultVelocity * 1.2f) { m_Velocity = m_DefaultVelocity * 1.2f; } } //do we exceed range to host field if (m_DefaultBehaviour == PATROL_FIELD) { if (m_BehaviourList.size() > 0) { Object *f = m_BehaviourList[0]; if (RangeFrom(f->Position(), true) > (5000.0f + f->Signature())) { LogMessage("Break off pursuit, range to field centre = %.2f.\n", RangeFrom(f->Position(), true)); m_Behaviour = m_DefaultBehaviour; //break off pursuit } } else { m_DefaultBehaviour = DRIFT; m_Behaviour = DRIFT; } } m_DestinationFlag = true; //calculate destination m_DestinationPos[0] = PosX() + ( travel_distance * _heading[0] ); m_DestinationPos[1] = PosY() + ( travel_distance * _heading[1] ); m_DestinationPos[2] = PosZ() + ( travel_distance * _heading[2] ); if (range < travel_distance) { m_ArrivalTime = GetNet7TickCount() + UpdateRate()*20; } else { m_ArrivalTime = GetNet7TickCount() + UpdateRate()*100; } SendLocationAndSpeed(true); } } void MOB::SetDefaultStats(float turn, short behaviour, float velocity, long update_rate) { m_DefaultTurn = turn; m_DefaultBehaviour = behaviour; m_DefaultVelocity = velocity; m_DefaultUpdateRate = update_rate; } void MOB::TravelToDestination() { m_ZInput = 0; m_Position_info.RotZ = 0; if (m_Destination == 0) return; Face(m_Destination); //calculate arrival time m_ArrivalTime = GetNet7TickCount() + (u32)((m_Destination->RangeFrom(Position())/m_Velocity)*1000.0f); /*m_DestinationPos[0] = m_Destination->PosX(); m_DestinationPos[1] = m_Destination->PosY(); m_DestinationPos[2] = m_Destination->PosZ();*/ SendLocationAndSpeed(true); } bool MOB::IsDetectable(Player *p) { if (p && p->ShipIndex()->GetIsCloaked() && GetStealthDetectionLevel() < p->GetStealthLevel()) { return false; } else { return true; } } void MOB::HandleMovementChanges() { float range; long rand_roll = rand()%50 + rand()%50; Object *target = (0); if (m_SpawnGroup) { switch (m_SpawnGroup->ObjectType()) { case OT_MOBSPAWN: if (m_Behaviour == DRIFT) { //first see if MOB is too far out from spawn //get spawn range range = m_SpawnGroup->RangeFrom(Position()); //get spawn radius if (range > m_SpawnGroup->SpawnRadius()) { //out of range, reflect the MOB back to the centre Face(m_SpawnGroup); SetupRandomMovement(); SendLocationAndSpeed(false); break; } //now introduce random position changes if (rand_roll > 75) { //head toward an object on the range list if any target = PickObject(); if (target) { Face(target); m_Behaviour = CURIOUS; m_Destination = target; m_YInput = 0; m_ZInput = 0; } } else if (rand_roll < 5) { //change heading SetupRandomMovement(); SendLocationAndSpeed(false); } } break; default: break; } } } void MOB::SetupRandomMovement() { float velocity = (float)(rand()%100 + 130); float turn = (float)((rand()%11) - 5) * 0.01f; if (turn == 0.0f) turn = 0.02f; SetVelocity(velocity); SetDefaultStats(turn, DRIFT, velocity, 50); Turn(turn); } void MOB::ResetMOBSpawnPosition() { float pos[3]; if (m_SpawnGroup && m_SpawnGroup->ObjectType() == OT_MOBSPAWN) { SetPosition(m_SpawnGroup->Position()); //work out a position vector & angle float vector = (float)(rand()%200 - 99)*0.01f; float angle = (float)(rand()%360)/(360.0f / (2*PI)); pos[0] = (m_SpawnGroup->SpawnRadius() * vector) * cosf(angle); pos[1] = (m_SpawnGroup->SpawnRadius() * vector) * sinf(angle); MovePosition(pos[0], pos[1], ((float)((rand()%11) - 5) * 200.0f)); } } Object * MOB::PickObject() { Object *obj = (0); Player *p = (0); long player_count = 0; while (g_PlayerMgr->GetNextPlayerOnList(p, m_PlayerVisibleList)) { //do not allow players who are cloaked at a lvl insufficient for the mob's targeting to be added to the target list if(!IsDetectable(p)) { continue; } ++player_count; } if (player_count > 0) { //pick a target long targ_num = rand()%player_count; p = (0); while (g_PlayerMgr->GetNextPlayerOnList(p, m_PlayerVisibleList)) { if(!IsDetectable(p)) { continue; } if (targ_num == 0) break; --targ_num; } if (p) obj = (Object*)p; } return obj; } void MOB::MOBEffect(long player_id, long effect_id) { Player *p = g_PlayerMgr->GetPlayer(player_id); if (p) { ObjectEffect ObjEffect; ObjEffect.Bitmask = 0x07; ObjEffect.GameID = GameID(); ObjEffect.EffectDescID = (short) effect_id; ObjEffect.EffectID = m_ObjectMgr->GetAvailableSectorID(); ObjEffect.TimeStamp = GetNet7TickCount(); ObjEffect.Duration = 1000; p->SendObjectEffect(&ObjEffect); } } void MOB::ExamineTarget() { if (m_Destination && m_Destination->Active()) { Player * p = (Player*)m_Destination; if (p->AdminLevel() == 90) //this is for checking MOBs are behaving correctly. { MOBEffect(p->GameID(), 106); } Face(m_Destination); m_Velocity = m_DefaultVelocity; // work out position to target, with distance to travel before next update float *_heading = Heading(); float range = RangeFrom(m_Destination->Position()); float travel_distance = m_Velocity*((float)UpdateRate()*0.1f); if (range < travel_distance) { travel_distance = range - 900.0f; //aim for within a good fair distance m_Velocity = (travel_distance)/((float)UpdateRate()*0.1f); if (range < 1000.0f) { m_Velocity = 0.0f; travel_distance = 0.0f; m_Destination = (0); //return to drift } else if (m_Velocity > m_DefaultVelocity * 0.9f) { m_Velocity = m_DefaultVelocity * 0.9f; } } SendLocationAndSpeed(true); } else { m_Behaviour = m_DefaultBehaviour; LogMessage("Mob %s return to drift\n", Name()); Turn(m_DefaultTurn); m_YInput = 0; m_Position_info.RotY = 0; m_ZInput = 0; m_Position_info.RotZ = 0; m_Behaviour = m_DefaultBehaviour; m_Velocity = m_DefaultVelocity; m_Destination = (0); SetUpdateRate(m_DefaultUpdateRate); m_Destination = (0); } } char * MOB::GetSpawnName() { char *spawn = 0; if (m_SpawnGroup) { spawn = m_SpawnGroup->Name(); } return spawn; }