Skip to content
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

Closed
wants to merge 12 commits into from
4 changes: 4 additions & 0 deletions data/creaturescripts/creaturescripts.xml
Expand Up @@ -8,4 +8,8 @@
<event type="death" name="PlayerDeath" script="playerdeath.lua" />
<event type="death" name="DropLoot" script="droploot.lua" />
<event type="extendedopcode" name="ExtendedOpcode" script="extendedopcode.lua" />

<event type="death" name="BossDeath" script="boss.lua" />
<event type="healthchange" name="BossParticipation" script="boss.lua" />
<event type="think" name="BossThink" script="boss.lua" />
</creaturescripts>
229 changes: 229 additions & 0 deletions data/creaturescripts/scripts/boss.lua
@@ -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;',
Copy link
Member

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.

Copy link
Contributor Author

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.

Copy link
Member

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.

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]
Copy link
Member

Choose a reason for hiding this comment

The 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
Copy link
Member

Choose a reason for hiding this comment

The 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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

con => score?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not find the idiom score.score appealing.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The inner score should be named value then since if you have a list of scores , every item is a score by itself.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Singular an plural.
Plural = the table
Singular = the row of the table

Items => item
Positions => position
etc

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
Copy link
Member

Choose a reason for hiding this comment

The 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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

???

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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.

Copy link
Member

Choose a reason for hiding this comment

The 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.
One less thing to load for now.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can someone confirm what @socket2810 said?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a super easy thing to check xD
Best to try and talk to experienced players and or using wikia etc

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
13 changes: 13 additions & 0 deletions data/creaturescripts/scripts/login.lua
Expand Up @@ -29,8 +29,21 @@ function onLogin(player)
player:setVocation(vocation:getDemotion())
end

-- Rewards notice
local rewards = #player:getRewardList()
if rewards > 0 then
player:sendTextMessage(MESSAGE_EVENT_ADVANCE, string.format("You have %s %s in your reward chest.", rewards == 1 and 'one' or rewards, rewards > 1 and "rewards" or "reward"))
end

-- Update player id
local stats = player:inBossFight()
if stats then
stats.playerId = player:getId()
end

-- Events
player:registerEvent("PlayerDeath")
player:registerEvent("DropLoot")
player:registerEvent("BossParticipation")
return true
end
14 changes: 14 additions & 0 deletions data/creaturescripts/scripts/logout.lua
Expand Up @@ -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]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

damage

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

damage made or damage taken?

Copy link
Member

Choose a reason for hiding this comment

The 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.
But just try to convert 0.x scripts to 1.x, where all variables are named b_2 etc
As long as the name describes what is stored in the variable it's fine.

if dmgOut then
stats.damageOut = (stats.damageOut or 0) + dmgOut.total
end
stats.stamina = player:getStamina()
end
end

return true
end
2 changes: 1 addition & 1 deletion data/events/events.xml
Expand Up @@ -16,7 +16,7 @@
<event class="Player" method="onLookInBattleList" enabled="1" />
<event class="Player" method="onLookInTrade" enabled="1" />
<event class="Player" method="onLookInShop" enabled="0" />
<event class="Player" method="onMoveItem" enabled="0" />
<event class="Player" method="onMoveItem" enabled="1" />
<event class="Player" method="onMoveCreature" enabled="0" />
<event class="Player" method="onTurn" enabled="0" />
<event class="Player" method="onTradeRequest" enabled="0" />
Expand Down
38 changes: 34 additions & 4 deletions data/events/scripts/player.lua
Expand Up @@ -85,10 +85,6 @@ function Player:onLookInShop(itemType, count)
return true
end

function Player:onMoveItem(item, count, fromPosition, toPosition)
return true
end

function Player:onMoveCreature(creature, fromPosition, toPosition)
return true
end
Expand Down Expand Up @@ -180,3 +176,37 @@ function Player:onGainSkillTries(skill, tries)
end
return tries * configManager.getNumber(configKeys.RATE_SKILL)
end

function Player:onMoveItem(item, count, fromPosition, toPosition)
if toPosition.x == CONTAINER_POSITION then
local containerId = toPosition.y - 64
local container = self:getContainerById(containerId)
if not container then
return true
end

-- Do not let the player insert items into either the Reward Container or the Reward Chest
local itemId = container:getId()
if itemId == ITEM_REWARD_CONTAINER or itemId == ITEM_REWARD_CHEST then
self:sendCancelMessage('Sorry, not possible.')
return false
end

-- The player also shouldn't be able to insert items into the boss corpse
local tile = Tile(container:getPosition())
for _, item in ipairs(tile:getItems()) do
if item:getAttribute(ITEM_ATTRIBUTE_CORPSEOWNER) == 2^31 - 1 and item:getName() == container:getName() then
self:sendCancelMessage('Sorry, not possible.')
return false
end
end
end

-- Do not let the player move the boss corpse.
if item:getAttribute(ITEM_ATTRIBUTE_CORPSEOWNER) == 2^31 - 1 then
self:sendCancelMessage('Sorry, not possible.')
return false
end

return true
end
12 changes: 12 additions & 0 deletions data/items/items.xml
Expand Up @@ -29326,6 +29326,11 @@
<item fromid="21514" toid="21515" article="a" name="strange shrine" />
<item id="21516" article="a" name="lift" />
<item id="21517" article="an" name="ornate door with a keyhole" />
<item id="21518" article="a" name="reward container">
<attribute key="containersize" value="32" />
<attribute key="pickupable" value="0" />
<attribute key="moveable" value="0" />
</item>
<item id="21536" article="a" name="hole">
<attribute key="floorchange" value="down" />
</item>
Expand Down Expand Up @@ -29381,6 +29386,13 @@
<attribute key="description" value="It contains the clothes of a dwarf." />
<attribute key="weight" value="800" />
</item>
<item id="21584" article="a" name="reward chest">
<attribute key="type" value="rewardchest" />
<attribute key="description" value="This chest contains your rewards earned in battles." />
<attribute key="containersize" value="32" />
<attribute key="pickupable" value="0" />
<attribute key="moveable" value="0" />
</item>
<item id="21690" article="a" name="triple bolt crossbow">
<attribute key="weight" value="6200" />
<attribute key="slotType" value="two-handed" />
Expand Down
2 changes: 2 additions & 0 deletions data/lib/lib.lua
Expand Up @@ -3,3 +3,5 @@ dofile('data/lib/core/core.lua')

-- Compatibility library for our old Lua API
dofile('data/lib/compat/compat.lua')

dofile('data/lib/rewardboss.lua')