changeset 160:bbb506feb55f

Server: implement new stats for character, closes #761 @2h Each character no longer have a global level integer. Instead, separate each stat with a different level, factor and experience which define the current stat level, the desired partition of experience (from 100%) for this stat and the current experience respectively.
author David Demelier <markand@malikania.fr>
date Mon, 05 Mar 2018 21:04:14 +0100
parents 7362fba6ff11
children be0d90706ded
files libdb-sqlite/malikania/server/db/sqlite_character_dao.cpp libdb-sqlite/malikania/server/db/sqlite_character_dao.hpp libdb-sqlite/malikania/server/db/sqlite_database.cpp libserver-test/malikania/server/db/broken_character_dao.cpp libserver-test/malikania/server/db/broken_character_dao.hpp libserver/malikania/server/db/account.hpp libserver/malikania/server/db/character.cpp libserver/malikania/server/db/character.hpp libserver/malikania/server/db/model.hpp libserver/malikania/server/db/spell.hpp tests/libserver/db/account/main.cpp
diffstat 11 files changed, 342 insertions(+), 55 deletions(-) [+]
line wrap: on
line diff
--- a/libdb-sqlite/malikania/server/db/sqlite_character_dao.cpp	Sat Mar 03 17:34:12 2018 +0100
+++ b/libdb-sqlite/malikania/server/db/sqlite_character_dao.cpp	Mon Mar 05 21:04:14 2018 +0100
@@ -16,8 +16,13 @@
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 
+#include <boost/format.hpp>
+
 #include "sqlite_database.hpp"
 
+using boost::format;
+using boost::str;
+
 namespace mlk {
 
 namespace server {
@@ -26,21 +31,62 @@
 {
     character ch(db_, to_string(stmt, 2), to_string(stmt, 3));
 
-    ch.set_level(to_int(stmt, 4));
     ch.set_id(to_int(stmt, 0));
+    ch.get_levels()[character::hp] = to_int(stmt, 4);
+    ch.get_factors()[character::hp] = to_int(stmt, 5);
+    ch.get_experience()[character::hp] = to_int(stmt, 6);
+    ch.get_levels()[character::defense] = to_int(stmt, 7);
+    ch.get_factors()[character::defense] = to_int(stmt, 8);
+    ch.get_experience()[character::defense] = to_int(stmt, 9);
+    ch.get_levels()[character::agility] = to_int(stmt, 10);
+    ch.get_factors()[character::agility] = to_int(stmt, 11);
+    ch.get_experience()[character::agility] = to_int(stmt, 12);
+    ch.get_levels()[character:: dodge] = to_int(stmt, 13);
+    ch.get_factors()[character:: dodge] = to_int(stmt, 14);
+    ch.get_experience()[character:: dodge] = to_int(stmt, 15);
+    ch.get_levels()[character::luck] = to_int(stmt, 16);
+    ch.get_factors()[character::luck] = to_int(stmt, 17);
+    ch.get_experience()[character::luck] = to_int(stmt, 18);
 
     return ch;
 }
 
-void sqlite_character_dao::set_level(const character& ch, std::uint16_t level)
+template <typename Array>
+void sqlite_character_dao::set_what(const character& ch, const Array& array, const std::string& key)
 {
-    const std::string sql(
+    const auto sql = str(format(
         "UPDATE character"
-        "   SET level = ?"
-        " WHERE id = ?"
+        "   SET hp_%1$ = ?"
+        "     , defense_%1% = ?"
+        "     , agility_%1$ = ?"
+        "     , dodge_%1% = ?"
+        "     , luck_%1% = ?"
+        " WHERE id = ?") % key
     );
 
-    exec(db_.instance(), sql, {level, ch.get_id()});
+    exec(db_.instance(), sql, {
+        array[character::hp],
+        array[character::defense],
+        array[character::agility],
+        array[character::dodge],
+        array[character::luck],
+        ch.get_id()
+    });
+}
+
+void sqlite_character_dao::set_levels(const character& ch, const std::array<uint8_t, 5>& levels)
+{
+    set_what(ch, levels, "level");
+}
+
+void sqlite_character_dao::set_factors(const character& ch, const std::array<uint8_t, 5>& factors)
+{
+    set_what(ch, factors, "factor");
+}
+
+void sqlite_character_dao::set_experience(const character& ch, const std::array<uint32_t, 5>& experience)
+{
+    set_what(ch, experience, "experience");
 }
 
 void sqlite_character_dao::publish(character& ch, const account& parent)
@@ -50,15 +96,43 @@
         "   account_id,"
         "   nickname,"
         "   classname,"
-        "   level"
-        ") VALUES (?, ?, ?, ?)"
+        "   hp_level,"
+        "   hp_factor,"
+        "   hp_experience,"
+        "   defense_level,"
+        "   defense_factor,"
+        "   defense_experience,"
+        "   agility_level,"
+        "   agility_factor,"
+        "   agility_experience,"
+        "   dodge_level,"
+        "   dodge_factor,"
+        "   dodge_experience,"
+        "   luck_level,"
+        "   luck_factor,"
+        "   luck_experience"
+        ") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
     );
 
     exec(db_.instance(), sql, {
         parent.get_id(),
         ch.get_nickname(),
         ch.get_classname(),
-        ch.get_level()
+        ch.get_levels()[character::hp],
+        ch.get_factors()[character::hp],
+        ch.get_experience()[character::hp],
+        ch.get_levels()[character::defense],
+        ch.get_factors()[character::defense],
+        ch.get_experience()[character::defense],
+        ch.get_levels()[character::agility],
+        ch.get_factors()[character::agility],
+        ch.get_experience()[character::agility],
+        ch.get_levels()[character::dodge],
+        ch.get_factors()[character::dodge],
+        ch.get_experience()[character::dodge],
+        ch.get_levels()[character::luck],
+        ch.get_factors()[character::luck],
+        ch.get_experience()[character::luck]
     });
 }
 
--- a/libdb-sqlite/malikania/server/db/sqlite_character_dao.hpp	Sat Mar 03 17:34:12 2018 +0100
+++ b/libdb-sqlite/malikania/server/db/sqlite_character_dao.hpp	Mon Mar 05 21:04:14 2018 +0100
@@ -44,6 +44,9 @@
 
     character get(stmt_ptr&);
 
+    template <typename Array>
+    void set_what(const character&, const Array&, const std::string&);
+
 public:
     /**
      * Constructor.
@@ -64,9 +67,19 @@
     std::vector<character> load(const account& parent);
 
     /**
-     * \copydoc character_dao::set_level
+     * \copydoc character_dao::set_levels
+     */
+    void set_levels(const character& ch, const std::array<uint8_t, 5>& levels) override;
+
+    /**
+     * \copydoc character_dao::set_factors
      */
-    void set_level(const character& ch, std::uint16_t level) override;
+    void set_factors(const character& ch, const std::array<uint8_t, 5>& levels) override;
+
+    /**
+     * \copydoc character_dao::set_experience
+     */
+    void set_experience(const character& ch, const std::array<uint32_t, 5>& experience) override;
 
     /**
      * \copydoc character_dao::publish
--- a/libdb-sqlite/malikania/server/db/sqlite_database.cpp	Sat Mar 03 17:34:12 2018 +0100
+++ b/libdb-sqlite/malikania/server/db/sqlite_database.cpp	Mon Mar 05 21:04:14 2018 +0100
@@ -43,7 +43,21 @@
     "    account_id INTEGER,"
     "    nickname TEXT,"
     "    classname TEXT,"
-    "    level INTEGER,"
+    "    hp_level INTEGER,"
+    "    hp_factor INTEGER,"
+    "    hp_experience INTEGER,"
+    "    defense_level INTEGER,"
+    "    defense_factor INTEGER,"
+    "    defense_experience INTEGER,"
+    "    agility_level INTEGER,"
+    "    agility_factor INTEGER,"
+    "    agility_experience INTEGER,"
+    "    dodge_level INTEGER,"
+    "    dodge_factor INTEGER,"
+    "    dodge_experience INTEGER,"
+    "    luck_level INTEGER,"
+    "    luck_factor INTEGER,"
+    "    luck_experience INTEGER,"
     "    FOREIGN KEY(account_id) REFERENCES account(id) ON DELETE CASCADE"
     ")"
 );
--- a/libserver-test/malikania/server/db/broken_character_dao.cpp	Sat Mar 03 17:34:12 2018 +0100
+++ b/libserver-test/malikania/server/db/broken_character_dao.cpp	Mon Mar 05 21:04:14 2018 +0100
@@ -38,10 +38,22 @@
         throw std::runtime_error("broken unpublish");
 }
 
-void broken_character_dao::set_level(const character&, std::uint16_t)
+void broken_character_dao::set_levels(const character&, const std::array<uint8_t, 5>&)
+{
+    if (!bool(allow_ & allow_flags::set_levels))
+        throw std::runtime_error("broken set_levels");
+}
+
+void broken_character_dao::set_factors(const character&, const std::array<uint8_t, 5>&)
 {
-    if (!bool(allow_ & allow_flags::set_level))
-        throw std::runtime_error("broken set_level");
+    if (!bool(allow_ & allow_flags::set_factors))
+        throw std::runtime_error("broken set_factors");
+}
+
+void broken_character_dao::set_experience(const character&, const std::array<uint32_t, 5>&)
+{
+    if (!bool(allow_ & allow_flags::set_experience))
+        throw std::runtime_error("broken set_experience");
 }
 
 } // !server
--- a/libserver-test/malikania/server/db/broken_character_dao.hpp	Sat Mar 03 17:34:12 2018 +0100
+++ b/libserver-test/malikania/server/db/broken_character_dao.hpp	Mon Mar 05 21:04:14 2018 +0100
@@ -42,7 +42,9 @@
         none                = 0,            //!< everything is broken
         publish             = (1 << 0),     //!< allow publish
         unpublish           = (1 << 1),     //!< allow unpublish
-        set_level           = (1 << 2),     //!< allow set_level
+        set_levels          = (1 << 2),     //!< allow set_levels
+        set_factors         = (1 << 3),     //!< allow set_factors
+        set_experience      = (1 << 4),     //!< allow set_experience
     };
 
 private:
@@ -60,9 +62,19 @@
     }
 
     /**
-     * \copydoc character_dao::set_level
+     * \copydoc character_dao::set_levels
+     */
+    void set_levels(const character& ch, const std::array<uint8_t, 5>& levels) override;
+
+    /**
+     * \copydoc character_dao::set_factors
      */
-    void set_level(const character& ch, std::uint16_t level) override;
+    void set_factors(const character& ch, const std::array<uint8_t, 5>& factors) override;
+
+    /**
+     * \copydoc character_dao::set_experience
+     */
+    void set_experience(const character& ch, const std::array<uint32_t, 5>& experience) override;
 
     /**
      * \copydoc character_dao::publish
--- a/libserver/malikania/server/db/account.hpp	Sat Mar 03 17:34:12 2018 +0100
+++ b/libserver/malikania/server/db/account.hpp	Mon Mar 05 21:04:14 2018 +0100
@@ -37,13 +37,13 @@
  * \brief Database account object.
  */
 class account : public model {
-protected:
-    std::string login_;                 //!< unique login (not null)
-    std::string password_;              //!< password stored as-is
-    std::string email_;                 //!< raw email
-    std::string firstname_;             //!< optional first name
-    std::string lastname_;              //!< optional last name
-    std::vector<character> characters_; //!< list of characters
+private:
+    std::string login_;
+    std::string password_;
+    std::string email_;
+    std::string firstname_;
+    std::string lastname_;
+    std::vector<character> characters_;
 
 public:
     /**
--- a/libserver/malikania/server/db/character.cpp	Sat Mar 03 17:34:12 2018 +0100
+++ b/libserver/malikania/server/db/character.cpp	Mon Mar 05 21:04:14 2018 +0100
@@ -16,6 +16,8 @@
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 
+#include <numeric>
+
 #include "character.hpp"
 #include "database.hpp"
 
@@ -23,12 +25,30 @@
 
 namespace server {
 
-void character::set_level(std::uint16_t level)
+void character::set_levels(std::array<std::uint8_t, 5> levels)
+{
+    if (is_published())
+        db_.characters().set_levels(*this, levels);
+
+    levels_ = std::move(levels);
+}
+
+void character::set_factors(std::array<std::uint8_t, 5> factors)
 {
-    if (is_published() && level_ != level)
-        db_.characters().set_level(*this, level);
+    assert(std::accumulate(factors.begin(), factors.end(), 0U) == 100U);
+
+    if (is_published())
+        db_.characters().set_factors(*this, factors);
 
-    level_ = level;
+    factors_ = std::move(factors);
+}
+
+void character::set_experience(std::array<std::uint32_t, 5> experience)
+{
+    if (is_published())
+        db_.characters().set_experience(*this, experience);
+
+    exp_ = std::move(experience);
 }
 
 void character::add(spell sp)
--- a/libserver/malikania/server/db/character.hpp	Sat Mar 03 17:34:12 2018 +0100
+++ b/libserver/malikania/server/db/character.hpp	Mon Mar 05 21:04:14 2018 +0100
@@ -24,6 +24,8 @@
  * \brief Database character object.
  */
 
+#include <cstdint>
+#include <array>
 #include <vector>
 
 #include "model.hpp"
@@ -39,11 +41,28 @@
  * \brief Database character object.
  */
 class character : public model {
-protected:
-    std::string nickname_;              //!< nickname (non null)
-    std::string classname_;             //!< class type to instanciate
-    std::uint16_t level_{1};            //!< character level
-    std::vector<spell> spells_;         //!< list of spells
+public:
+    /**
+     * \brief Stats index in arrays
+     * \see set_levels
+     * \see set_factors
+     * \see set_experience
+     */
+    enum stat {
+        hp = 0,             //!< hp index
+        defense,            //!< defense index
+        agility,            //!< agility index
+        dodge,              //!< dodge index
+        luck,               //!< luck index
+    };
+
+private:
+    std::string nickname_;
+    std::string classname_;
+    std::vector<spell> spells_;
+    std::array<std::uint8_t, 5> levels_{1U, 1U, 1U, 1U, 1U};
+    std::array<std::uint8_t, 5> factors_{20U, 20U, 20U, 20U, 20U};
+    std::array<std::uint32_t, 5> exp_{0U, 0U, 0U, 0U, 0U};
 
 public:
     /**
@@ -106,21 +125,95 @@
 #endif // !MALIKANIA_PRIVATE
 
     /**
-     * Get the account level.
+     * Get the list of levels.
      *
-     * \return the level
+     * \return the levels
+     */
+    inline const std::array<std::uint8_t, 5>& get_levels() const noexcept
+    {
+        return levels_;
+    }
+
+#if defined(MALIKANIA_PRIVATE)
+    /**
+     * Get the list of levels.
+     *
+     * \return the levels
+     * \note only available if MALIKANIA_PRIVATE macro is defined
      */
-    inline std::uint16_t get_level() const noexcept
+    inline std::array<std::uint8_t, 5>& get_levels() noexcept
     {
-        return level_;
+        return levels_;
+    }
+#endif // !MALIKANIA_PRIVATE
+
+    /**
+     * Set the new levels.
+     *
+     * \param levels the levels
+     */
+    void set_levels(std::array<std::uint8_t, 5> levels);
+
+    /**
+     * Get the list of factors.
+     *
+     * \return the factors
+     */
+    inline const std::array<std::uint8_t, 5>& get_factors() const noexcept
+    {
+        return factors_;
     }
 
+#if defined(MALIKANIA_PRIVATE)
     /**
-     * Set the account level.
+     * Get the list of factors.
+     *
+     * \return the factors
+     * \note only available if MALIKANIA_PRIVATE macro is defined
+     */
+    inline std::array<std::uint8_t, 5>& get_factors() noexcept
+    {
+        return factors_;
+    }
+#endif // !MALIKANIA_PRIVATE
+
+    /**
+     * Set progression factors.
+     *
+     * \pre factors sum must be 100
+     * \param factors the factors
+     */
+    void set_factors(std::array<std::uint8_t, 5> factors);
+
+    /**
+     * Get the stats experience.
      *
-     * \param level the level
+     * \return the experience
      */
-    void set_level(std::uint16_t level);
+    inline const std::array<std::uint32_t, 5>& get_experience() const noexcept
+    {
+        return exp_;
+    }
+
+#if defined(MALIKANIA_PRIVATE)
+    /**
+     * Get the stats experience.
+     *
+     * \return the experience
+     * \note only available if MALIKANIA_PRIVATE macro is defined
+     */
+    inline std::array<std::uint32_t, 5>& get_experience() noexcept
+    {
+        return exp_;
+    }
+#endif // !MALIKANIA_PRIVATE
+
+    /**
+     * Set stats experience.
+     *
+     * \param experience the experience
+     */
+    void set_experience(std::array<std::uint32_t, 5> experience);
 
     /**
      * Add a spell.
@@ -181,17 +274,34 @@
     virtual void unpublish(const character& ch) = 0;
 
     /**
-     * Update the character level in database.
+     * Update the character levels in database.
      *
-     * Only called when the level needs to be changed, the implementation does
-     * not need to update level_ field.
+     * \param ch the character
+     * \param levels the new levels
+     * \note called from character::set_levels helper
+     * \throw std::exception if the operation could not succeed
+     */
+    virtual void set_levels(const character& ch, const std::array<std::uint8_t, 5>& levels) = 0;
+
+    /**
+     * Update the character progression factors in database.
      *
      * \param ch the character
-     * \param level the new level
-     * \note called from character::set_level helper
+     * \param factors the new factors
+     * \note called from character::set_levels helper
      * \throw std::exception if the operation could not succeed
      */
-    virtual void set_level(const character& ch, std::uint16_t level) = 0;
+    virtual void set_factors(const character& ch, const std::array<std::uint8_t, 5>& levels) = 0;
+
+    /**
+     * Update the current level experiences in database.
+     *
+     * \param ch the character
+     * \param experience the new experience
+     * \note called from character::set_levels helper
+     * \throw std::exception if the operation could not succeed
+     */
+    virtual void set_experience(const character& ch, const std::array<std::uint32_t, 5>& levels) = 0;
 };
 
 } // !server
--- a/libserver/malikania/server/db/model.hpp	Sat Mar 03 17:34:12 2018 +0100
+++ b/libserver/malikania/server/db/model.hpp	Mon Mar 05 21:04:14 2018 +0100
@@ -24,7 +24,6 @@
  * \brief Abstract database object.
  */
 
-#include <cassert>
 #include <cstdint>
 
 namespace mlk {
--- a/libserver/malikania/server/db/spell.hpp	Sat Mar 03 17:34:12 2018 +0100
+++ b/libserver/malikania/server/db/spell.hpp	Mon Mar 05 21:04:14 2018 +0100
@@ -24,6 +24,7 @@
  * \brief Database spell object.
  */
 
+#include <cassert>
 #include <string>
 
 #include "model.hpp"
@@ -38,9 +39,9 @@
  * \brief Describe a spell.
  */
 class spell : public model {
-protected:
-    std::string classname_;             //!< class type to instanciate
-    std::uint8_t level_{1};             //!< spell level
+private:
+    std::string classname_;
+    std::uint8_t level_{1};
 
 public:
     /**
--- a/tests/libserver/db/account/main.cpp	Sat Mar 03 17:34:12 2018 +0100
+++ b/tests/libserver/db/account/main.cpp	Mon Mar 05 21:04:14 2018 +0100
@@ -153,14 +153,18 @@
     {
         character ch(db, "erekin", "blackmage");
 
-        ch.set_level(100);
+        ch.set_levels({10U, 7U, 12U, 9U, 8U});
+        ch.set_factors({20U, 20U, 20U, 20U, 20U});
+        ch.set_experience({1000U, 2000U, 3000U, 4000U, 5000U});
         ac.add(std::move(ch));
     }
 
     {
         character ch(db, "luna", "fairy");
 
-        ch.set_level(45);
+        ch.set_levels({12U, 6U, 12U, 9U, 8U});
+        ch.set_factors({20U, 20U, 20U, 20U, 20U});
+        ch.set_experience({2000U, 3000U, 4000U, 5000U, 6000U});
         ac.add(std::move(ch));
     }
 }
@@ -188,7 +192,21 @@
 
     BOOST_TEST(erekin->get_nickname() == "erekin");
     BOOST_TEST(erekin->get_classname() == "blackmage");
-    BOOST_TEST(erekin->get_level() == 100U);
+    BOOST_TEST(erekin->get_levels()[character::hp] == 10U);
+    BOOST_TEST(erekin->get_levels()[character::defense] == 7U);
+    BOOST_TEST(erekin->get_levels()[character::agility] == 12U);
+    BOOST_TEST(erekin->get_levels()[character::dodge] == 9U);
+    BOOST_TEST(erekin->get_levels()[character::luck] == 8U);
+    BOOST_TEST(erekin->get_factors()[character::hp] == 20U);
+    BOOST_TEST(erekin->get_factors()[character::defense] == 20U);
+    BOOST_TEST(erekin->get_factors()[character::agility] == 20U);
+    BOOST_TEST(erekin->get_factors()[character::dodge] == 20U);
+    BOOST_TEST(erekin->get_factors()[character::luck] == 20U);
+    BOOST_TEST(erekin->get_experience()[character::hp] == 1000U);
+    BOOST_TEST(erekin->get_experience()[character::defense] == 2000U);
+    BOOST_TEST(erekin->get_experience()[character::agility] == 3000U);
+    BOOST_TEST(erekin->get_experience()[character::dodge] == 4000U);
+    BOOST_TEST(erekin->get_experience()[character::luck] == 5000U);
 
     const auto luna = std::find_if(characters.begin(), characters.end(), [] (const auto& c) {
         return c.get_nickname() == "luna";
@@ -196,7 +214,21 @@
 
     BOOST_TEST(luna->get_nickname() == "luna");
     BOOST_TEST(luna->get_classname() == "fairy");
-    BOOST_TEST(luna->get_level() == 45U);
+    BOOST_TEST(luna->get_levels()[character::hp] == 12U);
+    BOOST_TEST(luna->get_levels()[character::defense] == 6U);
+    BOOST_TEST(luna->get_levels()[character::agility] == 12U);
+    BOOST_TEST(luna->get_levels()[character::dodge] == 9U);
+    BOOST_TEST(luna->get_levels()[character::luck] == 8U);
+    BOOST_TEST(luna->get_factors()[character::hp] == 20U);
+    BOOST_TEST(luna->get_factors()[character::defense] == 20U);
+    BOOST_TEST(luna->get_factors()[character::agility] == 20U);
+    BOOST_TEST(luna->get_factors()[character::dodge] == 20U);
+    BOOST_TEST(luna->get_factors()[character::luck] == 20U);
+    BOOST_TEST(luna->get_experience()[character::hp] == 2000U);
+    BOOST_TEST(luna->get_experience()[character::defense] == 3000U);
+    BOOST_TEST(luna->get_experience()[character::agility] == 4000U);
+    BOOST_TEST(luna->get_experience()[character::dodge] == 5000U);
+    BOOST_TEST(luna->get_experience()[character::luck] == 6000U);
 }
 
 BOOST_AUTO_TEST_CASE_TEMPLATE(set_password, Database, database_types)