New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Reward System [remake of #1628] #1641
Changes from all commits
61db176
7b572a1
2b59bd8
57a9ed5
a8e430a
82e9786
5cd0ef7
0fb0f7b
4b7c9fd
b76cdfe
df0af75
2eb1cfd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,229 @@ | ||
local function pushSeparated(buffer, sep, ...) | ||
local argv = {...} | ||
local argc = #argv | ||
for k, v in ipairs(argv) do | ||
table.insert(buffer, v) | ||
if k < argc and sep then | ||
table.insert(buffer, sep) | ||
end | ||
end | ||
end | ||
|
||
local function insertItems(buffer, info, parent, items) | ||
local start = info.running | ||
for _, item in ipairs(items) do | ||
if _ ~= 1 or parent > 100 then | ||
table.insert(buffer, ",") | ||
end | ||
|
||
info.running = info.running + 1 | ||
table.insert(buffer, "(") | ||
pushSeparated(buffer, ",", info.playerGuid, parent, info.running, item:getId(), item:getSubType(), db.escapeBlob(item:serializeAttributes())) | ||
table.insert(buffer, ")") | ||
|
||
if item:isContainer() then | ||
local size = item:getSize() | ||
if size > 0 then | ||
local subItems = {} | ||
for i = 1, size do | ||
table.insert(subItems, item:getItem(i - 1)) | ||
end | ||
|
||
insertItems(buffer, info, info.running, subItems) | ||
end | ||
end | ||
end | ||
return info.running - start | ||
end | ||
|
||
local function insertRewardItems(playerGuid, timestamp, itemList) | ||
db.asyncStoreQuery('SELECT `pid`, `sid` FROM `player_rewards` WHERE player_id = ' .. playerGuid .. ' ORDER BY `sid` ASC;', | ||
function(query) | ||
local lastReward = 0 | ||
local lastStoreId | ||
if query then | ||
repeat | ||
local sid = result.getDataInt(query, 'sid') | ||
local pid = result.getDataInt(query, 'pid') | ||
|
||
if pid < 100 then | ||
lastReward = pid | ||
end | ||
lastStoreId = sid | ||
until not result.next(query) | ||
end | ||
|
||
local buffer = {'INSERT INTO `player_rewards` (`player_id`, `pid`, `sid`, `itemtype`, `count`, `attributes`) VALUES'} | ||
|
||
--reward bag | ||
local info = { | ||
playerGuid = playerGuid, | ||
running = lastStoreId or 100 | ||
} | ||
|
||
local bag = Game.createItem(ITEM_REWARD_CONTAINER) | ||
bag:setAttribute(ITEM_ATTRIBUTE_DATE, timestamp) | ||
|
||
if itemList then | ||
for _, item in ipairs(itemList) do | ||
bag:addItem(item[1], item[2]) | ||
end | ||
end | ||
|
||
local total = insertItems(buffer, info, lastReward + 1, {bag}) | ||
table.insert(buffer, ";") | ||
|
||
if total ~= 0 then | ||
db.query(table.concat(buffer)) | ||
end | ||
end | ||
) | ||
end | ||
|
||
local function getPlayerStats(bossId, playerGuid, autocreate) | ||
local ret = globalBosses[bossId][playerGuid] | ||
if not ret and autocreate then | ||
ret = { | ||
bossId = bossId, | ||
damageIn = 0, -- damage taken from the boss | ||
healing = 0, -- healing (other players) done | ||
} | ||
globalBosses[bossId][playerGuid] = ret | ||
return ret | ||
end | ||
return ret | ||
end | ||
|
||
function onDeath(creature, corpse, killer, mostDamageKiller, lastHitUnjustified, mostDamageUnjustified) | ||
local monsterType = creature:getType() | ||
if monsterType:isRewardBoss() then -- Make sure it is a boss | ||
local bossId = creature:getId() | ||
local timestamp = os.time() | ||
|
||
local totalDamageOut, totalDamageIn, totalHealing = 0.1, 0.1, 0.1 -- avoid dividing by zero | ||
|
||
local scores = {} | ||
local info = globalBosses[bossId] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Place this above the loop, this way it's easier to find the "info". |
||
local damageMap = creature:getDamageMap() | ||
|
||
for guid, stats in pairs(info) do | ||
local player = Player(stats.playerId) | ||
local part = damageMap[stats.playerId] | ||
local damageOut, damageIn, healing = (stats.damageOut or 0) + (part and part.total or 0), stats.damageIn or 0, stats.healing or 0 | ||
|
||
totalDamageOut = totalDamageOut + damageOut | ||
totalDamageIn = totalDamageIn + damageIn | ||
totalHealing = totalHealing + healing | ||
|
||
table.insert(scores, { | ||
player = player, | ||
guid = guid, | ||
damageOut = damageOut, | ||
damageIn = damageIn, | ||
healing = healing, | ||
}) | ||
end | ||
|
||
local participants = 0 | ||
for _, con in ipairs(scores) do | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. con => score? |
||
local score = (con.damageOut / totalDamageOut) + (con.damageIn / totalDamageIn) + (con.healing / totalHealing) | ||
con.score = score / 3 -- normalize to 0-1 | ||
if score ~= 0 then | ||
participants = participants + 1 | ||
end | ||
end | ||
table.sort(scores, function(a, b) return a.score > b.score end) | ||
|
||
local expectedScore = 1 / participants | ||
|
||
for _, con in ipairs(scores) do | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. con => score? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I do not find the idiom score.score appealing. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The inner There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Singular an plural. Items => item |
||
local reward, stamina -- ignoring stamina for now because I heard you receive rewards even when it's depleted | ||
if con.player then | ||
reward = con.player:getReward(timestamp, true) | ||
stamina = con.player:getStamina() | ||
else | ||
stamina = con.stamina or 0 | ||
end | ||
|
||
local playerLoot | ||
if --[[stamina > 840 and]] con.score ~= 0 then | ||
local lootFactor = 1 | ||
lootFactor = lootFactor / participants ^ (1 / 3) -- tone down the loot a notch if there are many participants | ||
lootFactor = lootFactor * (1 + lootFactor) ^ (con.score / expectedScore) -- increase the loot multiplicatively by how many times the player surpassed the expected score | ||
playerLoot = monsterType:getBossReward(lootFactor, _ == 1) | ||
|
||
if con.player then | ||
for _, p in ipairs(playerLoot) do | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. p => loot or item |
||
reward:addItem(p[1], p[2]) | ||
end | ||
end | ||
end | ||
|
||
if con.player then | ||
local lootMessage = {"The following items are available in your reward chest: "} | ||
|
||
if --[[stamina > 840]]true then | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ??? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Normally you can't loot items in Tibia without that much stamina, however I was informed this limit does not apply to bosses, I'm leaving it like that until someone confirms or denies this information. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In that case add a comment above, and id comment out the if statment so when we know about it we can remove the comments. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can someone confirm what @socket2810 said? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not a super easy thing to check xD Pretty rare (that atleast I) go below 30~hs, botters might but then they won't kill raid bosses xD |
||
reward:getContentDescription(lootMessage) | ||
else | ||
table.insert(lootMessage, 'nothing (due to low stamina)') | ||
end | ||
table.insert(lootMessage, ".") | ||
con.player:sendTextMessage(MESSAGE_EVENT_ADVANCE, table.concat(lootMessage)) | ||
else | ||
insertRewardItems(con.guid, timestamp, playerLoot) | ||
end | ||
end | ||
|
||
globalBosses[bossId] = nil | ||
end | ||
return true | ||
end | ||
|
||
function onThink(creature, interval) | ||
local bossId = creature:getId() | ||
local info = globalBosses[bossId] | ||
-- Reset all players' status | ||
for _, player in pairs(info) do | ||
player.active = false | ||
end | ||
|
||
-- Set all players in boss' target list as active in the fight | ||
local targets = creature:getTargetList() | ||
for _, target in ipairs(targets) do | ||
if target:isPlayer() then | ||
local stats = getPlayerStats(bossId, target:getGuid(), true) | ||
stats.playerId = target:getId() -- Update player id | ||
stats.active = true | ||
end | ||
end | ||
end | ||
|
||
function onHealthChange(creature, attacker, primaryDamage, primaryType, secondaryDamage, secondaryType, origin) | ||
if not next(globalBosses) then | ||
return primaryDamage, primaryType, secondaryDamage, secondaryType | ||
end | ||
|
||
if not creature or not attacker then | ||
return primaryDamage, primaryType, secondaryDamage, secondaryType | ||
end | ||
|
||
local stats = creature:inBossFight() | ||
if not stats then | ||
return primaryDamage, primaryType, secondaryDamage, secondaryType | ||
end | ||
|
||
local creatureId, attackerId = creature:getId(), attacker:getId() | ||
stats.playerId = creatureId -- Update player id | ||
|
||
-- Account for healing of others active in the boss fight | ||
if primaryType == COMBAT_HEALING and attacker:isPlayer() and attackerId ~= creatureId then | ||
local healerStats = getPlayerStats(stats.bossId, attacker:getGuid(), true) | ||
healerStats.active = true | ||
healerStats.playerId = attackerId -- Update player id | ||
healerStats.healing = healerStats.healing + primaryDamage | ||
elseif stats.bossId == attackerId then | ||
-- Account for damage taken from the boss | ||
stats.damageIn = stats.damageIn + primaryDamage | ||
end | ||
return primaryDamage, primaryType, secondaryDamage, secondaryType | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,5 +3,19 @@ function onLogout(player) | |
if nextUseStaminaTime[playerId] ~= nil then | ||
nextUseStaminaTime[playerId] = nil | ||
end | ||
|
||
local stats = player:inBossFight() | ||
if stats then | ||
-- Player logged out (or died) in the middle of a boss fight, store his damageOut and stamina | ||
local boss = Monster(stats.bossId) | ||
if boss then | ||
local dmgOut = boss:getDamageMap()[playerId] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. damage There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. damage made or damage taken? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. damageTaken I suppose, just avoid writing names like p insted of player, it's fine if it's your own server since you coded it - you know what everything is. |
||
if dmgOut then | ||
stats.damageOut = (stats.damageOut or 0) + dmgOut.total | ||
end | ||
stats.stamina = player:getStamina() | ||
end | ||
end | ||
|
||
return true | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Variable names here aswell, they should mean something.
Is pid playerId and sid storageId?
No matter, change them to what they actually mean.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
pid and sid are already used in player_depotitems, player_inboxitems and player_items. I'm just following the current standard.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh okay, well then it should wait till we update the SQL schema.