summaryrefslogtreecommitdiffstats
path: root/src/Entities
diff options
context:
space:
mode:
Diffstat (limited to 'src/Entities')
-rw-r--r--src/Entities/CMakeLists.txt2
-rw-r--r--src/Entities/Entity.cpp20
-rw-r--r--src/Entities/Entity.h3
-rw-r--r--src/Entities/Pickup.cpp10
-rw-r--r--src/Entities/Player.cpp185
-rw-r--r--src/Entities/Player.h21
6 files changed, 226 insertions, 15 deletions
diff --git a/src/Entities/CMakeLists.txt b/src/Entities/CMakeLists.txt
index c9ca44d38..205cb2cca 100644
--- a/src/Entities/CMakeLists.txt
+++ b/src/Entities/CMakeLists.txt
@@ -10,3 +10,5 @@ file(GLOB SOURCE
)
add_library(Entities ${SOURCE})
+
+target_link_libraries(Entities WorldStorage)
diff --git a/src/Entities/Entity.cpp b/src/Entities/Entity.cpp
index 4cf10a219..31ad66779 100644
--- a/src/Entities/Entity.cpp
+++ b/src/Entities/Entity.cpp
@@ -312,12 +312,16 @@ bool cEntity::DoTakeDamage(TakeDamageInfo & a_TDI)
if ((a_TDI.Attacker != NULL) && (a_TDI.Attacker->IsPlayer()))
{
+ cPlayer * Player = (cPlayer *)a_TDI.Attacker;
+
// IsOnGround() only is false if the player is moving downwards
- if (!((cPlayer *)a_TDI.Attacker)->IsOnGround()) // TODO: Better damage increase, and check for enchantments (and use magic critical instead of plain)
+ if (!Player->IsOnGround()) // TODO: Better damage increase, and check for enchantments (and use magic critical instead of plain)
{
a_TDI.FinalDamage += 2;
m_World->BroadcastEntityAnimation(*this, 4); // Critical hit
}
+
+ Player->GetStatManager().AddValue(statDamageDealt, (StatValue)floor(a_TDI.FinalDamage * 10 + 0.5));
}
m_Health -= (short)a_TDI.FinalDamage;
@@ -370,6 +374,11 @@ bool cEntity::DoTakeDamage(TakeDamageInfo & a_TDI)
if (m_Health <= 0)
{
KilledBy(a_TDI.Attacker);
+
+ if (a_TDI.Attacker != NULL)
+ {
+ a_TDI.Attacker->Killed(this);
+ }
}
return true;
}
@@ -575,9 +584,16 @@ void cEntity::Tick(float a_Dt, cChunk & a_Chunk)
if (m_AttachedTo != NULL)
{
- if ((m_Pos - m_AttachedTo->GetPosition()).Length() > 0.5)
+ Vector3d DeltaPos = m_Pos - m_AttachedTo->GetPosition();
+ if (DeltaPos.Length() > 0.5)
{
SetPosition(m_AttachedTo->GetPosition());
+
+ if (IsPlayer())
+ {
+ cPlayer * Player = (cPlayer *)this;
+ Player->UpdateMovementStats(DeltaPos);
+ }
}
}
else
diff --git a/src/Entities/Entity.h b/src/Entities/Entity.h
index df03d635b..a111b128d 100644
--- a/src/Entities/Entity.h
+++ b/src/Entities/Entity.h
@@ -299,6 +299,9 @@ public:
/// Called when the health drops below zero. a_Killer may be NULL (environmental damage)
virtual void KilledBy(cEntity * a_Killer);
+ /// Called when the entity kills another entity
+ virtual void Killed(cEntity * a_Victim) {}
+
/// Heals the specified amount of HPs
void Heal(int a_HitPoints);
diff --git a/src/Entities/Pickup.cpp b/src/Entities/Pickup.cpp
index 497b41683..0fd006485 100644
--- a/src/Entities/Pickup.cpp
+++ b/src/Entities/Pickup.cpp
@@ -192,6 +192,16 @@ bool cPickup::CollectedBy(cPlayer * a_Dest)
int NumAdded = a_Dest->GetInventory().AddItem(m_Item);
if (NumAdded > 0)
{
+ // Check achievements
+ switch (m_Item.m_ItemType)
+ {
+ case E_BLOCK_LOG: a_Dest->AwardAchievement(achMineWood); break;
+ case E_ITEM_LEATHER: a_Dest->AwardAchievement(achKillCow); break;
+ case E_ITEM_DIAMOND: a_Dest->AwardAchievement(achDiamonds); break;
+ case E_ITEM_BLAZE_ROD: a_Dest->AwardAchievement(achBlazeRod); break;
+ default: break;
+ }
+
m_Item.m_ItemCount -= NumAdded;
m_World->BroadcastCollectPickup(*this, *a_Dest);
// Also send the "pop" sound effect with a somewhat random pitch (fast-random using EntityID ;)
diff --git a/src/Entities/Player.cpp b/src/Entities/Player.cpp
index 6ac11c270..c3b763278 100644
--- a/src/Entities/Player.cpp
+++ b/src/Entities/Player.cpp
@@ -16,6 +16,9 @@
#include "../Items/ItemHandler.h"
#include "../Vector3.h"
+#include "../WorldStorage/StatSerializer.h"
+#include "../CompositeChat.h"
+
#include "inifile/iniFile.h"
#include "json/json.h"
@@ -191,6 +194,8 @@ void cPlayer::Tick(float a_Dt, cChunk & a_Chunk)
return;
}
}
+
+ m_Stats.AddValue(statMinutesPlayed, 1);
if (!a_Chunk.IsValid())
{
@@ -815,7 +820,7 @@ bool cPlayer::DoTakeDamage(TakeDamageInfo & a_TDI)
if ((a_TDI.Attacker != NULL) && (a_TDI.Attacker->IsPlayer()))
{
- cPlayer* Attacker = (cPlayer*) a_TDI.Attacker;
+ cPlayer * Attacker = (cPlayer *)a_TDI.Attacker;
if ((m_Team != NULL) && (m_Team == Attacker->m_Team))
{
@@ -832,6 +837,8 @@ bool cPlayer::DoTakeDamage(TakeDamageInfo & a_TDI)
// Any kind of damage adds food exhaustion
AddFoodExhaustion(0.3f);
SendHealth();
+
+ m_Stats.AddValue(statDamageTaken, (StatValue)floor(a_TDI.FinalDamage * 10 + 0.5));
return true;
}
return false;
@@ -862,6 +869,8 @@ void cPlayer::KilledBy(cEntity * a_Killer)
Pickups.Add(cItem(E_ITEM_RED_APPLE));
}
+ m_Stats.AddValue(statItemsDropped, Pickups.Size());
+
m_World->SpawnItemPickups(Pickups, GetPosX(), GetPosY(), GetPosZ(), 10);
SaveToDisk(); // Save it, yeah the world is a tough place !
@@ -871,9 +880,9 @@ void cPlayer::KilledBy(cEntity * a_Killer)
}
else if (a_Killer->IsPlayer())
{
- GetWorld()->BroadcastChatDeath(Printf("%s was killed by %s", GetName().c_str(), ((cPlayer *)a_Killer)->GetName().c_str()));
+ cPlayer * Killer = (cPlayer *)a_Killer;
- m_World->GetScoreBoard().AddPlayerScore(((cPlayer *)a_Killer)->GetName(), cObjective::otPlayerKillCount, 1);
+ GetWorld()->BroadcastChatDeath(Printf("%s was killed by %s", GetName().c_str(), Killer->GetName().c_str()));
}
else
{
@@ -883,6 +892,8 @@ void cPlayer::KilledBy(cEntity * a_Killer)
GetWorld()->BroadcastChatDeath(Printf("%s was killed by a %s", GetName().c_str(), KillerClass.c_str()));
}
+ m_Stats.AddValue(statDeaths);
+
m_World->GetScoreBoard().AddPlayerScore(GetName(), cObjective::otDeathCount, 1);
}
@@ -890,6 +901,33 @@ void cPlayer::KilledBy(cEntity * a_Killer)
+void cPlayer::Killed(cEntity * a_Victim)
+{
+ cScoreboard & ScoreBoard = m_World->GetScoreBoard();
+
+ if (a_Victim->IsPlayer())
+ {
+ m_Stats.AddValue(statPlayerKills);
+
+ ScoreBoard.AddPlayerScore(GetName(), cObjective::otPlayerKillCount, 1);
+ }
+ else if (a_Victim->IsMob())
+ {
+ if (((cMonster *)a_Victim)->GetMobFamily() == cMonster::mfHostile)
+ {
+ AwardAchievement(achKillMonster);
+ }
+
+ m_Stats.AddValue(statMobKills);
+ }
+
+ ScoreBoard.AddPlayerScore(GetName(), cObjective::otTotalKillCount, 1);
+}
+
+
+
+
+
void cPlayer::Respawn(void)
{
m_Health = GetMaxHealth();
@@ -1108,6 +1146,47 @@ void cPlayer::SetIP(const AString & a_IP)
+unsigned int cPlayer::AwardAchievement(const eStatistic a_Ach)
+{
+ eStatistic Prerequisite = cStatInfo::GetPrerequisite(a_Ach);
+
+ // Check if the prerequisites are met
+ if (Prerequisite != statInvalid)
+ {
+ if (m_Stats.GetValue(Prerequisite) == 0)
+ {
+ return 0;
+ }
+ }
+
+ StatValue Old = m_Stats.GetValue(a_Ach);
+
+ if (Old > 0)
+ {
+ return m_Stats.AddValue(a_Ach);
+ }
+ else
+ {
+ // First time, announce it
+ cCompositeChat Msg;
+ Msg.AddTextPart(m_PlayerName + " has just earned the achievement ");
+ Msg.AddTextPart(cStatInfo::GetName(a_Ach)); // TODO 2014-05-12 xdot: Use the proper cCompositeChat part (cAchievement)
+ m_World->BroadcastChat(Msg);
+
+ // Increment the statistic
+ StatValue New = m_Stats.AddValue(a_Ach);
+
+ // Achievement Get!
+ m_ClientHandle->SendStatistics(m_Stats);
+
+ return New;
+ }
+}
+
+
+
+
+
void cPlayer::TeleportToCoords(double a_PosX, double a_PosY, double a_PosZ)
{
SetPosition(a_PosX, a_PosY, a_PosZ);
@@ -1192,6 +1271,9 @@ void cPlayer::MoveTo( const Vector3d & a_NewPos )
// TODO: should do some checks to see if player is not moving through terrain
// TODO: Official server refuses position packets too far away from each other, kicking "hacked" clients; we should, too
+
+ Vector3d DeltaPos = a_NewPos - GetPosition();
+ UpdateMovementStats(DeltaPos);
SetPosition( a_NewPos );
SetStance(a_NewPos.y + 1.62);
@@ -1422,10 +1504,7 @@ void cPlayer::TossEquippedItem(char a_Amount)
Drops.push_back(DroppedItem);
}
- double vX = 0, vY = 0, vZ = 0;
- EulerToVector(-GetYaw(), GetPitch(), vZ, vX, vY);
- vY = -vY * 2 + 1.f;
- m_World->SpawnItemPickups(Drops, GetPosX(), GetEyeHeight(), GetPosZ(), vX * 3, vY * 3, vZ * 3, true); // 'true' because created by player
+ TossItems(Drops);
}
@@ -1441,6 +1520,7 @@ void cPlayer::TossHeldItem(char a_Amount)
char OriginalItemAmount = Item.m_ItemCount;
Item.m_ItemCount = std::min(OriginalItemAmount, a_Amount);
Drops.push_back(Item);
+
if (OriginalItemAmount > a_Amount)
{
Item.m_ItemCount = OriginalItemAmount - a_Amount;
@@ -1451,10 +1531,7 @@ void cPlayer::TossHeldItem(char a_Amount)
}
}
- double vX = 0, vY = 0, vZ = 0;
- EulerToVector(-GetYaw(), GetPitch(), vZ, vX, vY);
- vY = -vY * 2 + 1.f;
- m_World->SpawnItemPickups(Drops, GetPosX(), GetEyeHeight(), GetPosZ(), vX * 3, vY * 3, vZ * 3, true); // 'true' because created by player
+ TossItems(Drops);
}
@@ -1466,10 +1543,21 @@ void cPlayer::TossPickup(const cItem & a_Item)
cItems Drops;
Drops.push_back(a_Item);
+ TossItems(Drops);
+}
+
+
+
+
+
+void cPlayer::TossItems(const cItems & a_Items)
+{
+ m_Stats.AddValue(statItemsDropped, a_Items.Size());
+
double vX = 0, vY = 0, vZ = 0;
EulerToVector(-GetYaw(), GetPitch(), vZ, vX, vY);
vY = -vY * 2 + 1.f;
- m_World->SpawnItemPickups(Drops, GetPosX(), GetEyeHeight(), GetPosZ(), vX * 3, vY * 3, vZ * 3, true); // 'true' because created by player
+ m_World->SpawnItemPickups(a_Items, GetPosX(), GetEyeHeight(), GetPosZ(), vX * 3, vY * 3, vZ * 3, true); // 'true' because created by player
}
@@ -1621,6 +1709,11 @@ bool cPlayer::LoadFromDisk()
m_Inventory.LoadFromJson(root["inventory"]);
m_LoadedWorldName = root.get("world", "world").asString();
+
+ // Load the player stats.
+ // We use the default world name (like bukkit) because stats are shared between dimensions/worlds.
+ cStatSerializer StatSerializer(cRoot::Get()->GetDefaultWorld()->GetName(), GetName(), &m_Stats);
+ StatSerializer.Load();
LOGD("Player \"%s\" was read from file, spawning at {%.2f, %.2f, %.2f} in world \"%s\"",
m_PlayerName.c_str(), GetPosX(), GetPosY(), GetPosZ(), m_LoadedWorldName.c_str()
@@ -1692,6 +1785,16 @@ bool cPlayer::SaveToDisk()
LOGERROR("ERROR WRITING PLAYER JSON TO FILE \"%s\"", SourceFile.c_str());
return false;
}
+
+ // Save the player stats.
+ // We use the default world name (like bukkit) because stats are shared between dimensions/worlds.
+ cStatSerializer StatSerializer(cRoot::Get()->GetDefaultWorld()->GetName(), m_PlayerName, &m_Stats);
+ if (!StatSerializer.Save())
+ {
+ LOGERROR("Could not save stats for player %s", m_PlayerName.c_str());
+ return false;
+ }
+
return true;
}
@@ -1706,7 +1809,10 @@ cPlayer::StringList cPlayer::GetResolvedPermissions()
const PermissionMap& ResolvedPermissions = m_ResolvedPermissions;
for( PermissionMap::const_iterator itr = ResolvedPermissions.begin(); itr != ResolvedPermissions.end(); ++itr )
{
- if( itr->second ) Permissions.push_back( itr->first );
+ if (itr->second)
+ {
+ Permissions.push_back( itr->first );
+ }
}
return Permissions;
@@ -1845,6 +1951,59 @@ void cPlayer::HandleFloater()
+void cPlayer::UpdateMovementStats(const Vector3d & a_DeltaPos)
+{
+ StatValue Value = (StatValue)floor(a_DeltaPos.Length() * 100 + 0.5);
+
+ if (m_AttachedTo == NULL)
+ {
+ int PosX = POSX_TOINT;
+ int PosY = POSY_TOINT;
+ int PosZ = POSZ_TOINT;
+
+ BLOCKTYPE Block;
+ NIBBLETYPE Meta;
+ if (!m_World->GetBlockTypeMeta(PosX, PosY, PosZ, Block, Meta))
+ {
+ return;
+ }
+
+ if ((Block == E_BLOCK_LADDER) && (a_DeltaPos.y > 0.0)) // Going up
+ {
+ m_Stats.AddValue(statDistClimbed, (StatValue)floor(a_DeltaPos.y * 100 + 0.5));
+ }
+ else
+ {
+ // TODO 2014-05-12 xdot: Other types
+ m_Stats.AddValue(statDistWalked, Value);
+ }
+ }
+ else
+ {
+ switch (m_AttachedTo->GetEntityType())
+ {
+ case cEntity::etMinecart: m_Stats.AddValue(statDistMinecart, Value); break;
+ case cEntity::etBoat: m_Stats.AddValue(statDistBoat, Value); break;
+ case cEntity::etMonster:
+ {
+ cMonster * Monster = (cMonster *)m_AttachedTo;
+ switch (Monster->GetMobType())
+ {
+ case cMonster::mtPig: m_Stats.AddValue(statDistPig, Value); break;
+ case cMonster::mtHorse: m_Stats.AddValue(statDistHorse, Value); break;
+ default: break;
+ }
+ break;
+ }
+ default: break;
+ }
+ }
+}
+
+
+
+
+
void cPlayer::ApplyFoodExhaustionFromMovement()
{
if (IsGameModeCreative())
diff --git a/src/Entities/Player.h b/src/Entities/Player.h
index 6fc7e2875..78b534d83 100644
--- a/src/Entities/Player.h
+++ b/src/Entities/Player.h
@@ -7,6 +7,8 @@
#include "../World.h"
#include "../ClientHandle.h"
+#include "../Statistics.h"
+
@@ -174,6 +176,15 @@ public:
cTeam * UpdateTeam(void);
// tolua_end
+
+ /** Return the associated statistic and achievement manager. */
+ cStatManager & GetStatManager() { return m_Stats; }
+
+ /** Awards the player an achievement.
+ If all prerequisites are met, this method will award the achievement and will broadcast a chat message.
+ If the achievement has been already awarded to the player, this method will just increment the stat counter.
+ Returns the _new_ stat value. (0 = Could not award achievement) */
+ unsigned int AwardAchievement(const eStatistic a_Ach);
void SetIP(const AString & a_IP);
@@ -306,6 +317,8 @@ public:
void AbortEating(void);
virtual void KilledBy(cEntity * a_Killer) override;
+
+ virtual void Killed(cEntity * a_Victim) override;
void Respawn(void); // tolua_export
@@ -375,6 +388,9 @@ public:
/** If true the player can fly even when he's not in creative. */
void SetCanFly(bool a_CanFly);
+ /** Update movement-related statistics. */
+ void UpdateMovementStats(const Vector3d & a_DeltaPos);
+
/** Returns wheter the player can fly or not. */
virtual bool CanFly(void) const { return m_CanFly; }
// tolua_end
@@ -487,6 +503,8 @@ protected:
cTeam * m_Team;
+ cStatManager m_Stats;
+
void ResolvePermissions(void);
@@ -506,6 +524,9 @@ protected:
/** Called in each tick if the player is fishing to make sure the floater dissapears when the player doesn't have a fishing rod as equipped item. */
void HandleFloater(void);
+ /** Tosses a list of items. */
+ void TossItems(const cItems & a_Items);
+
/** Adds food exhaustion based on the difference between Pos and LastPos, sprinting status and swimming (in water block) */
void ApplyFoodExhaustionFromMovement();