diff --git a/.lua-format b/.lua-format
index df17791..8ca0b8c 100644
--- a/.lua-format
+++ b/.lua-format
@@ -3,4 +3,4 @@ continuation_indent_width: 1
use_tab: true
tab_width: 4
chop_down_table: true
-column_limit: 100
\ No newline at end of file
+column_limit: 90
\ No newline at end of file
diff --git a/data/creaturescripts/scripts/login.lua b/data/creaturescripts/scripts/login.lua
index ce79844..7fc7e53 100644
--- a/data/creaturescripts/scripts/login.lua
+++ b/data/creaturescripts/scripts/login.lua
@@ -15,7 +15,7 @@ function onLogin(player)
local promotion = vocation:getPromotion()
if player:isPremium() then
local value = player:getStorageValue(PlayerStorageKeys.promotion)
- if value == 1 then player:setVocation(promotion) end
+ if value and value == 1 then player:setVocation(promotion) end
elseif not promotion then
player:setVocation(vocation:getDemotion())
end
@@ -23,5 +23,8 @@ function onLogin(player)
-- Events
player:registerEvent("PlayerDeath")
player:registerEvent("DropLoot")
+
+ -- Update Experience Rate Stamina
+ player:updateStamina()
return true
end
diff --git a/data/creaturescripts/scripts/logout.lua b/data/creaturescripts/scripts/logout.lua
index 80c52b3..6438881 100644
--- a/data/creaturescripts/scripts/logout.lua
+++ b/data/creaturescripts/scripts/logout.lua
@@ -1,5 +1,5 @@
function onLogout(player)
local playerId = player:getId()
- if nextUseStaminaTime[playerId] then nextUseStaminaTime[playerId] = nil end
+ nextUseStaminaTime[playerId] = nil
return true
end
diff --git a/data/creaturescripts/scripts/playerdeath.lua b/data/creaturescripts/scripts/playerdeath.lua
index a58dcfd..23f2da6 100644
--- a/data/creaturescripts/scripts/playerdeath.lua
+++ b/data/creaturescripts/scripts/playerdeath.lua
@@ -1,85 +1,97 @@
local deathListEnabled = true
local maxDeathRecords = 5
+local playerDeathQuery =
+ "INSERT INTO `player_deaths` (`player_id`, `time`, `level`, `killed_by`, `is_player`, `mostdamage_by`, `mostdamage_is_player`, `unjustified`, `mostdamage_unjustified`) VALUES (%d, %d, %d, %s, %d, %s, %d, %d, %d)"
+local format = string.format
+
+---@param killer Creature
+---@return boolean, string
local function getKiller(killer)
if not killer then return false, "field item" end
if killer:isPlayer() then return true, killer:getName() end
local master = killer:getMaster()
- if master and master ~= killer and master:isPlayer() then
- return true, master:getName()
- end
+ if master and master ~= killer and master:isPlayer() then return true, master:getName() end
return false, killer:getName()
end
-function onDeath(player, corpse, killer, mostDamageKiller, lastHitUnjustified,
- mostDamageUnjustified)
- local playerId = player:getId()
- if nextUseStaminaTime[playerId] then nextUseStaminaTime[playerId] = nil end
-
- player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You are dead.")
- if not deathListEnabled then return end
-
- local byPlayer, killerName = getKiller(killer)
- local byPlayerMostDamage, killerNameMostDamage = getKiller(mostDamageKiller)
-
- local playerGuid = player:getGuid()
- db.query(
- "INSERT INTO `player_deaths` (`player_id`, `time`, `level`, `killed_by`, `is_player`, `mostdamage_by`, `mostdamage_is_player`, `unjustified`, `mostdamage_unjustified`) VALUES (" ..
- playerGuid .. ", " .. os.time() .. ", " .. player:getLevel() .. ", " ..
- db.escapeString(killerName) .. ", " .. (byPlayer and 1 or 0) .. ", " ..
- db.escapeString(killerNameMostDamage) .. ", " ..
- (byPlayerMostDamage and 1 or 0) .. ", " .. (lastHitUnjustified and 1 or 0) ..
- ", " .. (mostDamageUnjustified and 1 or 0) .. ")")
- local resultId = db.storeQuery(
- "SELECT `player_id` FROM `player_deaths` WHERE `player_id` = " ..
- playerGuid)
+---@param playerId integer
+---@param playerName string
+---@param killerId integer
+---@param playerGuid integer
+---@param byPlayer boolean
+---@param killerName string
+---@param playerGuildId integer
+---@param killerGuildId integer
+---@param timeNow integer
+---@return nil
+local function playerDeathSuccess(playerId, playerName, killerId, playerGuid, byPlayer, killerName, playerGuildId, killerGuildId,
+ timeNow)
+ local resultId = db.storeQuery("SELECT `player_id` FROM `player_deaths` WHERE `player_id` = " .. playerGuid)
+ if not resultId then return end
local deathRecords = 0
- local tmpResultId = resultId
- while tmpResultId ~= false do
+ local tmpResultId = true
+ while tmpResultId do
tmpResultId = result.next(resultId)
deathRecords = deathRecords + 1
end
- if resultId ~= false then result.free(resultId) end
+ result.free(resultId)
local limit = deathRecords - maxDeathRecords
if limit > 0 then
- db.asyncQuery(
- "DELETE FROM `player_deaths` WHERE `player_id` = " .. playerGuid ..
- " ORDER BY `time` LIMIT " .. limit)
+ db.asyncQuery(format("DELETE FROM `player_deaths` WHERE `player_id` = %d ORDER BY `time` LIMIT %d", playerGuid, limit))
end
if byPlayer then
- local targetGuild = player:getGuild()
- targetGuild = targetGuild and targetGuild:getId() or 0
- if targetGuild ~= 0 then
- local killerGuild = killer:getGuild()
- killerGuild = killerGuild and killerGuild:getId() or 0
- if killerGuild ~= 0 and targetGuild ~= killerGuild and
- isInWar(playerId, killer:getId()) then
- local warId = false
- resultId = db.storeQuery(
- "SELECT `id` FROM `guild_wars` WHERE `status` = 1 AND ((`guild1` = " ..
- killerGuild .. " AND `guild2` = " .. targetGuild ..
- ") OR (`guild1` = " .. targetGuild .. " AND `guild2` = " ..
- killerGuild .. "))")
- if resultId ~= false then
+ if playerGuildId ~= 0 then
+ if killerGuildId ~= 0 and playerGuildId ~= killerGuildId and isInWar(playerId, killerId) then
+ resultId = db.storeQuery(format(
+ "SELECT `id` FROM `guild_wars` WHERE `status` = 1 AND ((`guild1` = %d AND `guild2` = %d) OR (`guild1` = %d AND `guild2` = %d))",
+ killerGuildId, playerGuildId, playerGuildId, killerGuildId))
+
+ local warId = nil
+ if resultId then
warId = result.getNumber(resultId, "id")
result.free(resultId)
end
- if warId ~= false then
- db.asyncQuery(
- "INSERT INTO `guildwar_kills` (`killer`, `target`, `killerguild`, `targetguild`, `time`, `warid`) VALUES (" ..
- db.escapeString(killerName) .. ", " .. db.escapeString(player:getName()) ..
- ", " .. killerGuild .. ", " .. targetGuild .. ", " .. os.time() .. ", " ..
- warId .. ")")
+ if warId then
+ db.asyncQuery(format(
+ "INSERT INTO `guildwar_kills` (`killer`, `target`, `killerguild`, `targetguild`, `time`, `warid`) VALUES (%s, %s, %d, %d, %d, %d)",
+ db.escapeString(killerName), db.escapeString(playerName), killerGuildId, playerGuildId, timeNow, warId))
end
end
end
end
end
+
+function onDeath(player, corpse, killer, mostDamageKiller, lastHitUnjustified, mostDamageUnjustified)
+ local playerId = player:getId()
+ nextUseStaminaTime[playerId] = nil
+
+ player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You are dead.")
+ if not deathListEnabled then return end
+
+ local timeNow = os.time()
+ local byPlayer, killerName = getKiller(killer)
+ local byPlayerMostDamage, killerNameMostDamage = getKiller(mostDamageKiller)
+ local playerGuid = player:getGuid()
+ local playerName = player:getName()
+ local playerGuild = player:getGuild()
+ local playerGuildId = playerGuild and playerGuild:getId() or 0
+ local killerGuild = byPlayer and killer:getGuild() or nil
+ local killerGuildId = killerGuild and killerGuild:getId() or 0
+ local killerId = byPlayer and killer:getId() or 0
+ db.asyncQuery(format(playerDeathQuery, playerGuid, timeNow, player:getLevel(), db.escapeString(killerName), byPlayer and 1 or 0,
+ db.escapeString(killerNameMostDamage), byPlayerMostDamage and 1 or 0, lastHitUnjustified and 1 or 0,
+ mostDamageUnjustified and 1 or 0), function(success)
+ if success then
+ playerDeathSuccess(playerId, playerName, killerId, playerGuid, byPlayer, killerName, playerGuildId, killerGuildId, timeNow)
+ end
+ end)
+end
diff --git a/data/events/events.xml b/data/events/events.xml
index 6bd4c24..77982c9 100644
--- a/data/events/events.xml
+++ b/data/events/events.xml
@@ -2,8 +2,8 @@
-
-
+
+
@@ -23,14 +23,14 @@
-
-
+
+
-
-
-
+
+
+
@@ -42,5 +42,5 @@
-
+
diff --git a/data/global.lua b/data/global.lua
index 3246b48..b65182b 100644
--- a/data/global.lua
+++ b/data/global.lua
@@ -1,70 +1,84 @@
math.randomseed(os.time())
dofile('data/lib/lib.lua')
-ropeSpots = {384, 418, 8278, 8592}
+-- LuaFormatter off
+ropeSpots = {
+ 384, 418, 8278, 8592, 13189, 14435, 14436, 14857, 15635, 19518, 24621, 24622, 24623, 24624, 26019
+}
-keys = {2086, 2087, 2088, 2089, 2090, 2091, 2092, 10032}
+keys = {
+ 2086, 2087, 2088, 2089, 2090, 2091, 2092, 10032
+}
openDoors = {
- 1211, 1214, 1233, 1236, 1251, 1254, 3546, 3537, 4915, 4918, 5100, 5109, 5118,
- 5127, 5136, 5139, 5142, 5145, 5280, 5283, 5734, 5737, 6194, 6197, 6251, 6254,
- 6893, 6902, 7035, 7044, 8543, 8546, 9167, 9170, 9269, 9272, 10270, 10273,
- 10470, 10479, 10777, 10786, 12094, 12101, 12190, 12199
+ 1211, 1214, 1233, 1236, 1251, 1254, 3537, 3546, 4915, 4918, 5100, 5109, 5118, 5127, 5136, 5139, 5142,
+ 5145, 5280, 5283, 5734, 5737, 6194, 6197, 6251, 6254, 6893, 6902, 7035, 7044, 8543, 8546, 9167, 9170,
+ 9269, 9272, 10270, 10273, 10470, 10479, 10777, 10786, 12094, 12101, 12190, 12199, 12695, 12703, 14635,
+ 17435, 19842, 19851, 19982, 19991, 20275, 20284, 22816, 22825, 25285, 25292, 26533, 26534, 31176, 31024,
+ 31025, 32691, 32692, 32695, 32696, 33432, 33433, 33493, 33494, 36292, 36293, 36869, 36872, 37293, 37296,
+ 37299, 37302, 37596, 37597, 39119, 39120
}
closedDoors = {
- 1210, 1213, 1232, 1235, 1250, 1253, 3536, 3545, 4914, 4917, 5099, 5108, 5117,
- 5126, 5135, 5138, 5141, 5144, 5279, 5282, 5733, 5736, 6193, 6196, 6250, 6253,
- 6892, 6901, 7034, 7043, 8542, 8545, 9166, 9169, 9268, 9271, 10269, 10272,
- 10766, 10785, 10469, 10478, 12093, 12100, 12189, 12198
+ 1210, 1213, 1232, 1235, 1250, 1253, 3536, 3545, 4914, 4917, 5099, 5108, 5117, 5126, 5135, 5138, 5141,
+ 5144, 5279, 5282, 5733, 5736, 6193, 6196, 6250, 6253, 6892, 6901, 7034, 7043, 8542, 8545, 9166, 9169,
+ 9268, 9271, 10269, 10272, 10469, 10478, 10776, 10785, 12093, 12100, 12189, 12198, 12692, 12701, 14633,
+ 14640, 19841, 19850, 19981, 19990, 20274, 20283, 22815, 22824, 25284, 25291, 26529, 26531, 27559, 31020,
+ 31022, 32689, 32690, 32693, 32694, 33428, 33430, 33489, 33491, 36288, 36290, 36868, 36871, 37292, 37295,
+ 37298, 37301, 37592, 37594, 39115, 39117
}
lockedDoors = {
- 1209, 1212, 1231, 1234, 1249, 1252, 3535, 3544, 4913, 4916, 5098, 5107, 5116,
- 5125, 5134, 5137, 5140, 5143, 5278, 5281, 5732, 5735, 6192, 6195, 6249, 6252,
- 6891, 6900, 7033, 7042, 8541, 8544, 9165, 9168, 9267, 9270, 10268, 10271,
- 10468, 10477, 10775, 10784, 12092, 12099, 12188, 12197
+ 1209, 1212, 1231, 1234, 1249, 1252, 3535, 3544, 4913, 4916, 5098, 5107, 5116, 5125, 5134, 5137, 5140,
+ 5143, 5278, 5281, 5732, 5735, 6192, 6195, 6249, 6252, 6891, 6900, 7033, 7042, 8541, 8544, 9165, 9168,
+ 9267, 9270, 10268, 10271, 10468, 10477, 10775, 10784, 12092, 12099, 12188, 12197, 13236, 13237, 14634,
+ 14641, 19840, 19849, 19980, 19989, 20273, 20282, 22814, 22823, 25283, 25290, 26530, 26532, 31175, 31021,
+ 31023, 32705, 32706, 32707, 32708, 33429, 33431, 33490, 33492, 36289, 36291, 36867, 36870, 37291, 37294,
+ 37297, 37300, 37593, 37595, 39116, 39118
}
-openExtraDoors = {1540, 1542, 6796, 6798, 6800, 6802, 7055, 7057}
-closedExtraDoors = {1539, 1541, 6795, 6797, 6799, 6801, 7054, 7056}
+openExtraDoors = {
+ 1540, 1542, 6796, 6798, 6800, 6802, 6960, 6962, 7055, 7057, 25159, 25161, 27198, 27200, 27243, 27245,
+ 31541, 31542, 34152, 34153, 36878, 36880, 39204
+}
+closedExtraDoors = {
+ 1539, 1541, 6795, 6797, 6799, 6801, 6959, 6961, 7054, 7056, 25158, 25160, 27197, 27199, 27242, 27244,
+ 31314, 31315, 34150, 34151, 36877, 36879, 39203
+}
openHouseDoors = {
- 1220, 1222, 1238, 1240, 3539, 3548, 5083, 5085, 5102, 5111, 5120, 5129, 5285,
- 5287, 5516, 5518, 6199, 6201, 6256, 6258, 6895, 6904, 7037, 7046, 8548, 8550,
- 9172, 9174, 9274, 9276, 10275, 10277, 10472, 10481
+ 1220, 1222, 1238, 1240, 3539, 3548, 5083, 5085, 5102, 5111, 5120, 5129, 5285, 5287, 5516, 5518, 6199,
+ 6201, 6256, 6258, 6895, 6904, 7037, 7046, 8548, 8550, 9172, 9174, 9274, 9276, 10275, 10277, 10472, 10481,
+ 13021, 13023, 17236, 17238, 18209, 19844, 19853, 19984, 19993, 20277, 20286, 22818, 22827, 35928, 35930
}
closedHouseDoors = {
- 1219, 1221, 1237, 1239, 3538, 3547, 5082, 5084, 5101, 5110, 5119, 5128, 5284,
- 5286, 5515, 5517, 6198, 6200, 6255, 6257, 6894, 6903, 7036, 7045, 8547, 8549,
- 9171, 9173, 9273, 9275, 10274, 10276, 10471, 10480
+ 1219, 1221, 1237, 1239, 3538, 3547, 5082, 5084, 5101, 5110, 5119, 5128, 5284, 5286, 5515, 5517, 6198,
+ 6200, 6255, 6257, 6894, 6903, 7036, 7045, 8547, 8549, 9171, 9173, 9273, 9275, 10274, 10276, 10471, 10480,
+ 13020, 13022, 17235, 17237, 18208, 19843, 19852, 19983, 19992, 20276, 20285, 22817, 22826, 35927, 35929
}
---[[ (Not currently used, but probably useful to keep up to date)
openQuestDoors = {
1224, 1226, 1242, 1244, 1256, 1258, 3543, 3552, 5106, 5115, 5124, 5133, 5289, 5291, 5746, 5749, 6203,
6205, 6260, 6262, 6899, 6908, 7041, 7050, 8552, 8554, 9176, 9178, 9278, 9280, 10279, 10281, 10476, 10485,
- 10783, 10792, 12098, 12105, 12194, 12203
+ 10783, 10792, 12098, 12105, 12196, 12205, 14639, 14646, 19848, 19857, 19988, 19997, 20281, 20290, 22822,
+ 22831, 25163, 25165, 25289, 25296, 32698, 32700, 32702, 32704, 34320, 34322, 34225, 34227
}
-]] --
closedQuestDoors = {
- 1223, 1225, 1241, 1243, 1255, 1257, 3542, 3551, 5105, 5114, 5123, 5132, 5288,
- 5290, 5745, 5748, 6202, 6204, 6259, 6261, 6898, 6907, 7040, 7049, 8551, 8553,
- 9175, 9177, 9277, 9279, 10278, 10280, 10475, 10484, 10782, 10791, 12097, 12104,
- 12193, 12202
+ 1223, 1225, 1241, 1243, 1255, 1257, 3542, 3551, 5105, 5114, 5123, 5132, 5288, 5290, 5745, 5748, 6202,
+ 6204, 6259, 6261, 6898, 6907, 7040, 7049, 8551, 8553, 9175, 9177, 9277, 9279, 10278, 10280, 10475, 10484,
+ 10782, 10791, 12097, 12104, 12195, 12204, 14638, 14645, 19847, 19856, 19987, 19996, 20280, 20289, 22821,
+ 22830, 25162, 25164, 25288, 25295, 32697, 32699, 32701, 32703, 34319, 34321, 34224, 34226
}
---[[ (Not currently used, but probably useful to keep up to date)
openLevelDoors = {
1228, 1230, 1246, 1248, 1260, 1262, 3541, 3550, 5104, 5113, 5122, 5131, 5293, 5295, 6207, 6209, 6264,
6266, 6897, 6906, 7039, 7048, 8556, 8558, 9180, 9182, 9282, 9284, 10283, 10285, 10474, 10483, 10781,
- 10790, 12096, 12103, 12196, 12205
+ 10790, 12096, 12103, 12194, 12203, 19846, 19855, 19986, 19995, 20279, 20288, 22820, 22829, 25287, 25294
}
-]] --
closedLevelDoors = {
- 1227, 1229, 1245, 1247, 1259, 1261, 3540, 3549, 5103, 5112, 5121, 5130, 5292,
- 5294, 6206, 6208, 6263, 6265, 6896, 6905, 7038, 7047, 8555, 8557, 9179, 9181,
- 9281, 9283, 10282, 10284, 10473, 10482, 10780, 10789, 12095, 12102, 12195,
- 12204
+ 1227, 1229, 1245, 1247, 1259, 1261, 3540, 3549, 5103, 5112, 5121, 5130, 5292, 5294, 6206, 6208, 6263,
+ 6265, 6896, 6905, 7038, 7047, 8555, 8557, 9179, 9181, 9281, 9283, 10282, 10284, 10473, 10482, 10780,
+ 10789, 12095, 12102, 12193, 12202, 19845, 19854, 19985, 19994, 20278, 20287, 22819, 22828, 25286, 25293
}
+-- LuaFormatter on
function getDistanceBetween(firstPosition, secondPosition)
local xDif = math.abs(firstPosition.x - secondPosition.x)
@@ -84,32 +98,39 @@ function getFormattedWorldTime()
end
function getLootRandom()
- return math.random(0, MAX_LOOTCHANCE) /
- configManager.getNumber(configKeys.RATE_LOOT)
+ return math.random(0, MAX_LOOTCHANCE) / configManager.getNumber(configKeys.RATE_LOOT)
end
+---@generic T: table, K, V
+---@param array T The table to search in
+---@param value K The value to search for
+---@return boolean found Whether the value was found
table.contains = function(array, value)
- for _, targetColumn in pairs(array) do
- if targetColumn == value then return true end
- end
+ for _, targetColumn in pairs(array) do if targetColumn == value then return true end end
return false
end
+---@param str string
+---@param sep string
+---@return table
string.split = function(str, sep)
local res = {}
for v in str:gmatch("([^" .. sep .. "]+)") do res[#res + 1] = v end
return res
end
+---@param str string
+---@param sep string
+---@return table
string.splitTrimmed = function(str, sep)
local res = {}
for v in str:gmatch("([^" .. sep .. "]+)") do res[#res + 1] = v:trim() end
return res
end
-string.trim = function(str)
- return str:match '^()%s*$' and '' or str:match '^%s*(.*%S)'
-end
+---@param str string
+---@return string
+string.trim = function(str) return str:match '^()%s*$' and '' or str:match '^%s*(.*%S)' end
do
local function tchelper(first, rest) return first:upper() .. rest:lower() end
@@ -122,8 +143,6 @@ do
string.titleCase = function(str) return str:gsub("(%a)([%w_']*)", tchelper) end
end
-if not nextUseStaminaTime then nextUseStaminaTime = {} end
-
function getPlayerDatabaseInfo(name_or_guid)
local sql_where = ""
diff --git a/data/lib/compat/compat.lua b/data/lib/compat/compat.lua
index 6afaa90..79d2946 100644
--- a/data/lib/compat/compat.lua
+++ b/data/lib/compat/compat.lua
@@ -1839,6 +1839,17 @@ do
function getStatName(stat) return stats[stat] or "unknown" end
end
+do
+ local rates = {
+ [ExperienceRateType.BASE] = "base",
+ [ExperienceRateType.LOW_LEVEL] = "low level",
+ [ExperienceRateType.BONUS] = "bonus",
+ [ExperienceRateType.STAMINA] = "stamina"
+ }
+
+ function getExperienceRateName(rate) return rates[rate] or "unknown" end
+end
+
function indexToCombatType(idx) return 1 << idx end
function showpos(v) return v > 0 and "+" or "-" end
@@ -1873,4 +1884,17 @@ do
---@param obj any
---@param class table
function isClass(obj, class) return getmetatable(obj) == class end
+
+ ---@param cylinder Thing
+ ---@return Player?
+ function getPlayerFromCylinder(cylinder)
+ if isClass(cylinder, Player) then
+ ---@cast cylinder Player
+ return cylinder
+ elseif isClass(cylinder, Item) or isClass(cylinder, Container) then
+ ---@cast cylinder Item
+ local topParent = cylinder:getTopParent()
+ if topParent then return topParent:getPlayer() end
+ end
+ end
end
diff --git a/data/lib/core/actionids.lua b/data/lib/core/actionids.lua
index 2d5b292..0cd0e8e 100644
--- a/data/lib/core/actionids.lua
+++ b/data/lib/core/actionids.lua
@@ -5,3 +5,20 @@ actionIds = {
citizenship = 30020, -- citizenship teleport
citizenshipLast = 30050 -- citizenship teleport last
}
+
+-- Check duplicates actionIds
+do
+ local duplicates = {}
+ for name, id in pairs(actionIds) do
+ if duplicates[id] then error("Duplicate actionId: " .. id) end
+ duplicates[id] = name
+ end
+
+ local __index = function(self, key)
+ local aid = actionIds[key]
+ if not aid then debugPrint("Invalid actionId: " .. key) end
+ return aid
+ end
+
+ setmetatable(actionIds, {__index = __index})
+end
diff --git a/data/lib/core/container.lua b/data/lib/core/container.lua
index d1e02dd..ac2df79 100644
--- a/data/lib/core/container.lua
+++ b/data/lib/core/container.lua
@@ -1,15 +1,18 @@
-function Container.isContainer(self) return true end
+function Container:isContainer() return true end
-function Container.createLootItem(self, item)
+function Container:createLootItem(lootItem)
if self:getEmptySlots() == 0 then return true end
local itemCount = 0
local randvalue = getLootRandom()
- local itemType = ItemType(item.itemId)
+ local itemType = ItemType(lootItem.itemId)
- if randvalue < item.chance then
+ if randvalue < lootItem.chance then
if itemType:isStackable() then
- itemCount = math.floor(randvalue % item.maxCount) + 1
+ local max = math.floor(randvalue % lootItem.maxCount) + 1
+ local min = lootItem.minCount ~= 0 and math.floor(randvalue % lootItem.minCount) + 1 or max
+ if min > max then min, max = max, min end
+ itemCount = math.random(min, max)
else
itemCount = 1
end
@@ -19,33 +22,31 @@ function Container.createLootItem(self, item)
local count = math.min(itemType:getStackSize(), itemCount)
local subType = count
- if itemType:isFluidContainer() then subType = math.max(0, item.subType) end
+ if itemType:isFluidContainer() then subType = math.max(0, lootItem.subType) end
- local tmpItem = Game.createItem(item.itemId, subType)
+ local tmpItem = Game.createItem(lootItem.itemId, subType)
if not tmpItem then return false end
local tmpContainer = tmpItem:getContainer()
if tmpContainer then
- for i = 1, #item.childLoot do
- if not tmpContainer:createLootItem(item.childLoot[i]) then
+ for i = 1, #lootItem.childLoot do
+ if not tmpContainer:createLootItem(lootItem.childLoot[i]) then
tmpContainer:remove()
return false
end
end
- if #item.childLoot > 0 and tmpContainer:getSize() == 0 then
+ if #lootItem.childLoot > 0 and tmpContainer:getSize() == 0 then
tmpItem:remove()
return true
end
end
- if item.subType ~= -1 then
- tmpItem:setAttribute(ITEM_ATTRIBUTE_CHARGES, item.subType)
- end
+ if lootItem.subType ~= -1 then tmpItem:setAttribute(ITEM_ATTRIBUTE_CHARGES, lootItem.subType) end
- if item.actionId ~= -1 then tmpItem:setActionId(item.actionId) end
+ if lootItem.actionId ~= -1 then tmpItem:setActionId(lootItem.actionId) end
- if item.text and item.text ~= "" then tmpItem:setText(item.text) end
+ if lootItem.text and lootItem.text ~= "" then tmpItem:setText(lootItem.text) end
local ret = self:addItemEx(tmpItem)
if ret ~= RETURNVALUE_NOERROR then tmpItem:remove() end
@@ -59,9 +60,8 @@ function Container:getContentDescription()
local items = self:getItems()
if items and #items > 0 then
local loot = {}
- for _, item in ipairs(items) do
- loot[#loot + 1] = string.format("%s", item:getNameDescription(
- item:getSubType(), true))
+ for _, lootItem in ipairs(items) do
+ loot[#loot + 1] = lootItem:getNameDescription(lootItem:getSubType(), true)
end
return table.concat(loot, ", ")
diff --git a/data/lib/core/game.lua b/data/lib/core/game.lua
index 5ab4304..41661b7 100644
--- a/data/lib/core/game.lua
+++ b/data/lib/core/game.lua
@@ -44,12 +44,6 @@ function Game.getSkillType(weaponType)
return SKILL_FIST
end
-if not globalStorageTable then globalStorageTable = {} end
-
-function Game.getStorageValue(key) return globalStorageTable[key] or -1 end
-
-function Game.setStorageValue(key, value) globalStorageTable[key] = value end
-
do
local cdShort = {"d", "h", "m", "s"}
local cdLong = {" day", " hour", " minute", " second"}
diff --git a/data/lib/core/item.lua b/data/lib/core/item.lua
index 4bd11f8..5c4f745 100644
--- a/data/lib/core/item.lua
+++ b/data/lib/core/item.lua
@@ -138,6 +138,10 @@ do
function StringStream() return setmetatable({}, StreamMeta) end
+ ---@param it ItemType
+ ---@param item Item
+ ---@param subType integer
+ ---@param addArticle boolean
local function internalItemGetNameDescription(it, item, subType, addArticle)
subType = subType or (item and item:getSubType() or -1)
local ss = StringStream()
@@ -278,6 +282,8 @@ do
-- melee weapons and missiles
-- atk x physical +y% element
+ elseif itemType:isWand() then
+ descriptions[#descriptions + 1] = fmt("Magic Atk:%d", attack)
elseif table.contains(showAtkWeaponTypes, weaponType) then
local atkString = fmt("Atk:%d", attack)
local elementDmg = itemType:getElementDamage()
@@ -349,15 +355,16 @@ do
-- display the buffs
for _, statData in pairs(stats) do
local displayValues = {}
- if statData.flat then displayValues[#displayValues + 1] = statData.flat end
+ if statData.flat then displayValues[#displayValues + 1] = fmt("%+d", statData.flat) end
- if statData.percent then displayValues[#displayValues + 1] = statData.percent end
+ if statData.percent then
+ displayValues[#displayValues + 1] = fmt("%+d%%", statData.percent - 100)
+ end
-- desired format examples:
-- +5%
-- +20 and 5%
- if #displayValues > 0 then
- displayValues[1] = fmt("%+d", displayValues[1])
+ if #displayValues ~= 0 then
descriptions[#descriptions + 1] = fmt("%s %s", statData.name, concat(displayValues, " and "))
end
end
@@ -376,7 +383,17 @@ do
do
for element, value in pairs(abilities.specialMagicLevel) do
if value ~= 0 then
- descriptions[#descriptions + 1] = fmt("%s magic level %+d", getCombatName(2^(element-1)), value)
+ descriptions[#descriptions + 1] = fmt("%s magic level %+d", getCombatName(2 ^ (element - 1)),
+ value)
+ end
+ end
+ end
+
+ -- experience rates
+ do
+ for type, rate in ipairs(abilities.experienceRate) do
+ if rate ~= 0 then
+ descriptions[#descriptions + 1] = fmt("xp rate %s %+d%%", getExperienceRateName(type), rate)
end
end
end
@@ -401,20 +418,63 @@ do
-- protections
do
- local protections = {}
- for element, value in pairs(abilities.absorbPercent) do
- if value ~= 0 then
- protections[#protections + 1] = fmt("%s %+d%%", getCombatName(2 ^ (element - 1)), value)
+ local absorbPercent = abilities.absorbPercent
+ local protectionPhysical = absorbPercent[1] --[[@as integer?]]
+ for elem = 2, #absorbPercent do
+ local val = absorbPercent[elem]
+ if protectionPhysical ~= val then
+ protectionPhysical = nil
+ break
end
end
- if #protections > 0 then
- descriptions[#descriptions + 1] = fmt("protection %s", concat(protections, ", "))
+ if protectionPhysical and protectionPhysical ~= 0 then
+ descriptions[#descriptions + 1] = fmt("protection all %+d%%", protectionPhysical)
+ else
+ local protections = {}
+ for element, value in pairs(abilities.absorbPercent) do
+ if value ~= 0 then
+ protections[#protections + 1] = fmt("%s %+d%%", getCombatName(2 ^ (element - 1)), value)
+ end
+ end
+
+ if #protections > 0 then
+ descriptions[#descriptions + 1] = fmt("protection %s", concat(protections, ", "))
+ end
end
end
-- damage reflection
-- to do
+ do
+ local reflectPercent = abilities.reflectPercent
+ local reflectChance = abilities.reflectChance
+ end
+
+ -- boost percent
+ do
+ local all = abilities.boostPercent[1] --[[@as integer?]]
+ for elem = 2, #abilities.boostPercent do
+ local val = abilities.boostPercent[elem]
+ if all ~= val then
+ all = nil
+ break
+ end
+ end
+
+ if all and all ~= 0 then
+ descriptions[#descriptions + 1] = fmt("boost all %+d%%", all)
+ else
+ local boosts = {}
+ for element, value in pairs(abilities.boostPercent) do
+ if value ~= 0 then
+ boosts[#boosts + 1] = fmt("%s %+d%%", getCombatName(2 ^ (element - 1)), value)
+ end
+ end
+
+ if #boosts > 0 then descriptions[#descriptions + 1] = fmt("boost %s", concat(boosts, ", ")) end
+ end
+ end
-- magic shield (classic)
if abilities.manaShield then descriptions[#descriptions + 1] = "magic shield" end
@@ -423,8 +483,35 @@ do
-- to do
-- regeneration
- if abilities.manaGain > 0 or abilities.healthGain > 0 or abilities.regeneration then
- descriptions[#descriptions + 1] = "faster regeneration"
+ if abilities.regeneration then
+ local displayHealth = {}
+ local displayMana = {}
+
+ if abilities.healthGain ~= 0 then
+ displayHealth[#displayHealth + 1] = fmt("%+d", abilities.healthGain)
+ end
+
+ if abilities.healthGainPercent ~= 0 then
+ displayHealth[#displayHealth + 1] = fmt("%+d%%", abilities.healthGainPercent - 100)
+ end
+
+ if abilities.manaGain ~= 0 then displayMana[#displayMana + 1] = fmt("%+d", abilities.manaGain) end
+
+ if abilities.manaGainPercent ~= 0 then
+ displayMana[#displayMana + 1] = fmt("%+d%%", abilities.manaGainPercent - 100)
+ end
+
+ local displayValues = {}
+ if #displayHealth ~= 0 then
+ displayValues[#displayValues + 1] = fmt("hp %s", concat(displayHealth, ", "))
+ end
+ if #displayMana ~= 0 then
+ displayValues[#displayValues + 1] = fmt("mp %s", concat(displayMana, ", "))
+ end
+
+ if #displayValues ~= 0 then
+ descriptions[#descriptions + 1] = fmt("regeneration %s", concat(displayValues, " and "))
+ end
end
-- invisibility
diff --git a/data/lib/core/player.lua b/data/lib/core/player.lua
index 07fda9c..0ffa120 100644
--- a/data/lib/core/player.lua
+++ b/data/lib/core/player.lua
@@ -78,7 +78,7 @@ end
function Player.isUsingOtClient(self) return self:getClient().os >= CLIENTOS_OTCLIENT_LINUX end
function Player.sendExtendedOpcode(self, opcode, buffer)
- if not self:isUsingOtClient() then return false end
+ if not self:isUsingOtcV8() then return false end
local networkMessage = NetworkMessage()
networkMessage:addByte(0x32)
@@ -296,3 +296,52 @@ function Player.getExhaustion(self, key)
end
function Player.hasExhaustion(self, key) return self:getExhaustion(key) > 0 end
+
+---@param type ExperienceRateType
+---@param value integer
+function Player:addExperienceRate(type, value)
+ return self:setExperienceRate(type, self:getExperienceRate(type) + value)
+end
+
+do
+ if not nextUseStaminaTime then nextUseStaminaTime = {} end
+
+ local function useStamina(player)
+ local staminaMinutes = player:getStamina()
+ if staminaMinutes == 0 then return end
+
+ local playerId = player:getId()
+ if not nextUseStaminaTime[playerId] then nextUseStaminaTime[playerId] = 0 end
+
+ local currentTime = os.time()
+ local timePassed = currentTime - nextUseStaminaTime[playerId]
+ if timePassed <= 0 then return end
+
+ if timePassed > 60 then
+ if staminaMinutes > 2 then
+ staminaMinutes = staminaMinutes - 2
+ else
+ staminaMinutes = 0
+ end
+ nextUseStaminaTime[playerId] = currentTime + 120
+ else
+ staminaMinutes = staminaMinutes - 1
+ nextUseStaminaTime[playerId] = currentTime + 60
+ end
+ player:setStamina(math.floor(staminaMinutes))
+ end
+
+ function Player:updateStamina()
+ if not configManager.getBoolean(configKeys.STAMINA_SYSTEM) then return false end
+
+ useStamina(self)
+
+ local staminaMinutes = self:getStamina()
+ if staminaMinutes > 2400 and self:isPremium() then
+ self:addExperienceRate(ExperienceRateType.STAMINA, 50)
+ elseif staminaMinutes <= 840 then
+ self:addExperienceRate(ExperienceRateType.STAMINA, -50)
+ end
+ return true
+ end
+end
diff --git a/data/lib/core/position.lua b/data/lib/core/position.lua
index 9f34ae4..2562240 100644
--- a/data/lib/core/position.lua
+++ b/data/lib/core/position.lua
@@ -1,3 +1,34 @@
+local mt = rawgetmetatable("Position")
+
+---@param lhs Position
+---@param rhs Position
+function mt.__add(lhs, rhs)
+ local stackpos = lhs.stackpos or rhs.stackpos
+ return Position(lhs.x + (rhs.x or 0), lhs.y + (rhs.y or 0), lhs.z + (rhs.z or 0),
+ stackpos)
+end
+
+---@param lhs Position
+---@param rhs Position
+function mt.__sub(lhs, rhs)
+ local stackpos = lhs.stackpos or rhs.stackpos
+ return Position(lhs.x - (rhs.x or 0), lhs.y - (rhs.y or 0), lhs.z - (rhs.z or 0),
+ stackpos)
+end
+
+---@param lhs Position
+---@param rhs Position
+function mt.__concat(lhs, rhs) return tostring(lhs) .. tostring(rhs) end
+
+---@param lhs Position
+---@param rhs Position
+function mt.__eq(lhs, rhs) return lhs.x == rhs.x and lhs.y == rhs.y and lhs.z == rhs.z end
+
+---@param self Position
+function mt.__tostring(self)
+ return string.format("Position(%d, %d, %d)", self.x, self.y, self.z)
+end
+
Position.directionOffset = {
[DIRECTION_NORTH] = {x = 0, y = -1},
[DIRECTION_EAST] = {x = 1, y = 0},
@@ -9,8 +40,17 @@ Position.directionOffset = {
[DIRECTION_NORTHEAST] = {x = 1, y = -1}
}
+local abs, max = math.abs, math.max
+function Position:getDistance(positionEx)
+ local dx = abs(self.x - positionEx.x)
+ local dy = abs(self.y - positionEx.y)
+ local dz = abs(self.z - positionEx.z)
+ return max(dx, dy, dz)
+end
+
function Position:getTile() return Tile(self) end
+--- This method modifies the position and returns self
function Position:getNextPosition(direction, steps)
local offset = Position.directionOffset[direction]
if offset then
@@ -18,33 +58,36 @@ function Position:getNextPosition(direction, steps)
self.x = self.x + offset.x * steps
self.y = self.y + offset.y * steps
end
+ return self
end
-function Position:moveUpstairs()
+do
local swap = function(lhs, rhs)
lhs.x, rhs.x = rhs.x, lhs.x
lhs.y, rhs.y = rhs.y, lhs.y
lhs.z, rhs.z = rhs.z, lhs.z
end
- self.z = self.z - 1
+ function Position:moveUpstairs()
+ self.z = self.z - 1
- local defaultPosition = self + Position.directionOffset[DIRECTION_SOUTH]
- local toTile = Tile(defaultPosition)
- if not toTile or not toTile:isWalkable() then
- for direction = DIRECTION_NORTH, DIRECTION_NORTHEAST do
- if direction == DIRECTION_SOUTH then direction = DIRECTION_WEST end
+ local defaultPosition = self + Position.directionOffset[DIRECTION_SOUTH]
+ local toTile = Tile(defaultPosition)
+ if not toTile or not toTile:isWalkable() then
+ for direction = DIRECTION_NORTH, DIRECTION_NORTHEAST do
+ if direction == DIRECTION_SOUTH then direction = DIRECTION_WEST end
- local position = self + Position.directionOffset[direction]
- toTile = Tile(position)
- if toTile and toTile:isWalkable() then
- swap(self, position)
- return self
+ local position = self + Position.directionOffset[direction]
+ toTile = Tile(position)
+ if toTile and toTile:isWalkable() then
+ swap(self, position)
+ return self
+ end
end
end
+ swap(self, defaultPosition)
+ return self
end
- swap(self, defaultPosition)
- return self
end
function Position:isInRange(from, to)
@@ -63,18 +106,15 @@ function Position:isInRange(from, to)
}
}
- if self.x >= zone.nW.x and self.x <= zone.sE.x and self.y >= zone.nW.y and
- self.y <= zone.sE.y and self.z >= zone.nW.z and self.z <= zone.sE.z then
- return true
- end
+ if self.x >= zone.nW.x and self.x <= zone.sE.x and self.y >= zone.nW.y and self.y <=
+ zone.sE.y and self.z >= zone.nW.z and self.z <= zone.sE.z then return true end
return false
end
function Position:notifySummonAppear(summon)
local spectators = Game.getSpectators(self)
for _, spectator in ipairs(spectators) do
- if spectator:isMonster() and spectator ~= summon then
- spectator:addTarget(summon)
- end
+ local monster = spectator:getMonster()
+ if monster and monster ~= summon then monster:addTarget(summon) end
end
end
diff --git a/data/lib/core/storages.lua b/data/lib/core/storages.lua
index bda11ba..1555b17 100644
--- a/data/lib/core/storages.lua
+++ b/data/lib/core/storages.lua
@@ -17,3 +17,37 @@ PlayerStorageKeys = {
}
GlobalStorageKeys = {}
+
+-- Check duplicates player storage keys
+do
+ local duplicates = {}
+ for name, id in pairs(PlayerStorageKeys) do
+ if duplicates[id] then error("Duplicate keyStorage: " .. id) end
+ duplicates[id] = name
+ end
+
+ local __index = function(self, key)
+ local keyStorage = PlayerStorageKeys[key]
+ if not keyStorage then debugPrint("Invalid keyStorage: " .. key) end
+ return keyStorage
+ end
+
+ setmetatable(PlayerStorageKeys, {__index = __index})
+end
+
+-- Check duplicates global storage keys
+do
+ local duplicates = {}
+ for name, id in pairs(GlobalStorageKeys) do
+ if duplicates[id] then error("Duplicate keyStorage: " .. id) end
+ duplicates[id] = name
+ end
+
+ local __index = function(self, key)
+ local keyStorage = GlobalStorageKeys[key]
+ if not keyStorage then debugPrint("Invalid keyStorage: " .. key) end
+ return keyStorage
+ end
+
+ setmetatable(GlobalStorageKeys, {__index = __index})
+end
diff --git a/data/scripts/eventcallbacks/player/default_onGainExperience.lua b/data/scripts/eventcallbacks/player/default_onGainExperience.lua
index 6de9e1b..dae1acb 100644
--- a/data/scripts/eventcallbacks/player/default_onGainExperience.lua
+++ b/data/scripts/eventcallbacks/player/default_onGainExperience.lua
@@ -2,31 +2,6 @@ local soulCondition = Condition(CONDITION_SOUL, CONDITIONID_DEFAULT)
soulCondition:setTicks(4 * 60 * 1000)
soulCondition:setParameter(CONDITION_PARAM_SOULGAIN, 1)
-local function useStamina(player)
- local staminaMinutes = player:getStamina()
- if staminaMinutes == 0 then return end
-
- local playerId = player:getId()
- if not nextUseStaminaTime[playerId] then nextUseStaminaTime[playerId] = 0 end
-
- local currentTime = os.time()
- local timePassed = currentTime - nextUseStaminaTime[playerId]
- if timePassed <= 0 then return end
-
- if timePassed > 60 then
- if staminaMinutes > 2 then
- staminaMinutes = staminaMinutes - 2
- else
- staminaMinutes = 0
- end
- nextUseStaminaTime[playerId] = currentTime + 120
- else
- staminaMinutes = staminaMinutes - 1
- nextUseStaminaTime[playerId] = currentTime + 60
- end
- player:setStamina(staminaMinutes)
-end
-
local event = Event()
function event.onGainExperience(player, source, exp, rawExp)
@@ -35,8 +10,7 @@ function event.onGainExperience(player, source, exp, rawExp)
-- Soul regeneration
local vocation = player:getVocation()
if player:getSoul() < vocation:getMaxSoul() and exp >= player:getLevel() then
- soulCondition:setParameter(CONDITION_PARAM_SOULTICKS,
- vocation:getSoulGainTicks() * 1000)
+ soulCondition:setParameter(CONDITION_PARAM_SOULTICKS, vocation:getSoulGainTicks() * 1000)
player:addCondition(soulCondition)
end
@@ -44,16 +18,21 @@ function event.onGainExperience(player, source, exp, rawExp)
exp = exp * Game.getExperienceStage(player:getLevel())
-- Stamina modifier
- if configManager.getBoolean(configKeys.STAMINA_SYSTEM) then
- useStamina(player)
-
- local staminaMinutes = player:getStamina()
- if staminaMinutes > 2400 and player:isPremium() then
- exp = exp * 1.5
- elseif staminaMinutes <= 840 then
- exp = exp * 0.5
- end
- end
+ player:updateStamina()
+
+ -- Experience Rates
+ local staminaRate = player:getExperienceRate(ExperienceRateType.STAMINA)
+ if staminaRate ~= 100 then exp = exp * staminaRate / 100 end
+
+ local baseRate = player:getExperienceRate(ExperienceRateType.BASE)
+ if baseRate ~= 100 then exp = exp * baseRate / 100 end
+
+ local lowLevelRate = player:getExperienceRate(ExperienceRateType.LOW_LEVEL)
+ if lowLevelRate ~= 100 then exp = exp * lowLevelRate / 100 end
+
+ local bonusRate = player:getExperienceRate(ExperienceRateType.BONUS)
+ if bonusRate ~= 100 then exp = exp * bonusRate / 100 end
+
return exp
end
diff --git a/data/scripts/eventcallbacks/player/default_onLook.lua b/data/scripts/eventcallbacks/player/default_onLook.lua
index 5c6f50b..99c0a30 100644
--- a/data/scripts/eventcallbacks/player/default_onLook.lua
+++ b/data/scripts/eventcallbacks/player/default_onLook.lua
@@ -1,58 +1,69 @@
+local fmt = string.format
+
local event = Event()
-event.onLook = function(self, thing, position, distance, description)
- local description = "You see " .. thing:getDescription(distance)
- if self:getGroup():getAccess() then
- if thing:isItem() then
- description = string.format("%s\nItem ID: %d", description, thing:getId())
+event.onLook = function(player, thing, position, distance, description)
+ description = "You see " .. thing:getDescription(distance)
+ if player:getGroup():getAccess() then
+ local item = thing:getItem()
+ if item then
+ description = fmt("%s\nItem ID: %d", description, item:getId())
- local actionId = thing:getActionId()
- if actionId ~= 0 then
- description = string.format("%s, Action ID: %d", description, actionId)
- end
+ local actionId = item:getActionId()
+ if actionId ~= 0 then description = fmt("%s, Action ID: %d", description, actionId) end
- local uniqueId = thing:getAttribute(ITEM_ATTRIBUTE_UNIQUEID)
+ local uniqueId = item:getAttribute(ITEM_ATTRIBUTE_UNIQUEID)
if uniqueId > 0 and uniqueId < 65536 then
- description = string.format("%s, Unique ID: %d", description, uniqueId)
+ description = fmt("%s, Unique ID: %d", description, uniqueId)
end
- local itemType = thing:getType()
+ local itemType = item:getType()
local transformEquipId = itemType:getTransformEquipId()
local transformDeEquipId = itemType:getTransformDeEquipId()
if transformEquipId ~= 0 then
- description = string.format("%s\nTransforms to: %d (onEquip)", description,
- transformEquipId)
+ description = fmt("%s\nTransforms to: %d (onEquip)", description, transformEquipId)
elseif transformDeEquipId ~= 0 then
- description = string.format("%s\nTransforms to: %d (onDeEquip)",
- description, transformDeEquipId)
+ description =
+ fmt("%s\nTransforms to: %d (onDeEquip)", description, transformDeEquipId)
end
local decayId = itemType:getDecayId()
- if decayId ~= -1 then
- description = string.format("%s\nDecays to: %d", description, decayId)
- end
- elseif thing:isCreature() then
- local str = "%s\nHealth: %d / %d"
- if thing:isPlayer() and thing:getMaxMana() > 0 then
- str = string.format("%s, Mana: %d / %d", str, thing:getMana(),
- thing:getMaxMana())
- elseif thing:isMonster() then
- local raceId = thing:getType():raceId()
- if raceId ~= 0 then str = string.format("%s\nRaceId: %d", str, raceId) end
+ if decayId ~= -1 then description = fmt("%s\nDecays to: %d", description, decayId) end
+ else
+ local thingCreature = thing:getCreature()
+ local thingPlayer = thing:getPlayer()
+ local thingMonster = thing:getMonster()
+ if thingCreature then
+ local str = "%s\nHealth: %d / %d"
+
+ if thingPlayer then
+ local thinPlayerMana = thingPlayer:getMana()
+ if thinPlayerMana > 0 then
+ str = fmt("%s, Mana: %d / %d", str, thinPlayerMana, thingPlayer:getMaxMana())
+ end
+ elseif thingMonster then
+ local raceId = thingMonster:getType():raceId()
+ if raceId ~= 0 then str = fmt("%s\nRaceId: %d", str, raceId) end
+ end
+
+ description = fmt(str, description, thingCreature:getHealth(),
+ thingCreature:getMaxHealth()) .. "."
end
- description = string.format(str, description, thing:getHealth(),
- thing:getMaxHealth()) .. "."
- end
- local position = thing:getPosition()
- description = string.format("%s\nPosition: %d, %d, %d", description,
- position.x, position.y, position.z)
+ local thingPosition = thing:getPosition()
+ if thingPosition then
+ description = fmt("%s\nPosition: %d, %d, %d", description, thingPosition.x,
+ thingPosition.y, thingPosition.z)
+ end
- if thing:isCreature() then
- if thing:isPlayer() then
- description = string.format("%s\nIP: %s.", description,
- Game.convertIpToString(thing:getIp()))
+ if thingPlayer then
+ description = fmt("%s\nIP: %s.", description,
+ Game.convertIpToString(thingPlayer:getIp()))
+ if thingPlayer:getGroup():getAccess() then
+ description = fmt("%s\nVocation: %s.", description,
+ thingPlayer:getVocation():getName())
+ end
end
end
end
diff --git a/data/talkactions/scripts/attributes.lua b/data/talkactions/scripts/attributes.lua
index 842fec3..307e228 100644
--- a/data/talkactions/scripts/attributes.lua
+++ b/data/talkactions/scripts/attributes.lua
@@ -1,34 +1,59 @@
+local function message(player, msg, ...)
+ player:sendTextMessage(MESSAGE_STATUS_CONSOLE_BLUE, string.format(msg, ...))
+end
+
local function setAttribute(player, thing, attribute, value)
local attributeId = Game.getItemAttributeByName(attribute)
if attributeId == ITEM_ATTRIBUTE_NONE then return "Invalid attribute name." end
if not thing:setAttribute(attribute, value) then return "Could not set attribute." end
- player:sendTextMessage(MESSAGE_STATUS_CONSOLE_BLUE,
- string.format("Attribute %s set to: %s", attribute, thing:getAttribute(attributeId)))
+ message(player, "Attribute %s set to: %s", attribute, thing:getAttribute(attributeId))
thing:getPosition():sendMagicEffect(CONST_ME_MAGIC_GREEN)
return true
end
+---@type {[string]: fun(player: Player, creature: Creature, value: string): boolean}
+local creatureAttrs = {
+ health = function(player, creature, value)
+ creature:setHealth(math.floor(tonumber(value) or 0), player)
+ return true
+ end,
+
+ addSummon = function(player, creature, value)
+ local summon = Game.createMonster(value, creature:getPosition())
+ if not summon then return false end
+
+ creature:addSummon(summon)
+ return true
+ end,
+
+ event = function (player, creature, value)
+ creature:registerEvent(value)
+ return true
+ end
+}
+
function onSay(player, words, param)
local position = player:getPosition()
position:getNextPosition(player:getDirection())
local tile = Tile(position)
if not tile then
- player:sendTextMessage(MESSAGE_STATUS_CONSOLE_BLUE, "There is no tile in front of you.")
+ message(player, "There is no tile in front of you.")
return false
end
local thing = tile:getTopVisibleThing(player)
if not thing then
- player:sendTextMessage(MESSAGE_STATUS_CONSOLE_BLUE, "There is an empty tile in front of you.")
+ message(player, "There is an empty tile in front of you.")
return false
end
+ ---@type string, string, string
local attribute, value, extra = unpack(param:splitTrimmed(","))
if attribute == "" then
- player:sendTextMessage(MESSAGE_STATUS_CONSOLE_BLUE, string.format("Usage: %s attribute, value.", words))
+ message(player, "Usage: %s attribute, value.", words)
return false
end
@@ -44,18 +69,36 @@ function onSay(player, words, param)
end
if not item:setCustomAttribute(value, extra) then
- player:sendTextMessage(MESSAGE_STATUS_CONSOLE_BLUE, "Could not set custom attribute.")
+ message(player, "Could not set custom attribute.")
return false
end
- player:sendTextMessage(MESSAGE_STATUS_CONSOLE_BLUE,
- string.format("Custom attribute %s set to: %s", value, item:getCustomAttribute(value)))
+ message(player, "Custom attribute %s set to: %s", value, item:getCustomAttribute(value))
position:sendMagicEffect(CONST_ME_MAGIC_GREEN)
else
- player:sendTextMessage(MESSAGE_STATUS_CONSOLE_BLUE, response)
+ message(player, response)
end
- else
- player:sendTextMessage(MESSAGE_STATUS_CONSOLE_BLUE, "Thing in front of you is not supported.")
+ return false
end
+
+ local creature = thing:getCreature()
+ if creature then
+ local creatureAttr = creatureAttrs[attribute]
+ if not creatureAttr then
+ message(player, "Invalid attribute name.")
+ return false
+ end
+
+ local response = creatureAttr(player, creature, value)
+ if not response then
+ message(player, "Could not set attribute.")
+ return false
+ end
+
+ position:sendMagicEffect(CONST_ME_MAGIC_GREEN)
+ return true
+ end
+
+ player:sendCancelMessage("You can only use this command on items or creatures.")
return false
end
diff --git a/data/talkactions/scripts/serverinfo.lua b/data/talkactions/scripts/serverinfo.lua
index 41a85bc..a87ad2e 100644
--- a/data/talkactions/scripts/serverinfo.lua
+++ b/data/talkactions/scripts/serverinfo.lua
@@ -1,12 +1,21 @@
+local fmt = string.format
+
function onSay(player, words, param)
- player:sendTextMessage(MESSAGE_STATUS_CONSOLE_BLUE,
- "Server Info:" .. "\nExp rate: " ..
- Game.getExperienceStage(player:getLevel()) ..
- "\nSkill rate: " ..
- configManager.getNumber(configKeys.RATE_SKILL) ..
- "\nMagic rate: " ..
- configManager.getNumber(configKeys.RATE_MAGIC) ..
- "\nLoot rate: " ..
- configManager.getNumber(configKeys.RATE_LOOT))
+
+ local desc = {"Server Info:\n"}
+
+ -- Global Rates
+ desc[#desc + 1] = fmt("Exp rate: %s", Game.getExperienceStage(player:getLevel()))
+ desc[#desc + 1] = fmt("Skill rate: %s", configManager.getNumber(configKeys.RATE_SKILL))
+ desc[#desc + 1] = fmt("Magic rate: %s", configManager.getNumber(configKeys.RATE_MAGIC))
+ desc[#desc + 1] = fmt("Loot rate: %s", configManager.getNumber(configKeys.RATE_LOOT))
+
+ -- Player Rates
+ desc[#desc + 1] = fmt("XP rate base: %+d%%", player:getExperienceRate(ExperienceRateType.BASE) - 100)
+ desc[#desc + 1] = fmt("XP rate low level: %+d%%", player:getExperienceRate(ExperienceRateType.LOW_LEVEL) - 100)
+ desc[#desc + 1] = fmt("XP rate bonus: %+d%%", player:getExperienceRate(ExperienceRateType.BONUS) - 100)
+ desc[#desc + 1] = fmt("XP rate stamina: %+d%%", player:getExperienceRate(ExperienceRateType.STAMINA) - 100)
+
+ player:popupFYI(table.concat(desc, "\n"))
return false
end