diff --git a/config.lua b/config.lua index 5a68061..72de76c 100644 --- a/config.lua +++ b/config.lua @@ -1,6 +1,6 @@ -- Custom -knightCloseAttackDamageIncreasePercent = 50 -paladinRangeAttackDamageIncreasePercent = 40 +knightCloseAttackDamageIncreasePercent = 20 +paladinRangeAttackDamageIncreasePercent = 15 -- Combat settings -- NOTE: valid values for worldType are: "pvp", "no-pvp" and "pvp-enforced" diff --git a/data/actions/actions.xml b/data/actions/actions.xml index c2ee247..e05c8ad 100644 --- a/data/actions/actions.xml +++ b/data/actions/actions.xml @@ -239,6 +239,7 @@ + diff --git a/data/actions/scripts/misc/skill_trainer.lua b/data/actions/scripts/misc/skill_trainer.lua new file mode 100644 index 0000000..a35d2c6 --- /dev/null +++ b/data/actions/scripts/misc/skill_trainer.lua @@ -0,0 +1,23 @@ +local statues = { + [2032] = SKILL_SWORD, + [18489] = SKILL_AXE, + [18490] = SKILL_CLUB, + [18491] = SKILL_DISTANCE, + [18492] = SKILL_MAGLEVEL +} + +function onUse(player, item, fromPosition, target, toPosition, isHotkey) + local skill = statues[item:getId()] + if not player:isPremium() then + player:sendCancelMessage(RETURNVALUE_YOUNEEDPREMIUMACCOUNT) + return true + end + + if player:isPzLocked() then + return false + end + + player:setOfflineTrainingSkill(skill) + player:remove() + return true +end diff --git a/data/creaturescripts/creaturescripts.xml b/data/creaturescripts/creaturescripts.xml index bf0570a..c2dc909 100644 --- a/data/creaturescripts/creaturescripts.xml +++ b/data/creaturescripts/creaturescripts.xml @@ -6,7 +6,8 @@ - + + diff --git a/data/creaturescripts/scripts/offlinetraining.lua b/data/creaturescripts/scripts/offlinetraining.lua new file mode 100644 index 0000000..325f6dd --- /dev/null +++ b/data/creaturescripts/scripts/offlinetraining.lua @@ -0,0 +1,75 @@ +function onLogin(player) + local lastLogout = player:getLastLogout() + local offlineTime = lastLogout ~= 0 and math.min(os.time() - lastLogout, 86400 * 21) or 0 + local offlineTrainingSkill = player:getOfflineTrainingSkill() + if offlineTrainingSkill == -1 then + player:addOfflineTrainingTime(offlineTime * 1000) + return true + end + + player:setOfflineTrainingSkill(-1) + + if offlineTime < 600 then + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You must be logged out for more than 10 minutes to start offline training.") + return true + end + + local trainingTime = math.max(0, math.min(offlineTime, math.min(43200, player:getOfflineTrainingTime() / 1000))) + player:removeOfflineTrainingTime(trainingTime * 1000) + + local remainder = offlineTime - trainingTime + if remainder > 0 then + player:addOfflineTrainingTime(remainder * 1000) + end + + if trainingTime < 60 then + return true + end + + local text = "During your absence you trained for" + local hours = math.floor(trainingTime / 3600) + if hours > 1 then + text = string.format("%s %d hours", text, hours) + elseif hours == 1 then + text = string.format("%s 1 hour", text) + end + + local minutes = math.floor((trainingTime % 3600) / 60) + if minutes ~= 0 then + if hours ~= 0 then + text = string.format("%s and", text) + end + + if minutes > 1 then + text = string.format("%s %d minutes", text, minutes) + else + text = string.format("%s 1 minute", text) + end + end + + text = string.format("%s.", text) + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, text) + + local vocation = player:getVocation() + local promotion = vocation:getPromotion() + local topVocation = not promotion and vocation or promotion + + local updateSkills = false + if table.contains({SKILL_CLUB, SKILL_SWORD, SKILL_AXE, SKILL_DISTANCE}, offlineTrainingSkill) then + local modifier = topVocation:getAttackSpeed() / 1000 + updateSkills = player:addOfflineTrainingTries(offlineTrainingSkill, (trainingTime / modifier) / (offlineTrainingSkill == SKILL_DISTANCE and 4 or 2)) + elseif offlineTrainingSkill == SKILL_MAGLEVEL then + local gainTicks = topVocation:getManaGainTicks() * 2 + if gainTicks == 0 then + gainTicks = 1 + end + + updateSkills = player:addOfflineTrainingTries(SKILL_MAGLEVEL, trainingTime * (vocation:getManaGainAmount() / gainTicks)) + end + + if updateSkills then + player:addOfflineTrainingTries(SKILL_SHIELD, trainingTime / 4) + end + + return true +end diff --git a/data/lib/compat/compat.lua b/data/lib/compat/compat.lua index aed2428..789451e 100644 --- a/data/lib/compat/compat.lua +++ b/data/lib/compat/compat.lua @@ -372,6 +372,7 @@ function setPlayerGroupId(cid, groupId) local p = Player(cid) return p ~= nil an function doPlayerSetSex(cid, sex) local p = Player(cid) return p ~= nil and p:setSex(sex) or false end function doPlayerSetGuildLevel(cid, level) local p = Player(cid) return p ~= nil and p:setGuildLevel(level) or false end function doPlayerSetGuildNick(cid, nick) local p = Player(cid) return p ~= nil and p:setGuildNick(nick) or false end +function doPlayerSetOfflineTrainingSkill(cid, skillId) local p = Player(cid) return p and p:setOfflineTrainingSkill(skillId) or false end function doShowTextDialog(cid, itemId, text) local p = Player(cid) return p ~= nil and p:showTextDialog(itemId, text) or false end function doPlayerAddItemEx(cid, uid, ...) local p = Player(cid) return p ~= nil and p:addItemEx(Item(uid), ...) or false end function doPlayerRemoveItem(cid, itemid, count, ...) local p = Player(cid) return p ~= nil and p:removeItem(itemid, count, ...) or false end diff --git a/sabrehaven.sql b/sabrehaven.sql index 6dd3015..e0a2b8b 100644 --- a/sabrehaven.sql +++ b/sabrehaven.sql @@ -1138,6 +1138,8 @@ CREATE TABLE `players` ( `onlinetime` int(11) NOT NULL DEFAULT '0', `deletion` bigint(15) NOT NULL DEFAULT '0', `balance` bigint(20) UNSIGNED NOT NULL DEFAULT '0', + `offlinetraining_time` smallint(5) unsigned NOT NULL DEFAULT '43200', + `offlinetraining_skill` int(11) NOT NULL DEFAULT '-1', `stamina` smallint(5) NOT NULL DEFAULT '3360', `skill_fist` int(10) UNSIGNED NOT NULL DEFAULT '10', `skill_fist_tries` bigint(20) UNSIGNED NOT NULL DEFAULT '0', diff --git a/src/iologindata.cpp b/src/iologindata.cpp index 8501bd8..ebe3a04 100644 --- a/src/iologindata.cpp +++ b/src/iologindata.cpp @@ -190,7 +190,7 @@ bool IOLoginData::preloadPlayer(Player* player, const std::string& name) bool IOLoginData::loadPlayerById(Player* player, uint32_t id) { std::ostringstream query; - query << "SELECT `id`, `name`, `account_id`, `group_id`, `sex`, `vocation`, `experience`, `level`, `maglevel`, `health`, `healthmax`, `blessings`, `mana`, `manamax`, `manaspent`, `soul`, `lookbody`, `lookfeet`, `lookhead`, `looklegs`, `looktype`, `lookaddons`, `posx`, `posy`, `posz`, `cap`, `lastlogin`, `lastlogout`, `lastip`, `conditions`, `skulltime`, `skull`, `town_id`, `balance`, `stamina`, `skill_fist`, `skill_fist_tries`, `skill_club`, `skill_club_tries`, `skill_sword`, `skill_sword_tries`, `skill_axe`, `skill_axe_tries`, `skill_dist`, `skill_dist_tries`, `skill_shielding`, `skill_shielding_tries`, `skill_fishing`, `skill_fishing_tries` FROM `players` WHERE `id` = " << id; + query << "SELECT `id`, `name`, `account_id`, `group_id`, `sex`, `vocation`, `experience`, `level`, `maglevel`, `health`, `healthmax`, `blessings`, `mana`, `manamax`, `manaspent`, `soul`, `lookbody`, `lookfeet`, `lookhead`, `looklegs`, `looktype`, `lookaddons`, `posx`, `posy`, `posz`, `cap`, `lastlogin`, `lastlogout`, `lastip`, `conditions`, `skulltime`, `skull`, `town_id`, `balance`, `offlinetraining_time`, `offlinetraining_skill`, `stamina`, `skill_fist`, `skill_fist_tries`, `skill_club`, `skill_club_tries`, `skill_sword`, `skill_sword_tries`, `skill_axe`, `skill_axe_tries`, `skill_dist`, `skill_dist_tries`, `skill_shielding`, `skill_shielding_tries`, `skill_fishing`, `skill_fishing_tries` FROM `players` WHERE `id` = " << id; return loadPlayer(player, Database::getInstance()->storeQuery(query.str())); } @@ -198,7 +198,7 @@ bool IOLoginData::loadPlayerByName(Player* player, const std::string& name) { Database* db = Database::getInstance(); std::ostringstream query; - query << "SELECT `id`, `name`, `account_id`, `group_id`, `sex`, `vocation`, `experience`, `level`, `maglevel`, `health`, `healthmax`, `blessings`, `mana`, `manamax`, `manaspent`, `soul`, `lookbody`, `lookfeet`, `lookhead`, `looklegs`, `looktype`, `lookaddons`, `posx`, `posy`, `posz`, `cap`, `lastlogin`, `lastlogout`, `lastip`, `conditions`, `skulltime`, `skull`, `town_id`, `balance`, `stamina`, `skill_fist`, `skill_fist_tries`, `skill_club`, `skill_club_tries`, `skill_sword`, `skill_sword_tries`, `skill_axe`, `skill_axe_tries`, `skill_dist`, `skill_dist_tries`, `skill_shielding`, `skill_shielding_tries`, `skill_fishing`, `skill_fishing_tries` FROM `players` WHERE `name` = " << db->escapeString(name); + query << "SELECT `id`, `name`, `account_id`, `group_id`, `sex`, `vocation`, `experience`, `level`, `maglevel`, `health`, `healthmax`, `blessings`, `mana`, `manamax`, `manaspent`, `soul`, `lookbody`, `lookfeet`, `lookhead`, `looklegs`, `looktype`, `lookaddons`, `posx`, `posy`, `posz`, `cap`, `lastlogin`, `lastlogout`, `lastip`, `conditions`, `skulltime`, `skull`, `town_id`, `balance`, `offlinetraining_time`, `offlinetraining_skill`, `stamina`, `skill_fist`, `skill_fist_tries`, `skill_club`, `skill_club_tries`, `skill_sword`, `skill_sword_tries`, `skill_axe`, `skill_axe_tries`, `skill_dist`, `skill_dist_tries`, `skill_shielding`, `skill_shielding_tries`, `skill_fishing`, `skill_fishing_tries` FROM `players` WHERE `name` = " << db->escapeString(name); return loadPlayer(player, db->storeQuery(query.str())); } @@ -321,6 +321,9 @@ bool IOLoginData::loadPlayer(Player* player, DBResult_ptr result) player->lastLoginSaved = result->getNumber("lastlogin"); player->lastLogout = result->getNumber("lastlogout"); + player->offlineTrainingTime = result->getNumber("offlinetraining_time") * 1000; + player->offlineTrainingSkill = result->getNumber("offlinetraining_skill"); + Town* town = g_game.map.towns.getTown(result->getNumber("town_id")); if (!town) { std::cout << "[Error - IOLoginData::loadPlayer] " << player->name << " has Town ID " << result->getNumber("town_id") << " which doesn't exist" << std::endl; @@ -660,6 +663,8 @@ bool IOLoginData::savePlayer(Player* player) query << "`lastlogout` = " << player->getLastLogout() << ','; query << "`balance` = " << player->bankBalance << ','; + query << "`offlinetraining_time` = " << player->getOfflineTrainingTime() / 1000 << ','; + query << "`offlinetraining_skill` = " << player->getOfflineTrainingSkill() << ','; query << "`stamina` = " << player->getStaminaMinutes() << ','; query << "`skill_fist` = " << player->skills[SKILL_FIST].level << ','; diff --git a/src/luascript.cpp b/src/luascript.cpp index 51015e0..b207b86 100644 --- a/src/luascript.cpp +++ b/src/luascript.cpp @@ -2008,6 +2008,15 @@ void LuaScriptInterface::registerFunctions() registerMethod("Player", "getSkillTries", LuaScriptInterface::luaPlayerGetSkillTries); registerMethod("Player", "addSkillTries", LuaScriptInterface::luaPlayerAddSkillTries); + registerMethod("Player", "addOfflineTrainingTime", LuaScriptInterface::luaPlayerAddOfflineTrainingTime); + registerMethod("Player", "getOfflineTrainingTime", LuaScriptInterface::luaPlayerGetOfflineTrainingTime); + registerMethod("Player", "removeOfflineTrainingTime", LuaScriptInterface::luaPlayerRemoveOfflineTrainingTime); + + registerMethod("Player", "addOfflineTrainingTries", LuaScriptInterface::luaPlayerAddOfflineTrainingTries); + + registerMethod("Player", "getOfflineTrainingSkill", LuaScriptInterface::luaPlayerGetOfflineTrainingSkill); + registerMethod("Player", "setOfflineTrainingSkill", LuaScriptInterface::luaPlayerSetOfflineTrainingSkill); + registerMethod("Player", "getItemCount", LuaScriptInterface::luaPlayerGetItemCount); registerMethod("Player", "getItemById", LuaScriptInterface::luaPlayerGetItemById); @@ -7530,6 +7539,95 @@ int LuaScriptInterface::luaPlayerAddSkillTries(lua_State* L) return 1; } +int LuaScriptInterface::luaPlayerAddOfflineTrainingTime(lua_State* L) +{ + // player:addOfflineTrainingTime(time) + Player* player = getUserdata(L, 1); + if (player) { + int32_t time = getNumber(L, 2); + player->addOfflineTrainingTime(time); + player->sendStats(); + pushBoolean(L, true); + } + else { + lua_pushnil(L); + } + return 1; +} + + +int LuaScriptInterface::luaPlayerGetOfflineTrainingTime(lua_State* L) +{ + // player:getOfflineTrainingTime() + Player* player = getUserdata(L, 1); + if (player) { + lua_pushnumber(L, player->getOfflineTrainingTime()); + } + else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerRemoveOfflineTrainingTime(lua_State* L) +{ + // player:removeOfflineTrainingTime(time) + Player* player = getUserdata(L, 1); + if (player) { + int32_t time = getNumber(L, 2); + player->removeOfflineTrainingTime(time); + player->sendStats(); + pushBoolean(L, true); + } + else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerAddOfflineTrainingTries(lua_State* L) +{ + // player:addOfflineTrainingTries(skillType, tries) + Player* player = getUserdata(L, 1); + if (player) { + skills_t skillType = getNumber(L, 2); + uint64_t tries = getNumber(L, 3); + pushBoolean(L, player->addOfflineTrainingTries(skillType, tries)); + } + else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerGetOfflineTrainingSkill(lua_State* L) +{ + // player:getOfflineTrainingSkill() + Player* player = getUserdata(L, 1); + if (player) { + lua_pushnumber(L, player->getOfflineTrainingSkill()); + } + else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerSetOfflineTrainingSkill(lua_State* L) +{ + // player:setOfflineTrainingSkill(skillId) + Player* player = getUserdata(L, 1); + if (player) { + uint32_t skillId = getNumber(L, 2); + player->setOfflineTrainingSkill(skillId); + pushBoolean(L, true); + } + else { + lua_pushnil(L); + } + return 1; +} + int LuaScriptInterface::luaPlayerGetItemCount(lua_State* L) { // player:getItemCount(itemId[, subType = -1]) diff --git a/src/luascript.h b/src/luascript.h index 3c8467f..7f9508e 100644 --- a/src/luascript.h +++ b/src/luascript.h @@ -833,6 +833,15 @@ class LuaScriptInterface static int luaPlayerGetSkillTries(lua_State* L); static int luaPlayerAddSkillTries(lua_State* L); + static int luaPlayerAddOfflineTrainingTime(lua_State* L); + static int luaPlayerGetOfflineTrainingTime(lua_State* L); + static int luaPlayerRemoveOfflineTrainingTime(lua_State* L); + + static int luaPlayerAddOfflineTrainingTries(lua_State* L); + + static int luaPlayerGetOfflineTrainingSkill(lua_State* L); + static int luaPlayerSetOfflineTrainingSkill(lua_State* L); + static int luaPlayerGetItemCount(lua_State* L); static int luaPlayerGetItemById(lua_State* L); diff --git a/src/player.cpp b/src/player.cpp index 96ac98b..597da0e 100644 --- a/src/player.cpp +++ b/src/player.cpp @@ -1194,6 +1194,8 @@ void Player::onThink(uint32_t interval) if (g_game.getWorldType() != WORLD_TYPE_PVP_ENFORCED) { checkSkullTicks(); } + + addOfflineTrainingTime(interval); } uint32_t Player::isMuted() const @@ -3737,6 +3739,141 @@ void Player::sendClosePrivate(uint16_t channelId) } } +bool Player::addOfflineTrainingTries(skills_t skill, uint64_t tries) +{ + if (tries == 0 || skill == SKILL_LEVEL) { + return false; + } + + bool sendUpdate = false; + uint32_t oldSkillValue, newSkillValue; + long double oldPercentToNextLevel, newPercentToNextLevel; + + if (skill == SKILL_MAGLEVEL) { + uint64_t currReqMana = vocation->getReqMana(magLevel); + uint64_t nextReqMana = vocation->getReqMana(magLevel + 1); + + if (currReqMana >= nextReqMana) { + return false; + } + + oldSkillValue = magLevel; + oldPercentToNextLevel = static_cast(manaSpent * 100) / nextReqMana; + + g_events->eventPlayerOnGainSkillTries(this, SKILL_MAGLEVEL, tries); + uint32_t currMagLevel = magLevel; + + while ((manaSpent + tries) >= nextReqMana) { + tries -= nextReqMana - manaSpent; + + magLevel++; + manaSpent = 0; + + g_creatureEvents->playerAdvance(this, SKILL_MAGLEVEL, magLevel - 1, magLevel); + + sendUpdate = true; + currReqMana = nextReqMana; + nextReqMana = vocation->getReqMana(magLevel + 1); + + if (currReqMana >= nextReqMana) { + tries = 0; + break; + } + } + + manaSpent += tries; + + if (magLevel != currMagLevel) { + std::ostringstream ss; + ss << "You advanced to magic level " << magLevel << '.'; + sendTextMessage(MESSAGE_EVENT_ADVANCE, ss.str()); + } + + uint8_t newPercent; + if (nextReqMana > currReqMana) { + newPercent = Player::getPercentLevel(manaSpent, nextReqMana); + newPercentToNextLevel = static_cast(manaSpent * 100) / nextReqMana; + } + else { + newPercent = 0; + newPercentToNextLevel = 0; + } + + if (newPercent != magLevelPercent) { + magLevelPercent = newPercent; + sendUpdate = true; + } + + newSkillValue = magLevel; + } + else { + uint64_t currReqTries = vocation->getReqSkillTries(skill, skills[skill].level); + uint64_t nextReqTries = vocation->getReqSkillTries(skill, skills[skill].level + 1); + if (currReqTries >= nextReqTries) { + return false; + } + + oldSkillValue = skills[skill].level; + oldPercentToNextLevel = static_cast(skills[skill].tries * 100) / nextReqTries; + + g_events->eventPlayerOnGainSkillTries(this, skill, tries); + uint32_t currSkillLevel = skills[skill].level; + + while ((skills[skill].tries + tries) >= nextReqTries) { + tries -= nextReqTries - skills[skill].tries; + + skills[skill].level++; + skills[skill].tries = 0; + skills[skill].percent = 0; + + g_creatureEvents->playerAdvance(this, skill, (skills[skill].level - 1), skills[skill].level); + + sendUpdate = true; + currReqTries = nextReqTries; + nextReqTries = vocation->getReqSkillTries(skill, skills[skill].level + 1); + + if (currReqTries >= nextReqTries) { + tries = 0; + break; + } + } + + skills[skill].tries += tries; + + if (currSkillLevel != skills[skill].level) { + std::ostringstream ss; + ss << "You advanced to " << getSkillName(skill) << " level " << skills[skill].level << '.'; + sendTextMessage(MESSAGE_EVENT_ADVANCE, ss.str()); + } + + uint8_t newPercent; + if (nextReqTries > currReqTries) { + newPercent = Player::getPercentLevel(skills[skill].tries, nextReqTries); + newPercentToNextLevel = static_cast(skills[skill].tries * 100) / nextReqTries; + } + else { + newPercent = 0; + newPercentToNextLevel = 0; + } + + if (skills[skill].percent != newPercent) { + skills[skill].percent = newPercent; + sendUpdate = true; + } + + newSkillValue = skills[skill].level; + } + + if (sendUpdate) { + sendSkills(); + } + + std::ostringstream ss; + ss << std::fixed << std::setprecision(2) << "Your " << ucwords(getSkillName(skill)) << " skill changed from level " << oldSkillValue << " (with " << oldPercentToNextLevel << "% progress towards level " << (oldSkillValue + 1) << ") to level " << newSkillValue << " (with " << newPercentToNextLevel << "% progress towards level " << (newSkillValue + 1) << ')'; + sendTextMessage(MESSAGE_EVENT_ADVANCE, ss.str()); + return sendUpdate; +} + uint64_t Player::getMoney() const { std::vector containers; diff --git a/src/player.h b/src/player.h index 15703a4..eb55b22 100644 --- a/src/player.h +++ b/src/player.h @@ -154,6 +154,25 @@ class Player final : public Creature, public Cylinder return staminaMinutes; } + bool addOfflineTrainingTries(skills_t skill, uint64_t tries); + + void addOfflineTrainingTime(int32_t addTime) { + offlineTrainingTime = std::min(12 * 3600 * 1000, offlineTrainingTime + addTime); + } + void removeOfflineTrainingTime(int32_t removeTime) { + offlineTrainingTime = std::max(0, offlineTrainingTime - removeTime); + } + int32_t getOfflineTrainingTime() const { + return offlineTrainingTime; + } + + int32_t getOfflineTrainingSkill() const { + return offlineTrainingSkill; + } + void setOfflineTrainingSkill(int32_t skill) { + offlineTrainingSkill = skill; + } + uint64_t getBankBalance() const { return bankBalance; } @@ -1069,6 +1088,8 @@ class Player final : public Creature, public Cylinder int32_t premiumDays = 0; int32_t bloodHitCount = 0; int32_t shieldBlockCount = 0; + int32_t offlineTrainingSkill = -1; + int32_t offlineTrainingTime = 0; int32_t idleTime = 0; int32_t lastWalkingTime = 0;