canary/data/scripts/actions/items/exercise_training_weapons.lua
Eduardo Dantas 5e357d8a81
feat: add Way of the Monk quest and monk-related content (#3794)
feat: add Way of the Monk quest, monster updates, and core data improvements

This commit introduces the complete implementation of the Way of the Monk quest, along with multiple updates to monsters, bosses, items, and core gameplay data.

Quest:
• Added full quest definition for Way of the Monk, including mission flow, shrine tracking, and storage handling.
• Registered the new quest module in the quest catalog.
• Introduced new storage keys related to monk quest progression and items.
• Added a sample monk player migration (DB version 55) for testing purposes.
• Added Blue Valley to the towns list to support quest locations.

Monsters and Bosses:
• Added new monster Tame Terror Bird with complete stats, behaviors, and loot.
• Reworked Mitmah Vanguard boss with updated events, attributes, attacks, defenses, elemental modifiers, loot, and lifecycle hooks.
• Expanded loot tables for bosses such as The Brainstealer and The Monster, including monk-related items and potions.

Items and Loot:
• Updated frazzlemaw and guzzlemaw loot to drop traditional sai instead of sai.
• Added monk-related item IDs to destruction scripts and Dawnport cleanup logic.

Gameplay:
• Added Monk’s Apparition to the Soul War quest apparition list.
• Introduced a new combat formula for fist fighting skill.
• Removed legacy fist fighting attack speed configuration from server settings.

Workflow:
• Improved GitHub Actions workflow to ensure reusable checks run on the correct branch.
2026-02-12 23:03:25 -03:00

225 lines
7.7 KiB
Lua

local exhaustionTime = 10
local exerciseWeaponsTable = {
-- MELE
[50292] = { skill = SKILL_FIST },
[50293] = { skill = SKILL_FIST },
[50294] = { skill = SKILL_FIST },
[50295] = { skill = SKILL_FIST },
[28540] = { skill = SKILL_SWORD },
[28552] = { skill = SKILL_SWORD },
[35279] = { skill = SKILL_SWORD },
[35285] = { skill = SKILL_SWORD },
[28553] = { skill = SKILL_AXE },
[28541] = { skill = SKILL_AXE },
[35280] = { skill = SKILL_AXE },
[35286] = { skill = SKILL_AXE },
[28554] = { skill = SKILL_CLUB },
[28542] = { skill = SKILL_CLUB },
[35281] = { skill = SKILL_CLUB },
[35287] = { skill = SKILL_CLUB },
[44064] = { skill = SKILL_SHIELD },
[44065] = { skill = SKILL_SHIELD },
[44066] = { skill = SKILL_SHIELD },
[44067] = { skill = SKILL_SHIELD },
-- ROD
[28544] = { skill = SKILL_MAGLEVEL, effect = CONST_ANI_SMALLICE, allowFarUse = true },
[28556] = { skill = SKILL_MAGLEVEL, effect = CONST_ANI_SMALLICE, allowFarUse = true },
[35283] = { skill = SKILL_MAGLEVEL, effect = CONST_ANI_SMALLICE, allowFarUse = true },
[35289] = { skill = SKILL_MAGLEVEL, effect = CONST_ANI_SMALLICE, allowFarUse = true },
-- RANGE
[28543] = { skill = SKILL_DISTANCE, effect = CONST_ANI_SIMPLEARROW, allowFarUse = true },
[28555] = { skill = SKILL_DISTANCE, effect = CONST_ANI_SIMPLEARROW, allowFarUse = true },
[35282] = { skill = SKILL_DISTANCE, effect = CONST_ANI_SIMPLEARROW, allowFarUse = true },
[35288] = { skill = SKILL_DISTANCE, effect = CONST_ANI_SIMPLEARROW, allowFarUse = true },
-- WAND
[28545] = { skill = SKILL_MAGLEVEL, effect = CONST_ANI_FIRE, allowFarUse = true },
[28557] = { skill = SKILL_MAGLEVEL, effect = CONST_ANI_FIRE, allowFarUse = true },
[35284] = { skill = SKILL_MAGLEVEL, effect = CONST_ANI_FIRE, allowFarUse = true },
[35290] = { skill = SKILL_MAGLEVEL, effect = CONST_ANI_FIRE, allowFarUse = true },
}
local dummies = Game.getDummies()
local function leaveExerciseTraining(playerId, targetItem)
if _G.OnExerciseTraining[playerId] then
stopEvent(_G.OnExerciseTraining[playerId].event)
_G.OnExerciseTraining[playerId] = nil
end
local player = Player(playerId)
if player then
player:setTraining(false)
if targetItem then
targetItem:actor(false)
end
end
return
end
local function exerciseTrainingEvent(playerId, tilePosition, weaponId, dummyId)
local player = Player(playerId)
if not player then
return leaveExerciseTraining(playerId)
end
local targetItem = Tile(tilePosition):getItemById(dummyId)
if not targetItem then
player:sendTextMessage(MESSAGE_FAILURE, "Someone has moved the dummy, the training has stopped.")
leaveExerciseTraining(playerId, targetItem)
return false
end
if player:isTraining() == 0 then
player:sendTextMessage(MESSAGE_FAILURE, "You have stopped training.")
return leaveExerciseTraining(playerId, targetItem)
end
local playerPosition = player:getPosition()
if not playerPosition:isProtectionZoneTile() then
player:sendTextMessage(MESSAGE_FAILURE, "You are no longer in a protection zone, the training has stopped.")
leaveExerciseTraining(playerId)
return false
end
if player:getItemCount(weaponId) <= 0 then
player:sendTextMessage(MESSAGE_FAILURE, "You need the training weapon in the backpack, the training has stopped.")
leaveExerciseTraining(playerId, targetItem)
return false
end
local weapon = player:getItemById(weaponId, true)
if not weapon:isItem() or not weapon:hasAttribute(ITEM_ATTRIBUTE_CHARGES) then
player:sendTextMessage(MESSAGE_FAILURE, "The selected item is not a training weapon, the training has stopped.")
leaveExerciseTraining(playerId, targetItem)
return false
end
local weaponCharges = weapon:getAttribute(ITEM_ATTRIBUTE_CHARGES)
if not weaponCharges or weaponCharges <= 0 then
weapon:remove(1) -- ??
player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "Your training weapon has disappeared.")
leaveExerciseTraining(playerId, targetItem)
return false
end
if not dummies[dummyId] then
return false
end
local rate = dummies[dummyId] / 100
local isMagic = exerciseWeaponsTable[weaponId].skill == SKILL_MAGLEVEL
if isMagic then
player:addManaSpent(600 * rate)
else
player:addSkillTries(exerciseWeaponsTable[weaponId].skill, 7 * rate)
end
weapon:setAttribute(ITEM_ATTRIBUTE_CHARGES, (weaponCharges - 1))
tilePosition:sendMagicEffect(CONST_ME_HITAREA)
if exerciseWeaponsTable[weaponId].effect then
playerPosition:sendDistanceEffect(tilePosition, exerciseWeaponsTable[weaponId].effect)
end
if weapon:getAttribute(ITEM_ATTRIBUTE_CHARGES) <= 0 then
weapon:remove(1)
player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "Your training weapon has disappeared.")
leaveExerciseTraining(playerId, targetItem)
return false
end
local eventSpeedMultiplier = 1
local scopedFastExercise = KV.scoped("eventscheduler"):get("fast-exercise")
if scopedFastExercise then
eventSpeedMultiplier = 0.5
logger.debug("Fast exercise is enabled.")
end
local vocation = player:getVocation()
_G.OnExerciseTraining[playerId].event = addEvent(exerciseTrainingEvent, (vocation:getBaseAttackSpeed() / configManager.getFloat(configKeys.RATE_EXERCISE_TRAINING_SPEED)) * eventSpeedMultiplier, playerId, tilePosition, weaponId, dummyId)
return true
end
local function isDummy(id)
return dummies[id] and dummies[id] > 0
end
local exerciseTraining = Action()
function exerciseTraining.onUse(player, item, fromPosition, target, toPosition, isHotkey)
if not target or type(target) ~= "userdata" or not target:isItem() then
return true
end
local playerId = player:getId()
local targetId = target:getId()
local targetItem = Item(target.uid)
if targetItem and isDummy(targetId) then
if _G.OnExerciseTraining[playerId] then
player:sendTextMessage(MESSAGE_FAILURE, "You are already training!")
return true
end
local playerPos = player:getPosition()
if not exerciseWeaponsTable[item.itemid].allowFarUse and (playerPos:getDistance(target:getPosition()) > 1) then
player:sendTextMessage(MESSAGE_FAILURE, "Get closer to the dummy.")
return true
end
if not playerPos:isProtectionZoneTile() then
player:sendTextMessage(MESSAGE_FAILURE, "You need to be in a protection zone.")
return true
end
local playerHouse = player:getTile():getHouse()
local targetPos = target:getPosition()
local targetHouse = Tile(targetPos):getHouse()
if targetHouse and isDummy(targetId) then
if playerHouse ~= targetHouse then
player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You must be inside the house to use this dummy.")
return true
end
local playersOnDummy = 0
for _, playerTraining in pairs(_G.OnExerciseTraining) do
if playerTraining.dummyPos == targetPos then
playersOnDummy = playersOnDummy + 1
end
if playersOnDummy >= configManager.getNumber(configKeys.MAX_ALLOWED_ON_A_DUMMY) then
player:sendTextMessage(MESSAGE_FAILURE, "That exercise dummy is busy.")
return true
end
end
end
if player:hasExhaustion("training-exhaustion") then
player:sendTextMessage(MESSAGE_FAILURE, "This exercise dummy can only be used after a " .. exhaustionTime .. " seconds cooldown.")
return true
end
_G.OnExerciseTraining[playerId] = {}
if not _G.OnExerciseTraining[playerId].event then
_G.OnExerciseTraining[playerId].event = addEvent(exerciseTrainingEvent, 0, playerId, targetPos, item.itemid, targetId)
_G.OnExerciseTraining[playerId].dummyPos = targetPos
targetItem:actor(true)
player:setTraining(true)
player:setExhaustion("training-exhaustion", exhaustionTime)
player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You have started training on an exercise dummy.")
end
return true
end
return false
end
for weaponId, weapon in pairs(exerciseWeaponsTable) do
exerciseTraining:id(weaponId)
if weapon.allowFarUse then
exerciseTraining:allowFarUse(true)
end
end
exerciseTraining:register()