Initial commit
273
README.md
Normal file
@ -0,0 +1,273 @@
|
||||
# RollFor
|
||||
A World of Warcraft (1.12.1 and 2.5.2) addon that manages rolling for items.
|
||||
|
||||
## New in this fork
|
||||
This version includes the following new features:
|
||||
* New option `/rf config auto-class-announce` Toggle replace normal roll message with classes for items that has class restrictions.
|
||||
* New option `/rf config auto-tmog-disable` Toggle automatically disable tmog roll option on trash loot.
|
||||
* New option `/rf config loot-frame-cursor` Toggle loot frame being positioned at cursor location.
|
||||
* New popup to keep track of winners. Shift-click map icon or type `/rfw` to access.
|
||||
* Right click headers to customize filters
|
||||
* Click headers to sort the list
|
||||
* Tracks raid trades
|
||||
* Show roll popup for all group/raid members who have the addon installed when loot master starts a roll.
|
||||
* Enable with `/rf config client show-roll Eligible` or `Always`
|
||||
* `/rf config client` to view additional client options
|
||||
|
||||
## Demo
|
||||
|
||||
### NEW
|
||||
|
||||
**Classic Look**
|
||||
|
||||
<img src="docs/classic-look.png?v=2">
|
||||
|
||||
Enable: `/rf config classic-look`
|
||||
|
||||
See the classic-look in action: https://youtu.be/G37j5XXBKxs
|
||||
|
||||
|
||||
### Overview
|
||||
|
||||
In this example, the addon shows the soft-ressed items in the loot list.
|
||||
The Master Looter raid-rolls the trash item, then rolls non-SR items.
|
||||
Then the SR items are rolled.
|
||||
|
||||
<img src="docs/bindings.gif?v=2" alt="overview" style="width:1350px;height:350">
|
||||
|
||||
View better quality (fullscreen): https://youtu.be/f5nY-CxreIM
|
||||
|
||||
|
||||
### Tie roll
|
||||
|
||||
In this example, the addon automatically detects that the item is soft-reserved by two players.
|
||||
It restricts rolling for the item to these players only and resolves any tie automatically.
|
||||
The Master Looter then assigns the item directly to the winner.
|
||||
|
||||
<img src="docs/gui-sr-tie.gif" alt="soft-res rolling" style="width:1024;height:380">
|
||||
|
||||
|
||||
### Two top rolls win
|
||||
|
||||
In this example, two identical items drop. When clicked on any of these, the addon selects
|
||||
both and performs a "two-top-rolls-win" roll. Then the Master Looter is presented with
|
||||
individual award buttons for each winner.
|
||||
|
||||
<img src="docs/two-items.gif" alt="two top rolls win" style="width:1100px;height:306">
|
||||
|
||||
|
||||
## Features
|
||||
|
||||
### Fully SR-integrated loot list
|
||||
<img src="docs/loot-list.gif" alt="SR-integrated loot list" style="width:720px;height:350">
|
||||
|
||||
---
|
||||
|
||||
### Automatically enables Master Loot when a boss is targeted
|
||||
Disable this feature with:
|
||||
```
|
||||
/rf config auto-master-loot
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Shows the loot that dropped (and who soft reserved)
|
||||
<img src="docs/dropped-loot.gif" alt="Shows dropped loot" style="width:720px;height:350">
|
||||
|
||||
---
|
||||
|
||||
### Makes Master Loot window pretty and safe
|
||||
* one window with players sorted by class
|
||||
* adds confirmation window
|
||||
|
||||
<img src="docs/master-loot-window.gif" alt="Pretty Master Loot window" style="width:720px;height:350">
|
||||
|
||||
---
|
||||
|
||||
### Fully automated
|
||||
* Detects if someone rolls too many times and ignores extra rolls.
|
||||
* If multiple players roll the same number, it automatically shows it and
|
||||
waits for these players to re-roll.
|
||||
|
||||
<img src="docs/tie-winners.gif" alt="Tie winners" style="width:720px;height:350">
|
||||
|
||||
---
|
||||
|
||||
### Soft res integration
|
||||
* Integrates with https://raidres.fly.dev (1.12.1).
|
||||
* Integrates with https://softres.it (2.5.2) via Gargul Export.
|
||||
* Minimap icon shows soft res status and who did not soft res.
|
||||
* Fully automated (shows who soft ressed, only accepts rolls from players who SR).
|
||||
|
||||
---
|
||||
|
||||
### And more
|
||||
* Supports "**two top rolls win**" rolling.
|
||||
* Supports **raid rolls**.
|
||||
* Supports offspec rolls (`/roll 99`).
|
||||
* Supports transmog rolls (`/roll 98`) (1.12.1).
|
||||
* Automatically resolves tied rolls.
|
||||
* Highly customizable - see `/rf config` and `/rf config help`.
|
||||
|
||||
<img src="docs/raid-roll.gif" alt="Raid roll" style="width:720px;height:350">
|
||||
|
||||
---
|
||||
|
||||
### See it in action
|
||||
https://youtu.be/vZdafun0nYo
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
### Roll item
|
||||
```
|
||||
/rf <item link>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Raid-roll item from your bags
|
||||
```
|
||||
/rr <item link>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Insta Raid-roll item from your bags
|
||||
```
|
||||
/irr <item link>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Roll for 2 items (two top rolls win)
|
||||
```
|
||||
/rf 2x<item link>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
|
||||
### Ignore SR and allow everyone to roll
|
||||
If the item is SRed, the addon will only watch rolls for players who SRed.
|
||||
However, if you want everyone to roll, even if the item is SRed, use `/arf`
|
||||
instead of `/rf`. "arf" stands for "All Roll For".
|
||||
|
||||
---
|
||||
|
||||
|
||||
## Soft-Res setup
|
||||
|
||||
1. Create a Soft Res list at https://raidres.fly.dev (1.12.1) or https://softres.it (2.5.2).
|
||||
2. Ask raiders to add their items.
|
||||
3. When ready, lock the raid and click on **RollFor export** (raidres.fly.dev) or **Gargul Export** (softes.it) button.
|
||||
|
||||
<img src="docs/raidres-export.jpg" alt="Raidres export" style="width:720px;height:350">
|
||||
|
||||
4. Click on **Copy RollFor data to clipboard** buton.
|
||||
|
||||
<img src="docs/raidres-copy-to-clipboard.jpg" alt="Raidres copy to clipboard" style="width:720px;height:350">
|
||||
|
||||
5. Click on the minimap icon or type `/sr`.
|
||||
6. Paste the data into the window.
|
||||
7. Click **Import!**.
|
||||
|
||||
<img src="docs/softres-import.jpg" alt="softres-import" style="width:720px;height:350">
|
||||
|
||||
The addon will tell you the status of SR import.
|
||||
Hovering over the minimap icon will tell you who did not soft-res.
|
||||
|
||||
The minimap icon will be **green** if everyone in the group is soft-ressing.
|
||||
The minimap icon will be **orange** if someone has not soft-ressed.
|
||||
The minimap icon will be **red** if you have an outdated soft-res data.
|
||||
The minimap icon will be **white** if there is no soft-res data.
|
||||
|
||||
To show the SR items type:
|
||||
```
|
||||
/srs
|
||||
```
|
||||
|
||||
If someone needs to update their items, repeat the process and copy the data again.
|
||||
|
||||
|
||||
### Soft-Res data format
|
||||
|
||||
The SR data from *Raidres* is a **Base64** encoded **JSON**. Decode it to see what's inside.
|
||||
|
||||
---
|
||||
|
||||
|
||||
### Fixing mistyped player names in SR setup
|
||||
|
||||
When using soft-res, the players sometimes mistype their nickname, e.g.
|
||||
`Johnny` in game will be `Jonnhy` in the raidres.fly.dev website.
|
||||
The addon is smart enough to fix simple typos like that for you.
|
||||
It will also deal with special characters in player names.
|
||||
However, sometimes there's so many typos and the addon can't match the
|
||||
player's name - you have to fix it manually.
|
||||
|
||||
`/sro` (stands for SR Override) is the command to do this.
|
||||
|
||||
---
|
||||
|
||||
|
||||
### Finish rolls early
|
||||
```
|
||||
/fr
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
|
||||
### Cancel rolls
|
||||
```
|
||||
/cr
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
|
||||
### Show soft-ressed items
|
||||
```
|
||||
/srs
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
|
||||
### Check soft-res status (to see if everyone is soft-ressing)
|
||||
```
|
||||
/src
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
|
||||
### Clear soft-res data
|
||||
Click on the minimap icon and click **Clear** or type:
|
||||
```
|
||||
/sr init
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
|
||||
## Shoutouts
|
||||
|
||||
Thank you to:
|
||||
* **Turtle WoW devs** for amazing content. You cunts should switch to a better client.
|
||||
* **Itamedruids** for *Raidres* and adding the export function. Love your work.
|
||||
* My fellow raiders (there's too many to mention).
|
||||
* All bug reporters, testers and feature suggesters.
|
||||
|
||||
|
||||
## Need more help?
|
||||
|
||||
The best way to contact me is to message me on Discord.
|
||||
Username: **Obszczymucha**
|
||||
|
||||
My character **Jogobobek** will no longer be available on Turtle WoW.
|
||||
I'm switching to Netherwing 3.0, perhaps under a different name :P
|
||||
|
||||
Thanks Turtle for fun.
|
||||
|
||||
94
RollFor-BCC.toc
Normal file
@ -0,0 +1,94 @@
|
||||
## Interface: 20502
|
||||
## Title: RollFor
|
||||
## Author: Obszczymucha
|
||||
## Version: 4.7.2
|
||||
## Notes: An automated item roller with soft ressing support via softres.it.
|
||||
## SavedVariables: RollForDb
|
||||
## SavedVariablesPerCharacter: RollForCharDb
|
||||
|
||||
libs\bcc\LibStub\LibStub.lua
|
||||
libs\bcc\Libs.xml
|
||||
|
||||
src\bcc\compat.lua
|
||||
src\bcc\Json.lua
|
||||
|
||||
src\modules.lua
|
||||
src\DebugBuffer.lua
|
||||
src\Module.lua
|
||||
src\Db.lua
|
||||
src\Types.lua
|
||||
src\Interface.lua
|
||||
src\WowApi.lua
|
||||
src\Config.lua
|
||||
src\ItemUtils.lua
|
||||
src\EventFrame.lua
|
||||
src\LootFacade.lua
|
||||
src\RollingLogicUtils.lua
|
||||
src\DroppedLoot.lua
|
||||
src\DroppedLootAnnounce.lua
|
||||
src\TradeTracker.lua
|
||||
src\SoftResDataTransformer.lua
|
||||
src\SoftRes.lua
|
||||
src\SoftResAwardedLootDecorator.lua
|
||||
src\SoftResAbsentPlayersDecorator.lua
|
||||
src\SoftResPresentPlayersDecorator.lua
|
||||
src\SoftResMatchedNameDecorator.lua
|
||||
src\AwardedLoot.lua
|
||||
src\GroupRoster.lua
|
||||
src\NameAutoMatcher.lua
|
||||
src\NameManualMatcher.lua
|
||||
src\NameMatchReport.lua
|
||||
src\EventHandler.lua
|
||||
src\SoftResGui.lua
|
||||
src\VersionBroadcast.lua
|
||||
src\MasterLoot.lua
|
||||
src\MasterLootCandidateSelectionFrame.lua
|
||||
src\SoftResCheck.lua
|
||||
src\SoftResRollingLogic.lua
|
||||
src\NonSoftResRollingLogic.lua
|
||||
src\TieRollingLogic.lua
|
||||
src\RaidRollRollingLogic.lua
|
||||
src\UsagePrinter.lua
|
||||
src\MinimapButton.lua
|
||||
src\MasterLootWarning.lua
|
||||
src\AutoLoot.lua
|
||||
src\WinnerTracker.lua
|
||||
src\FrameBuilder.lua
|
||||
src\PopupBuilder.lua
|
||||
src\LootAwardPopup.lua
|
||||
src\MasterLootCandidates.lua
|
||||
src\NewGroupEvent.lua
|
||||
src\AutoGroupLoot.lua
|
||||
src\BossList.lua
|
||||
src\AutoMasterLoot.lua
|
||||
src\RollTracker.lua
|
||||
src\RollController.lua
|
||||
src\RollingPopup.lua
|
||||
src\SoftResRollGuiData.lua
|
||||
src\TieRollGuiData.lua
|
||||
src\RollingPopupContentTransformer.lua
|
||||
src\GuiElements.lua
|
||||
src\WelcomePopup.lua
|
||||
src\InstaRaidRollRollingLogic.lua
|
||||
src\EventBus.lua
|
||||
src\LootList.lua
|
||||
src\SoftResLootListDecorator.lua
|
||||
src\LootFrame.lua
|
||||
src\RollForAd.lua
|
||||
src\RollingStrategyFactory.lua
|
||||
src\RollingLogic.lua
|
||||
src\ArgsParser.lua
|
||||
src\RollResultAnnouncer.lua
|
||||
src\ChatApi.lua
|
||||
src\Chat.lua
|
||||
src\PlayerInfo.lua
|
||||
src\LootAwardCallback.lua
|
||||
src\LootFacadeListener.lua
|
||||
src\LootController.lua
|
||||
src\TooltipReader.lua
|
||||
src\UiReloadPopup.lua
|
||||
src\Sandbox.lua
|
||||
src\ModernLootFrameSkin.lua
|
||||
src\OgLootFrameSkin.lua
|
||||
|
||||
main.lua
|
||||
100
RollFor.toc
Normal file
@ -0,0 +1,100 @@
|
||||
## Interface: 11200
|
||||
## Title: RollFor
|
||||
## Author: Obszczymucha
|
||||
## Version: 4.7.2
|
||||
## Notes: An automated item roller with soft ressing support via raidres.fly.dev.
|
||||
## SavedVariables: RollForDb
|
||||
## SavedVariablesPerCharacter: RollForCharDb
|
||||
|
||||
libs\vanilla\LibStub\LibStub.lua
|
||||
libs\vanilla\Libs.xml
|
||||
|
||||
src\vanilla\backport.lua
|
||||
src\vanilla\compat.lua
|
||||
src\vanilla\Json.lua
|
||||
|
||||
src\modules.lua
|
||||
src\DebugBuffer.lua
|
||||
src\Module.lua
|
||||
src\Db.lua
|
||||
src\Types.lua
|
||||
src\Interface.lua
|
||||
src\WowApi.lua
|
||||
src\Config.lua
|
||||
src\ItemUtils.lua
|
||||
src\EventFrame.lua
|
||||
src\LootFacade.lua
|
||||
src\RollingLogicUtils.lua
|
||||
src\DroppedLoot.lua
|
||||
src\DroppedLootAnnounce.lua
|
||||
src\TradeTracker.lua
|
||||
src\SoftResDataTransformer.lua
|
||||
src\SoftRes.lua
|
||||
src\SoftResAwardedLootDecorator.lua
|
||||
src\SoftResAbsentPlayersDecorator.lua
|
||||
src\SoftResPresentPlayersDecorator.lua
|
||||
src\SoftResMatchedNameDecorator.lua
|
||||
src\AwardedLoot.lua
|
||||
src\GroupRoster.lua
|
||||
src\NameAutoMatcher.lua
|
||||
src\NameManualMatcher.lua
|
||||
src\NameMatchReport.lua
|
||||
src\EventHandler.lua
|
||||
src\SoftResGui.lua
|
||||
src\VersionBroadcast.lua
|
||||
src\MasterLoot.lua
|
||||
src\MasterLootCandidateSelectionFrame.lua
|
||||
src\SoftResCheck.lua
|
||||
src\SoftResRollingLogic.lua
|
||||
src\NonSoftResRollingLogic.lua
|
||||
src\TieRollingLogic.lua
|
||||
src\RaidRollRollingLogic.lua
|
||||
src\UsagePrinter.lua
|
||||
src\MinimapButton.lua
|
||||
src\MasterLootWarning.lua
|
||||
src\AutoLoot.lua
|
||||
src\WinnerTracker.lua
|
||||
src\FrameBuilder.lua
|
||||
src\PopupBuilder.lua
|
||||
src\LootAwardPopup.lua
|
||||
src\MasterLootCandidates.lua
|
||||
src\NewGroupEvent.lua
|
||||
src\AutoGroupLoot.lua
|
||||
src\BossList.lua
|
||||
src\AutoMasterLoot.lua
|
||||
src\RollTracker.lua
|
||||
src\RollController.lua
|
||||
src\RollingPopup.lua
|
||||
src\WinnersPopup.lua
|
||||
src\WinnersPopupGui.lua
|
||||
src\ConfirmPopup.lua
|
||||
src\SoftResRollGuiData.lua
|
||||
src\TieRollGuiData.lua
|
||||
src\RollingPopupContentTransformer.lua
|
||||
src\GuiElements.lua
|
||||
src\WelcomePopup.lua
|
||||
src\InstaRaidRollRollingLogic.lua
|
||||
src\EventBus.lua
|
||||
src\LootList.lua
|
||||
src\SoftResLootListDecorator.lua
|
||||
src\LootFrame.lua
|
||||
src\RollForAd.lua
|
||||
src\RollingStrategyFactory.lua
|
||||
src\RollingLogic.lua
|
||||
src\ArgsParser.lua
|
||||
src\RollResultAnnouncer.lua
|
||||
src\ChatApi.lua
|
||||
src\Chat.lua
|
||||
src\PlayerInfo.lua
|
||||
src\LootAwardCallback.lua
|
||||
src\LootFacadeListener.lua
|
||||
src\LootController.lua
|
||||
src\TooltipReader.lua
|
||||
src\UiReloadPopup.lua
|
||||
src\Sandbox.lua
|
||||
src\ModernLootFrameSkin.lua
|
||||
src\OgLootFrameSkin.lua
|
||||
src\Client.lua
|
||||
src\ClientBroadcast.lua
|
||||
|
||||
main.lua
|
||||
4
TODO.md
Normal file
@ -0,0 +1,4 @@
|
||||
# TODO
|
||||
|
||||
1. Add configurable loot frame colors with color pickers.
|
||||
2. Add non-master looter popup display with an ability to roll.
|
||||
BIN
assets/col.tga
Normal file
BIN
assets/icon-green.tga
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
assets/icon-orange.tga
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
assets/icon-red.tga
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
assets/icon-white.tga
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
assets/icon-white2.tga
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
assets/info.tga
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
assets/og-loot-frame.tga
Normal file
BIN
assets/resize-grip.tga
Normal file
BIN
assets/star-gold.tga
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
assets/tiny-button-down.tga
Normal file
BIN
assets/tiny-button-up.tga
Normal file
BIN
assets/titlebar-top.tga
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
assets/titlebar-topleft.tga
Normal file
BIN
assets/titlebar-topright.tga
Normal file
278
libs/bcc/AceTimer-3.0/AceTimer-3.0.lua
Normal file
@ -0,0 +1,278 @@
|
||||
--- **AceTimer-3.0** provides a central facility for registering timers.
|
||||
-- AceTimer supports one-shot timers and repeating timers. All timers are stored in an efficient
|
||||
-- data structure that allows easy dispatching and fast rescheduling. Timers can be registered
|
||||
-- or canceled at any time, even from within a running timer, without conflict or large overhead.\\
|
||||
-- AceTimer is currently limited to firing timers at a frequency of 0.01s as this is what the WoW timer API
|
||||
-- restricts us to.
|
||||
--
|
||||
-- All `:Schedule` functions will return a handle to the current timer, which you will need to store if you
|
||||
-- need to cancel the timer you just registered.
|
||||
--
|
||||
-- **AceTimer-3.0** can be embeded into your addon, either explicitly by calling AceTimer:Embed(MyAddon) or by
|
||||
-- specifying it as an embeded library in your AceAddon. All functions will be available on your addon object
|
||||
-- and can be accessed directly, without having to explicitly call AceTimer itself.\\
|
||||
-- It is recommended to embed AceTimer, otherwise you'll have to specify a custom `self` on all calls you
|
||||
-- make into AceTimer.
|
||||
-- @class file
|
||||
-- @name AceTimer-3.0
|
||||
-- @release $Id: AceTimer-3.0.lua 1284 2022-09-25 09:15:30Z nevcairiel $
|
||||
|
||||
local MAJOR, MINOR = "AceTimer-3.0", 17 -- Bump minor on changes
|
||||
local AceTimer, oldminor = LibStub:NewLibrary(MAJOR, MINOR)
|
||||
|
||||
if not AceTimer then return end -- No upgrade needed
|
||||
AceTimer.activeTimers = AceTimer.activeTimers or {} -- Active timer list
|
||||
local activeTimers = AceTimer.activeTimers -- Upvalue our private data
|
||||
|
||||
-- Lua APIs
|
||||
local type, unpack, next, error, select = type, unpack, next, error, select
|
||||
-- WoW APIs
|
||||
local GetTime, C_TimerAfter = GetTime, C_Timer.After
|
||||
|
||||
local function new(self, loop, func, delay, ...)
|
||||
if delay < 0.01 then
|
||||
delay = 0.01 -- Restrict to the lowest time that the C_Timer API allows us
|
||||
end
|
||||
|
||||
local timer = {
|
||||
object = self,
|
||||
func = func,
|
||||
looping = loop,
|
||||
argsCount = select("#", ...),
|
||||
delay = delay,
|
||||
ends = GetTime() + delay,
|
||||
...
|
||||
}
|
||||
|
||||
activeTimers[timer] = timer
|
||||
|
||||
-- Create new timer closure to wrap the "timer" object
|
||||
timer.callback = function()
|
||||
if not timer.cancelled then
|
||||
if type(timer.func) == "string" then
|
||||
-- We manually set the unpack count to prevent issues with an arg set that contains nil and ends with nil
|
||||
-- e.g. local t = {1, 2, nil, 3, nil} print(#t) will result in 2, instead of 5. This fixes said issue.
|
||||
timer.object[timer.func](timer.object, unpack(timer, 1, timer.argsCount))
|
||||
else
|
||||
timer.func(unpack(timer, 1, timer.argsCount))
|
||||
end
|
||||
|
||||
if timer.looping and not timer.cancelled then
|
||||
-- Compensate delay to get a perfect average delay, even if individual times don't match up perfectly
|
||||
-- due to fps differences
|
||||
local time = GetTime()
|
||||
local ndelay = timer.delay - (time - timer.ends)
|
||||
-- Ensure the delay doesn't go below the threshold
|
||||
if ndelay < 0.01 then ndelay = 0.01 end
|
||||
C_TimerAfter(ndelay, timer.callback)
|
||||
timer.ends = time + ndelay
|
||||
else
|
||||
activeTimers[timer.handle or timer] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
C_TimerAfter(delay, timer.callback)
|
||||
return timer
|
||||
end
|
||||
|
||||
--- Schedule a new one-shot timer.
|
||||
-- The timer will fire once in `delay` seconds, unless canceled before.
|
||||
-- @param callback Callback function for the timer pulse (funcref or method name).
|
||||
-- @param delay Delay for the timer, in seconds.
|
||||
-- @param ... An optional, unlimited amount of arguments to pass to the callback function.
|
||||
-- @usage
|
||||
-- MyAddOn = LibStub("AceAddon-3.0"):NewAddon("MyAddOn", "AceTimer-3.0")
|
||||
--
|
||||
-- function MyAddOn:OnEnable()
|
||||
-- self:ScheduleTimer("TimerFeedback", 5)
|
||||
-- end
|
||||
--
|
||||
-- function MyAddOn:TimerFeedback()
|
||||
-- print("5 seconds passed")
|
||||
-- end
|
||||
function AceTimer:ScheduleTimer(func, delay, ...)
|
||||
if not func or not delay then
|
||||
error(MAJOR..": ScheduleTimer(callback, delay, args...): 'callback' and 'delay' must have set values.", 2)
|
||||
end
|
||||
if type(func) == "string" then
|
||||
if type(self) ~= "table" then
|
||||
error(MAJOR..": ScheduleTimer(callback, delay, args...): 'self' - must be a table.", 2)
|
||||
elseif not self[func] then
|
||||
error(MAJOR..": ScheduleTimer(callback, delay, args...): Tried to register '"..func.."' as the callback, but it doesn't exist in the module.", 2)
|
||||
end
|
||||
end
|
||||
return new(self, nil, func, delay, ...)
|
||||
end
|
||||
|
||||
--- Schedule a repeating timer.
|
||||
-- The timer will fire every `delay` seconds, until canceled.
|
||||
-- @param callback Callback function for the timer pulse (funcref or method name).
|
||||
-- @param delay Delay for the timer, in seconds.
|
||||
-- @param ... An optional, unlimited amount of arguments to pass to the callback function.
|
||||
-- @usage
|
||||
-- MyAddOn = LibStub("AceAddon-3.0"):NewAddon("MyAddOn", "AceTimer-3.0")
|
||||
--
|
||||
-- function MyAddOn:OnEnable()
|
||||
-- self.timerCount = 0
|
||||
-- self.testTimer = self:ScheduleRepeatingTimer("TimerFeedback", 5)
|
||||
-- end
|
||||
--
|
||||
-- function MyAddOn:TimerFeedback()
|
||||
-- self.timerCount = self.timerCount + 1
|
||||
-- print(("%d seconds passed"):format(5 * self.timerCount))
|
||||
-- -- run 30 seconds in total
|
||||
-- if self.timerCount == 6 then
|
||||
-- self:CancelTimer(self.testTimer)
|
||||
-- end
|
||||
-- end
|
||||
function AceTimer:ScheduleRepeatingTimer(func, delay, ...)
|
||||
if not func or not delay then
|
||||
error(MAJOR..": ScheduleRepeatingTimer(callback, delay, args...): 'callback' and 'delay' must have set values.", 2)
|
||||
end
|
||||
if type(func) == "string" then
|
||||
if type(self) ~= "table" then
|
||||
error(MAJOR..": ScheduleRepeatingTimer(callback, delay, args...): 'self' - must be a table.", 2)
|
||||
elseif not self[func] then
|
||||
error(MAJOR..": ScheduleRepeatingTimer(callback, delay, args...): Tried to register '"..func.."' as the callback, but it doesn't exist in the module.", 2)
|
||||
end
|
||||
end
|
||||
return new(self, true, func, delay, ...)
|
||||
end
|
||||
|
||||
--- Cancels a timer with the given id, registered by the same addon object as used for `:ScheduleTimer`
|
||||
-- Both one-shot and repeating timers can be canceled with this function, as long as the `id` is valid
|
||||
-- and the timer has not fired yet or was canceled before.
|
||||
-- @param id The id of the timer, as returned by `:ScheduleTimer` or `:ScheduleRepeatingTimer`
|
||||
function AceTimer:CancelTimer(id)
|
||||
local timer = activeTimers[id]
|
||||
|
||||
if not timer then
|
||||
return false
|
||||
else
|
||||
timer.cancelled = true
|
||||
activeTimers[id] = nil
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
--- Cancels all timers registered to the current addon object ('self')
|
||||
function AceTimer:CancelAllTimers()
|
||||
for k,v in next, activeTimers do
|
||||
if v.object == self then
|
||||
AceTimer.CancelTimer(self, k)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Returns the time left for a timer with the given id, registered by the current addon object ('self').
|
||||
-- This function will return 0 when the id is invalid.
|
||||
-- @param id The id of the timer, as returned by `:ScheduleTimer` or `:ScheduleRepeatingTimer`
|
||||
-- @return The time left on the timer.
|
||||
function AceTimer:TimeLeft(id)
|
||||
local timer = activeTimers[id]
|
||||
if not timer then
|
||||
return 0
|
||||
else
|
||||
return timer.ends - GetTime()
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- ---------------------------------------------------------------------
|
||||
-- Upgrading
|
||||
|
||||
-- Upgrade from old hash-bucket based timers to C_Timer.After timers.
|
||||
if oldminor and oldminor < 10 then
|
||||
-- disable old timer logic
|
||||
AceTimer.frame:SetScript("OnUpdate", nil)
|
||||
AceTimer.frame:SetScript("OnEvent", nil)
|
||||
AceTimer.frame:UnregisterAllEvents()
|
||||
-- convert timers
|
||||
for object,timers in next, AceTimer.selfs do
|
||||
for handle,timer in next, timers do
|
||||
if type(timer) == "table" and timer.callback then
|
||||
local newTimer
|
||||
if timer.delay then
|
||||
newTimer = AceTimer.ScheduleRepeatingTimer(timer.object, timer.callback, timer.delay, timer.arg)
|
||||
else
|
||||
newTimer = AceTimer.ScheduleTimer(timer.object, timer.callback, timer.when - GetTime(), timer.arg)
|
||||
end
|
||||
-- Use the old handle for old timers
|
||||
activeTimers[newTimer] = nil
|
||||
activeTimers[handle] = newTimer
|
||||
newTimer.handle = handle
|
||||
end
|
||||
end
|
||||
end
|
||||
AceTimer.selfs = nil
|
||||
AceTimer.hash = nil
|
||||
AceTimer.debug = nil
|
||||
elseif oldminor and oldminor < 17 then
|
||||
-- Upgrade from old animation based timers to C_Timer.After timers.
|
||||
AceTimer.inactiveTimers = nil
|
||||
AceTimer.frame = nil
|
||||
local oldTimers = AceTimer.activeTimers
|
||||
-- Clear old timer table and update upvalue
|
||||
AceTimer.activeTimers = {}
|
||||
activeTimers = AceTimer.activeTimers
|
||||
for handle, timer in next, oldTimers do
|
||||
local newTimer
|
||||
-- Stop the old timer animation
|
||||
local duration, elapsed = timer:GetDuration(), timer:GetElapsed()
|
||||
timer:GetParent():Stop()
|
||||
if timer.looping then
|
||||
newTimer = AceTimer.ScheduleRepeatingTimer(timer.object, timer.func, duration, unpack(timer.args, 1, timer.argsCount))
|
||||
else
|
||||
newTimer = AceTimer.ScheduleTimer(timer.object, timer.func, duration - elapsed, unpack(timer.args, 1, timer.argsCount))
|
||||
end
|
||||
-- Use the old handle for old timers
|
||||
activeTimers[newTimer] = nil
|
||||
activeTimers[handle] = newTimer
|
||||
newTimer.handle = handle
|
||||
end
|
||||
|
||||
-- Migrate transitional handles
|
||||
if oldminor < 13 and AceTimer.hashCompatTable then
|
||||
for handle, id in next, AceTimer.hashCompatTable do
|
||||
local t = activeTimers[id]
|
||||
if t then
|
||||
activeTimers[id] = nil
|
||||
activeTimers[handle] = t
|
||||
t.handle = handle
|
||||
end
|
||||
end
|
||||
AceTimer.hashCompatTable = nil
|
||||
end
|
||||
end
|
||||
|
||||
-- ---------------------------------------------------------------------
|
||||
-- Embed handling
|
||||
|
||||
AceTimer.embeds = AceTimer.embeds or {}
|
||||
|
||||
local mixins = {
|
||||
"ScheduleTimer", "ScheduleRepeatingTimer",
|
||||
"CancelTimer", "CancelAllTimers",
|
||||
"TimeLeft"
|
||||
}
|
||||
|
||||
function AceTimer:Embed(target)
|
||||
AceTimer.embeds[target] = true
|
||||
for _,v in next, mixins do
|
||||
target[v] = AceTimer[v]
|
||||
end
|
||||
return target
|
||||
end
|
||||
|
||||
-- AceTimer:OnEmbedDisable(target)
|
||||
-- target (object) - target object that AceTimer is embedded in.
|
||||
--
|
||||
-- cancel all timers registered for the object
|
||||
function AceTimer:OnEmbedDisable(target)
|
||||
target:CancelAllTimers()
|
||||
end
|
||||
|
||||
for addon in next, AceTimer.embeds do
|
||||
AceTimer:Embed(addon)
|
||||
end
|
||||
4
libs/bcc/AceTimer-3.0/AceTimer-3.0.xml
Normal file
@ -0,0 +1,4 @@
|
||||
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/
|
||||
..\FrameXML\UI.xsd">
|
||||
<Script file="AceTimer-3.0.lua"/>
|
||||
</Ui>
|
||||
207
libs/bcc/CallbackHandler-1.0/CallbackHandler-1.0.lua
Normal file
@ -0,0 +1,207 @@
|
||||
--[[ $Id: CallbackHandler-1.0.lua 1284 2022-09-25 09:15:30Z nevcairiel $ ]]
|
||||
local MAJOR, MINOR = "CallbackHandler-1.0", 7
|
||||
local CallbackHandler = LibStub:NewLibrary(MAJOR, MINOR)
|
||||
|
||||
if not CallbackHandler then return end -- No upgrade needed
|
||||
|
||||
local meta = {__index = function(tbl, key) tbl[key] = {} return tbl[key] end}
|
||||
|
||||
-- Lua APIs
|
||||
local error = error
|
||||
local setmetatable, rawget = setmetatable, rawget
|
||||
local next, select, pairs, type, tostring = next, select, pairs, type, tostring
|
||||
|
||||
local xpcall = xpcall
|
||||
|
||||
local function errorhandler(err)
|
||||
return geterrorhandler()(err)
|
||||
end
|
||||
|
||||
local function Dispatch(handlers, ...)
|
||||
local index, method = next(handlers)
|
||||
if not method then return end
|
||||
repeat
|
||||
xpcall(method, errorhandler, ...)
|
||||
index, method = next(handlers, index)
|
||||
until not method
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------
|
||||
-- CallbackHandler:New
|
||||
--
|
||||
-- target - target object to embed public APIs in
|
||||
-- RegisterName - name of the callback registration API, default "RegisterCallback"
|
||||
-- UnregisterName - name of the callback unregistration API, default "UnregisterCallback"
|
||||
-- UnregisterAllName - name of the API to unregister all callbacks, default "UnregisterAllCallbacks". false == don't publish this API.
|
||||
|
||||
function CallbackHandler.New(_self, target, RegisterName, UnregisterName, UnregisterAllName)
|
||||
|
||||
RegisterName = RegisterName or "RegisterCallback"
|
||||
UnregisterName = UnregisterName or "UnregisterCallback"
|
||||
if UnregisterAllName==nil then -- false is used to indicate "don't want this method"
|
||||
UnregisterAllName = "UnregisterAllCallbacks"
|
||||
end
|
||||
|
||||
-- we declare all objects and exported APIs inside this closure to quickly gain access
|
||||
-- to e.g. function names, the "target" parameter, etc
|
||||
|
||||
|
||||
-- Create the registry object
|
||||
local events = setmetatable({}, meta)
|
||||
local registry = { recurse=0, events=events }
|
||||
|
||||
-- registry:Fire() - fires the given event/message into the registry
|
||||
function registry:Fire(eventname, ...)
|
||||
if not rawget(events, eventname) or not next(events[eventname]) then return end
|
||||
local oldrecurse = registry.recurse
|
||||
registry.recurse = oldrecurse + 1
|
||||
|
||||
Dispatch(events[eventname], eventname, ...)
|
||||
|
||||
registry.recurse = oldrecurse
|
||||
|
||||
if registry.insertQueue and oldrecurse==0 then
|
||||
-- Something in one of our callbacks wanted to register more callbacks; they got queued
|
||||
for event,callbacks in pairs(registry.insertQueue) do
|
||||
local first = not rawget(events, event) or not next(events[event]) -- test for empty before. not test for one member after. that one member may have been overwritten.
|
||||
for object,func in pairs(callbacks) do
|
||||
events[event][object] = func
|
||||
-- fire OnUsed callback?
|
||||
if first and registry.OnUsed then
|
||||
registry.OnUsed(registry, target, event)
|
||||
first = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
registry.insertQueue = nil
|
||||
end
|
||||
end
|
||||
|
||||
-- Registration of a callback, handles:
|
||||
-- self["method"], leads to self["method"](self, ...)
|
||||
-- self with function ref, leads to functionref(...)
|
||||
-- "addonId" (instead of self) with function ref, leads to functionref(...)
|
||||
-- all with an optional arg, which, if present, gets passed as first argument (after self if present)
|
||||
target[RegisterName] = function(self, eventname, method, ... --[[actually just a single arg]])
|
||||
if type(eventname) ~= "string" then
|
||||
error("Usage: "..RegisterName.."(eventname, method[, arg]): 'eventname' - string expected.", 2)
|
||||
end
|
||||
|
||||
method = method or eventname
|
||||
|
||||
local first = not rawget(events, eventname) or not next(events[eventname]) -- test for empty before. not test for one member after. that one member may have been overwritten.
|
||||
|
||||
if type(method) ~= "string" and type(method) ~= "function" then
|
||||
error("Usage: "..RegisterName.."(\"eventname\", \"methodname\"): 'methodname' - string or function expected.", 2)
|
||||
end
|
||||
|
||||
local regfunc
|
||||
|
||||
if type(method) == "string" then
|
||||
-- self["method"] calling style
|
||||
if type(self) ~= "table" then
|
||||
error("Usage: "..RegisterName.."(\"eventname\", \"methodname\"): self was not a table?", 2)
|
||||
elseif self==target then
|
||||
error("Usage: "..RegisterName.."(\"eventname\", \"methodname\"): do not use Library:"..RegisterName.."(), use your own 'self'", 2)
|
||||
elseif type(self[method]) ~= "function" then
|
||||
error("Usage: "..RegisterName.."(\"eventname\", \"methodname\"): 'methodname' - method '"..tostring(method).."' not found on self.", 2)
|
||||
end
|
||||
|
||||
if select("#",...)>=1 then -- this is not the same as testing for arg==nil!
|
||||
local arg=select(1,...)
|
||||
regfunc = function(...) self[method](self,arg,...) end
|
||||
else
|
||||
regfunc = function(...) self[method](self,...) end
|
||||
end
|
||||
else
|
||||
-- function ref with self=object or self="addonId" or self=thread
|
||||
if type(self)~="table" and type(self)~="string" and type(self)~="thread" then
|
||||
error("Usage: "..RegisterName.."(self or \"addonId\", eventname, method): 'self or addonId': table or string or thread expected.", 2)
|
||||
end
|
||||
|
||||
if select("#",...)>=1 then -- this is not the same as testing for arg==nil!
|
||||
local arg=select(1,...)
|
||||
regfunc = function(...) method(arg,...) end
|
||||
else
|
||||
regfunc = method
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
if events[eventname][self] or registry.recurse<1 then
|
||||
-- if registry.recurse<1 then
|
||||
-- we're overwriting an existing entry, or not currently recursing. just set it.
|
||||
events[eventname][self] = regfunc
|
||||
-- fire OnUsed callback?
|
||||
if registry.OnUsed and first then
|
||||
registry.OnUsed(registry, target, eventname)
|
||||
end
|
||||
else
|
||||
-- we're currently processing a callback in this registry, so delay the registration of this new entry!
|
||||
-- yes, we're a bit wasteful on garbage, but this is a fringe case, so we're picking low implementation overhead over garbage efficiency
|
||||
registry.insertQueue = registry.insertQueue or setmetatable({},meta)
|
||||
registry.insertQueue[eventname][self] = regfunc
|
||||
end
|
||||
end
|
||||
|
||||
-- Unregister a callback
|
||||
target[UnregisterName] = function(self, eventname)
|
||||
if not self or self==target then
|
||||
error("Usage: "..UnregisterName.."(eventname): bad 'self'", 2)
|
||||
end
|
||||
if type(eventname) ~= "string" then
|
||||
error("Usage: "..UnregisterName.."(eventname): 'eventname' - string expected.", 2)
|
||||
end
|
||||
if rawget(events, eventname) and events[eventname][self] then
|
||||
events[eventname][self] = nil
|
||||
-- Fire OnUnused callback?
|
||||
if registry.OnUnused and not next(events[eventname]) then
|
||||
registry.OnUnused(registry, target, eventname)
|
||||
end
|
||||
end
|
||||
if registry.insertQueue and rawget(registry.insertQueue, eventname) and registry.insertQueue[eventname][self] then
|
||||
registry.insertQueue[eventname][self] = nil
|
||||
end
|
||||
end
|
||||
|
||||
-- OPTIONAL: Unregister all callbacks for given selfs/addonIds
|
||||
if UnregisterAllName then
|
||||
target[UnregisterAllName] = function(...)
|
||||
if select("#",...)<1 then
|
||||
error("Usage: "..UnregisterAllName.."([whatFor]): missing 'self' or \"addonId\" to unregister events for.", 2)
|
||||
end
|
||||
if select("#",...)==1 and ...==target then
|
||||
error("Usage: "..UnregisterAllName.."([whatFor]): supply a meaningful 'self' or \"addonId\"", 2)
|
||||
end
|
||||
|
||||
|
||||
for i=1,select("#",...) do
|
||||
local self = select(i,...)
|
||||
if registry.insertQueue then
|
||||
for eventname, callbacks in pairs(registry.insertQueue) do
|
||||
if callbacks[self] then
|
||||
callbacks[self] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
for eventname, callbacks in pairs(events) do
|
||||
if callbacks[self] then
|
||||
callbacks[self] = nil
|
||||
-- Fire OnUnused callback?
|
||||
if registry.OnUnused and not next(callbacks) then
|
||||
registry.OnUnused(registry, target, eventname)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return registry
|
||||
end
|
||||
|
||||
|
||||
-- CallbackHandler purposefully does NOT do explicit embedding. Nor does it
|
||||
-- try to upgrade old implicit embeds since the system is selfcontained and
|
||||
-- relies on closures to work.
|
||||
|
||||
4
libs/bcc/CallbackHandler-1.0/CallbackHandler-1.0.xml
Normal file
@ -0,0 +1,4 @@
|
||||
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/
|
||||
..\FrameXML\UI.xsd">
|
||||
<Script file="CallbackHandler-1.0.lua"/>
|
||||
</Ui>
|
||||
3525
libs/bcc/LibDeflate/LibDeflate.lua
Normal file
11
libs/bcc/LibDeflate/LibDeflate.toc
Normal file
@ -0,0 +1,11 @@
|
||||
## Interface: 80300
|
||||
## Title: Lib: LibDeflate
|
||||
## Notes: Compressor and decompressor with high compression ratio using DEFLATE/zlib format.
|
||||
## Author: Haoqian He (WoW: Safetyy at Illidan-US (Horde))
|
||||
## Version: @project-version@
|
||||
## X-Website: https://wow.curseforge.com/projects/libdeflate
|
||||
## X-Category: Library
|
||||
## X-License: zlib
|
||||
|
||||
LibStub\LibStub.lua
|
||||
lib.xml
|
||||
122
libs/bcc/LibDeflate/examples/example.lua
Normal file
@ -0,0 +1,122 @@
|
||||
--[[
|
||||
zlib License
|
||||
|
||||
(C) 2018-2020 Haoqian He
|
||||
|
||||
This software is provided 'as-is', without any express or implied
|
||||
warranty. In no event will the authors be held liable for any damages
|
||||
arising from the use of this software.
|
||||
|
||||
Permission is granted to anyone to use this software for any purpose,
|
||||
including commercial applications, and to alter it and redistribute it
|
||||
freely, subject to the following restrictions:
|
||||
|
||||
1. The origin of this software must not be misrepresented; you must not
|
||||
claim that you wrote the original software. If you use this software
|
||||
in a product, an acknowledgment in the product documentation would be
|
||||
appreciated but is not required.
|
||||
2. Altered source versions must be plainly marked as such, and must not be
|
||||
misrepresented as being the original software.
|
||||
3. This notice may not be removed or altered from any source distribution.
|
||||
--]]
|
||||
|
||||
--- LibDeflate usage example
|
||||
-- @author Haoqian He
|
||||
-- @file example.lua
|
||||
|
||||
local LibDeflate
|
||||
|
||||
if LibStub then -- You are using LibDeflate as WoW addon
|
||||
LibDeflate = LibStub:GetLibrary("LibDeflate")
|
||||
else
|
||||
-- You are using LibDeflate as Lua library.
|
||||
-- Setup the path to locate LibDeflate.lua,
|
||||
-- if 'require("LibDeflate")' fails, for example:
|
||||
-- package.path = ("?.lua;../?.lua;")..(package.path or "")
|
||||
LibDeflate = require("LibDeflate")
|
||||
end
|
||||
|
||||
local example_input = "12123123412345123456123456712345678123456789"
|
||||
|
||||
-- Compress using raw deflate format
|
||||
local compress_deflate = LibDeflate:CompressDeflate(example_input)
|
||||
|
||||
-- decompress
|
||||
local decompress_deflate = LibDeflate:DecompressDeflate(compress_deflate)
|
||||
-- Check if the first return value of DecompressXXXX is non-nil to know if the
|
||||
-- decompression succeeds.
|
||||
if decompress_deflate == nil then
|
||||
error("Decompression fails.")
|
||||
else
|
||||
-- Decompression succeeds.
|
||||
assert(example_input == decompress_deflate)
|
||||
end
|
||||
|
||||
|
||||
-- If it is to transmit through WoW addon channel,
|
||||
-- compressed data must be encoded so NULL ("\000") is not transmitted.
|
||||
local data_to_trasmit_WoW_addon = LibDeflate:EncodeForWoWAddonChannel(
|
||||
compress_deflate)
|
||||
-- When the receiver gets the data, decoded it first.
|
||||
local data_decoded_WoW_addon = LibDeflate:DecodeForWoWAddonChannel(
|
||||
data_to_trasmit_WoW_addon)
|
||||
-- Then decomrpess it
|
||||
assert(LibDeflate:DecompressDeflate(data_decoded_WoW_addon) == example_input)
|
||||
|
||||
-- The compressed output is not printable. EncodeForPrint will convert to
|
||||
-- a printable format, in case you want to export to the user to
|
||||
-- copy and paste. This encoding will make the data 25% bigger.
|
||||
local printable_compressed = LibDeflate:EncodeForPrint(compress_deflate)
|
||||
|
||||
-- DecodeForPrint to convert back.
|
||||
-- DecodeForPrint will remove prefixed and trailing control or space characters
|
||||
-- in the string before decode it.
|
||||
assert(LibDeflate:DecodeForPrint(printable_compressed) == compress_deflate)
|
||||
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
-------------------------------------------------------------------------------
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
--- Compress and decompress using zlib format
|
||||
local compress_zlib = LibDeflate:CompressZlib(example_input)
|
||||
local decompress_zlib = LibDeflate:DecompressZlib(compress_zlib)
|
||||
assert(decompress_zlib == example_input)
|
||||
|
||||
--- Control the compression level
|
||||
-- NOTE: High compression level does not make a difference here,
|
||||
-- because the input data is very small
|
||||
local compress_deflate_with_level = LibDeflate:CompressDeflate(example_input
|
||||
, {level = 9})
|
||||
local decompress_deflate_with_level = LibDeflate:DecompressDeflate(
|
||||
compress_deflate_with_level)
|
||||
assert(decompress_deflate_with_level == example_input)
|
||||
|
||||
|
||||
-- Compress with a preset dictionary
|
||||
local dict_str = "121231234" -- example preset dictionary string.
|
||||
-- print(LibDeflate:Adler32(dict_str), #dict_str)
|
||||
-- 9 147325380
|
||||
-- hardcode the print result above, the ensure it is not modified
|
||||
-- accidenttaly during the program development.
|
||||
--
|
||||
-- WARNING: The compressor and decompressor must use the same dictionary.
|
||||
-- You should be aware of this when tranmitting compressed data over the
|
||||
-- internet.
|
||||
local dict = LibDeflate:CreateDictionary(dict_str, 9, 147325380)
|
||||
|
||||
-- Using the dictionary with raw deflate format
|
||||
local compress_deflate_with_dict = LibDeflate:CompressDeflateWithDict(
|
||||
example_input, dict)
|
||||
assert(#compress_deflate_with_dict < #compress_deflate)
|
||||
local decompress_deflate_with_dict = LibDeflate:DecompressDeflateWithDict(
|
||||
compress_deflate_with_dict, dict)
|
||||
assert(decompress_deflate_with_dict == example_input)
|
||||
|
||||
-- Using the dictionary with zlib format, specifying compression level
|
||||
local compress_zlib_with_dict = LibDeflate:CompressZlibWithDict(
|
||||
example_input, dict, {level = 9})
|
||||
assert(#compress_zlib_with_dict < #compress_zlib)
|
||||
local decompress_zlib_with_dict = LibDeflate:DecompressZlibWithDict(
|
||||
compress_zlib_with_dict, dict)
|
||||
assert(decompress_zlib_with_dict == example_input)
|
||||
4
libs/bcc/LibDeflate/lib.xml
Normal file
@ -0,0 +1,4 @@
|
||||
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/
|
||||
..\FrameXML\UI.xsd">
|
||||
<Script file="LibDeflate.lua" />
|
||||
</Ui>
|
||||
1281
libs/bcc/LibDeflate/tests/LibCompress/LibCompress.lua
Normal file
3251
libs/bcc/LibDeflate/tests/Test.lua
Normal file
30
libs/bcc/LibStub/LibStub.lua
Normal file
@ -0,0 +1,30 @@
|
||||
-- LibStub is a simple versioning stub meant for use in Libraries. http://www.wowace.com/wiki/LibStub for more info
|
||||
-- LibStub is hereby placed in the Public Domain Credits: Kaelten, Cladhaire, ckknight, Mikk, Ammo, Nevcairiel, joshborke
|
||||
local LIBSTUB_MAJOR, LIBSTUB_MINOR = "LibStub", 2 -- NEVER MAKE THIS AN SVN REVISION! IT NEEDS TO BE USABLE IN ALL REPOS!
|
||||
local LibStub = _G[LIBSTUB_MAJOR]
|
||||
|
||||
if not LibStub or LibStub.minor < LIBSTUB_MINOR then
|
||||
LibStub = LibStub or {libs = {}, minors = {} }
|
||||
_G[LIBSTUB_MAJOR] = LibStub
|
||||
LibStub.minor = LIBSTUB_MINOR
|
||||
|
||||
function LibStub:NewLibrary(major, minor)
|
||||
assert(type(major) == "string", "Bad argument #2 to `NewLibrary' (string expected)")
|
||||
minor = assert(tonumber(string.match(minor, "%d+")), "Minor version must either be a number or contain a number.")
|
||||
|
||||
local oldminor = self.minors[major]
|
||||
if oldminor and oldminor >= minor then return nil end
|
||||
self.minors[major], self.libs[major] = minor, self.libs[major] or {}
|
||||
return self.libs[major], oldminor
|
||||
end
|
||||
|
||||
function LibStub:GetLibrary(major, silent)
|
||||
if not self.libs[major] and not silent then
|
||||
error(("Cannot find a library instance of %q."):format(tostring(major)), 2)
|
||||
end
|
||||
return self.libs[major], self.minors[major]
|
||||
end
|
||||
|
||||
function LibStub:IterateLibraries() return pairs(self.libs) end
|
||||
setmetatable(LibStub, { __call = LibStub.GetLibrary })
|
||||
end
|
||||
6
libs/bcc/Libs.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/
|
||||
..\FrameXML\UI.xsd">
|
||||
<Include file="CallbackHandler-1.0\CallbackHandler-1.0.xml"/>
|
||||
<Include file="AceTimer-3.0\AceTimer-3.0.xml"/>
|
||||
<Include file="LibDeflate\lib.xml"/>
|
||||
</Ui>
|
||||
209
libs/vanilla/AceCore-3.0/AceCore-3.0.lua
Normal file
@ -0,0 +1,209 @@
|
||||
local ACECORE_MAJOR, ACECORE_MINOR = "AceCore-3.0", 2
|
||||
local AceCore, oldminor = LibStub:NewLibrary(ACECORE_MAJOR, ACECORE_MINOR)
|
||||
|
||||
if not AceCore then return end -- No upgrade needed
|
||||
|
||||
AceCore._G = AceCore._G or getfenv()
|
||||
local _G = AceCore._G
|
||||
local strsub, strgsub, strfind = string.sub, string.gsub, string.find
|
||||
local tremove, tconcat = table.remove, table.concat
|
||||
local tgetn, tsetn = table.getn, table.setn
|
||||
|
||||
local new, del
|
||||
do
|
||||
local list = setmetatable({}, {__mode = "k"})
|
||||
function new()
|
||||
local t = next(list)
|
||||
if not t then
|
||||
return {}
|
||||
end
|
||||
list[t] = nil
|
||||
return t
|
||||
end
|
||||
|
||||
function del(t)
|
||||
setmetatable(t, nil)
|
||||
for k in pairs(t) do
|
||||
t[k] = nil
|
||||
end
|
||||
tsetn(t,0)
|
||||
list[t] = true
|
||||
end
|
||||
|
||||
print = print or function(text)
|
||||
DEFAULT_CHAT_FRAME:AddMessage(text)
|
||||
end
|
||||
|
||||
-- debug
|
||||
function AceCore.listcount()
|
||||
local count = 0
|
||||
for k in list do
|
||||
count = count + 1
|
||||
end
|
||||
return count
|
||||
end
|
||||
end -- AceCore.new, AceCore.del
|
||||
AceCore.new, AceCore.del = new, del
|
||||
|
||||
local function errorhandler(err)
|
||||
return geterrorhandler()(err)
|
||||
end
|
||||
AceCore.errorhandler = errorhandler
|
||||
|
||||
local function CreateSafeDispatcher(argCount)
|
||||
local code = [[
|
||||
local errorhandler = LibStub("AceCore-3.0").errorhandler
|
||||
local method, UP_ARGS
|
||||
local function call()
|
||||
local func, ARGS = method, UP_ARGS
|
||||
method, UP_ARGS = nil, NILS
|
||||
return func(ARGS)
|
||||
end
|
||||
return function(func, ARGS)
|
||||
method, UP_ARGS = func, ARGS
|
||||
return xpcall(call, errorhandler)
|
||||
end
|
||||
]]
|
||||
local c = 4*argCount-1
|
||||
local s = "b01,b02,b03,b04,b05,b06,b07,b08,b09,b10,b11,b12,b13,b14,b15,b16,b17,b18,b19,b20"
|
||||
code = strgsub(code, "UP_ARGS", string.sub(s,1,c))
|
||||
s = "a01,a02,a03,a04,a05,a06,a07,a08,a09,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20"
|
||||
code = strgsub(code, "ARGS", string.sub(s,1,c))
|
||||
s = "nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil"
|
||||
code = strgsub(code, "NILS", string.sub(s,1,c))
|
||||
return assert(loadstring(code, "safecall SafeDispatcher["..tostring(argCount).."]"))()
|
||||
end
|
||||
|
||||
local SafeDispatchers = setmetatable({}, {__index=function(self, argCount)
|
||||
local dispatcher
|
||||
if not tonumber(argCount) then dbg(debugstack()) end
|
||||
if argCount > 0 then
|
||||
dispatcher = CreateSafeDispatcher(argCount)
|
||||
else
|
||||
dispatcher = function(func) return xpcall(func,errorhandler) end
|
||||
end
|
||||
rawset(self, argCount, dispatcher)
|
||||
return dispatcher
|
||||
end})
|
||||
|
||||
local function safecall(func,argc,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20)
|
||||
-- we check to see if the func is passed is actually a function here and don't error when it isn't
|
||||
-- this safecall is used for optional functions like OnInitialize OnEnable etc. When they are not
|
||||
-- present execution should continue without hinderance
|
||||
if type(func) == "function" then
|
||||
return SafeDispatchers[argc](func,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20)
|
||||
end
|
||||
end
|
||||
AceCore.safecall = safecall
|
||||
|
||||
local function CreateDispatcher(argCount)
|
||||
local code = [[
|
||||
return function(func,ARGS)
|
||||
return func(ARGS)
|
||||
end
|
||||
]]
|
||||
local s = "a01,a02,a03,a04,a05,a06,a07,a08,a09,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20"
|
||||
code = strgsub(code, "ARGS", string.sub(s,1,4*argCount-1))
|
||||
return assert(loadstring(code, "call Dispatcher["..tostring(argCount).."]"))()
|
||||
end
|
||||
|
||||
AceCore.Dispatchers = setmetatable({}, {__index=function(self, argCount)
|
||||
local dispatcher
|
||||
if argCount > 0 then
|
||||
dispatcher = CreateDispatcher(argCount)
|
||||
else
|
||||
dispatcher = function(func) return func() end
|
||||
end
|
||||
rawset(self, argCount, dispatcher)
|
||||
return dispatcher
|
||||
end})
|
||||
|
||||
-- some string functions
|
||||
-- vanilla available string operations:
|
||||
-- sub, gfind, rep, gsub, char, dump, find, upper, len, format, byte, lower
|
||||
-- we will just replace every string.match with string.find in the code
|
||||
function AceCore.strtrim(s)
|
||||
return strgsub(s, "^%s*(.-)%s*$", "%1")
|
||||
end
|
||||
|
||||
local function strsplit(delim, s, n)
|
||||
if n and n < 2 then return s end
|
||||
beg = beg or 1
|
||||
local i,j = string.find(s,delim,beg)
|
||||
if not i then
|
||||
return s, nil
|
||||
end
|
||||
return string.sub(s,1,j-1), strsplit(delim, string.sub(s,j+1), n and n-1 or nil)
|
||||
end
|
||||
AceCore.strsplit = strsplit
|
||||
|
||||
-- Ace3v: fonctions copied from AceHook-2.1
|
||||
local protFuncs = {
|
||||
CameraOrSelectOrMoveStart = true, CameraOrSelectOrMoveStop = true,
|
||||
TurnOrActionStart = true, TurnOrActionStop = true,
|
||||
PitchUpStart = true, PitchUpStop = true,
|
||||
PitchDownStart = true, PitchDownStop = true,
|
||||
MoveBackwardStart = true, MoveBackwardStop = true,
|
||||
MoveForwardStart = true, MoveForwardStop = true,
|
||||
Jump = true, StrafeLeftStart = true,
|
||||
StrafeLeftStop = true, StrafeRightStart = true,
|
||||
StrafeRightStop = true, ToggleMouseMove = true,
|
||||
ToggleRun = true, TurnLeftStart = true,
|
||||
TurnLeftStop = true, TurnRightStart = true,
|
||||
TurnRightStop = true,
|
||||
}
|
||||
|
||||
local function issecurevariable(x)
|
||||
return protFuncs[x] and 1 or nil
|
||||
end
|
||||
AceCore.issecurevariable = issecurevariable
|
||||
|
||||
local function hooksecurefunc(arg1, arg2, arg3)
|
||||
if type(arg1) == "string" then
|
||||
arg1, arg2, arg3 = _G, arg1, arg2
|
||||
end
|
||||
local orig = arg1[arg2]
|
||||
if type(orig) ~= "function" then
|
||||
error("The function "..arg2.." does not exist", 2)
|
||||
end
|
||||
arg1[arg2] = function(...)
|
||||
local tmp = {orig(unpack(arg))}
|
||||
arg3(unpack(arg))
|
||||
return unpack(tmp)
|
||||
end
|
||||
end
|
||||
AceCore.hooksecurefunc = hooksecurefunc
|
||||
|
||||
-- pickfirstset() - picks the first non-nil value and returns it
|
||||
local function pickfirstset(argc,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10)
|
||||
if (argc <= 1) or (a1 ~= nil) then
|
||||
return a1
|
||||
else
|
||||
return pickfirstset(argc-1,a2,a3,a4,a5,a6,a7,a8,a9,a10)
|
||||
end
|
||||
end
|
||||
AceCore.pickfirstset = pickfirstset
|
||||
|
||||
local function countargs(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10)
|
||||
if (a1 == nil) then return 0 end
|
||||
return 1 + countargs(a2,a3,a4,a5,a6,a7,a8,a9,a10)
|
||||
end
|
||||
AceCore.countargs = countargs
|
||||
|
||||
-- wipe preserves metatable
|
||||
function AceCore.wipe(t)
|
||||
for k,v in pairs(t) do t[k] = nil end
|
||||
tsetn(t,0)
|
||||
return t
|
||||
end
|
||||
|
||||
function AceCore.truncate(t,e)
|
||||
e = e or tgetn(t)
|
||||
for i=1,e do
|
||||
if t[i] == nil then
|
||||
tsetn(t,i-1)
|
||||
return
|
||||
end
|
||||
end
|
||||
tsetn(t,e)
|
||||
end
|
||||
4
libs/vanilla/AceCore-3.0/AceCore-3.0.xml
Normal file
@ -0,0 +1,4 @@
|
||||
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/
|
||||
..\FrameXML\UI.xsd">
|
||||
<Script file="AceCore-3.0.lua"/>
|
||||
</Ui>
|
||||
379
libs/vanilla/AceTimer-3.0/AceTimer-3.0.lua
Normal file
@ -0,0 +1,379 @@
|
||||
--- **AceTimer-3.0** provides a central facility for registering timers.
|
||||
-- AceTimer supports one-shot timers and repeating timers. All timers are stored in an efficient
|
||||
-- data structure that allows easy dispatching and fast rescheduling. Timers can be registered
|
||||
-- or canceled at any time, even from within a running timer, without conflict or large overhead.\\
|
||||
-- AceTimer is currently limited to firing timers at a frequency of 0.01s as this is what the WoW timer API
|
||||
-- restricts us to.
|
||||
--
|
||||
-- All `:Schedule` functions will return a handle to the current timer, which you will need to store if you
|
||||
-- need to cancel the timer you just registered.
|
||||
--
|
||||
-- **AceTimer-3.0** can be embeded into your addon, either explicitly by calling AceTimer:Embed(MyAddon) or by
|
||||
-- specifying it as an embeded library in your AceAddon. All functions will be available on your addon object
|
||||
-- and can be accessed directly, without having to explicitly call AceTimer itself.\\
|
||||
-- It is recommended to embed AceTimer, otherwise you'll have to specify a custom `self` on all calls you
|
||||
-- make into AceTimer.
|
||||
-- @class file
|
||||
-- @name AceTimer-3.0
|
||||
-- @release $Id: AceTimer-3.0.lua 1119 2014-10-14 17:23:29Z nevcairiel $
|
||||
|
||||
local MAJOR, MINOR = "AceTimer-3.0", 18 -- Bump minor on changes
|
||||
local AceTimer, oldminor = LibStub:NewLibrary(MAJOR, MINOR)
|
||||
|
||||
if not AceTimer then return end -- No upgrade needed
|
||||
|
||||
local AceCore = LibStub("AceCore-3.0")
|
||||
local safecall = AceCore.safecall
|
||||
|
||||
AceTimer.counter = AceTimer.counter or {}
|
||||
AceTimer.hash = AceTimer.hash or {} -- Array of [1..BUCKETS] = linked list of timers (using .next member)
|
||||
AceTimer.activeTimers = AceTimer.activeTimers or {} -- Active timer list
|
||||
AceTimer.frame = AceTimer.frame or CreateFrame("Frame", "AceTimer30Frame")
|
||||
|
||||
local counter = AceTimer.counter
|
||||
local activeTimers = AceTimer.activeTimers -- Upvalue our private data
|
||||
local timerFrame = AceTimer.frame
|
||||
|
||||
-- Lua APIs
|
||||
local type, unpack, next, error = type, unpack, next, error
|
||||
local floor, max, min, mod = math.floor, math.max, math.min, math.mod
|
||||
local tostring = tostring
|
||||
|
||||
-- WoW APIs
|
||||
local GetTime = GetTime
|
||||
|
||||
--[[
|
||||
Timers will not be fired more often than HZ-1 times per second.
|
||||
Keep at intended speed PLUS ONE or we get bitten by floating point rounding errors (n.5 + 0.1 can be n.599999)
|
||||
If this is ever LOWERED, all existing timers need to be enforced to have a delay >= 1/HZ on lib upgrade.
|
||||
If this number is ever changed, all entries need to be rehashed on lib upgrade.
|
||||
]]
|
||||
local HZ = 11
|
||||
local minDelay = 1/(HZ-1)
|
||||
|
||||
--[[
|
||||
Prime for good distribution
|
||||
If this number is ever changed, all entries need to be rehashed on lib upgrade.
|
||||
]]
|
||||
local BUCKETS = 131
|
||||
|
||||
local hash = AceTimer.hash
|
||||
for i=1,BUCKETS do
|
||||
hash[i] = hash[i] or false -- make it an integer-indexed array; it's faster than hashes
|
||||
end
|
||||
|
||||
local new, del
|
||||
do
|
||||
local list = setmetatable({}, {__mode = "k"})
|
||||
function new(self, loop, func, delay, argc,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10)
|
||||
local name = loop and "ScheduleRepeatingTimer" or "ScheduleTimer"
|
||||
if self == AceTimer then
|
||||
error(MAJOR..": " .. name .. "(callback, delay, argc, args...): use your own 'self'", 3)
|
||||
end
|
||||
if not func or not delay then
|
||||
error(MAJOR..": " .. name .. "(callback, delay, argc, args...): 'callback' and 'delay' must have set values.", 3)
|
||||
end
|
||||
if argc and (type(argc) ~= "number" or floor(argc) ~= argc) then
|
||||
error(MAJOR..": " .. name .. "(callback, delay, argc, args...): 'argc' must be an integer.", 3)
|
||||
end
|
||||
if type(func) == "string" then
|
||||
if type(self) ~= "table" then
|
||||
error(MAJOR..": " .. name .. "(callback, delay, argc, args...): 'self' - must be a table.", 3)
|
||||
elseif type(self[func]) ~= "function" then
|
||||
error(MAJOR..": " .. name .. "(callback, delay, argc, args...): Tried to register '"..func.."' as the callback, but it is not a method.", 3)
|
||||
end
|
||||
elseif type(func) ~= "function" then
|
||||
error(MAJOR..": " .. name .. "(callback, delay, argc, args...): Tried to register '"..tostring(func).."' as the callback, but it is not a function.", 3)
|
||||
end
|
||||
|
||||
if delay < minDelay then
|
||||
delay = minDelay
|
||||
end
|
||||
|
||||
-- Create and stuff timer in the correct hash bucket
|
||||
local now = GetTime()
|
||||
|
||||
local timer = next(list) or {}
|
||||
list[timer] = nil
|
||||
|
||||
timer.object = self
|
||||
timer.func = func
|
||||
timer.delay = delay
|
||||
timer.status = loop and "loop" or "once"
|
||||
timer.ends = now + delay
|
||||
timer.argsCount = argc or 0
|
||||
timer[1] = a1
|
||||
timer[2] = a2
|
||||
timer[3] = a3
|
||||
timer[4] = a4
|
||||
timer[5] = a5
|
||||
timer[6] = a6
|
||||
timer[7] = a7
|
||||
timer[8] = a8
|
||||
timer[9] = a9
|
||||
timer[10] = a10
|
||||
|
||||
local bucket = floor(mod((now+delay)*HZ,BUCKETS)) + 1
|
||||
timer.next = hash[bucket]
|
||||
hash[bucket] = timer
|
||||
|
||||
local id = tostring(timer) -- user has only access to the id but not the table itself
|
||||
activeTimers[id] = timer
|
||||
|
||||
counter[self] = (counter[self] or 0) + 1
|
||||
|
||||
timerFrame:Show()
|
||||
return id
|
||||
end
|
||||
|
||||
function del(t)
|
||||
local id = tostring(t)
|
||||
activeTimers[id] = nil
|
||||
if not next(activeTimers) then
|
||||
timerFrame:Hide()
|
||||
end
|
||||
local self = t.object
|
||||
for k in pairs(t) do t[k] = nil end
|
||||
list[t] = true
|
||||
if counter[self] then
|
||||
counter[self] = counter[self] - 1
|
||||
else
|
||||
counter[self] = nil
|
||||
end
|
||||
end
|
||||
end -- new, del
|
||||
|
||||
--- Schedule a new one-shot timer.
|
||||
-- The timer will fire once in `delay` seconds, unless canceled before.
|
||||
-- @param callback Callback function for the timer pulse (funcref or method name).
|
||||
-- @param delay Delay for the timer, in seconds.
|
||||
-- @param argc The numbers of arguments to be passed to the callback function
|
||||
-- @param a1,...,a10 The arguments
|
||||
-- @usage
|
||||
-- MyAddOn = LibStub("AceAddon-3.0"):NewAddon("MyAddOn", "AceTimer-3.0")
|
||||
--
|
||||
-- function MyAddOn:OnEnable()
|
||||
-- self:ScheduleTimer("TimerFeedback", 5)
|
||||
-- end
|
||||
--
|
||||
-- function MyAddOn:TimerFeedback()
|
||||
-- print("5 seconds passed")
|
||||
-- end
|
||||
function AceTimer:ScheduleTimer(func, delay, argc,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10)
|
||||
return new(self, nil, func, delay, argc,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10)
|
||||
end
|
||||
|
||||
--- Schedule a repeating timer.
|
||||
-- The timer will fire every `delay` seconds, until canceled.
|
||||
-- @param callback Callback function for the timer pulse (funcref or method name).
|
||||
-- @param delay Delay for the timer, in seconds.
|
||||
-- @param argc The numbers of arguments to be passed to the callback function
|
||||
-- @param a1,...,a10 The arguments
|
||||
-- @usage
|
||||
-- MyAddOn = LibStub("AceAddon-3.0"):NewAddon("MyAddOn", "AceTimer-3.0")
|
||||
--
|
||||
-- function MyAddOn:OnEnable()
|
||||
-- self.timerCount = 0
|
||||
-- self.testTimer = self:ScheduleRepeatingTimer("TimerFeedback", 5)
|
||||
-- end
|
||||
--
|
||||
-- function MyAddOn:TimerFeedback()
|
||||
-- self.timerCount = self.timerCount + 1
|
||||
-- print(("%d seconds passed"):format(5 * self.timerCount))
|
||||
-- -- run 30 seconds in total
|
||||
-- if self.timerCount == 6 then
|
||||
-- self:CancelTimer(self.testTimer)
|
||||
-- end
|
||||
-- end
|
||||
function AceTimer:ScheduleRepeatingTimer(func, delay, argc,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10)
|
||||
return new(self, true, func, delay, argc,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10)
|
||||
end
|
||||
|
||||
--- Cancels a timer with the given id, registered by the same addon object as used for `:ScheduleTimer`
|
||||
-- Both one-shot and repeating timers can be canceled with this function, as long as the `id` is valid
|
||||
-- and the timer has not fired yet or was canceled before.
|
||||
-- @param id The id of the timer, as returned by `:ScheduleTimer` or `:ScheduleRepeatingTimer`
|
||||
function AceTimer:CancelTimer(id)
|
||||
local timer = activeTimers[id]
|
||||
|
||||
if not timer then
|
||||
return false
|
||||
else
|
||||
-- Ace3v: the timer will always be collected in the next update but not here
|
||||
-- this is necessary for AceBucket to determinate if the bucket has been unregistered
|
||||
-- in the callback
|
||||
timer.status = nil
|
||||
activeTimers[id] = nil
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
--- Cancels all timers registered to the current addon object ('self')
|
||||
function AceTimer:CancelAllTimers()
|
||||
if type(self) ~= "table" then
|
||||
error(MAJOR..": CancelAllTimers(): 'self' - must be a table",2)
|
||||
end
|
||||
if self == AceTimer then
|
||||
error(MAJOR..": CancelAllTimers(): supply a meaningful 'self'", 2)
|
||||
end
|
||||
|
||||
for k,v in pairs(activeTimers) do
|
||||
if v.object == self then
|
||||
AceTimer.CancelTimer(self, k)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Returns the time left for a timer with the given id, registered by the current addon object ('self').
|
||||
-- This function will return 0 when the id is invalid.
|
||||
-- @param id The id of the timer, as returned by `:ScheduleTimer` or `:ScheduleRepeatingTimer`
|
||||
-- @return The time left on the timer.
|
||||
function AceTimer:TimeLeft(id)
|
||||
local timer = activeTimers[id]
|
||||
if not timer then
|
||||
return 0
|
||||
else
|
||||
return timer.ends - GetTime()
|
||||
end
|
||||
end
|
||||
|
||||
function AceTimer:TimerStatus(id)
|
||||
local timer = activeTimers[id]
|
||||
if not timer then
|
||||
return nil
|
||||
else
|
||||
return timer.status
|
||||
end
|
||||
end
|
||||
|
||||
-- ---------------------------------------------------------------------
|
||||
-- Embed handling
|
||||
|
||||
AceTimer.embeds = AceTimer.embeds or {}
|
||||
|
||||
local mixins = {
|
||||
"ScheduleTimer", "ScheduleRepeatingTimer",
|
||||
"CancelTimer", "CancelAllTimers",
|
||||
"TimeLeft", "TimerStatus"
|
||||
}
|
||||
|
||||
function AceTimer:Embed(target)
|
||||
AceTimer.embeds[target] = true
|
||||
for _,v in pairs(mixins) do
|
||||
target[v] = AceTimer[v]
|
||||
end
|
||||
return target
|
||||
end
|
||||
|
||||
-- AceTimer:OnEmbedDisable(target)
|
||||
-- target (object) - target object that AceTimer is embedded in.
|
||||
--
|
||||
-- cancel all timers registered for the object
|
||||
function AceTimer:OnEmbedDisable(target)
|
||||
target:CancelAllTimers()
|
||||
end
|
||||
|
||||
for addon in pairs(AceTimer.embeds) do
|
||||
AceTimer:Embed(addon)
|
||||
end
|
||||
|
||||
-- --------------------------------------------------------------------
|
||||
-- OnUpdate handler
|
||||
--
|
||||
-- traverse buckets, always chasing "now", and fire timers that have expired
|
||||
local lastint = floor(GetTime() * HZ)
|
||||
local function OnUpdate()
|
||||
local now = GetTime()
|
||||
local nowint = floor(now * HZ)
|
||||
|
||||
-- Have we passed into a new hash bucket?
|
||||
if nowint == lastint then return end
|
||||
|
||||
local soon = now + 1 -- +1 is safe as long as 1 < HZ < BUCKETS/2
|
||||
|
||||
-- Pass through each bucket at most once
|
||||
-- Happens on e.g. instance loads, but COULD happen on high local load situations also
|
||||
for curint = (max(lastint, nowint-BUCKETS) + 1), nowint do -- loop until we catch up with "now", usually only 1 iteratio
|
||||
local curbucket = mod(curint,BUCKETS) + 1 -- Ace3v: both int so no floor here
|
||||
-- Yank the list of timers out of the bucket and empty it. This allows reinsertion in the currently-processed bucket from callbacks.
|
||||
local nexttimer = hash[curbucket]
|
||||
hash[curbucket] = false -- false rather than nil to prevent the array from becoming a hash
|
||||
|
||||
while nexttimer do
|
||||
local timer = nexttimer
|
||||
nexttimer = timer.next
|
||||
local status = timer.status
|
||||
if not status then
|
||||
del(timer)
|
||||
else
|
||||
local ends = timer.ends
|
||||
if (status == "loop" or status == "once") and ends < soon then
|
||||
local object = timer.object
|
||||
local callback = timer.func
|
||||
if type(callback) == "string" then
|
||||
callback = (type(object) == "table") and object[callback]
|
||||
if type(callback) == "function" then
|
||||
safecall(callback, timer.argsCount+1, object,
|
||||
timer[1], timer[2], timer[3], timer[4], timer[5],
|
||||
timer[6], timer[7], timer[8], timer[9], timer[10])
|
||||
else
|
||||
status = "once"
|
||||
end
|
||||
elseif type(callback) == "function" then
|
||||
safecall(callback, timer.argsCount,
|
||||
timer[1], timer[2], timer[3], timer[4], timer[5],
|
||||
timer[6], timer[7], timer[8], timer[9], timer[10])
|
||||
else
|
||||
-- probably nilled out by CancelTimer
|
||||
status = "once" -- don't reschedule it
|
||||
end
|
||||
|
||||
if status == "once" then
|
||||
del(timer)
|
||||
else
|
||||
local delay = timer.delay
|
||||
local newends = ends + delay
|
||||
if newends < now then -- Keep lag from making us firing a timer unnecessarily. (Note that this still won't catch too-short-delay timers though.)
|
||||
newends = now + delay
|
||||
end
|
||||
timer.ends = newends
|
||||
-- add next timer execution to the correct bucket
|
||||
local bucket = floor(mod(newends*HZ,BUCKETS)) + 1
|
||||
timer.next = hash[bucket]
|
||||
hash[bucket] = timer
|
||||
end
|
||||
else
|
||||
-- reinsert (yeah, somewhat expensive, but shouldn't be happening too often either due to hash distribution)
|
||||
timer.next = hash[curbucket]
|
||||
hash[curbucket] = timer
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
lastint = nowint
|
||||
end
|
||||
|
||||
local lastchecked = nil
|
||||
local function OnEvent()
|
||||
if event ~= "PLAYER_REGEN_ENABLED" then return end
|
||||
|
||||
local addon = next(counter, lastchecked)
|
||||
if not addon then
|
||||
addon = next(counter)
|
||||
end
|
||||
lastchecked = addon
|
||||
if not addon then -- should only happen if counter is empty
|
||||
return
|
||||
end
|
||||
|
||||
local n = counter[addon]
|
||||
if n > BUCKETS then
|
||||
DEFAULT_CHAT_FRAME:AddMessage(MAJOR..": Warning: The addon/module '"..tostring(addon).."' has "..tostring(n).." live timers. Surely that's not intended?")
|
||||
end
|
||||
end
|
||||
|
||||
timerFrame:SetScript("OnUpdate", OnUpdate)
|
||||
timerFrame:SetScript("OnEvent", OnEvent)
|
||||
timerFrame:RegisterEvent("PLAYER_REGEN_ENABLED")
|
||||
timerFrame:Hide()
|
||||
4
libs/vanilla/AceTimer-3.0/AceTimer-3.0.xml
Normal file
@ -0,0 +1,4 @@
|
||||
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/
|
||||
..\FrameXML\UI.xsd">
|
||||
<Script file="AceTimer-3.0.lua"/>
|
||||
</Ui>
|
||||
280
libs/vanilla/CallbackHandler-1.0/CallbackHandler-1.0.lua
Normal file
@ -0,0 +1,280 @@
|
||||
--[[ $Id: CallbackHandler-1.0.lua 1131 2015-06-04 07:29:24Z nevcairiel $ ]]
|
||||
local MAJOR, MINOR = "CallbackHandler-1.0", 6
|
||||
local CallbackHandler = LibStub:NewLibrary(MAJOR, MINOR)
|
||||
|
||||
if not CallbackHandler then return end -- No upgrade needed
|
||||
|
||||
-- Lua APIs
|
||||
local tconcat, tinsert, tgetn, tsetn = table.concat, table.insert, table.getn, table.setn
|
||||
local assert, error, loadstring = assert, error, loadstring
|
||||
local setmetatable, rawset, rawget = setmetatable, rawset, rawget
|
||||
local next, pairs, type, tostring = next, pairs, type, tostring
|
||||
local strgsub = string.gsub
|
||||
|
||||
local new, del
|
||||
do
|
||||
local list = setmetatable({}, {__mode = "k"})
|
||||
function new()
|
||||
local t = next(list)
|
||||
if not t then
|
||||
return {}
|
||||
end
|
||||
list[t] = nil
|
||||
return t
|
||||
end
|
||||
|
||||
function del(t)
|
||||
setmetatable(t, nil)
|
||||
for k in pairs(t) do
|
||||
t[k] = nil
|
||||
end
|
||||
tsetn(t,0)
|
||||
list[t] = true
|
||||
end
|
||||
end
|
||||
|
||||
local meta = {__index = function(tbl, key) rawset(tbl, key, new()) return tbl[key] end}
|
||||
|
||||
-- Global vars/functions that we don't upvalue since they might get hooked, or upgraded
|
||||
-- List them here for Mikk's FindGlobals script
|
||||
-- GLOBALS: geterrorhandler
|
||||
|
||||
local function errorhandler(err)
|
||||
return geterrorhandler()(err)
|
||||
end
|
||||
CallbackHandler.errorhandler = errorhandler
|
||||
|
||||
local function CreateDispatcher(argCount)
|
||||
local code = [[
|
||||
local xpcall, errorhandler = xpcall, LibStub("CallbackHandler-1.0").errorhandler
|
||||
local method, UP_ARGS
|
||||
local function call()
|
||||
local func, ARGS = method, UP_ARGS
|
||||
method, UP_ARGS = nil, NILS
|
||||
return func(ARGS)
|
||||
end
|
||||
return function(handlers, ARGS)
|
||||
local index
|
||||
index, method = next(handlers)
|
||||
if not method then return end
|
||||
repeat
|
||||
UP_ARGS = ARGS
|
||||
xpcall(call, errorhandler)
|
||||
index, method = next(handlers, index)
|
||||
until not method
|
||||
end
|
||||
]]
|
||||
local c = 4*argCount-1
|
||||
local s = "b01,b02,b03,b04,b05,b06,b07,b08,b09,b10"
|
||||
code = strgsub(code, "UP_ARGS", string.sub(s,1,c))
|
||||
s = "a01,a02,a03,a04,a05,a06,a07,a08,a09,a10"
|
||||
code = strgsub(code, "ARGS", string.sub(s,1,c))
|
||||
s = "nil,nil,nil,nil,nil,nil,nil,nil,nil,nil"
|
||||
code = strgsub(code, "NILS", string.sub(s,1,c))
|
||||
return assert(loadstring(code, "safecall Dispatcher["..tostring(argCount).."]"))()
|
||||
end
|
||||
|
||||
local Dispatchers = setmetatable({}, {__index=function(self, argCount)
|
||||
local dispatcher = CreateDispatcher(argCount)
|
||||
rawset(self, argCount, dispatcher)
|
||||
return dispatcher
|
||||
end})
|
||||
|
||||
--------------------------------------------------------------------------
|
||||
-- CallbackHandler:New
|
||||
--
|
||||
-- target - target object to embed public APIs in
|
||||
-- RegisterName - name of the callback registration API, default "RegisterCallback"
|
||||
-- UnregisterName - name of the callback unregistration API, default "UnregisterCallback"
|
||||
-- UnregisterAllName - name of the API to unregister all callbacks, default "UnregisterAllCallbacks". false == don't publish this API.
|
||||
function CallbackHandler:New(target, RegisterName, UnregisterName, UnregisterAllName)
|
||||
|
||||
RegisterName = RegisterName or "RegisterCallback"
|
||||
UnregisterName = UnregisterName or "UnregisterCallback"
|
||||
if UnregisterAllName==nil then -- false is used to indicate "don't want this method"
|
||||
UnregisterAllName = "UnregisterAllCallbacks"
|
||||
end
|
||||
|
||||
-- we declare all objects and exported APIs inside this closure to quickly gain access
|
||||
-- to e.g. function names, the "target" parameter, etc
|
||||
|
||||
|
||||
-- Create the registry object
|
||||
local events = setmetatable({}, meta)
|
||||
local registry = { recurse=0, events=events }
|
||||
|
||||
-- registry:Fire() - fires the given event/message into the registry
|
||||
function registry:Fire(eventname, argc, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10)
|
||||
if not rawget(events, eventname) or not next(events[eventname]) then return end
|
||||
local oldrecurse = registry.recurse
|
||||
registry.recurse = oldrecurse + 1
|
||||
|
||||
argc = argc or 0
|
||||
Dispatchers[argc+1](events[eventname], eventname, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10)
|
||||
|
||||
registry.recurse = oldrecurse
|
||||
|
||||
if registry.insertQueue and oldrecurse==0 then
|
||||
-- Something in one of our callbacks wanted to register more callbacks; they got queued
|
||||
for eventname,callbacks in pairs(registry.insertQueue) do
|
||||
local first = not rawget(events, eventname) or not next(events[eventname]) -- test for empty before. not test for one member after. that one member may have been overwritten.
|
||||
for self,func in pairs(callbacks) do
|
||||
events[eventname][self] = func
|
||||
-- fire OnUsed callback?
|
||||
if first and registry.OnUsed then
|
||||
registry.OnUsed(registry, target, eventname)
|
||||
first = nil
|
||||
end
|
||||
end
|
||||
del(callbacks)
|
||||
end
|
||||
del(registry.insertQueue)
|
||||
registry.insertQueue = nil
|
||||
end
|
||||
end
|
||||
|
||||
-- Registration of a callback, handles:
|
||||
-- self["method"], leads to self["method"](self, ...)
|
||||
-- self with function ref, leads to functionref(...)
|
||||
-- "addonId" (instead of self) with function ref, leads to functionref(...)
|
||||
-- all with an optional arg, which, if present, gets passed as first argument (after self if present)
|
||||
target[RegisterName] = function(self, eventname, method, ...)
|
||||
if type(eventname) ~= "string" then
|
||||
error("Usage: "..RegisterName.."(eventname, method[, arg]): 'eventname' - string expected.", 2)
|
||||
end
|
||||
|
||||
method = method or eventname
|
||||
|
||||
local first = not rawget(events, eventname) or not next(events[eventname]) -- test for empty before. not test for one member after. that one member may have been overwritten.
|
||||
|
||||
if type(method) ~= "string" and type(method) ~= "function" then
|
||||
error("Usage: "..RegisterName.."(eventname, method[, arg]): 'method' - string or function expected.", 2)
|
||||
end
|
||||
|
||||
local regfunc
|
||||
local a1 = arg[1]
|
||||
|
||||
if type(method) == "string" then
|
||||
-- self["method"] calling style
|
||||
if type(self) ~= "table" then
|
||||
error("Usage: "..RegisterName.."(eventname, method[, arg]): self was not a table?", 2)
|
||||
elseif self==target then
|
||||
error("Usage: "..RegisterName.."(eventname, method[, arg]): do not use Library:"..RegisterName.."(), use your own 'self'.", 2)
|
||||
elseif type(self[method]) ~= "function" then
|
||||
error("Usage: "..RegisterName.."(eventname, method[, arg]): 'method' - method '"..tostring(method).."' not found on 'self'.", 2)
|
||||
end
|
||||
|
||||
if tgetn(arg) >= 1 then
|
||||
regfunc = function (...) return self[method](self,a1,unpack(arg)) end
|
||||
else
|
||||
regfunc = function (...) return self[method](self,unpack(arg)) end
|
||||
end
|
||||
else
|
||||
-- function ref with self=object or self="addonId"
|
||||
if type(self)~="table" and type(self)~="string" then
|
||||
error("Usage: "..RegisterName.."(self or addonId, eventname, method[, arg]): 'self or addonId': table or string expected.", 2)
|
||||
end
|
||||
|
||||
if tgetn(arg) >= 1 then
|
||||
regfunc = function (...) return method(a1, unpack(arg)) end
|
||||
else
|
||||
regfunc = method
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
if events[eventname][self] or registry.recurse<1 then
|
||||
-- if registry.recurse<1 then
|
||||
-- we're overwriting an existing entry, or not currently recursing. just set it.
|
||||
events[eventname][self] = regfunc
|
||||
-- fire OnUsed callback?
|
||||
if registry.OnUsed and first then
|
||||
registry.OnUsed(registry, target, eventname)
|
||||
end
|
||||
else
|
||||
-- we're currently processing a callback in this registry, so delay the registration of this new entry!
|
||||
-- yes, we're a bit wasteful on garbage, but this is a fringe case, so we're picking low implementation overhead over garbage efficiency
|
||||
registry.insertQueue = registry.insertQueue or setmetatable(new(),meta)
|
||||
registry.insertQueue[eventname][self] = regfunc
|
||||
end
|
||||
end
|
||||
|
||||
-- Unregister a callback
|
||||
target[UnregisterName] = function(self, eventname)
|
||||
if not self or self==target then
|
||||
error("Usage: "..UnregisterName.."(eventname): bad 'self'", 2)
|
||||
end
|
||||
if type(eventname) ~= "string" then
|
||||
error("Usage: "..UnregisterName.."(eventname): 'eventname' - string expected.", 2)
|
||||
end
|
||||
if rawget(events, eventname) and events[eventname][self] then
|
||||
events[eventname][self] = nil
|
||||
|
||||
-- Fire OnUnused callback?
|
||||
if registry.OnUnused and not next(events[eventname]) then
|
||||
registry.OnUnused(registry, target, eventname)
|
||||
end
|
||||
|
||||
if rawget(events, eventname) and not next(events[eventname]) then
|
||||
del(events[eventname])
|
||||
events[eventname] = nil
|
||||
end
|
||||
end
|
||||
if registry.insertQueue and rawget(registry.insertQueue, eventname) and registry.insertQueue[eventname][self] then
|
||||
registry.insertQueue[eventname][self] = nil
|
||||
end
|
||||
end
|
||||
|
||||
-- OPTIONAL: Unregister all callbacks for given selfs/addonIds
|
||||
if UnregisterAllName then
|
||||
target[UnregisterAllName] = function(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10)
|
||||
if not a1 then
|
||||
error("Usage: "..UnregisterAllName.."([whatFor]): missing 'self' or 'addonId' to unregister events for.", 2)
|
||||
end
|
||||
if a1 == target then
|
||||
error("Usage: "..UnregisterAllName.."([whatFor]): supply a meaningful 'self' or 'addonId'", 2)
|
||||
end
|
||||
|
||||
-- use our registry table as argument table
|
||||
registry[1] = a1
|
||||
registry[2] = a2
|
||||
registry[3] = a3
|
||||
registry[4] = a4
|
||||
registry[5] = a5
|
||||
registry[6] = a6
|
||||
registry[7] = a7
|
||||
registry[8] = a8
|
||||
registry[9] = a9
|
||||
registry[10] = a10
|
||||
for i=1,10 do
|
||||
local self = registry[i]
|
||||
registry[i] = nil
|
||||
if self then
|
||||
if registry.insertQueue then
|
||||
for eventname, callbacks in pairs(registry.insertQueue) do
|
||||
if callbacks[self] then
|
||||
callbacks[self] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
for eventname, callbacks in pairs(events) do
|
||||
if callbacks[self] then
|
||||
callbacks[self] = nil
|
||||
-- Fire OnUnused callback?
|
||||
if registry.OnUnused and not next(callbacks) then
|
||||
registry.OnUnused(registry, target, eventname)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return registry
|
||||
end
|
||||
|
||||
-- CallbackHandler purposefully does NOT do explicit embedding. Nor does it
|
||||
-- try to upgrade old implicit embeds since the system is selfcontained and
|
||||
-- relies on closures to work.
|
||||
|
||||
4
libs/vanilla/CallbackHandler-1.0/CallbackHandler-1.0.xml
Normal file
@ -0,0 +1,4 @@
|
||||
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/
|
||||
..\FrameXML\UI.xsd">
|
||||
<Script file="CallbackHandler-1.0.lua"/>
|
||||
</Ui>
|
||||
3696
libs/vanilla/LibDeflate/LibDeflate.lua
Normal file
11
libs/vanilla/LibDeflate/LibDeflate.toc
Normal file
@ -0,0 +1,11 @@
|
||||
## Interface: 80300
|
||||
## Title: Lib: LibDeflate
|
||||
## Notes: Compressor and decompressor with high compression ratio using DEFLATE/zlib format.
|
||||
## Author: Haoqian He (WoW: Safetyy at Illidan-US (Horde))
|
||||
## Version: @project-version@
|
||||
## X-Website: https://wow.curseforge.com/projects/libdeflate
|
||||
## X-Category: Library
|
||||
## X-License: zlib
|
||||
|
||||
LibStub\LibStub.lua
|
||||
lib.xml
|
||||
122
libs/vanilla/LibDeflate/examples/example.lua
Normal file
@ -0,0 +1,122 @@
|
||||
--[[
|
||||
zlib License
|
||||
|
||||
(C) 2018-2020 Haoqian He
|
||||
|
||||
This software is provided 'as-is', without any express or implied
|
||||
warranty. In no event will the authors be held liable for any damages
|
||||
arising from the use of this software.
|
||||
|
||||
Permission is granted to anyone to use this software for any purpose,
|
||||
including commercial applications, and to alter it and redistribute it
|
||||
freely, subject to the following restrictions:
|
||||
|
||||
1. The origin of this software must not be misrepresented; you must not
|
||||
claim that you wrote the original software. If you use this software
|
||||
in a product, an acknowledgment in the product documentation would be
|
||||
appreciated but is not required.
|
||||
2. Altered source versions must be plainly marked as such, and must not be
|
||||
misrepresented as being the original software.
|
||||
3. This notice may not be removed or altered from any source distribution.
|
||||
--]]
|
||||
|
||||
--- LibDeflate usage example
|
||||
-- @author Haoqian He
|
||||
-- @file example.lua
|
||||
|
||||
local LibDeflate
|
||||
|
||||
if LibStub then -- You are using LibDeflate as WoW addon
|
||||
LibDeflate = LibStub:GetLibrary("LibDeflate")
|
||||
else
|
||||
-- You are using LibDeflate as Lua library.
|
||||
-- Setup the path to locate LibDeflate.lua,
|
||||
-- if 'require("LibDeflate")' fails, for example:
|
||||
-- package.path = ("?.lua;../?.lua;")..(package.path or "")
|
||||
LibDeflate = require("LibDeflate")
|
||||
end
|
||||
|
||||
local example_input = "12123123412345123456123456712345678123456789"
|
||||
|
||||
-- Compress using raw deflate format
|
||||
local compress_deflate = LibDeflate:CompressDeflate(example_input)
|
||||
|
||||
-- decompress
|
||||
local decompress_deflate = LibDeflate:DecompressDeflate(compress_deflate)
|
||||
-- Check if the first return value of DecompressXXXX is non-nil to know if the
|
||||
-- decompression succeeds.
|
||||
if decompress_deflate == nil then
|
||||
error("Decompression fails.")
|
||||
else
|
||||
-- Decompression succeeds.
|
||||
assert(example_input == decompress_deflate)
|
||||
end
|
||||
|
||||
|
||||
-- If it is to transmit through WoW addon channel,
|
||||
-- compressed data must be encoded so NULL ("\000") is not transmitted.
|
||||
local data_to_trasmit_WoW_addon = LibDeflate:EncodeForWoWAddonChannel(
|
||||
compress_deflate)
|
||||
-- When the receiver gets the data, decoded it first.
|
||||
local data_decoded_WoW_addon = LibDeflate:DecodeForWoWAddonChannel(
|
||||
data_to_trasmit_WoW_addon)
|
||||
-- Then decomrpess it
|
||||
assert(LibDeflate:DecompressDeflate(data_decoded_WoW_addon) == example_input)
|
||||
|
||||
-- The compressed output is not printable. EncodeForPrint will convert to
|
||||
-- a printable format, in case you want to export to the user to
|
||||
-- copy and paste. This encoding will make the data 25% bigger.
|
||||
local printable_compressed = LibDeflate:EncodeForPrint(compress_deflate)
|
||||
|
||||
-- DecodeForPrint to convert back.
|
||||
-- DecodeForPrint will remove prefixed and trailing control or space characters
|
||||
-- in the string before decode it.
|
||||
assert(LibDeflate:DecodeForPrint(printable_compressed) == compress_deflate)
|
||||
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
-------------------------------------------------------------------------------
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
--- Compress and decompress using zlib format
|
||||
local compress_zlib = LibDeflate:CompressZlib(example_input)
|
||||
local decompress_zlib = LibDeflate:DecompressZlib(compress_zlib)
|
||||
assert(decompress_zlib == example_input)
|
||||
|
||||
--- Control the compression level
|
||||
-- NOTE: High compression level does not make a difference here,
|
||||
-- because the input data is very small
|
||||
local compress_deflate_with_level = LibDeflate:CompressDeflate(example_input
|
||||
, {level = 9})
|
||||
local decompress_deflate_with_level = LibDeflate:DecompressDeflate(
|
||||
compress_deflate_with_level)
|
||||
assert(decompress_deflate_with_level == example_input)
|
||||
|
||||
|
||||
-- Compress with a preset dictionary
|
||||
local dict_str = "121231234" -- example preset dictionary string.
|
||||
-- print(LibDeflate:Adler32(dict_str), #dict_str)
|
||||
-- 9 147325380
|
||||
-- hardcode the print result above, the ensure it is not modified
|
||||
-- accidenttaly during the program development.
|
||||
--
|
||||
-- WARNING: The compressor and decompressor must use the same dictionary.
|
||||
-- You should be aware of this when tranmitting compressed data over the
|
||||
-- internet.
|
||||
local dict = LibDeflate:CreateDictionary(dict_str, 9, 147325380)
|
||||
|
||||
-- Using the dictionary with raw deflate format
|
||||
local compress_deflate_with_dict = LibDeflate:CompressDeflateWithDict(
|
||||
example_input, dict)
|
||||
assert(#compress_deflate_with_dict < #compress_deflate)
|
||||
local decompress_deflate_with_dict = LibDeflate:DecompressDeflateWithDict(
|
||||
compress_deflate_with_dict, dict)
|
||||
assert(decompress_deflate_with_dict == example_input)
|
||||
|
||||
-- Using the dictionary with zlib format, specifying compression level
|
||||
local compress_zlib_with_dict = LibDeflate:CompressZlibWithDict(
|
||||
example_input, dict, {level = 9})
|
||||
assert(#compress_zlib_with_dict < #compress_zlib)
|
||||
local decompress_zlib_with_dict = LibDeflate:DecompressZlibWithDict(
|
||||
compress_zlib_with_dict, dict)
|
||||
assert(decompress_zlib_with_dict == example_input)
|
||||
4
libs/vanilla/LibDeflate/lib.xml
Normal file
@ -0,0 +1,4 @@
|
||||
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/
|
||||
..\FrameXML\UI.xsd">
|
||||
<Script file="LibDeflate.lua" />
|
||||
</Ui>
|
||||
1281
libs/vanilla/LibDeflate/tests/LibCompress/LibCompress.lua
Normal file
3251
libs/vanilla/LibDeflate/tests/Test.lua
Normal file
34
libs/vanilla/LibStub/LibStub.lua
Normal file
@ -0,0 +1,34 @@
|
||||
-- LibStub is a simple versioning stub meant for use in Libraries. http://www.wowace.com/wiki/LibStub for more info
|
||||
-- LibStub is hereby placed in the Public Domain Credits: Kaelten, Cladhaire, ckknight, Mikk, Ammo, Nevcairiel, joshborke
|
||||
local LIBSTUB_MAJOR, LIBSTUB_MINOR = "LibStub", 2 -- NEVER MAKE THIS AN SVN REVISION! IT NEEDS TO BE USABLE IN ALL REPOS!
|
||||
local _G = getfenv()
|
||||
local strfind, strfmt = string.find, string.format
|
||||
local LibStub = _G[ LIBSTUB_MAJOR ]
|
||||
|
||||
if not LibStub or LibStub.minor < LIBSTUB_MINOR then
|
||||
LibStub = LibStub or { libs = {}, minors = {} }
|
||||
_G[ LIBSTUB_MAJOR ] = LibStub
|
||||
LibStub.minor = LIBSTUB_MINOR
|
||||
|
||||
function LibStub:NewLibrary( major, minor )
|
||||
assert( type( major ) == "string", "Bad argument #2 to `NewLibrary' (string expected)" )
|
||||
local _, _, num = strfind( minor, "(%d+)" )
|
||||
minor = assert( tonumber( num ), "Minor version must either be a number or contain a number." )
|
||||
|
||||
local oldminor = self.minors[ major ]
|
||||
if oldminor and oldminor >= minor then return nil end
|
||||
self.minors[ major ], self.libs[ major ] = minor, self.libs[ major ] or {}
|
||||
return self.libs[ major ], oldminor
|
||||
end
|
||||
|
||||
function LibStub:GetLibrary( major, silent )
|
||||
if not self.libs[ major ] and not silent then
|
||||
error( strfmt( "Cannot find a library instance of %q.", tostring( major ) ), 2 )
|
||||
end
|
||||
return self.libs[ major ], self.minors[ major ]
|
||||
end
|
||||
|
||||
function LibStub:IterateLibraries() return pairs( self.libs ) end
|
||||
|
||||
setmetatable( LibStub, { __call = LibStub.GetLibrary } )
|
||||
end
|
||||
7
libs/vanilla/Libs.xml
Normal file
@ -0,0 +1,7 @@
|
||||
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/
|
||||
..\FrameXML\UI.xsd">
|
||||
<Include file="CallbackHandler-1.0\CallbackHandler-1.0.xml"/>
|
||||
<Include file="AceCore-3.0\AceCore-3.0.xml"/>
|
||||
<Include file="AceTimer-3.0\AceTimer-3.0.xml"/>
|
||||
<!--<Include file="LibDeflate\lib.xml"/>-->
|
||||
</Ui>
|
||||
794
main.lua
Normal file
@ -0,0 +1,794 @@
|
||||
RollFor = RollFor or {}
|
||||
local m = RollFor
|
||||
|
||||
---@diagnostic disable-next-line: undefined-global
|
||||
local lib_stub = LibStub
|
||||
local version = m.get_addon_version()
|
||||
|
||||
local M = {}
|
||||
|
||||
local getn = m.getn
|
||||
local info = m.pretty_print
|
||||
local hl = m.colors.highlight
|
||||
local RollSlashCommand = m.Types.RollSlashCommand
|
||||
|
||||
local function clear_data()
|
||||
M.softres_gui.clear()
|
||||
M.name_matcher.clear( true )
|
||||
M.softres.clear( true )
|
||||
M.minimap_button.set_icon( M.minimap_button.ColorType.White )
|
||||
M.winner_tracker.clear()
|
||||
end
|
||||
|
||||
local function update_minimap_icon()
|
||||
local result = M.softres_check.check_softres( true )
|
||||
|
||||
if result == M.softres_check.ResultType.NoItemsFound then
|
||||
M.minimap_button.set_icon( M.minimap_button.ColorType.White )
|
||||
elseif result == M.softres_check.ResultType.SomeoneIsNotSoftRessing then
|
||||
M.minimap_button.set_icon( M.minimap_button.ColorType.Orange )
|
||||
elseif result == M.softres_check.ResultType.FoundOutdatedData then
|
||||
M.minimap_button.set_icon( M.minimap_button.ColorType.Red )
|
||||
else
|
||||
M.minimap_button.set_icon( M.minimap_button.ColorType.Green )
|
||||
end
|
||||
end
|
||||
|
||||
local function on_softres_status_changed()
|
||||
update_minimap_icon()
|
||||
end
|
||||
|
||||
local function on_raid_trade( giver_name, recipient_name, item_name )
|
||||
local item_id = M.dropped_loot.get_dropped_item_id( item_name )
|
||||
|
||||
if item_id then
|
||||
local quality, _ = m.get_item_quality_and_texture( m.api, item_id )
|
||||
local item_link = m.fetch_item_link( item_id, quality )
|
||||
|
||||
M.loot_award_callback.on_loot_awarded( item_id, item_link, recipient_name, nil, true )
|
||||
if item_id and M.awarded_loot.has_item_been_awarded( giver_name, item_id ) then
|
||||
info( string.format( "%s traded %s to %s.", hl( giver_name ), item_link, hl( recipient_name ) ) )
|
||||
M.awarded_loot.unaward( giver_name, item_id )
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function trade_complete_callback( recipient_name, items_given, items_received )
|
||||
for i = 1, getn( items_given ) do
|
||||
local item = items_given[ i ]
|
||||
if item then
|
||||
local item_id = M.item_utils.get_item_id( item.link )
|
||||
local item_name = item_id and M.dropped_loot.get_dropped_item_name( item_id )
|
||||
|
||||
if item_id and item_name then
|
||||
M.loot_award_callback.on_loot_awarded( item_id, item.link, recipient_name )
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
for i = 1, getn( items_received ) do
|
||||
local item = items_received[ i ]
|
||||
|
||||
if item then
|
||||
local item_id = M.item_utils.get_item_id( item.link )
|
||||
|
||||
if item_id and M.awarded_loot.has_item_been_awarded( recipient_name, item_id ) then
|
||||
M.unaward_item( recipient_name, item_id, item.link )
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function create_components()
|
||||
---@type AceTimer
|
||||
M.ace_timer = lib_stub( "AceTimer-3.0" )
|
||||
|
||||
local db = m.Db.new( M.char_db )
|
||||
|
||||
---@type EventBus
|
||||
M.config_event_bus = m.EventBus.new()
|
||||
|
||||
---@type Config
|
||||
M.config = m.Config.new( db( "config" ), M.config_event_bus )
|
||||
|
||||
local classic = M.config.classic_look()
|
||||
local popup_bottom_margin, popup_bottom_button_margin = classic and 37 or 24, classic and 14 or 7
|
||||
local popup_side_margin = classic and 50 or 35
|
||||
local popup_builder_factory = classic and m.PopupBuilder.classic or m.PopupBuilder.modern
|
||||
|
||||
---@type fun(): PopupBuilder
|
||||
---@param bottom_margin number?
|
||||
---@param side_margin number?
|
||||
local function popup_builder( bottom_margin, side_margin )
|
||||
return popup_builder_factory( m.FrameBuilder, bottom_margin or popup_bottom_margin, popup_bottom_button_margin, side_margin or popup_side_margin )
|
||||
end
|
||||
|
||||
---@type UiReloadPopup
|
||||
M.ui_reload_popup = m.UiReloadPopup.new( popup_builder( classic and 37 or 27 ), M.config )
|
||||
|
||||
---@type ConfirmPopup
|
||||
M.confirm_popup = m.ConfirmPopup.new( popup_builder( classic and 37 or 27 ), M.config )
|
||||
|
||||
M.api = function() return m.api end
|
||||
|
||||
---@type PlayerInfo
|
||||
M.player_info = m.PlayerInfo.new( M.api() )
|
||||
|
||||
---@type GroupRoster
|
||||
M.group_roster = m.GroupRoster.new( M.api(), M.player_info )
|
||||
|
||||
M.chat_api = m.ChatApi.new()
|
||||
|
||||
---@type Chat
|
||||
M.chat = m.Chat.new( M.chat_api, M.group_roster, M.player_info )
|
||||
|
||||
---@alias GroupAwareSoftResFn fun ( softres: SoftRes ): GroupAwareSoftRes
|
||||
---@type GroupAwareSoftResFn
|
||||
M.present_softres = function( softres ) return m.SoftResPresentPlayersDecorator.new( M.group_roster, softres ) end
|
||||
---@type GroupAwareSoftResFn
|
||||
M.absent_softres = function( softres ) return m.SoftResAbsentPlayersDecorator.new( M.group_roster, softres ) end
|
||||
|
||||
---@type ItemUtils
|
||||
M.item_utils = m.ItemUtils
|
||||
|
||||
---@type TooltipReader
|
||||
M.tooltip_reader = m.TooltipReader.new( M.api() )
|
||||
|
||||
-- TODO: Add type.
|
||||
M.version_broadcast = m.VersionBroadcast.new( db( "version_broadcast" ), M.player_info, version.str )
|
||||
|
||||
---@type AwardedLoot
|
||||
M.awarded_loot = m.AwardedLoot.new( db( "awarded_loot" ), M.group_roster, M.config )
|
||||
|
||||
-- TODO: Add type.
|
||||
M.softres_db = db( "softres" )
|
||||
|
||||
-- TODO: Add type.
|
||||
M.unfiltered_softres = m.SoftRes.new( M.softres_db )
|
||||
|
||||
-- TODO: Add type.
|
||||
M.name_matcher = m.NameManualMatcher.new(
|
||||
db( "name_matcher" ), M.api,
|
||||
M.absent_softres( M.unfiltered_softres ),
|
||||
m.NameAutoMatcher.new( M.group_roster, M.unfiltered_softres, 0.57, 0.4 ),
|
||||
on_softres_status_changed
|
||||
)
|
||||
|
||||
---@type SoftRes
|
||||
M.matched_name_softres = m.SoftResMatchedNameDecorator.new( M.name_matcher, M.unfiltered_softres )
|
||||
|
||||
---@type SoftRes
|
||||
M.awarded_loot_softres = m.SoftResAwardedLootDecorator.new( M.awarded_loot, M.matched_name_softres )
|
||||
|
||||
---@type GroupAwareSoftRes
|
||||
M.softres = M.present_softres( M.awarded_loot_softres )
|
||||
|
||||
---@type DroppedLoot
|
||||
M.dropped_loot = m.DroppedLoot.new( db( "dropped_loot" ) )
|
||||
M.softres_check = m.SoftResCheck.new( M.matched_name_softres, M.group_roster, M.name_matcher, M.ace_timer,
|
||||
M.absent_softres, db( "softres_check" ) )
|
||||
|
||||
---@type WinnerTracker
|
||||
M.winner_tracker = m.WinnerTracker.new( db( "winner_tracker" ) )
|
||||
|
||||
---@type LootFacade
|
||||
M.loot_facade = m.LootFacade.new( m.EventFrame.new( m.api ), m.api )
|
||||
|
||||
-- TODO: Add type.
|
||||
---@diagnostic disable-next-line: unused-local, unused-function
|
||||
local function get_dummy_items()
|
||||
---@diagnostic disable-next-line: unused-function
|
||||
local function item_link( name, id, quality )
|
||||
local color = m.api.ITEM_QUALITY_COLORS[ quality ].hex or "|cffffffff"
|
||||
return string.format( "%s|Hitem:%s::::::::20:257::::::|h[%s]|h|r", color, id or "3299", name )
|
||||
end
|
||||
|
||||
-- local ids = { 17204, 16961, 18842, 16961, 16961, 18842, 16865, 16961, 17109, 16961, 18466, 11980, 12820, 3676 }
|
||||
local ids = { 17109, 17109, 17109, 3676 }
|
||||
local result = {}
|
||||
---@type MakeDroppedItemFn
|
||||
local make_dropped_item = m.ItemUtils.make_dropped_item
|
||||
local boe = m.ItemUtils.BindType.BindOnEquip
|
||||
|
||||
---@diagnostic disable-next-line: unused-local
|
||||
for i, item_id in ipairs( ids ) do
|
||||
local name, tooltip_link, quality, texture
|
||||
|
||||
if m.vanilla then
|
||||
name, tooltip_link, quality, _, _, _, _, _, texture = m.api.GetItemInfo( item_id )
|
||||
else
|
||||
name, tooltip_link, quality, _, _, _, _, _, _, texture = m.api.GetItemInfo( item_id )
|
||||
end
|
||||
|
||||
local link = item_link( name, item_id, quality )
|
||||
local item = make_dropped_item( item_id, name, link, tooltip_link, quality, 1, texture, boe, nil, true )
|
||||
|
||||
table.insert( result, item )
|
||||
end
|
||||
|
||||
table.sort( result, function( a, b ) return a.quality > b.quality end )
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
-- Enable this for testing in game. It will replace dropped items with the above.
|
||||
local mock_items = false
|
||||
|
||||
---@type LootList
|
||||
M.raw_loot_list = m.LootList.new( M.loot_facade, M.item_utils, M.tooltip_reader, m.BossList.zones, mock_items and get_dummy_items or nil )
|
||||
|
||||
---@type SoftResLootList
|
||||
M.loot_list = m.SoftResLootListDecorator.new( M.raw_loot_list, M.softres )
|
||||
|
||||
---@type MasterLootCandidates
|
||||
M.master_loot_candidates = m.MasterLootCandidates.new( M.api(), M.group_roster, M.raw_loot_list ) -- remove group_roster for testing (dummy candidates)
|
||||
|
||||
---@type MasterLootCandidateSelectionFrame
|
||||
M.player_selection_frame = m.MasterLootCandidateSelectionFrame.new( m.FrameBuilder, M.config )
|
||||
|
||||
local rolling_popup_db = db( "rolling_popup" )
|
||||
|
||||
---@type RollingPopupContentTransformer
|
||||
local rolling_popup_content_transformer = m.RollingPopupContentTransformer.new( M.config )
|
||||
|
||||
---@type RollingPopup
|
||||
M.rolling_popup = m.RollingPopup.new(
|
||||
popup_builder(),
|
||||
rolling_popup_content_transformer,
|
||||
rolling_popup_db,
|
||||
M.config
|
||||
)
|
||||
|
||||
---@type LootFrameSkin
|
||||
local skin = M.config.classic_look() and m.OgLootFrameSkin.new( m.FrameBuilder ) or m.ModernLootFrameSkin.new( m.FrameBuilder )
|
||||
|
||||
---@type LootFrame
|
||||
M.loot_frame = m.LootFrame.new(
|
||||
skin,
|
||||
db( "loot_frame" ),
|
||||
M.config
|
||||
)
|
||||
|
||||
---@type LootAwardPopup
|
||||
M.loot_award_popup = m.LootAwardPopup.new(
|
||||
popup_builder( classic and 38 or 30, classic and 65 or 55 ),
|
||||
M.config,
|
||||
M.rolling_popup
|
||||
)
|
||||
|
||||
---@type RollController
|
||||
M.roll_controller = m.RollController.new(
|
||||
M.master_loot_candidates,
|
||||
M.softres,
|
||||
M.loot_list,
|
||||
M.config,
|
||||
M.rolling_popup,
|
||||
M.loot_award_popup,
|
||||
M.player_selection_frame
|
||||
)
|
||||
|
||||
---@type LootAwardCallback
|
||||
M.loot_award_callback = m.LootAwardCallback.new( M.awarded_loot, M.roll_controller, M.winner_tracker, M.group_roster, M.softres )
|
||||
|
||||
---@type MasterLoot
|
||||
M.master_loot = m.MasterLoot.new(
|
||||
M.master_loot_candidates,
|
||||
M.loot_award_callback,
|
||||
M.loot_list,
|
||||
M.roll_controller
|
||||
)
|
||||
|
||||
---@type AutoLoot
|
||||
M.auto_loot = m.AutoLoot.new( M.loot_list, M.api, db( "auto_loot" ), M.config, M.player_info )
|
||||
|
||||
---@type DroppedLootAnnounce
|
||||
M.dropped_loot_announce = m.DroppedLootAnnounce.new(
|
||||
M.loot_list,
|
||||
M.chat,
|
||||
M.dropped_loot,
|
||||
M.softres,
|
||||
M.winner_tracker,
|
||||
M.player_info,
|
||||
M.auto_loot,
|
||||
M.config
|
||||
)
|
||||
|
||||
---@type WinnersPopup
|
||||
M.winners_popup = m.WinnersPopup.new(
|
||||
popup_builder (),
|
||||
m.FrameBuilder,
|
||||
db( "winners_popup" ),
|
||||
M.awarded_loot,
|
||||
M.roll_controller,
|
||||
M.confirm_popup,
|
||||
M.config
|
||||
)
|
||||
|
||||
-- TODO: Add type.
|
||||
M.softres_gui = m.SoftResGui.new( M.api, M.import_encoded_softres_data, M.softres_check, M.softres, clear_data, M.dropped_loot_announce.reset )
|
||||
|
||||
-- TODO: Add type.
|
||||
M.trade_tracker = m.TradeTracker.new( M.ace_timer, M.chat, trade_complete_callback )
|
||||
|
||||
-- TODO: Add type.
|
||||
M.usage_printer = m.UsagePrinter.new( M.chat )
|
||||
|
||||
-- TODO: Add type.
|
||||
M.minimap_button = m.MinimapButton.new( M.api, db( "minimap_button" ), M.softres_gui.toggle, M.winners_popup.toggle, M.softres_check, M.config )
|
||||
|
||||
-- TODO: Add type.
|
||||
M.master_loot_warning = m.MasterLootWarning.new( M.api, M.config, m.BossList.zones, M.player_info )
|
||||
|
||||
-- TODO: Add type.
|
||||
M.new_group_event = m.NewGroupEvent.new( M.group_roster )
|
||||
|
||||
-- TODO: Add type.
|
||||
M.auto_group_loot = m.AutoGroupLoot.new( M.loot_list, M.config, m.BossList.zones, M.player_info )
|
||||
|
||||
-- TODO: Add type.
|
||||
M.auto_master_loot = m.AutoMasterLoot.new( M.config, m.BossList.zones, M.player_info )
|
||||
|
||||
-- TODO: Add type.
|
||||
M.softres_roll_gui_data = m.SoftResRollGuiData.new( M.softres, M.group_roster )
|
||||
|
||||
-- TODO: Add type.
|
||||
M.tie_roll_gui_data = m.TieRollGuiData.new( M.group_roster )
|
||||
|
||||
-- TODO: Add type.
|
||||
M.welcome_popup = m.WelcomePopup.new( m.FrameBuilder, M.ace_timer, db( "welcome_popup" ) )
|
||||
|
||||
-- TODO: Add type.
|
||||
M.roll_for_ad = m.RollForAd.new( M.player_info )
|
||||
|
||||
---@type RollingStrategyFactory
|
||||
M.rolling_strategy_factory = m.RollingStrategyFactory.new(
|
||||
M.group_roster,
|
||||
M.loot_list,
|
||||
M.master_loot_candidates,
|
||||
M.chat,
|
||||
M.ace_timer,
|
||||
M.winner_tracker,
|
||||
M.config,
|
||||
M.softres,
|
||||
M.player_info
|
||||
)
|
||||
|
||||
---@type RollingLogic
|
||||
M.rolling_logic = m.RollingLogic.new(
|
||||
M.chat,
|
||||
M.ace_timer,
|
||||
M.roll_controller,
|
||||
M.rolling_strategy_factory,
|
||||
M.master_loot_candidates,
|
||||
M.winner_tracker,
|
||||
M.config
|
||||
)
|
||||
|
||||
M.loot_controller = m.LootController.new(
|
||||
M.player_info,
|
||||
M.loot_facade,
|
||||
M.loot_list,
|
||||
M.loot_frame,
|
||||
M.roll_controller,
|
||||
M.softres,
|
||||
M.rolling_logic,
|
||||
M.chat
|
||||
)
|
||||
|
||||
---@type ArgsParser
|
||||
M.args_parser = m.ArgsParser.new( m.ItemUtils, M.config )
|
||||
|
||||
-- TODO: Add type.
|
||||
M.roll_result_announcer = m.RollResultAnnouncer.new( M.chat, M.roll_controller, M.softres, M.config )
|
||||
|
||||
M.loot_facade_listener = m.LootFacadeListener.new(
|
||||
M.loot_facade,
|
||||
M.auto_loot,
|
||||
M.dropped_loot_announce,
|
||||
M.master_loot,
|
||||
M.auto_group_loot,
|
||||
M.roll_controller,
|
||||
M.player_info
|
||||
)
|
||||
|
||||
---@type ClientBroadcast
|
||||
M.client_broadcast = m.ClientBroadcast.new(
|
||||
M.roll_controller,
|
||||
M.softres,
|
||||
M.config
|
||||
)
|
||||
|
||||
---@type Client
|
||||
M.client = m.Client.new(
|
||||
M.ace_timer,
|
||||
M.player_info,
|
||||
M.rolling_popup,
|
||||
M.config
|
||||
)
|
||||
|
||||
M.sandbox = m.Sandbox.new()
|
||||
end
|
||||
|
||||
local function subscribe_for_component_events()
|
||||
M.config.subscribe( "show_ml_warning", function( enabled )
|
||||
if enabled then
|
||||
M.master_loot_warning.on_player_target_changed()
|
||||
else
|
||||
M.master_loot_warning.hide()
|
||||
end
|
||||
end )
|
||||
|
||||
M.new_group_event.subscribe( function()
|
||||
M.awarded_loot.clear()
|
||||
M.dropped_loot.clear()
|
||||
end )
|
||||
|
||||
M.config_event_bus.subscribe( "config_change_requires_ui_reload", function()
|
||||
M.ui_reload_popup.show()
|
||||
end )
|
||||
end
|
||||
|
||||
function M.import_softres_data( softres_data )
|
||||
M.unfiltered_softres.import( softres_data )
|
||||
M.name_matcher.auto_match()
|
||||
end
|
||||
|
||||
function M.import_encoded_softres_data( data, data_loaded_callback )
|
||||
local sr = m.SoftRes
|
||||
local softres_data = sr.decode( data )
|
||||
|
||||
if not softres_data and data and string.len( data ) > 0 then
|
||||
info( "Could not load soft-res data!", m.colors.red )
|
||||
return
|
||||
elseif not softres_data then
|
||||
M.minimap_button.set_icon( M.minimap_button.ColorType.White )
|
||||
return
|
||||
end
|
||||
|
||||
M.import_softres_data( softres_data )
|
||||
|
||||
info( "Soft-res data loaded successfully!" )
|
||||
if data_loaded_callback then data_loaded_callback( softres_data ) end
|
||||
|
||||
update_minimap_icon()
|
||||
end
|
||||
|
||||
local function on_roll_command( roll_slash_command )
|
||||
return function( args )
|
||||
if M.rolling_logic.is_rolling() then
|
||||
M.chat.info( "Rolling is in progress." )
|
||||
return
|
||||
end
|
||||
|
||||
if string.find( args, "^debug" ) then
|
||||
m.DebugBuffer.on_command( args )
|
||||
return
|
||||
end
|
||||
|
||||
if string.find( args, "^config" ) then
|
||||
M.config.on_command( args )
|
||||
return
|
||||
end
|
||||
|
||||
if args == "versioncheck guild" then
|
||||
M.version_broadcast.guild_version_request()
|
||||
return
|
||||
end
|
||||
|
||||
if not M.api().IsInGroup() then
|
||||
M.chat.info( "Not in a group." )
|
||||
return
|
||||
end
|
||||
|
||||
if args == "versioncheck" then
|
||||
M.version_broadcast.group_version_request()
|
||||
return
|
||||
end
|
||||
|
||||
if string.find( args, "^client enable" ) and M.player_info.is_master_looter() then
|
||||
M.client_broadcast.enable_roll_popup()
|
||||
return
|
||||
end
|
||||
|
||||
local item, count, seconds, message = M.args_parser.parse( args )
|
||||
|
||||
if not item then
|
||||
M.usage_printer.print_usage( roll_slash_command )
|
||||
return
|
||||
end
|
||||
|
||||
local strategy_type = m.Types.slash_command_to_strategy_type( roll_slash_command )
|
||||
|
||||
if not strategy_type then
|
||||
info( string.format( "Unsupported command: %s", hl( roll_slash_command and roll_slash_command.slash_command or "?" ) ) )
|
||||
return
|
||||
end
|
||||
|
||||
if M.softres.is_item_hardressed( item.id ) then
|
||||
M.roll_controller.preview( item, count )
|
||||
return
|
||||
end
|
||||
|
||||
M.roll_controller.start( strategy_type, item, count, seconds, message )
|
||||
end
|
||||
end
|
||||
|
||||
local function on_show_sorted_rolls_command( args )
|
||||
if M.rolling_logic.is_rolling() then
|
||||
info( "Rolling is in progress." )
|
||||
return
|
||||
end
|
||||
|
||||
if args then
|
||||
for limit in string.gmatch( args, "(%d+)" ) do
|
||||
M.rolling_logic.show_sorted_rolls( tonumber( limit ) )
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
M.rolling_logic.show_sorted_rolls( 5 )
|
||||
end
|
||||
|
||||
local function is_rolling_check( f )
|
||||
---@diagnostic disable-next-line: unused-vararg
|
||||
return function( ... )
|
||||
if not M.rolling_logic.is_rolling() then
|
||||
M.chat.info( "Rolling not in progress." )
|
||||
return
|
||||
end
|
||||
|
||||
f( unpack( arg ) )
|
||||
end
|
||||
end
|
||||
|
||||
local function in_group_check( f )
|
||||
return m.in_group_check( M.api(), M.chat, f )
|
||||
end
|
||||
|
||||
local function setup_storage()
|
||||
-- Reset old AceDB configuration. I don't give a fuck :)
|
||||
if RollForDb and RollForDb.global and RollForDb.global.version then
|
||||
RollForDb = nil
|
||||
end
|
||||
|
||||
RollForDb = RollForDb or {}
|
||||
RollForCharDb = RollForCharDb or {}
|
||||
|
||||
M.db = RollForDb
|
||||
M.char_db = RollForCharDb
|
||||
|
||||
if not M.db.version then
|
||||
M.db.version = version.str
|
||||
end
|
||||
end
|
||||
|
||||
local function on_softres_command( args )
|
||||
if args == "init" then
|
||||
clear_data()
|
||||
end
|
||||
|
||||
M.softres_gui.toggle()
|
||||
end
|
||||
|
||||
local function on_roll( player_name, roll, min, max )
|
||||
local player = M.group_roster.find_player( player_name )
|
||||
|
||||
if not player then
|
||||
m.err( string.format( "Player %s could not be found.", hl( player_name ) ) )
|
||||
return
|
||||
end
|
||||
|
||||
M.rolling_logic.on_roll( player, roll, min, max )
|
||||
end
|
||||
|
||||
local function on_loot_method_changed()
|
||||
M.master_loot_warning.on_party_loot_method_changed()
|
||||
end
|
||||
|
||||
local function on_master_looter_changed( player_name )
|
||||
if M.player_info.get_name() == player_name and m.is_master_loot() then
|
||||
M.ace_timer.ScheduleTimer( M, M.config.print_raid_roll_settings, 0.1 )
|
||||
end
|
||||
end
|
||||
|
||||
function M.on_chat_msg_system( message )
|
||||
for player_name, roll, min, max in string.gmatch( message, "([^%s]+) rolls (%d+) %((%d+)%-(%d+)%)" ) do
|
||||
on_roll( player_name, tonumber( roll ), tonumber( min ), tonumber( max ) )
|
||||
return
|
||||
end
|
||||
|
||||
if string.find( message, "^Looting changed to" ) then
|
||||
on_loot_method_changed()
|
||||
return
|
||||
end
|
||||
|
||||
for player_name in string.gmatch( message, "(.-) is now the loot master%." ) do
|
||||
on_master_looter_changed( player_name )
|
||||
return
|
||||
end
|
||||
|
||||
for giver_name, item_name, recipient_name in string.gmatch( message, "([^%s]+) trades item (.+) to ([^%s]+)%." ) do
|
||||
on_raid_trade( giver_name, recipient_name, item_name )
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
-- TODO: this can now be replaced by mocking LootList
|
||||
---@diagnostic disable-next-line: unused-local, unused-function
|
||||
local function simulate_loot_dropped( args )
|
||||
---@diagnostic disable-next-line: unused-function
|
||||
local function mock_table_function( name, values )
|
||||
M.api()[ name ] = function( key )
|
||||
local value = values[ key ]
|
||||
|
||||
if type( value ) == "function" then
|
||||
return value()
|
||||
else
|
||||
return value
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@diagnostic disable-next-line: unused-function
|
||||
local function make_loot_slot_info( count, quality )
|
||||
local result = {}
|
||||
|
||||
for i = 1, count do
|
||||
table.insert( result, function()
|
||||
if i == count then
|
||||
m.api = m.real_api
|
||||
m.real_api = nil
|
||||
end
|
||||
|
||||
return nil, nil, nil, quality or 4
|
||||
end )
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
local item_links = M.item_utils.parse_all_links( args )
|
||||
|
||||
if m.real_api then
|
||||
info( "Mocking in progress." )
|
||||
return
|
||||
end
|
||||
|
||||
m.real_api = m.api
|
||||
m.api = m.clone( m.api )
|
||||
M.api()[ "GetNumLootItems" ] = function() return getn( item_links ) end
|
||||
M.api()[ "UnitName" ] = function() return tostring( m.lua.time() ) end
|
||||
M.api()[ "GetLootThreshold" ] = function() return 4 end
|
||||
mock_table_function( "GetLootSlotLink", item_links )
|
||||
mock_table_function( "GetLootSlotInfo", make_loot_slot_info( getn( item_links ), 4 ) )
|
||||
|
||||
M.dropped_loot_announce.on_loot_opened()
|
||||
end
|
||||
|
||||
local function show_how_to_roll()
|
||||
M.chat.announce( "How to roll:" )
|
||||
local ms = M.config.ms_roll_threshold() ~= 100 and string.format( " (%s)", M.config.ms_roll_threshold() or "100" ) or ""
|
||||
|
||||
local sr = M.softres.get_all_rollers()
|
||||
local sr_count = getn( sr )
|
||||
|
||||
M.chat.announce( string.format( "For main-spec%s, type: /roll%s", sr_count > 0 and " and soft-res" or "", ms ) )
|
||||
M.chat.announce( string.format( "For off-spec, type: /roll %s", M.config.os_roll_threshold() ) )
|
||||
|
||||
if M.config.tmog_rolling_enabled() then
|
||||
M.chat.announce( string.format( "For transmog, type: /roll %s", M.config.tmog_roll_threshold() ) )
|
||||
end
|
||||
end
|
||||
|
||||
local function on_reset_dropped_loot_announce_command()
|
||||
M.dropped_loot_announce.reset()
|
||||
end
|
||||
|
||||
local function setup_slash_commands()
|
||||
-- Roll For commands
|
||||
SLASH_RF1 = RollSlashCommand.NormalRoll
|
||||
M.api().SlashCmdList[ "RF" ] = on_roll_command( RollSlashCommand.NormalRoll )
|
||||
SLASH_ARF1 = RollSlashCommand.NoSoftResRoll
|
||||
M.api().SlashCmdList[ "ARF" ] = in_group_check( on_roll_command( RollSlashCommand.NoSoftResRoll ) )
|
||||
SLASH_RR1 = RollSlashCommand.RaidRoll
|
||||
M.api().SlashCmdList[ "RR" ] = in_group_check( on_roll_command( RollSlashCommand.RaidRoll ) )
|
||||
SLASH_IRR1 = RollSlashCommand.InstaRaidRoll
|
||||
M.api().SlashCmdList[ "IRR" ] = in_group_check( on_roll_command( RollSlashCommand.InstaRaidRoll ) )
|
||||
SLASH_HTR1 = "/htr"
|
||||
M.api().SlashCmdList[ "HTR" ] = in_group_check( show_how_to_roll )
|
||||
SLASH_CR1 = "/cr"
|
||||
M.api().SlashCmdList[ "CR" ] = is_rolling_check( M.roll_controller.cancel_rolling )
|
||||
SLASH_FR1 = "/fr"
|
||||
M.api().SlashCmdList[ "FR" ] = is_rolling_check( M.roll_controller.finish_rolling_early )
|
||||
SLASH_SSR1 = "/ssr"
|
||||
M.api().SlashCmdList[ "SSR" ] = on_show_sorted_rolls_command
|
||||
SLASH_RFR1 = "/rfr"
|
||||
M.api().SlashCmdList[ "RFR" ] = on_reset_dropped_loot_announce_command
|
||||
|
||||
-- Soft Res commands
|
||||
SLASH_SR1 = "/sr"
|
||||
M.api().SlashCmdList[ "SR" ] = on_softres_command
|
||||
SLASH_SRS1 = "/srs"
|
||||
M.api().SlashCmdList[ "SRS" ] = M.softres_check.show_softres
|
||||
SLASH_SRC1 = "/src"
|
||||
M.api().SlashCmdList[ "SRC" ] = M.softres_check.check_softres
|
||||
SLASH_SRO1 = "/sro"
|
||||
M.api().SlashCmdList[ "SRO" ] = M.name_matcher.manual_match
|
||||
|
||||
SLASH_RFW1 = "/rfw"
|
||||
M.api().SlashCmdList[ "RFW" ] = M.winners_popup.show
|
||||
SLASH_RFT1 = "/rft"
|
||||
M.api().SlashCmdList[ "RFT" ] = M.sandbox.run
|
||||
|
||||
--SLASH_DROPPED1 = "/DROPPED"
|
||||
--M.api().SlashCmdList[ "DROPPED" ] = simulate_loot_dropped
|
||||
end
|
||||
|
||||
function M.on_player_login()
|
||||
setup_storage()
|
||||
create_components()
|
||||
subscribe_for_component_events()
|
||||
setup_slash_commands()
|
||||
|
||||
info( string.format( "Loaded (%s).", hl( string.format( "v%s", version.str ) ) ) )
|
||||
|
||||
M.version_broadcast.broadcast()
|
||||
M.import_encoded_softres_data( M.softres_db.data )
|
||||
M.softres_gui.load( M.softres_db.data )
|
||||
|
||||
if M.welcome_popup.should_show() then
|
||||
M.welcome_popup.show()
|
||||
end
|
||||
|
||||
---@diagnostic disable-next-line: undefined-global
|
||||
LootFrame:UnregisterAllEvents()
|
||||
---@diagnostic disable-next-line: undefined-global
|
||||
if pfLootFrame then pfLootFrame:UnregisterAllEvents() end
|
||||
end
|
||||
|
||||
---@diagnostic disable-next-line: unused-local, unused-function
|
||||
local function on_party_message( message, player )
|
||||
for name, roll in string.gmatch( message, "(%a+) rolls (%d+)" ) do
|
||||
on_roll( name, tonumber( roll ), 1, 100 )
|
||||
end
|
||||
for name, roll in string.gmatch( message, "(%a+) rolls os (%d+)" ) do
|
||||
on_roll( name, tonumber( roll ), 1, 99 )
|
||||
end
|
||||
end
|
||||
|
||||
function M.unaward_item( player_name, item_id, item_link )
|
||||
M.awarded_loot.unaward( player_name, item_id )
|
||||
info( string.format( "%s returned %s.", hl( player_name ), item_link ) )
|
||||
end
|
||||
|
||||
function M.on_group_changed()
|
||||
M.name_matcher.auto_match()
|
||||
update_minimap_icon()
|
||||
end
|
||||
|
||||
function M.on_chat_msg_addon( name, message, _, sender )
|
||||
if name ~= "RollFor" or not message then return end
|
||||
|
||||
for ver in string.gmatch( message, "VERSION::(.*)" ) do
|
||||
M.version_broadcast.on_version( ver )
|
||||
return
|
||||
end
|
||||
|
||||
for channel, requesting_player_name in string.gmatch( message, "VERSION_REQUEST::(.-)::(.*)" ) do
|
||||
M.version_broadcast.on_version_request( channel, requesting_player_name )
|
||||
return
|
||||
end
|
||||
|
||||
for requesting_player_name, channel, their_name, their_class, their_version in string.gmatch( message, "VERSION_RESPONSE::(.-)::(.-)::(.-)::(.-)::(.*)" ) do
|
||||
M.version_broadcast.on_version_response( requesting_player_name, channel, their_name, their_class, their_version )
|
||||
return
|
||||
end
|
||||
|
||||
for data in string.gmatch( message, "ROLL::(.*)" ) do
|
||||
M.client.on_message ( data, sender )
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
m.EventHandler.handle_events( M )
|
||||
return M
|
||||
42
src/ArgsParser.lua
Normal file
@ -0,0 +1,42 @@
|
||||
RollFor = RollFor or {}
|
||||
local m = RollFor
|
||||
|
||||
if m.ArgsParser then return end
|
||||
|
||||
local M = {}
|
||||
|
||||
---@type MakeItemFn
|
||||
local make_item = m.ItemUtils.make_item
|
||||
|
||||
---@alias ParseArgsFn fun( args: string ):
|
||||
--- Item,
|
||||
--- number, -- item count
|
||||
--- number, -- seconds
|
||||
--- string -- message
|
||||
|
||||
---@class ArgsParser
|
||||
---@field parse ParseArgsFn
|
||||
|
||||
---@return ArgsParser
|
||||
function M.new( item_utils, config )
|
||||
local function parse( args )
|
||||
for item_count, link, seconds, message in string.gmatch( args, "(%d*)[xX]?%s*(|%w+|Hitem.+|r)%s*(%d*)%s*(.*)" ) do
|
||||
local count = (not item_count or item_count == "") and 1 or tonumber( item_count )
|
||||
local id = item_utils.get_item_id( link )
|
||||
local name = item_utils.get_item_name( link )
|
||||
local quality, texture = m.get_item_quality_and_texture( m.api, id )
|
||||
|
||||
local item = make_item( id, name, link, quality, texture )
|
||||
local secs = seconds and seconds ~= "" and seconds ~= " " and tonumber( seconds ) or config.default_rolling_time_seconds()
|
||||
|
||||
return item, count, secs < 4 and 4 or secs > 15 and 15 or secs, message
|
||||
end
|
||||
end
|
||||
|
||||
return {
|
||||
parse = parse
|
||||
}
|
||||
end
|
||||
|
||||
m.ArgsParser = M
|
||||
return M
|
||||
59
src/AutoGroupLoot.lua
Normal file
@ -0,0 +1,59 @@
|
||||
RollFor = RollFor or {}
|
||||
local m = RollFor
|
||||
|
||||
if m.AutoGroupLoot then return end
|
||||
|
||||
local M = {}
|
||||
|
||||
local getn = m.getn
|
||||
|
||||
local ignore_zones = {
|
||||
"Blackwing Lair"
|
||||
}
|
||||
|
||||
---@class AutoGroupLoot
|
||||
---@field on_loot_opened fun()
|
||||
---@field on_loot_slot_cleared fun()
|
||||
|
||||
---@param loot_list LootList
|
||||
---@param config Config
|
||||
---@param boss_list BossList
|
||||
---@param player_info PlayerInfo
|
||||
function M.new( loot_list, config, boss_list, player_info )
|
||||
local m_target_name
|
||||
local m_item_count
|
||||
|
||||
local function on_loot_opened()
|
||||
m_target_name = m.target_name()
|
||||
m_item_count = getn( loot_list.get_items() )
|
||||
end
|
||||
|
||||
local function on_loot_slot_cleared()
|
||||
if m_item_count == nil then
|
||||
-- In case this is called before on_loot_opened
|
||||
return
|
||||
end
|
||||
|
||||
m_item_count = m_item_count - 1
|
||||
if m_item_count > 0 then return end
|
||||
if not m_item_count or m_item_count > 0 then return end
|
||||
|
||||
local zone_name = m.api.GetRealZoneText()
|
||||
if m.table_contains_value( ignore_zones, zone_name ) then return end
|
||||
local bosses = boss_list[ zone_name ] or {}
|
||||
local is_a_boss = m.table_contains_value( bosses, m_target_name )
|
||||
|
||||
if is_a_boss and config.auto_group_loot() and m.is_master_loot() and player_info.is_leader() then
|
||||
m.api.SetLootMethod( "group" )
|
||||
end
|
||||
end
|
||||
|
||||
---@type AutoGroupLoot
|
||||
return {
|
||||
on_loot_opened = on_loot_opened,
|
||||
on_loot_slot_cleared = on_loot_slot_cleared
|
||||
}
|
||||
end
|
||||
|
||||
m.AutoGroupLoot = M
|
||||
return M
|
||||
232
src/AutoLoot.lua
Normal file
@ -0,0 +1,232 @@
|
||||
RollFor = RollFor or {}
|
||||
local m = RollFor
|
||||
|
||||
if m.AutoLoot then return end
|
||||
|
||||
local item_utils = m.ItemUtils
|
||||
local info = m.pretty_print
|
||||
local hl = m.colors.hl
|
||||
local grey = m.colors.grey
|
||||
|
||||
local M = {}
|
||||
local getn = m.getn
|
||||
|
||||
M.interface = {
|
||||
on_loot_opened = "function",
|
||||
loot_item = "function"
|
||||
}
|
||||
|
||||
local button_visible = false
|
||||
local _G = getfenv( 0 )
|
||||
|
||||
---@class AutoLoot
|
||||
---@field is_auto_looted fun( item: DroppedItem ): boolean
|
||||
---@field on_loot_opened fun()
|
||||
---@field add fun( item_link: string )
|
||||
---@field remove fun( item_link: string )
|
||||
---@field clear fun()
|
||||
---@field loot_item fun( slot: number )
|
||||
|
||||
---@param loot_list LootList
|
||||
---@param api function
|
||||
---@param db table
|
||||
---@param config Config
|
||||
function M.new( loot_list, api, db, config, player_info )
|
||||
db.items = db.items or {}
|
||||
|
||||
local frame
|
||||
local items = db.items
|
||||
|
||||
local function find_my_candidate_index( slot )
|
||||
for i = 1, 40 do
|
||||
if m.vanilla then
|
||||
local name = m.api.GetMasterLootCandidate( i )
|
||||
|
||||
if name == api().UnitName( "player" ) then
|
||||
return i
|
||||
end
|
||||
else
|
||||
local name = m.api.GetMasterLootCandidate( slot, i )
|
||||
|
||||
if name == api().UnitName( "player" ) then
|
||||
return i
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function is_auto_looted( item )
|
||||
if not config.auto_loot() then
|
||||
return false
|
||||
end
|
||||
|
||||
local zone_name = api().GetRealZoneText()
|
||||
local item_ids = items[ zone_name ] or {}
|
||||
local threshold = api().GetLootThreshold()
|
||||
local quality = item.quality or 0
|
||||
|
||||
if item_ids[ item.id ] then
|
||||
return true
|
||||
end
|
||||
|
||||
if item.bind == item_utils.BindType.BindOnPickup or item.bind == item_utils.BindType.Quest then
|
||||
return false
|
||||
end
|
||||
|
||||
if quality < threshold then
|
||||
return true
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
local function on_auto_loot()
|
||||
if not player_info.is_master_looter() or not config.auto_loot() then
|
||||
return
|
||||
end
|
||||
|
||||
for _, item in ipairs( loot_list.get_items() ) do
|
||||
local slot = loot_list.get_slot( item.id )
|
||||
|
||||
-- Looting coins is hidden under a secure button and cannot be done
|
||||
-- through vanilla API. If the user has the SuperWoW mod, we can call an
|
||||
-- extra function instead.
|
||||
if config.superwow_auto_loot_coins() and api().SUPERWOW_VERSION and item.type == item_utils.LootType.Coin then
|
||||
api().LootSlot( slot, 1 )
|
||||
|
||||
local coin = item --[[@as Coin]]
|
||||
local amount = string.gsub( string.gsub( coin.amount_text, "\n", " " ), " $", "" )
|
||||
|
||||
if config.auto_loot_messages() then
|
||||
info( string.format( "Auto-looting %s.", grey( amount ) ) )
|
||||
end
|
||||
end
|
||||
|
||||
if item.id and slot then
|
||||
if is_auto_looted( item ) then
|
||||
local index = find_my_candidate_index( slot )
|
||||
|
||||
if index then
|
||||
api().GiveMasterLoot( slot, index )
|
||||
|
||||
if config.auto_loot_messages() then
|
||||
info( string.format( "Auto-looting %s.", item.link ) )
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function create_frame()
|
||||
frame = api().CreateFrame( "BUTTON", nil, api().LootFrame, "UIPanelButtonTemplate" )
|
||||
frame:SetWidth( 90 )
|
||||
frame:SetHeight( 23 )
|
||||
frame:SetText( "Auto Loot" )
|
||||
frame:SetPoint( "TOPRIGHT", api().LootFrame, "TOPRIGHT", -75, -44 )
|
||||
frame:SetScript( "OnClick", on_auto_loot )
|
||||
frame:Show()
|
||||
end
|
||||
|
||||
local function on_loot_opened()
|
||||
if button_visible then
|
||||
if not frame then create_frame() end
|
||||
|
||||
local zone_name = api().GetRealZoneText()
|
||||
local item_ids = items[ zone_name ]
|
||||
|
||||
if not item_ids or getn( item_ids ) == 0 then
|
||||
frame:Hide()
|
||||
else
|
||||
frame:Show()
|
||||
end
|
||||
end
|
||||
|
||||
if not m.is_shift_key_down() then on_auto_loot() end
|
||||
end
|
||||
|
||||
local function show_usage()
|
||||
info( string.format( "Usage: %s %s", hl( "/rfal <add||remove>" ), grey( "<item_link>" ) ) )
|
||||
end
|
||||
|
||||
local function add( item_link )
|
||||
local item_id = item_utils.get_item_id( item_link )
|
||||
|
||||
if not item_id then
|
||||
show_usage()
|
||||
return
|
||||
end
|
||||
|
||||
local zone_name = api().GetRealZoneText()
|
||||
|
||||
if not items[ zone_name ] then
|
||||
items[ zone_name ] = {}
|
||||
end
|
||||
|
||||
items[ zone_name ][ item_id ] = {
|
||||
item_name = item_utils.get_item_name( item_link ),
|
||||
item_link = item_link
|
||||
}
|
||||
|
||||
info( string.format( "%s added.", item_link ), "auto-loot" )
|
||||
end
|
||||
|
||||
local function remove( item_link )
|
||||
local item_id = item_utils.get_item_id( item_link )
|
||||
|
||||
if not item_id then
|
||||
show_usage()
|
||||
return
|
||||
end
|
||||
|
||||
local zone_name = api().GetRealZoneText()
|
||||
|
||||
if not items[ zone_name ] or not items[ zone_name ][ item_id ] then
|
||||
return
|
||||
end
|
||||
|
||||
items[ zone_name ][ item_id ] = nil
|
||||
info( string.format( "%s removed.", item_link ), "auto-loot" )
|
||||
end
|
||||
|
||||
local function clear()
|
||||
end
|
||||
|
||||
local function on_command( args )
|
||||
for item_link in string.gmatch( args, "add (.*)" ) do
|
||||
add( item_link )
|
||||
return
|
||||
end
|
||||
|
||||
for item_link in string.gmatch( args, "remove (.*)" ) do
|
||||
remove( item_link )
|
||||
return
|
||||
end
|
||||
|
||||
show_usage()
|
||||
end
|
||||
|
||||
local function loot_item( slot )
|
||||
local index = find_my_candidate_index()
|
||||
|
||||
if index then
|
||||
api().GiveMasterLoot( slot, index )
|
||||
end
|
||||
end
|
||||
|
||||
_G[ "SLASH_RFAL1" ] = "/rfal"
|
||||
_G[ "SlashCmdList" ][ "RFAL" ] = on_command
|
||||
|
||||
---@type AutoLoot
|
||||
return {
|
||||
is_auto_looted = is_auto_looted,
|
||||
on_loot_opened = on_loot_opened,
|
||||
add = add,
|
||||
remove = remove,
|
||||
clear = clear,
|
||||
loot_item = loot_item
|
||||
}
|
||||
end
|
||||
|
||||
m.AutoLoot = M
|
||||
return M
|
||||
40
src/AutoMasterLoot.lua
Normal file
@ -0,0 +1,40 @@
|
||||
RollFor = RollFor or {}
|
||||
local m = RollFor
|
||||
|
||||
if m.AutoMasterLoot then return end
|
||||
|
||||
local M = {}
|
||||
|
||||
---@class AutoMasterLoot
|
||||
---@field on_player_target_changed fun( arg1: string )
|
||||
|
||||
---@param config Config
|
||||
---@param boss_list BossList
|
||||
---@param player_info PlayerInfo
|
||||
function M.new( config, boss_list, player_info )
|
||||
local function on_player_target_changed( arg1 )
|
||||
if not config.auto_master_loot() then return end
|
||||
|
||||
local target_name = m.target_name()
|
||||
if not target_name or m.target_dead() then return end
|
||||
|
||||
local zone_name = m.api.GetRealZoneText()
|
||||
local bosses = boss_list[ zone_name ] or {}
|
||||
local is_a_boss = m.table_contains_value( bosses, target_name )
|
||||
|
||||
-- On Turtle, PLAYER_TARGET_CHANGED gets emitted with some float number as an argument automatically.
|
||||
-- We don't want to respond to these events.
|
||||
local auto_target = tonumber( arg1 )
|
||||
|
||||
if is_a_boss and not auto_target and not m.is_master_loot() and player_info.is_leader() then
|
||||
m.api.SetLootMethod( "master", player_info.get_name() )
|
||||
end
|
||||
end
|
||||
|
||||
return {
|
||||
on_player_target_changed = on_player_target_changed
|
||||
}
|
||||
end
|
||||
|
||||
m.AutoMasterLoot = M
|
||||
return M
|
||||
136
src/AwardedLoot.lua
Normal file
@ -0,0 +1,136 @@
|
||||
RollFor = RollFor or {}
|
||||
local m = RollFor
|
||||
|
||||
if m.AwardedLoot then return end
|
||||
|
||||
local M = m.Module.new( "AwardedLoot" )
|
||||
|
||||
local getn = m.getn
|
||||
|
||||
---@class AwardedLoot
|
||||
---@field award fun( player_name: string, item_id: number, roll_data: RollData?, rolling_strategy: RollingStrategyType?, item_link: ItemLink?, player_class: PlayerClass?, sr_plus: number? )
|
||||
---@field unaward fun( player_name: string, item_id: number )
|
||||
---@field get_winners fun()
|
||||
---@field has_item_been_awarded fun( player_name: string, item_id: number ): boolean
|
||||
---@field has_item_been_awarded_to_any_player fun( item_id: ItemId ): boolean
|
||||
---@field clear fun( force: boolean?)
|
||||
---@field subscribe fun( event_type: string, callback: fun( data: any ) )
|
||||
|
||||
---@param db table
|
||||
---@param group_roster GroupRoster
|
||||
---@param config Config
|
||||
function M.new( db, group_roster, config )
|
||||
db.awarded_items = db.awarded_items or {}
|
||||
local callbacks = {}
|
||||
|
||||
---@param player_name string
|
||||
---@param item_id number
|
||||
---@param roll_data RollData?
|
||||
---@param rolling_strategy RollingStrategyType?
|
||||
---@param item_link ItemLink?
|
||||
---@param player_class PlayerClass?
|
||||
---@param sr_plus number?
|
||||
local function award( player_name, item_id, roll_data, rolling_strategy, item_link, player_class, sr_plus )
|
||||
M.debug.add( "award" )
|
||||
if not player_class then
|
||||
if roll_data and roll_data.player_class then
|
||||
player_class = roll_data.player_class
|
||||
else
|
||||
local player = group_roster.find_player( player_name )
|
||||
player_class = player and player.class
|
||||
end
|
||||
end
|
||||
local quality, _ = m.get_item_quality_and_texture( m.api, item_id )
|
||||
if not item_link then
|
||||
item_link = m.fetch_item_link( item_id, quality )
|
||||
end
|
||||
|
||||
table.insert( db.awarded_items, {
|
||||
player_name = player_name,
|
||||
player_class = player_class,
|
||||
item_id = item_id,
|
||||
item_link = item_link,
|
||||
quality = quality,
|
||||
rolling_strategy = rolling_strategy,
|
||||
roll_type = roll_data and roll_data.roll_type,
|
||||
winning_roll = roll_data and roll_data.roll,
|
||||
sr_plus = sr_plus
|
||||
} )
|
||||
end
|
||||
|
||||
local function subscribe( event_type, callback )
|
||||
callbacks[ event_type ] = callbacks[ event_type ] or {}
|
||||
table.insert( callbacks[ event_type ], callback )
|
||||
end
|
||||
|
||||
local function notify_subscribers( event_type, data )
|
||||
M.debug.add( event_type )
|
||||
|
||||
for _, callback in ipairs( callbacks[ event_type ] or {} ) do
|
||||
callback( data )
|
||||
end
|
||||
end
|
||||
|
||||
---@return table
|
||||
local function get_winners()
|
||||
return db.awarded_items
|
||||
end
|
||||
|
||||
---@param player_name string
|
||||
---@param item_id number
|
||||
---@return boolean
|
||||
local function has_item_been_awarded( player_name, item_id )
|
||||
for _, item in pairs( db.awarded_items ) do
|
||||
if item.player_name == player_name and item.item_id == item_id then return true end
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
---@param item_id ItemId
|
||||
---@return boolean
|
||||
local function has_item_been_awarded_to_any_player( item_id )
|
||||
for _, item in pairs( db.awarded_items ) do
|
||||
if item.item_id == item_id then return true end
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
local function clear( force )
|
||||
M.debug.add( "clear" )
|
||||
if not config.keep_award_data() or force then
|
||||
m.clear_table( db.awarded_items )
|
||||
notify_subscribers( 'award_data_updated' )
|
||||
end
|
||||
end
|
||||
|
||||
---@param player_name string
|
||||
---@param item_id number
|
||||
local function unaward( player_name, item_id )
|
||||
M.debug.add( "unaward" )
|
||||
for i = getn( db.awarded_items ), 1, -1 do
|
||||
local awarded_item = db.awarded_items[ i ]
|
||||
|
||||
if awarded_item.player_name == player_name and awarded_item.item_id == item_id then
|
||||
table.remove( db.awarded_items, i )
|
||||
notify_subscribers( 'award_data_updated' )
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@type AwardedLoot
|
||||
return {
|
||||
award = award,
|
||||
unaward = unaward,
|
||||
get_winners = get_winners,
|
||||
has_item_been_awarded = has_item_been_awarded,
|
||||
has_item_been_awarded_to_any_player = has_item_been_awarded_to_any_player,
|
||||
clear = clear,
|
||||
subscribe = subscribe
|
||||
}
|
||||
end
|
||||
|
||||
m.AwardedLoot = M
|
||||
return M
|
||||
114
src/BossList.lua
Normal file
@ -0,0 +1,114 @@
|
||||
RollFor = RollFor or {}
|
||||
local m = RollFor
|
||||
|
||||
if m.BossList then return end
|
||||
|
||||
---@class BossList
|
||||
---@field zones table<string, string[]>
|
||||
|
||||
local M = {}
|
||||
|
||||
M.zones = {
|
||||
[ "Durotar" ] = {
|
||||
"Elder Mottled Boar"
|
||||
},
|
||||
[ "Tower of Karazhan" ] = {
|
||||
"Master Blacksmith Rolfen",
|
||||
"Brood Queen Araxxna",
|
||||
"Grizikil",
|
||||
"Clawlord Howlfang",
|
||||
"Lord Blackwald II",
|
||||
"Moroes"
|
||||
},
|
||||
[ "Zul'Gurub" ] = {
|
||||
"High Priestess Jeklik",
|
||||
"High Priest Venoxis",
|
||||
"Witherbark Speaker",
|
||||
"High Priestess Mar'li",
|
||||
"Vilebranch Speaker",
|
||||
"Broodlord Mandokir",
|
||||
"Ohgan",
|
||||
"Gri'lek",
|
||||
"Hazza'rah",
|
||||
"Renataki",
|
||||
"Wushoolay",
|
||||
"Gahz'ranka",
|
||||
"High Priest Thekal",
|
||||
"Zealot Zath",
|
||||
"Zealot Lor'Khan",
|
||||
"High Priestess Arlokk",
|
||||
"Jin'do the Hexxer",
|
||||
"Hakkar"
|
||||
},
|
||||
[ "Ruins of Ahn'Qiraj" ] = {
|
||||
"Kurinnaxx",
|
||||
"General Rajaxx",
|
||||
"Moam",
|
||||
"Buru the Gorger",
|
||||
"Ayamiss the Hunter",
|
||||
"Ossirian the Unscarred"
|
||||
},
|
||||
[ "Molten Core" ] = {
|
||||
"Lucifron",
|
||||
"Magmadar",
|
||||
"Gehennas",
|
||||
"Garr",
|
||||
"Shazzrah",
|
||||
"Baron Geddon",
|
||||
"Golemagg the Incinerator",
|
||||
"Sulfuron Harbinger",
|
||||
"Majordomo Executus",
|
||||
"Ragnaros"
|
||||
},
|
||||
[ "Blackwing Lair" ] = {
|
||||
"Razorgore the Untamed",
|
||||
"Vaelastrasz the Corrupt",
|
||||
"Broodlord Lashlayer",
|
||||
"Firemaw",
|
||||
"Ebonroc",
|
||||
"Flamegor",
|
||||
"Chromaggus",
|
||||
"Nefarian"
|
||||
},
|
||||
[ "Onyxia's Lair" ] = {
|
||||
"Onyxia"
|
||||
},
|
||||
[ "Temple of Ahn'Qiraj" ] = {
|
||||
"The Prophet Skeram",
|
||||
"Vem",
|
||||
"Lord Kri",
|
||||
"Princess Yauj",
|
||||
"Battle Guard Sartura",
|
||||
"Fankriss the Unyielding",
|
||||
"Viscidus",
|
||||
"Princess Huhuran",
|
||||
"Emperor Vek'lor",
|
||||
"Emperor Vek'nilash",
|
||||
"Ouro",
|
||||
"C'Thun"
|
||||
},
|
||||
[ "Naxxramas" ] = {
|
||||
"Patchwerk",
|
||||
"Grobbulus",
|
||||
"Gluth",
|
||||
"Thaddius",
|
||||
"Anub'Rekhan",
|
||||
"Grand Widow Faerlina",
|
||||
"Maexxna",
|
||||
"Noth the Plaguebringer",
|
||||
"Heigan the Unclean",
|
||||
"Loatheb",
|
||||
"Instructor Razuvious",
|
||||
"Gothik the Harvester",
|
||||
"Thane Korth'azz",
|
||||
"Lady Blaumeux",
|
||||
"Highlord Mograine",
|
||||
"Sir Zeliek",
|
||||
"Sapphiron",
|
||||
"Kel'Thuzad"
|
||||
}
|
||||
}
|
||||
|
||||
---@type BossList
|
||||
m.BossList = M
|
||||
return M
|
||||
53
src/Chat.lua
Normal file
@ -0,0 +1,53 @@
|
||||
RollFor = RollFor or {}
|
||||
local m = RollFor
|
||||
|
||||
if m.Chat then return end
|
||||
|
||||
local M = {}
|
||||
|
||||
---@class Chat
|
||||
---@field announce fun( text: string, use_raid_Warning: boolean? )
|
||||
---@field info fun( text: string, color_fn: ColorFn?, module_name: string? )
|
||||
|
||||
---@param api ChatApi
|
||||
---@param group_roster GroupRoster
|
||||
---@param player_info PlayerInfo
|
||||
function M.new( api, group_roster, player_info )
|
||||
local function get_group_chat_type()
|
||||
return group_roster.am_i_in_raid() and "RAID" or "PARTY"
|
||||
end
|
||||
|
||||
local function get_roll_announcement_chat_type( use_raid_warning )
|
||||
local chat_type = get_group_chat_type()
|
||||
if not use_raid_warning then return chat_type end
|
||||
|
||||
if chat_type == "RAID" and (player_info.is_leader() or player_info.is_assistant()) then
|
||||
return "RAID_WARNING"
|
||||
else
|
||||
return chat_type
|
||||
end
|
||||
end
|
||||
|
||||
local function announce( text, use_raid_warning )
|
||||
api.SendChatMessage( text, get_roll_announcement_chat_type( use_raid_warning ) )
|
||||
end
|
||||
|
||||
local function info( message, color_fn, module_name )
|
||||
if not message then return end
|
||||
|
||||
local c = color_fn and type( color_fn ) == "function" and color_fn or color_fn and type( color_fn ) == "string" and m.colors[ color_fn ] or m.colors.blue
|
||||
local module_str = module_name and string.format( "%s%s%s", c( " [" ), m.colors.white( module_name ), c( "]" ) ) or ""
|
||||
|
||||
local frame = api.DEFAULT_CHAT_FRAME
|
||||
if frame then frame:AddMessage( string.format( "%s%s: %s", c( "RollFor" ), module_str, message ) ) end
|
||||
end
|
||||
|
||||
---@type Chat
|
||||
return {
|
||||
announce = announce,
|
||||
info = info
|
||||
}
|
||||
end
|
||||
|
||||
m.Chat = M
|
||||
return M
|
||||
22
src/ChatApi.lua
Normal file
@ -0,0 +1,22 @@
|
||||
RollFor = RollFor or {}
|
||||
local m = RollFor
|
||||
|
||||
if m.ChatApi then return end
|
||||
|
||||
local _G = getfenv( 0 )
|
||||
local M = {}
|
||||
|
||||
---@class ChatApi
|
||||
---@field SendChatMessage fun( text: string, chat_type: string )
|
||||
---@field DEFAULT_CHAT_FRAME table
|
||||
|
||||
function M.new()
|
||||
---@type ChatApi
|
||||
return {
|
||||
SendChatMessage = _G.SendChatMessage or function() print( "PRINCESS KENI" ) end,
|
||||
DEFAULT_CHAT_FRAME = _G.DEFAULT_CHAT_FRAME or { AddMessage = function() end }
|
||||
}
|
||||
end
|
||||
|
||||
m.ChatApi = M
|
||||
return M
|
||||
391
src/Client.lua
Normal file
@ -0,0 +1,391 @@
|
||||
RollFor = RollFor or {}
|
||||
local m = RollFor
|
||||
|
||||
if m.Client then return end
|
||||
|
||||
---@class Client
|
||||
---@field on_message fun( data: string, sender: string )
|
||||
|
||||
local M = m.Module.new( "Client" )
|
||||
|
||||
local IU = m.ItemUtils ---@type ItemUtils
|
||||
local RT = m.Types.RollType
|
||||
local RS = m.Types.RollingStrategy
|
||||
local S = m.Types.RollingStatus
|
||||
|
||||
local getn = m.getn
|
||||
|
||||
---@param ace_timer AceTimer
|
||||
---@param player_info PlayerInfo
|
||||
---@param rolling_popup RollingPopup
|
||||
---@param config Config
|
||||
function M.new( ace_timer, player_info, rolling_popup, config )
|
||||
local roll_tracker ---@type RollTracker
|
||||
local roll_threshold = {}
|
||||
local show_rolling = false
|
||||
local player_can_roll = false
|
||||
local chunked_messages = {}
|
||||
local var_names = {
|
||||
i = "item",
|
||||
t = "type",
|
||||
l = "link",
|
||||
tx = "texture",
|
||||
q = "quality",
|
||||
ic = "item_count",
|
||||
s = "seconds",
|
||||
sl = "seconds_left",
|
||||
st = "strategy_type",
|
||||
pn = "player_name",
|
||||
pc = "player_class",
|
||||
rt = "roll_type",
|
||||
r = "roll",
|
||||
n = "name",
|
||||
c = "class",
|
||||
th = "roll_threshold",
|
||||
sr = "softressing_players",
|
||||
ro = "rolls",
|
||||
p = "players",
|
||||
cl = "classes",
|
||||
tm = "tmog"
|
||||
}
|
||||
setmetatable( var_names, { __index = function( _, key ) return key end } );
|
||||
|
||||
local function parse_table( str )
|
||||
local function parse_inner( pos )
|
||||
local tbl = {}
|
||||
local key
|
||||
local i = 1
|
||||
|
||||
while pos <= string.len( str ) do
|
||||
local char = string.sub( str, pos, pos )
|
||||
|
||||
if char == "{" then
|
||||
local newTable, newPos = parse_inner( pos + 1 )
|
||||
if key then
|
||||
tbl[ var_names[ key ] ] = newTable
|
||||
key = nil
|
||||
else
|
||||
tbl[ i ] = newTable
|
||||
i = i + 1
|
||||
end
|
||||
pos = newPos
|
||||
elseif char == "}" then
|
||||
return tbl, pos
|
||||
elseif char == "[" then
|
||||
local _, newPos, extracted_key = string.find( str, '%["*(.-)"*%]', pos )
|
||||
key = tonumber( extracted_key ) and tonumber( extracted_key ) or extracted_key
|
||||
pos = newPos
|
||||
elseif char == "=" then
|
||||
elseif char == "," then
|
||||
key = nil
|
||||
else
|
||||
local _, newPos, raw_value = string.find( str, '([^,%]}]+)', pos )
|
||||
if raw_value then
|
||||
local value = tonumber( raw_value ) and tonumber( raw_value ) or raw_value
|
||||
if key then
|
||||
tbl[ var_names[ key ] ] = value
|
||||
key = nil
|
||||
else
|
||||
tbl[ i ] = value
|
||||
i = i + 1
|
||||
end
|
||||
pos = newPos
|
||||
end
|
||||
end
|
||||
pos = pos + 1
|
||||
end
|
||||
return tbl, pos
|
||||
end
|
||||
|
||||
local final_table = parse_inner( 1 )
|
||||
return final_table[ 1 ]
|
||||
end
|
||||
|
||||
---@param item_id number
|
||||
---@param name string
|
||||
---@param quality number
|
||||
local function item_link( item_id, name, quality )
|
||||
if not item_id then return end
|
||||
|
||||
local id = tonumber( item_id )
|
||||
if not id or id == 0 then return end
|
||||
|
||||
local details = string.format( "item:%d:0:0:0", id )
|
||||
|
||||
return string.format( "%s|H%s|h[%s]|h|r", m.api.ITEM_QUALITY_COLORS[ quality or 0 ].hex, details, name )
|
||||
end
|
||||
|
||||
local function close_rolling()
|
||||
rolling_popup.hide()
|
||||
show_rolling = false
|
||||
end
|
||||
|
||||
---@param strategy_type RollingStrategyType
|
||||
local function roll_buttons( strategy_type )
|
||||
local buttons = {}
|
||||
|
||||
if player_can_roll then
|
||||
if strategy_type == RS.NormalRoll then
|
||||
table.insert( buttons, { type = "MSRoll", callback = function() m.api.RandomRoll( 1, roll_threshold[ RT.MainSpec ] ) end } )
|
||||
table.insert( buttons, { type = "OSRoll", callback = function() m.api.RandomRoll( 1, roll_threshold[ RT.OffSpec ] ) end } )
|
||||
if roll_threshold[ RT.Transmog ] > 0 then
|
||||
table.insert( buttons, { type = "TMOGRoll", callback = function() m.api.RandomRoll( 1, roll_threshold[ RT.Transmog ] ) end } )
|
||||
end
|
||||
elseif strategy_type == RS.SoftResRoll or strategy_type == RS.TieRoll then
|
||||
table.insert( buttons, { type = "Roll", callback = function() m.api.RandomRoll( 1, 100 ) end } )
|
||||
end
|
||||
end
|
||||
|
||||
table.insert( buttons, { type = "Close", callback = function() close_rolling() end } )
|
||||
|
||||
return buttons
|
||||
end
|
||||
|
||||
local function tie_content()
|
||||
if not roll_tracker then
|
||||
M.debug.add( "roll_tracker not initialized" )
|
||||
return
|
||||
end
|
||||
local tracker_data = roll_tracker.get()
|
||||
local first_iteration = tracker_data.iterations[ 1 ]
|
||||
local waiting = tracker_data.status.type == "Waiting" or false
|
||||
|
||||
local tie_iterations = {}
|
||||
for i, iteration in ipairs( tracker_data.iterations ) do
|
||||
if i > 1 then
|
||||
table.insert( tie_iterations,
|
||||
---@type TieIteration
|
||||
{
|
||||
tied_roll = iteration.tied_roll,
|
||||
rolls = iteration.rolls
|
||||
}
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
---@type RollingPopupTieData
|
||||
local rolling_popup_data = {
|
||||
---@type RollingPopupRollData
|
||||
roll_data = {
|
||||
item_link = tracker_data.item.link,
|
||||
item_tooltip_link = IU.get_tooltip_link( tracker_data.item.link ),
|
||||
item_texture = tracker_data.item.texture,
|
||||
item_count = tracker_data.item_count,
|
||||
rolls = first_iteration.rolls,
|
||||
winners = tracker_data.winners,
|
||||
strategy_type = first_iteration.rolling_strategy,
|
||||
buttons = roll_buttons( RS.TieRoll ),
|
||||
waiting_for_rolls = waiting or false,
|
||||
type = "Roll"
|
||||
},
|
||||
tie_iterations = tie_iterations,
|
||||
type = "Tie"
|
||||
}
|
||||
|
||||
rolling_popup:show()
|
||||
rolling_popup:refresh( rolling_popup_data )
|
||||
end
|
||||
|
||||
---@param type string?
|
||||
---@param awarded string?
|
||||
local function roll_content( type, awarded )
|
||||
if not roll_tracker then
|
||||
M.debug.add( "roll_tracker not initialized" )
|
||||
return
|
||||
end
|
||||
local tracker_data, current_iteration = roll_tracker.get()
|
||||
local strategy_type = current_iteration and current_iteration.rolling_strategy
|
||||
local waiting_for_rolls = tracker_data.status.type == "Waiting" or false
|
||||
local seconds = not waiting_for_rolls and tracker_data.status.seconds_left or nil
|
||||
|
||||
if strategy_type == "TieRoll" then
|
||||
tie_content()
|
||||
return
|
||||
end
|
||||
|
||||
---@type RollingPopupRollData
|
||||
local rolling_popup_data = {
|
||||
item_link = tracker_data.item.link,
|
||||
item_tooltip_link = IU.get_tooltip_link( tracker_data.item.link ),
|
||||
item_texture = tracker_data.item.texture,
|
||||
item_count = tracker_data.item_count,
|
||||
seconds_left = seconds,
|
||||
rolls = current_iteration.rolls,
|
||||
winners = tracker_data.winners,
|
||||
awarded = awarded or nil,
|
||||
buttons = roll_buttons( strategy_type ),
|
||||
strategy_type = strategy_type,
|
||||
waiting_for_rolls = waiting_for_rolls,
|
||||
type = type and type or "Roll"
|
||||
}
|
||||
|
||||
rolling_popup:show()
|
||||
rolling_popup:refresh( rolling_popup_data )
|
||||
|
||||
local color = m.get_popup_border_color( tracker_data.item.quality )
|
||||
rolling_popup:border_color( color )
|
||||
end
|
||||
|
||||
local function on_command( command, data )
|
||||
if command == "START_ROLL" then
|
||||
if getn( data.softressing_players ) == 0 then
|
||||
data.strategy_type = RS.NormalRoll
|
||||
player_can_roll = true
|
||||
elseif m.find( player_info.get_name(), data.softressing_players, 'name' ) then
|
||||
player_can_roll = true
|
||||
else
|
||||
player_can_roll = false
|
||||
end
|
||||
|
||||
if data.item.classes and getn( data.item.classes ) > 0 then
|
||||
if m.find( player_info.get_class(), data.item.classes ) then
|
||||
player_can_roll = true
|
||||
else
|
||||
player_can_roll = false
|
||||
end
|
||||
end
|
||||
|
||||
if not player_can_roll and config.client_show_roll_popup() ~= "Always" then
|
||||
show_rolling = false
|
||||
rolling_popup.hide()
|
||||
return
|
||||
end
|
||||
|
||||
show_rolling = true
|
||||
|
||||
roll_threshold.MainSpec = data.roll_threshold.ms
|
||||
roll_threshold.OffSpec = data.roll_threshold.os
|
||||
roll_threshold.Transmog = data.roll_threshold.tmog
|
||||
|
||||
data.item.texture = "Interface\\Icons\\" .. data.item.texture
|
||||
data.item.name = string.gsub( data.item.name, "_", " " )
|
||||
data.item.link = item_link( data.item.id, data.item.name, data.item.quality )
|
||||
|
||||
roll_tracker = m.RollTracker.new( data.item )
|
||||
roll_tracker.start( data.strategy_type, data.item_count, data.seconds, nil, data.softressing_players )
|
||||
|
||||
if getn( data.softressing_players ) == 1 then
|
||||
roll_tracker.finish( {} )
|
||||
roll_tracker.add_winners( data.softressing_players )
|
||||
end
|
||||
|
||||
roll_content()
|
||||
elseif command == "ENABLE_ROLL_POPUP" then
|
||||
config.enable_client_roll_popup()
|
||||
end
|
||||
|
||||
if show_rolling then
|
||||
if command == "ROLL" then
|
||||
roll_tracker.add( data.player_name, data.player_class, data.roll_type, data.roll )
|
||||
if data.player_name == player_info.get_name() then
|
||||
player_can_roll = false
|
||||
end
|
||||
|
||||
roll_content()
|
||||
elseif command == "TICK" then
|
||||
if not roll_tracker then
|
||||
M.debug.add( "roll_tracker not initialized" )
|
||||
return
|
||||
end
|
||||
local tracker_data = roll_tracker.get()
|
||||
if tracker_data.status.type == S.Finished or tracker_data.status.type == S.Canceled then
|
||||
return
|
||||
end
|
||||
|
||||
roll_tracker.tick( data.seconds_left )
|
||||
|
||||
if data.seconds_left == 1 then
|
||||
roll_tracker.waiting_for_rolls()
|
||||
ace_timer.ScheduleTimer( M, function()
|
||||
on_command( "TICK", { seconds_left = 0 } )
|
||||
end, 2 )
|
||||
end
|
||||
|
||||
roll_content()
|
||||
elseif command == "FINISH" then
|
||||
roll_tracker.finish( {} )
|
||||
roll_tracker.add_winners( data )
|
||||
|
||||
player_can_roll = false
|
||||
|
||||
roll_content()
|
||||
elseif command == "CANCEL_ROLL" then
|
||||
roll_tracker.rolling_canceled()
|
||||
player_can_roll = false
|
||||
|
||||
if config.client_auto_hide_popup() then
|
||||
show_rolling = false
|
||||
rolling_popup.hide()
|
||||
else
|
||||
roll_content( "RollingCanceled" )
|
||||
end
|
||||
elseif command == "TIE" then
|
||||
roll_tracker.tie( data.players, data.roll_type, data.roll )
|
||||
tie_content()
|
||||
elseif command == "TIESTART" then
|
||||
roll_tracker.tie_start()
|
||||
|
||||
local tracker_data = roll_tracker.get()
|
||||
local last_iteration = tracker_data.iterations[ getn( tracker_data.iterations ) ]
|
||||
|
||||
if m.find( player_info.get_name(), last_iteration.rolls, 'player_name' ) then
|
||||
player_can_roll = true
|
||||
end
|
||||
|
||||
tie_content()
|
||||
elseif command == "AWARDED" then
|
||||
if data.player_name == player_info.get_name() then
|
||||
m.api.PlaySound( "QUESTCOMPLETED" )
|
||||
end
|
||||
|
||||
if config.client_auto_hide_popup() then
|
||||
show_rolling = false
|
||||
rolling_popup.hide()
|
||||
else
|
||||
roll_content( "Awarded", data )
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function on_message( data_str, sender )
|
||||
local command = string.match( data_str, "^(.-)::" )
|
||||
if sender == player_info.get_name() then return end
|
||||
if config.client_show_roll_popup() == "Off" and command ~= "ENABLE_ROLL_POPUP" then return end
|
||||
|
||||
data_str = string.gsub( data_str, "^.-::", "" )
|
||||
|
||||
if command == "CHUNK" then
|
||||
local chunk_num, total_chunks, chunk_content = string.match( data_str, "^(%d+)::(%d+)::(.+)$" )
|
||||
chunked_messages[ sender ] = chunked_messages[ sender ] or {}
|
||||
|
||||
local sender_chunks = chunked_messages[ sender ]
|
||||
sender_chunks[ tonumber( chunk_num ) ] = chunk_content
|
||||
|
||||
M.debug.add( (string.format( "Got chunk %d of %d", tonumber( chunk_num ), tonumber( total_chunks ) )) )
|
||||
|
||||
if getn( sender_chunks ) == tonumber( total_chunks ) then
|
||||
data_str = table.concat( sender_chunks )
|
||||
command = string.match( data_str, "^(.-)::" )
|
||||
data_str = string.gsub( data_str, "^.-::", "" )
|
||||
|
||||
chunked_messages[ sender ] = nil
|
||||
else
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
local data = data_str ~= "" and parse_table( data_str ) or {}
|
||||
|
||||
M.debug.add( string.format( "Received command %s", command ) )
|
||||
on_command( command, data )
|
||||
end
|
||||
|
||||
---@type Client
|
||||
return {
|
||||
on_message = on_message
|
||||
}
|
||||
end
|
||||
|
||||
m.Client = M
|
||||
return M
|
||||
185
src/ClientBroadcast.lua
Normal file
@ -0,0 +1,185 @@
|
||||
RollFor = RollFor or {}
|
||||
local m = RollFor
|
||||
|
||||
if m.ClientBroadcast then return end
|
||||
|
||||
---@class ClientBroadcast
|
||||
---@field enable_roll_popup fun()
|
||||
|
||||
local M = m.Module.new( "ClientBroadcast" )
|
||||
|
||||
local ADDON_NAME = "RollFor"
|
||||
local RS = m.Types.RollingStrategy
|
||||
local getn = m.getn
|
||||
|
||||
---@param roll_controller RollController
|
||||
---@param softres GroupAwareSoftRes
|
||||
---@param config Config
|
||||
function M.new( roll_controller, softres, config )
|
||||
---@param command string
|
||||
---@param data table?
|
||||
local function broadcast( command, data )
|
||||
local channel = m.api.IsInRaid() and "RAID" or "PARTY"
|
||||
local data_str = data and string.gsub( m.dump( data ), "%s+", "" ) or ""
|
||||
local chunk_size = 220
|
||||
|
||||
local function split_message( message )
|
||||
local chunks = {}
|
||||
local message_length = string.len( message )
|
||||
|
||||
for i = 1, message_length, chunk_size do
|
||||
local chunk = string.sub( message, i, i + chunk_size - 1 )
|
||||
table.insert( chunks, chunk )
|
||||
end
|
||||
|
||||
return chunks
|
||||
end
|
||||
|
||||
if string.len( data_str ) > chunk_size then
|
||||
data_str = command .. "::" .. data_str
|
||||
local chunks = split_message( data_str )
|
||||
for i, chunk in ipairs( chunks ) do
|
||||
M.debug.add( string.format( "Broadcasting %s, chunk %d of %d", command, i, getn( chunks ) ) )
|
||||
m.api.SendAddonMessage( ADDON_NAME, string.format( "ROLL::CHUNK::%d::%d::%s", i, getn( chunks ), chunk ), channel )
|
||||
end
|
||||
else
|
||||
M.debug.add( string.format( "Broadcasting %s", command ) )
|
||||
m.api.SendAddonMessage( ADDON_NAME, string.format( "ROLL::%s::%s", command, data_str ), channel )
|
||||
end
|
||||
end
|
||||
|
||||
---@param data RollControllerStartData
|
||||
local function on_start( data )
|
||||
if data.strategy_type ~= RS.NormalRoll and data.strategy_type ~= RS.SoftResRoll then
|
||||
return
|
||||
end
|
||||
|
||||
local tex = data.item.texture and string.gsub( data.item.texture, "Interface\\Icons\\", "" ) or ""
|
||||
local tmog_rolling_enabled = config.tmog_rolling_enabled() and (data.item.is_boss_loot or not config.auto_tmog_disable() )
|
||||
local softressing_players = softres.get( data.item.id )
|
||||
|
||||
local sr_players = {}
|
||||
for _, player in ipairs( softressing_players ) do
|
||||
table.insert( sr_players, {
|
||||
t = player.type,
|
||||
n = player.name,
|
||||
c = player.class,
|
||||
ro = player.rolls
|
||||
} )
|
||||
end
|
||||
|
||||
broadcast( "START_ROLL", {
|
||||
i = {
|
||||
t = data.item.type,
|
||||
id = data.item.id,
|
||||
n = string.gsub( data.item.name, "%s", "_" ),
|
||||
tx = tex,
|
||||
q = data.item.quality,
|
||||
cl = data.item.classes
|
||||
},
|
||||
ic = data.item_count,
|
||||
s = data.seconds,
|
||||
st = data.strategy_type,
|
||||
sr = sr_players,
|
||||
th = {
|
||||
ms = config.ms_roll_threshold(),
|
||||
os = config.os_roll_threshold(),
|
||||
tm = tmog_rolling_enabled and config.tmog_roll_threshold() or 0
|
||||
}
|
||||
} )
|
||||
end
|
||||
|
||||
---@param event_data RollingFinishedData
|
||||
local function on_finish( event_data )
|
||||
if event_data.roll_tracker_data.iterations[ 1 ].rolling_strategy ~= RS.NormalRoll and event_data.roll_tracker_data.iterations[ 1 ].rolling_strategy ~= RS.SoftResRoll then
|
||||
return
|
||||
end
|
||||
|
||||
local data = {}
|
||||
for _, winner in ipairs( event_data.roll_tracker_data.winners ) do
|
||||
table.insert( data, {
|
||||
n = winner.name,
|
||||
c = winner.class,
|
||||
rt = winner.roll_type,
|
||||
r = winner.winning_roll,
|
||||
} )
|
||||
end
|
||||
|
||||
broadcast( "FINISH", data )
|
||||
end
|
||||
|
||||
---@param data { players: RollingPlayer[], item: Item, item_count: number, roll_type: RollType, roll: number, rerolling: boolean?, top_roll: boolean? }
|
||||
local function on_tie( data )
|
||||
local players = {}
|
||||
for _, player in ipairs( data.players ) do
|
||||
table.insert( players, {
|
||||
n = player.name,
|
||||
c = player.class,
|
||||
t = player.type,
|
||||
ro = player.rolls,
|
||||
} )
|
||||
end
|
||||
|
||||
broadcast( "TIE", {
|
||||
rt = data.roll_type,
|
||||
r = data.roll,
|
||||
p = players
|
||||
} )
|
||||
end
|
||||
|
||||
---@param event_data TieStartData
|
||||
local function on_tie_start( event_data )
|
||||
broadcast( "TIESTART" )
|
||||
end
|
||||
|
||||
---@param data { seconds_left: number }
|
||||
local function on_tick( data )
|
||||
broadcast( "TICK", {
|
||||
sl = data.seconds_left
|
||||
} )
|
||||
end
|
||||
|
||||
local function cancel_rolling()
|
||||
broadcast( "CANCEL_ROLL" )
|
||||
end
|
||||
|
||||
---@param data LootAwardedData
|
||||
local function on_loot_awarded( data )
|
||||
broadcast( "AWARDED", {
|
||||
pn = data.player_name,
|
||||
pc = data.player_class,
|
||||
id = data.item_id
|
||||
} )
|
||||
end
|
||||
|
||||
---@param data { player_name: PlayerName, player_class: PlayerClass, roll_type: RollType, roll: Roll }
|
||||
local function on_roll( data )
|
||||
broadcast( "ROLL", {
|
||||
pn = data.player_name,
|
||||
pc = data.player_class,
|
||||
rt = data.roll_type,
|
||||
r = data.roll
|
||||
} )
|
||||
end
|
||||
|
||||
local function enable_roll_popup()
|
||||
m.pretty_print( "Broadcasting ENABLE_ROLL_POPUP" )
|
||||
end
|
||||
|
||||
roll_controller.subscribe( "cancel_rolling", cancel_rolling )
|
||||
roll_controller.subscribe( "start", on_start )
|
||||
roll_controller.subscribe( "finish", on_finish )
|
||||
roll_controller.subscribe( "there_was_a_tie", on_tie )
|
||||
roll_controller.subscribe( "tie_start", on_tie_start )
|
||||
roll_controller.subscribe( "tick", on_tick )
|
||||
roll_controller.subscribe( "loot_awarded", on_loot_awarded )
|
||||
roll_controller.subscribe( "roll", on_roll )
|
||||
|
||||
---@type ClientBroadcast
|
||||
return {
|
||||
enable_roll_popup = enable_roll_popup
|
||||
}
|
||||
end
|
||||
|
||||
m.ClientBroadcast = M
|
||||
return M
|
||||
518
src/Config.lua
Normal file
@ -0,0 +1,518 @@
|
||||
RollFor = RollFor or {}
|
||||
local m = RollFor
|
||||
|
||||
if m.Config then return end
|
||||
|
||||
local info = m.pretty_print
|
||||
local print_header = m.print_header
|
||||
local hl = m.colors.hl
|
||||
local blue = m.colors.blue
|
||||
local grey = m.colors.grey
|
||||
local RollType = m.Types.RollType
|
||||
|
||||
local M = {}
|
||||
|
||||
---@alias Expansion
|
||||
---| "Vanilla"
|
||||
---| "BCC"
|
||||
|
||||
---@alias Config table
|
||||
|
||||
---@param db table
|
||||
---@param event_bus EventBus
|
||||
function M.new( db, event_bus )
|
||||
local callbacks = {}
|
||||
local toggles = {
|
||||
[ "auto_loot" ] = { cmd = "auto-loot", display = "Auto-loot", help = "toggle auto-loot" },
|
||||
[ "superwow_auto_loot_coins" ] = { cmd = "superwow-auto-loot-coins", display = "Auto-loot coins with SuperWoW", help = "toggle auto-loot coins with SuperWoW" },
|
||||
[ "auto_loot_messages" ] = { cmd = "auto-loot-messages", display = "Auto-loot messages", help = "toggle auto-loot messages" },
|
||||
[ "auto_loot_announce" ] = { cmd = "auto-loot-announce", display = "Announce auto-looted items", help = "toggle announcements of auto-loot items" },
|
||||
[ "auto_class_announce" ] = { cmd = "auto-class-announce", display = "Announce class restriction on items", help = "toggle announcing of class restriction on items" },
|
||||
[ "auto_tmog" ] = { cmd = "auto-tmog", display = "Disable transmog roll on trash loot", help ="toggle transmog roll on trash loot" },
|
||||
[ "show_ml_warning" ] = { cmd = "ml", display = "Master loot warning", help = "toggle master loot warning" },
|
||||
[ "auto_raid_roll" ] = { cmd = "auto-rr", display = "Auto raid-roll", help = "toggle auto raid-roll" },
|
||||
[ "auto_group_loot" ] = { cmd = "auto-group-loot", display = "Auto group loot", help = "toggle auto group loot" },
|
||||
[ "auto_master_loot" ] = { cmd = "auto-master-loot", display = "Auto master loot", help = "toggle auto master loot" },
|
||||
[ "rolling_popup_lock" ] = { cmd = "rolling-popup-lock", display = "Rolling popup lock", help = "toggle rolling popup lock" },
|
||||
[ "raid_roll_again" ] = { cmd = "raid-roll-again", display = string.format( "%s button", hl( "Raid roll again" ) ), help = string.format( "toggle %s button", hl( "Raid roll again" ) ) },
|
||||
[ "loot_frame_cursor" ] = { cmd = "loot-frame-cursor", display = "Display loot frame at cursor position", help = "toggle displaying loot frame at cursor position"},
|
||||
[ "classic_look" ] = { cmd = "classic-look", display = "Classic look", help = "toggle classic look", requires_reload = true },
|
||||
[ "client_auto_hide_popup" ] = { cmd = "auto-hide", display = "Hide popup when rolling is complete", help = "toggle hiding of roll popup", client = true },
|
||||
}
|
||||
|
||||
local function notify_subscribers( event, value )
|
||||
if not callbacks[ event ] then return end
|
||||
|
||||
for _, callback in ipairs( callbacks[ event ] ) do
|
||||
callback( value )
|
||||
end
|
||||
end
|
||||
|
||||
local function init()
|
||||
if not db.ms_roll_threshold then db.ms_roll_threshold = 100 end
|
||||
if not db.os_roll_threshold then db.os_roll_threshold = 99 end
|
||||
if not db.tmog_roll_threshold then db.tmog_roll_threshold = 98 end
|
||||
if not db.superwow_auto_loot_coins then db.superwow_auto_loot_coins = true end
|
||||
if db.tmog_rolling_enabled == nil then db.tmog_rolling_enabled = true end
|
||||
if db.auto_tmog_disable == nil then db.auto_tmog_disable = false end
|
||||
if db.show_ml_warning == nil then db.show_ml_warning = false end
|
||||
if db.default_rolling_time_seconds == nil then db.default_rolling_time_seconds = 8 end
|
||||
if db.master_loot_frame_rows == nil then db.master_loot_frame_rows = 5 end
|
||||
if db.auto_master_loot == nil then db.auto_master_loot = true end
|
||||
if db.auto_loot == nil then db.auto_loot = true end
|
||||
if db.auto_loot_announce == nil then db.auto_loot_announce = true end
|
||||
if db.loot_frame_cursor == nil then db.loot_frame_cursor = false end
|
||||
if db.client_show_roll_popup == nil then db.client_show_roll_popup = "Off" end
|
||||
if db.client_auto_hide_popup == nil then db.client_auto_hide_popup = false end
|
||||
if not db.award_filter then
|
||||
db.award_filter = {
|
||||
item_quality = { Uncommon = 1, Rare = 1, Epic = 1, Legendary = 1 },
|
||||
winning_roll = {},
|
||||
roll_type = { MainSpec = 1, OffSpec = 1, Transmog = 1, SoftRes = 1, RR = 1 }
|
||||
}
|
||||
end
|
||||
m.classic = db.classic_look
|
||||
end
|
||||
|
||||
local function print( toggle_key )
|
||||
local toggle = toggles[ toggle_key ]
|
||||
if not toggle then return end
|
||||
|
||||
local value = toggle.negate and not db[ toggle_key ] or db[ toggle_key ]
|
||||
info( string.format( "%s is %s.", toggles[ toggle_key ].display, value and m.msg.enabled or m.msg.disabled ) )
|
||||
notify_subscribers( toggle_key, value )
|
||||
end
|
||||
|
||||
local function toggle( toggle_key )
|
||||
return function()
|
||||
if db[ toggle_key ] then
|
||||
db[ toggle_key ] = false
|
||||
else
|
||||
db[ toggle_key ] = true
|
||||
end
|
||||
|
||||
print( toggle_key )
|
||||
|
||||
if toggles[ toggle_key ].requires_reload then
|
||||
event_bus.notify( "config_change_requires_ui_reload", { key = toggle_key } )
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function reset_rolling_popup()
|
||||
info( "Rolling popup position has been reset." )
|
||||
notify_subscribers( "reset_rolling_popup" )
|
||||
end
|
||||
|
||||
local function reset_loot_frame()
|
||||
info( "Loot frame position has been reset." )
|
||||
notify_subscribers( "reset_loot_frame" )
|
||||
end
|
||||
|
||||
local function print_roll_thresholds()
|
||||
local ms_threshold = db.ms_roll_threshold
|
||||
local os_threshold = db.os_roll_threshold
|
||||
local tmog_threshold = db.tmog_roll_threshold
|
||||
local tmog_info = string.format( ", %s %s", hl( "TMOG" ), tmog_threshold ) or ""
|
||||
|
||||
info( string.format( "Roll thresholds: %s %s, %s %s%s", hl( "MS" ), ms_threshold, hl( "OS" ), os_threshold, tmog_info ) )
|
||||
end
|
||||
|
||||
local function print_transmog_rolling_setting( show_threshold )
|
||||
if m.bcc then return end
|
||||
local tmog_rolling_enabled = db.tmog_rolling_enabled
|
||||
local threshold = show_threshold and tmog_rolling_enabled and string.format( " (%s)", hl( db.tmog_roll_threshold ) ) or ""
|
||||
info( string.format( "Transmog rolling is %s%s.", tmog_rolling_enabled and m.msg.enabled or m.msg.disabled, threshold ) )
|
||||
end
|
||||
|
||||
local function print_default_rolling_time()
|
||||
info( string.format( "Default rolling time: %s seconds", hl( db.default_rolling_time_seconds ) ) )
|
||||
end
|
||||
|
||||
local function print_master_loot_frame_rows()
|
||||
info( string.format( "Master loot frame rows: %s", hl( db.master_loot_frame_rows ) ) )
|
||||
end
|
||||
|
||||
local function print_settings()
|
||||
print_header( "RollFor Configuration" )
|
||||
print_default_rolling_time()
|
||||
print_master_loot_frame_rows()
|
||||
print_roll_thresholds()
|
||||
print_transmog_rolling_setting()
|
||||
|
||||
for toggle_key, setting in pairs( toggles ) do
|
||||
if not setting.hidden and not setting.client then
|
||||
print( toggle_key )
|
||||
end
|
||||
end
|
||||
|
||||
m.print( string.format( "For more info, type: %s", hl( "/rf config help" ) ) )
|
||||
end
|
||||
|
||||
local function print_client_roll()
|
||||
info( string.format( "Show roll popup: %s", hl( db.client_show_roll_popup ) ) )
|
||||
end
|
||||
|
||||
local function print_client_settings()
|
||||
print_header( "RollFor Client Configuration" )
|
||||
print_client_roll()
|
||||
|
||||
for toggle_key, setting in pairs( toggles ) do
|
||||
if not setting.hidden and setting.client then
|
||||
print( toggle_key )
|
||||
end
|
||||
end
|
||||
|
||||
m.print( string.format( "For more info, type: %s", hl( "/rf config client help" ) ) )
|
||||
end
|
||||
|
||||
local function print_client_help()
|
||||
local v = function( name ) return string.format( "%s%s%s", hl( "<" ), grey( name ), hl( ">" ) ) end
|
||||
local function rfc( cmd ) return string.format( "%s%s", blue( "/rf config client" ), cmd and string.format( " %s", hl( cmd ) ) or "" ) end
|
||||
|
||||
print_header( "RollFor Client Configuration Help" )
|
||||
m.print( string.format( "%s - show configuration", rfc() ) )
|
||||
m.print( string.format( "%s %s - set when to show roll popup", rfc( "show-roll" ), v( "Off|Always|Eligible" ) ) )
|
||||
|
||||
for _, setting in pairs( toggles ) do
|
||||
if not setting.hidden and setting.client then
|
||||
m.print( string.format( "%s - %s", rfc( setting.cmd ), setting.help ) )
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function configure_default_rolling_time( args )
|
||||
if args == "config default-rolling-time" then
|
||||
print_default_rolling_time()
|
||||
return
|
||||
end
|
||||
|
||||
for value in string.gmatch( args, "config default%-rolling%-time (%d+)" ) do
|
||||
local v = tonumber( value )
|
||||
|
||||
if v < 4 then
|
||||
info( string.format( "Default rolling time must be at least %s seconds.", hl( "4" ) ) )
|
||||
return
|
||||
end
|
||||
|
||||
if v > 15 then
|
||||
info( string.format( "Default rolling time must be at most %s seconds.", hl( "15" ) ) )
|
||||
return
|
||||
end
|
||||
|
||||
db.default_rolling_time_seconds = v
|
||||
print_default_rolling_time()
|
||||
return
|
||||
end
|
||||
|
||||
info( string.format( "Usage: %s <seconds>", hl( "/rf config default-rolling-time" ) ) )
|
||||
end
|
||||
|
||||
local function configure_master_loot_frame_rows( args )
|
||||
if args == "config master-loot-frame-rows" then
|
||||
print_master_loot_frame_rows()
|
||||
return
|
||||
end
|
||||
|
||||
for value in string.gmatch( args, "config master%-loot%-frame%-rows (%d+)" ) do
|
||||
local v = tonumber( value )
|
||||
|
||||
if v < 5 then
|
||||
info( string.format( "Master loot frame rows must be at least %s.", hl( "5" ) ) )
|
||||
return
|
||||
end
|
||||
|
||||
db.master_loot_frame_rows = v
|
||||
print_master_loot_frame_rows()
|
||||
notify_subscribers( "master_loot_frame_rows" )
|
||||
return
|
||||
end
|
||||
|
||||
info( string.format( "Usage: %s <rows>", hl( "/rf config master-loot-frame-rows" ) ) )
|
||||
end
|
||||
|
||||
local function configure_ms_threshold( args )
|
||||
for value in string.gmatch( args, "config ms (%d+)" ) do
|
||||
db.ms_roll_threshold = tonumber( value )
|
||||
print_roll_thresholds()
|
||||
return
|
||||
end
|
||||
|
||||
info( string.format( "Usage: %s <threshold>", hl( "/rf config ms" ) ) )
|
||||
end
|
||||
|
||||
local function configure_os_threshold( args )
|
||||
for value in string.gmatch( args, "config os (%d+)" ) do
|
||||
db.os_roll_threshold = tonumber( value )
|
||||
print_roll_thresholds()
|
||||
return
|
||||
end
|
||||
|
||||
info( string.format( "Usage: %s <threshold>", hl( "/rf config os" ) ) )
|
||||
end
|
||||
|
||||
local function configure_tmog_threshold( args )
|
||||
if args == "config tmog" then
|
||||
db.tmog_rolling_enabled = not db.tmog_rolling_enabled
|
||||
print_transmog_rolling_setting( true )
|
||||
return
|
||||
end
|
||||
|
||||
for value in string.gmatch( args, "config tmog (%d+)" ) do
|
||||
db.tmog_roll_threshold = tonumber( value )
|
||||
print_roll_thresholds()
|
||||
return
|
||||
end
|
||||
|
||||
info( string.format( "Usage: %s <threshold>", hl( "/rf config tmog" ) ) )
|
||||
end
|
||||
|
||||
local function configure_client_roll( args )
|
||||
for value in string.gmatch( args, "config client show%-roll (%a+)" ) do
|
||||
if ({ off = true, always = true, eligible = true })[ string.lower( value ) ] then
|
||||
db.client_show_roll_popup = string.upper( string.sub( value, 1, 1 ) ) .. string.lower( string.sub( value, 2 ) )
|
||||
print_client_roll()
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
print_client_roll()
|
||||
info( string.format( "Usage: %s <Off|Always|Eligible>", hl( "/rf config client show-roll" ) ) )
|
||||
end
|
||||
|
||||
local function print_help()
|
||||
local v = function( name ) return string.format( "%s%s%s", hl( "<" ), grey( name ), hl( ">" ) ) end
|
||||
local function rfc( cmd ) return string.format( "%s%s", blue( "/rf config" ), cmd and string.format( " %s", hl( cmd ) ) or "" ) end
|
||||
|
||||
print_header( "RollFor Configuration Help" )
|
||||
m.print( string.format( "%s - show configuration", rfc() ) )
|
||||
m.print( string.format( "%s - toggle minimap icon", rfc( "minimap" ) ) )
|
||||
m.print( string.format( "%s - lock/unlock minimap icon", rfc( "minimap lock" ) ) )
|
||||
m.print( string.format( "%s - show default rolling time", rfc( "default-rolling-time" ) ) )
|
||||
m.print( string.format( "%s - show master loot frame rows", rfc( "master-loot-frame-rows" ) ) )
|
||||
m.print( string.format( "%s %s - set default rolling time", rfc( "default-rolling-time" ), v( "seconds" ) ) )
|
||||
m.print( string.format( "%s - show MS rolling threshold ", rfc( "ms" ) ) )
|
||||
m.print( string.format( "%s %s - set MS rolling threshold ", rfc( "ms" ), v( "threshold" ) ) )
|
||||
m.print( string.format( "%s - show OS rolling threshold ", rfc( "os" ) ) )
|
||||
m.print( string.format( "%s %s - set OS rolling threshold ", rfc( "os" ), v( "threshold" ) ) )
|
||||
|
||||
if m.vanilla then
|
||||
m.print( string.format( "%s - toggle TMOG rolling", rfc( "tmog" ) ) )
|
||||
m.print( string.format( "%s %s - set TMOG rolling threshold", rfc( "tmog" ), v( "threshold" ) ) )
|
||||
end
|
||||
|
||||
for _, setting in pairs( toggles ) do
|
||||
if not setting.hidden and not setting.client then
|
||||
m.print( string.format( "%s - %s", rfc( setting.cmd ), setting.help ) )
|
||||
end
|
||||
end
|
||||
|
||||
m.print( string.format( "%s - reset rolling popup position", rfc( "reset-rolling-popup" ) ) )
|
||||
m.print( string.format( "%s - reset loot frame position", rfc( "reset-loot-frame" ) ) )
|
||||
m.print( string.format( "%s - show client configuration", rfc( "client" ) ) )
|
||||
end
|
||||
|
||||
local function lock_minimap_button()
|
||||
db.minimap_button_locked = true
|
||||
info( string.format( "Minimap button is %s.", m.msg.locked ) )
|
||||
notify_subscribers( "minimap_button_locked", true )
|
||||
end
|
||||
|
||||
local function unlock_minimap_button()
|
||||
db.minimap_button_locked = false
|
||||
info( string.format( "Minimap button is %s.", m.msg.unlocked ) )
|
||||
notify_subscribers( "minimap_button_locked", false )
|
||||
end
|
||||
|
||||
local function hide_minimap_button()
|
||||
db.minimap_button_hidden = true
|
||||
notify_subscribers( "minimap_button_hidden", true )
|
||||
end
|
||||
|
||||
local function show_minimap_button()
|
||||
db.minimap_button_hidden = false
|
||||
notify_subscribers( "minimap_button_hidden", false )
|
||||
end
|
||||
|
||||
local function on_command( args )
|
||||
if args == "config" then
|
||||
print_settings()
|
||||
return
|
||||
end
|
||||
|
||||
if args == "config help" then
|
||||
print_help()
|
||||
return
|
||||
end
|
||||
|
||||
if args == "config client" then
|
||||
print_client_settings()
|
||||
return
|
||||
end
|
||||
|
||||
if args == "config client help" then
|
||||
print_client_help()
|
||||
return
|
||||
end
|
||||
|
||||
if string.find( args, "^config client show%-roll" ) then
|
||||
configure_client_roll( args )
|
||||
return
|
||||
end
|
||||
|
||||
for toggle_key, setting in pairs( toggles ) do
|
||||
if args == string.format( "config client %s", setting.cmd ) and setting.client then
|
||||
toggle( toggle_key )()
|
||||
return
|
||||
elseif args == string.format( "config %s", setting.cmd ) and not setting.client then
|
||||
toggle( toggle_key )()
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
if args == "config reset-rolling-popup" then
|
||||
reset_rolling_popup()
|
||||
return
|
||||
end
|
||||
|
||||
if args == "config reset-loot-frame" then
|
||||
reset_loot_frame()
|
||||
return
|
||||
end
|
||||
|
||||
if args == "config minimap" then
|
||||
if db.minimap_button_hidden then
|
||||
show_minimap_button()
|
||||
else
|
||||
hide_minimap_button()
|
||||
end
|
||||
|
||||
return
|
||||
end
|
||||
|
||||
if args == "config minimap lock" then
|
||||
if db.minimap_button_locked then
|
||||
unlock_minimap_button()
|
||||
else
|
||||
lock_minimap_button()
|
||||
end
|
||||
|
||||
return
|
||||
end
|
||||
|
||||
if string.find( args, "^config ms" ) then
|
||||
configure_ms_threshold( args )
|
||||
return
|
||||
end
|
||||
|
||||
if string.find( args, "^config os" ) then
|
||||
configure_os_threshold( args )
|
||||
return
|
||||
end
|
||||
|
||||
if string.find( args, "^config tmog" ) then
|
||||
configure_tmog_threshold( args )
|
||||
return
|
||||
end
|
||||
|
||||
if string.find( args, "^config default%-rolling%-time" ) then
|
||||
configure_default_rolling_time( args )
|
||||
return
|
||||
end
|
||||
|
||||
if string.find( args, "^config master%-loot%-frame%-rows" ) then
|
||||
configure_master_loot_frame_rows( args )
|
||||
return
|
||||
end
|
||||
|
||||
print_help()
|
||||
end
|
||||
|
||||
local function subscribe( event, callback )
|
||||
callbacks[ event ] = callbacks[ event ] or {}
|
||||
table.insert( callbacks[ event ], callback )
|
||||
end
|
||||
|
||||
local function roll_threshold( roll_type )
|
||||
local threshold = (roll_type == RollType.MainSpec or roll_type == RollType.SoftRes) and db.ms_roll_threshold or
|
||||
roll_type == RollType.OffSpec and db.os_roll_threshold or
|
||||
db.tmog_roll_threshold
|
||||
local threshold_str = string.format( "/roll%s", threshold == 100 and "" or string.format( " %s", threshold ) )
|
||||
|
||||
return {
|
||||
value = threshold,
|
||||
str = threshold_str
|
||||
}
|
||||
end
|
||||
|
||||
local function enable_client_roll_popup()
|
||||
if db.client_show_roll_popup == "Off" then
|
||||
db.client_show_roll_popup = "Eligible"
|
||||
info( string.format( "Show roll popup has been set to %s by lootmaster.", hl( db.client_show_roll_popup ) ) )
|
||||
end
|
||||
end
|
||||
|
||||
init()
|
||||
|
||||
---@param setting_key string
|
||||
---@param expansion Expansion?
|
||||
---@param not_available_value any?
|
||||
local function get( setting_key, expansion, not_available_value )
|
||||
if expansion and (expansion == "Vanilla" and m.bcc or expansion == "BCC" and m.vanilla) then
|
||||
return function()
|
||||
return not_available_value
|
||||
end
|
||||
end
|
||||
|
||||
return function()
|
||||
return db[ setting_key ]
|
||||
end
|
||||
end
|
||||
|
||||
local function printfn( setting_key ) return function() print( setting_key ) end end
|
||||
|
||||
local config = {
|
||||
configure_ms_threshold = configure_ms_threshold,
|
||||
configure_os_threshold = configure_os_threshold,
|
||||
configure_tmog_threshold = configure_tmog_threshold,
|
||||
hide_minimap_button = hide_minimap_button,
|
||||
lock_minimap_button = lock_minimap_button,
|
||||
minimap_button_hidden = get( "minimap_button_hidden" ),
|
||||
minimap_button_locked = get( "minimap_button_locked" ),
|
||||
ms_roll_threshold = get( "ms_roll_threshold" ),
|
||||
on_command = on_command,
|
||||
os_roll_threshold = get( "os_roll_threshold" ),
|
||||
print = print,
|
||||
print_help = print_help,
|
||||
print_raid_roll_settings = printfn( "auto_raid_roll" ),
|
||||
reset_rolling_popup = reset_rolling_popup,
|
||||
reset_loot_frame = reset_loot_frame,
|
||||
roll_threshold = roll_threshold,
|
||||
show_minimap_button = show_minimap_button,
|
||||
subscribe = subscribe,
|
||||
tmog_roll_threshold = get( "tmog_roll_threshold" ),
|
||||
tmog_rolling_enabled = get( "tmog_rolling_enabled", "Vanilla", false ),
|
||||
unlock_minimap_button = unlock_minimap_button,
|
||||
default_rolling_time_seconds = get( "default_rolling_time_seconds" ),
|
||||
master_loot_frame_rows = get( "master_loot_frame_rows" ),
|
||||
configure_master_loot_frame_rows = configure_master_loot_frame_rows,
|
||||
client_show_roll_popup = get( "client_show_roll_popup" ),
|
||||
client_auto_hide_popup = get( "client_auto_hide_popup" ),
|
||||
enable_client_roll_popup = enable_client_roll_popup,
|
||||
auto_tmog_disable = get( "auto_tmog_disable" ),
|
||||
auto_class_announce = get( "auto_class_announce" ),
|
||||
award_filter = get( "award_filter" ),
|
||||
keep_award_data = get( "keep_award_data" ),
|
||||
loot_frame_cursor = get( "loot_frame_cursor" )
|
||||
}
|
||||
|
||||
for toggle_key, _ in pairs( toggles ) do
|
||||
config[ toggle_key ] = get( toggle_key )
|
||||
config[ "toggle_" .. toggle_key ] = toggle( toggle_key )
|
||||
end
|
||||
|
||||
return config
|
||||
end
|
||||
|
||||
m.Config = M
|
||||
return M
|
||||
124
src/ConfirmPopup.lua
Normal file
@ -0,0 +1,124 @@
|
||||
RollFor = RollFor or {}
|
||||
local m = RollFor
|
||||
|
||||
if m.ConfirmPopup then return end
|
||||
|
||||
local M = {}
|
||||
local getn = m.getn
|
||||
|
||||
local button_defaults = {
|
||||
width = 80,
|
||||
height = 24,
|
||||
scale = 0.76
|
||||
}
|
||||
|
||||
---@class ConfirmPopup
|
||||
---@field show fun( text: table, ufunc: function )
|
||||
---@field hide fun()
|
||||
---@field is_visible fun(): boolean
|
||||
|
||||
---@param popup_builder PopupBuilder
|
||||
---@param config Config
|
||||
function M.new( popup_builder, config )
|
||||
local popup
|
||||
local classic = config.classic_look()
|
||||
local top_padding = classic and 18 or 14
|
||||
|
||||
local function create_popup()
|
||||
local frame = popup_builder
|
||||
:name( "RollForConfirmPopup" )
|
||||
:point( { point = "CENTER", relative_point = "CENTER", x = 0, y = 100 } )
|
||||
:sound()
|
||||
:esc()
|
||||
:gui_elements( m.GuiElements )
|
||||
:backdrop_color( 0, 0, 0, 0.8 )
|
||||
:border_color( 0.125, 0.624, 0.976, 0.3 )
|
||||
:strata( "FULLSCREEN_DIALOG" )
|
||||
:movable()
|
||||
:build()
|
||||
|
||||
return frame
|
||||
end
|
||||
|
||||
---@param text table
|
||||
---@param on_yes function
|
||||
---@param on_no function
|
||||
local function make_content( text, on_yes, on_no )
|
||||
local content = {}
|
||||
|
||||
for _, line in pairs( text ) do
|
||||
table.insert( content, { type = "text", value = line } )
|
||||
end
|
||||
|
||||
table.insert( content, { type = "button", label = "Yes", width = 80, on_click = on_yes } )
|
||||
table.insert( content, { type = "button", label = "No", width = 80, on_click = on_no } )
|
||||
|
||||
return content
|
||||
end
|
||||
|
||||
local function show( text, ufunc )
|
||||
if not popup then popup = create_popup() end
|
||||
popup:clear()
|
||||
|
||||
local function on_yes()
|
||||
ufunc( true )
|
||||
popup:Hide()
|
||||
end
|
||||
|
||||
local function on_no()
|
||||
ufunc( false )
|
||||
popup:Hide()
|
||||
end
|
||||
|
||||
for _, v in ipairs( make_content( text, on_yes, on_no ) ) do
|
||||
popup.add_line( v.type, function( type, frame, lines )
|
||||
if type == "text" then
|
||||
frame:SetText( v.value )
|
||||
elseif type == "button" then
|
||||
frame:SetWidth( v.width or button_defaults.width )
|
||||
frame:SetHeight( v.height or button_defaults.height )
|
||||
frame:SetText( v.label or "" )
|
||||
frame:SetScale( v.scale or button_defaults.scale )
|
||||
frame:SetScript( "OnClick", v.on_click or function() end )
|
||||
frame:SetFrameLevel( popup:GetFrameLevel() + 1 )
|
||||
end
|
||||
|
||||
if type ~= "button" then
|
||||
local count = getn( lines )
|
||||
|
||||
if count == 0 then
|
||||
local y = -top_padding - (v.padding or 0)
|
||||
frame:ClearAllPoints()
|
||||
frame:SetPoint( "TOP", popup, "TOP", 0, y )
|
||||
else
|
||||
local line_anchor = lines[ count ].frame
|
||||
frame:ClearAllPoints()
|
||||
frame:SetPoint( "TOP", line_anchor, "BOTTOM", 0, v.padding and -v.padding or 0 )
|
||||
end
|
||||
end
|
||||
end, v.padding )
|
||||
end
|
||||
|
||||
popup:Show()
|
||||
end
|
||||
|
||||
local function hide()
|
||||
if popup then
|
||||
popup:Hide()
|
||||
end
|
||||
end
|
||||
|
||||
local function is_visible()
|
||||
return popup and popup:IsVisible() or false
|
||||
end
|
||||
|
||||
---@type ConfirmPopup
|
||||
return {
|
||||
show = show,
|
||||
hide = hide,
|
||||
is_visible = is_visible
|
||||
}
|
||||
end
|
||||
|
||||
m.ConfirmPopup = M
|
||||
return M
|
||||
28
src/Db.lua
Normal file
@ -0,0 +1,28 @@
|
||||
RollFor = RollFor or {}
|
||||
local m = RollFor
|
||||
|
||||
if m.Db then return end
|
||||
|
||||
local M = {}
|
||||
|
||||
function M.new( db )
|
||||
return function( module_name )
|
||||
db[ module_name ] = db[ module_name ] or {}
|
||||
|
||||
local proxy = {}
|
||||
local mt = {
|
||||
__index = function( _, key )
|
||||
return db[ module_name ][ key ]
|
||||
end,
|
||||
__newindex = function( _, key, value )
|
||||
db[ module_name ][ key ] = value
|
||||
end
|
||||
}
|
||||
|
||||
setmetatable( proxy, mt )
|
||||
return proxy
|
||||
end
|
||||
end
|
||||
|
||||
m.Db = M
|
||||
return M
|
||||
257
src/DebugBuffer.lua
Normal file
@ -0,0 +1,257 @@
|
||||
RollFor = RollFor or {}
|
||||
local m = RollFor
|
||||
|
||||
if m.DebugBuffer then return end
|
||||
|
||||
local M = {}
|
||||
|
||||
M.modules = {}
|
||||
|
||||
local getn = m.getn
|
||||
|
||||
-- Keeping a global index so we can later reconstruct the order of messages when printing only a subset of modules.
|
||||
local message_index = 0
|
||||
|
||||
local pp = m.pretty_print
|
||||
|
||||
---@class DebugMessage
|
||||
---@field index number
|
||||
---@field text string
|
||||
|
||||
local colors = {
|
||||
"ff8080", -- soft pink
|
||||
"8aeb9f", -- mint green
|
||||
"80b8ff", -- light blue
|
||||
"ffb380", -- peach
|
||||
"b880ff", -- lavender
|
||||
"80ffb3", -- seafoam green
|
||||
"ffa680", -- light orange
|
||||
"80ffb3", -- pale turquoise
|
||||
"b880ff", -- light purple
|
||||
"ffb3b3", -- baby pink
|
||||
"80e2ff", -- sky blue
|
||||
"ffb380", -- cream
|
||||
"b3ff80", -- lime cream
|
||||
"ff80b3", -- rose pink
|
||||
"80ffa6" -- pale mint
|
||||
}
|
||||
|
||||
---@param text string
|
||||
local function make_message( text )
|
||||
message_index = message_index + 1
|
||||
|
||||
---@type DebugMessage
|
||||
return { index = message_index, text = text }
|
||||
end
|
||||
|
||||
---@class DebugBuffer
|
||||
---@field add fun( message: string )
|
||||
---@field show fun()
|
||||
---@field enable fun( console: boolean )
|
||||
---@field disable fun()
|
||||
---@field toggle fun()
|
||||
function M.new( module_name, max_size )
|
||||
local messages = {} ---@type DebugMessage[]
|
||||
local head = 0
|
||||
local count = 0
|
||||
local debug_enabled = false
|
||||
local console_enabled = false
|
||||
|
||||
local function add( message )
|
||||
head = head + 1
|
||||
|
||||
if head > max_size then
|
||||
head = 1
|
||||
end
|
||||
|
||||
messages[ head ] = make_message( message )
|
||||
|
||||
if count < max_size then
|
||||
count = count + 1
|
||||
end
|
||||
|
||||
if debug_enabled then
|
||||
if console_enabled then
|
||||
print( string.format( "[%s]: %s", module_name, message ) )
|
||||
else
|
||||
pp( message, m.colors.grey, module_name )
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function get()
|
||||
local result = {}
|
||||
|
||||
local start = head - count + 1
|
||||
|
||||
if start < 1 then
|
||||
start = start + max_size
|
||||
end
|
||||
|
||||
for i = 1, count do
|
||||
local idx = start + i - 1
|
||||
|
||||
if idx > max_size then
|
||||
idx = idx - max_size
|
||||
end
|
||||
|
||||
table.insert( result, messages[ idx ] )
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
local function show()
|
||||
for _, message in ipairs( get() ) do
|
||||
pp( message.text, m.colors.grey, message.module_name )
|
||||
end
|
||||
end
|
||||
|
||||
local function print_debug_status()
|
||||
if console_enabled then
|
||||
print( string.format( "\n[%s]: Debug %s.", module_name, debug_enabled and "enabled" or "disabled" ) )
|
||||
else
|
||||
pp( string.format( "Debug %s.", debug_enabled and m.msg.enabled or m.msg.disabled, m.colors.grey, module_name ), m.colors.grey, module_name )
|
||||
end
|
||||
end
|
||||
|
||||
local function enable( console )
|
||||
debug_enabled = true
|
||||
console_enabled = console
|
||||
print_debug_status()
|
||||
end
|
||||
|
||||
local function disable()
|
||||
debug_enabled = false
|
||||
console_enabled = false
|
||||
print_debug_status()
|
||||
end
|
||||
|
||||
local function toggle()
|
||||
debug_enabled = not debug_enabled
|
||||
print_debug_status()
|
||||
end
|
||||
|
||||
local result = {
|
||||
add = add,
|
||||
get = get,
|
||||
show = show,
|
||||
enable = enable,
|
||||
disable = disable,
|
||||
toggle = toggle,
|
||||
is_enabled = function() return debug_enabled end
|
||||
}
|
||||
|
||||
M.modules[ module_name ] = result
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
M.disable_all = function()
|
||||
for _, module in pairs( M.modules ) do
|
||||
module.disable()
|
||||
end
|
||||
end
|
||||
|
||||
local function get_colors( module_names )
|
||||
local result = {}
|
||||
local color_count = getn( colors )
|
||||
local color_index = math.random( color_count ) - 1
|
||||
|
||||
for _, module_name in ipairs( module_names ) do
|
||||
if color_index == color_count then
|
||||
color_index = 0
|
||||
else
|
||||
color_index = color_index + 1
|
||||
end
|
||||
|
||||
result[ module_name ] = colors[ color_index ]
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
---@param module_names string[]
|
||||
local function show( module_names )
|
||||
local c = get_colors( module_names )
|
||||
local result = {}
|
||||
|
||||
for _, module_name in ipairs( module_names ) do
|
||||
local mod = m[ module_name ]
|
||||
|
||||
if mod and mod.debug then
|
||||
local dbg = mod.debug
|
||||
local messages = dbg.get() ---@type DebugMessage[]
|
||||
|
||||
for _, message in ipairs( messages ) do
|
||||
table.insert( result, { module_name = module_name, index = message.index, text = message.text } )
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
table.sort( result, function( a, b ) return a.index < b.index end )
|
||||
|
||||
if getn( result ) == 0 then
|
||||
pp( "No debug messages.", m.colors.grey )
|
||||
return
|
||||
end
|
||||
|
||||
local msg = function( tag )
|
||||
return string.format(
|
||||
"Debug messages %s (%s %s):",
|
||||
tag,
|
||||
m.colors.blue( "RollFor" ),
|
||||
m.colors.hl( "v" .. m.get_addon_version().str )
|
||||
)
|
||||
end
|
||||
|
||||
pp( msg( "start" ) )
|
||||
|
||||
for _, message in ipairs( result ) do
|
||||
pp( message.text, m.colors.grey, m.colorize( c[ message.module_name ], message.module_name ) )
|
||||
end
|
||||
|
||||
pp( msg( "end" ) )
|
||||
end
|
||||
|
||||
function M.on_command( args )
|
||||
---@param modules string
|
||||
local function parse_module_names( modules )
|
||||
local result = {}
|
||||
for module_name in string.gmatch( modules, "%S+" ) do
|
||||
table.insert( result, module_name )
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
if args == "debug show" then
|
||||
show( { "RollController", "LootController" } )
|
||||
return
|
||||
end
|
||||
|
||||
for command, modules in string.gmatch( args, "debug (.-) (.*)" ) do
|
||||
local module_names = parse_module_names( modules )
|
||||
|
||||
if command == "show" then
|
||||
show( module_names )
|
||||
return
|
||||
end
|
||||
|
||||
for _, module_name in ipairs( module_names ) do
|
||||
local mod = m[ module_name ]
|
||||
|
||||
if mod and mod.debug then
|
||||
local dbg = mod.debug
|
||||
local f = dbg[ command ]
|
||||
|
||||
if f then f() end
|
||||
end
|
||||
end
|
||||
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
m.DebugBuffer = M
|
||||
return M
|
||||
54
src/DroppedLoot.lua
Normal file
@ -0,0 +1,54 @@
|
||||
RollFor = RollFor or {}
|
||||
local m = RollFor
|
||||
|
||||
if m.DroppedLoot then return end
|
||||
|
||||
local M = {}
|
||||
local getn = m.getn
|
||||
|
||||
---@class DroppedLoot
|
||||
---@field get_dropped_item_id fun( item_name: string ): number
|
||||
---@field get_dropped_item_name fun( item_id: number ): string
|
||||
---@field add fun( item_id: number, item_name: string )
|
||||
---@field clear fun()
|
||||
|
||||
---@param db table
|
||||
---@return DroppedLoot
|
||||
function M.new( db )
|
||||
db.dropped_items = db.dropped_items or {}
|
||||
|
||||
local function get_dropped_item_id( item_name )
|
||||
for _, item in pairs( db.dropped_items ) do
|
||||
if item.name == item_name then return item.id end
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
local function get_dropped_item_name( item_id )
|
||||
for _, item in pairs( db.dropped_items ) do
|
||||
if item.id == item_id then return item.name end
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
local function add( item_id, item_name )
|
||||
table.insert( db.dropped_items, { id = item_id, name = item_name } )
|
||||
end
|
||||
|
||||
local function clear()
|
||||
if getn( db.dropped_items ) == 0 then return end
|
||||
m.clear_table( db.dropped_items )
|
||||
end
|
||||
|
||||
return {
|
||||
get_dropped_item_id = get_dropped_item_id,
|
||||
get_dropped_item_name = get_dropped_item_name,
|
||||
add = add,
|
||||
clear = clear
|
||||
}
|
||||
end
|
||||
|
||||
m.DroppedLoot = M
|
||||
return M
|
||||
358
src/DroppedLootAnnounce.lua
Normal file
@ -0,0 +1,358 @@
|
||||
RollFor = RollFor or {}
|
||||
local m = RollFor
|
||||
|
||||
if m.DroppedLootAnnounce then return end
|
||||
|
||||
local M = {}
|
||||
|
||||
local getn = m.getn
|
||||
local announce_limit = 6
|
||||
local filter = m.filter
|
||||
local BindType = m.ItemUtils.BindType
|
||||
local ItemQuality = m.Types.ItemQuality
|
||||
|
||||
local function distinct( items )
|
||||
local result = {}
|
||||
|
||||
local function exists( item )
|
||||
for i = 1, getn( result ) do
|
||||
if result[ i ].id == item.id then return true end
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
for i = 1, getn( items ) do
|
||||
local item = items[ i ]
|
||||
|
||||
if not exists( item ) then
|
||||
table.insert( result, item )
|
||||
end
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
local function commify( t, f )
|
||||
local result = ""
|
||||
|
||||
if getn( t ) == 0 then
|
||||
return result
|
||||
end
|
||||
|
||||
if getn( t ) == 1 then
|
||||
return (f and f( t[ 1 ] ) or t[ 1 ])
|
||||
end
|
||||
|
||||
for i = 1, getn( t ) - 1 do
|
||||
if result ~= "" then
|
||||
result = result .. ", "
|
||||
end
|
||||
|
||||
result = result .. (f and f( t[ i ] ) or t[ i ])
|
||||
end
|
||||
|
||||
result = result .. " and " .. (f and f( t[ getn( t ) ] ) or t[ getn( t ) ])
|
||||
return result
|
||||
end
|
||||
|
||||
local function stringify( announcements )
|
||||
local result = {}
|
||||
|
||||
local function print_player( show_rolls )
|
||||
return function( player )
|
||||
local rolls = show_rolls and player.rolls > 1 and string.format( " [%s rolls]", player.rolls ) or ""
|
||||
local sr_plus = player.sr_plus and string.format( " (+%s)", player.sr_plus ) or ""
|
||||
return string.format( "%s%s%s", player.name, rolls, sr_plus )
|
||||
end
|
||||
end
|
||||
|
||||
for i = 1, getn( announcements ) do
|
||||
local entry = announcements[ i ]
|
||||
|
||||
if entry.is_hardressed then
|
||||
table.insert( result, {
|
||||
text = string.format( "%s. %s (HR)", i, entry.item_link ),
|
||||
entry = entry
|
||||
} )
|
||||
elseif entry.softres_count > 0 then
|
||||
local count = entry.how_many_dropped
|
||||
local prefix = count == 1 and "" or string.format( "%sx", count )
|
||||
local f = print_player( entry.softres_count > 1 )
|
||||
table.insert( result, {
|
||||
text = string.format( "%s. %s%s (SR by %s)", i, prefix, entry.item_link, commify( entry.softressers, f ) ),
|
||||
entry = entry
|
||||
} )
|
||||
else
|
||||
local count = entry.how_many_dropped
|
||||
local prefix = count == 1 and "" or string.format( "%sx", count )
|
||||
table.insert( result, {
|
||||
text = string.format( "%s. %s%s", i, prefix, entry.item_link ),
|
||||
entry = entry
|
||||
} )
|
||||
end
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
local function sort( announcements )
|
||||
local hr = {}
|
||||
local sr = {}
|
||||
local free_roll = {}
|
||||
|
||||
for _, v in pairs( announcements ) do
|
||||
if v.is_hardressed then
|
||||
table.insert( hr, v )
|
||||
elseif v.softres_count > 0 then
|
||||
table.insert( sr, v )
|
||||
else
|
||||
table.insert( free_roll, v )
|
||||
end
|
||||
end
|
||||
|
||||
table.sort( free_roll, function( left, right )
|
||||
if left.item_quality ~= right.item_quality then
|
||||
return left.item_quality > right.item_quality
|
||||
else
|
||||
return left.item_name < right.item_name
|
||||
end
|
||||
end )
|
||||
|
||||
table.sort( sr, function( left, right )
|
||||
if left.softres_count == 1 and left.softres_count == right.softres_count then
|
||||
if left.item_quality == right.item_quality then
|
||||
return left.softressers[ 1 ].name < right.softressers[ 1 ].name
|
||||
else
|
||||
return left.item_quality > right.item_quality
|
||||
end
|
||||
elseif left.softres_count ~= right.softres_count then
|
||||
return left.softres_count < right.softres_count
|
||||
else
|
||||
if left.item_quality == right.item_quality then
|
||||
return left.item_name < right.item_name
|
||||
else
|
||||
return left.item_quality > right.item_quality
|
||||
end
|
||||
end
|
||||
end )
|
||||
|
||||
return m.merge( {}, hr, sr, free_roll )
|
||||
end
|
||||
|
||||
function M.create_item_announcements( summary )
|
||||
local result = {}
|
||||
|
||||
for i = 1, getn( summary ) do
|
||||
local entry = summary[ i ]
|
||||
local softres_count = getn( entry.softressers )
|
||||
|
||||
if entry.is_hardressed then
|
||||
table.insert( result, {
|
||||
item_link = entry.item.link,
|
||||
item_name = entry.item.name,
|
||||
item_quality = entry.item.quality,
|
||||
is_hardressed = true,
|
||||
softres_count = 0
|
||||
} )
|
||||
elseif softres_count == 0 then
|
||||
table.insert( result, {
|
||||
item_link = entry.item.link,
|
||||
item_name = entry.item.name,
|
||||
item_quality = entry.item.quality,
|
||||
softres_count = 0,
|
||||
how_many_dropped = entry.how_many_dropped
|
||||
} )
|
||||
elseif entry.how_many_dropped == softres_count then
|
||||
for j = 1, softres_count do
|
||||
table.insert( result, {
|
||||
item_link = entry.item.link,
|
||||
item_name = entry.item.name,
|
||||
item_quality = entry.item.quality,
|
||||
softres_count = 1,
|
||||
how_many_dropped = 1,
|
||||
softressers = { entry.softressers[ j ] }
|
||||
} )
|
||||
end
|
||||
else
|
||||
table.insert( result, {
|
||||
item_link = entry.item.link,
|
||||
item_name = entry.item.name,
|
||||
item_quality = entry.item.quality,
|
||||
softres_count = getn( entry.softressers ),
|
||||
how_many_dropped = entry.how_many_dropped,
|
||||
softressers = entry.softressers
|
||||
} )
|
||||
end
|
||||
end
|
||||
|
||||
return stringify( sort( result ) )
|
||||
end
|
||||
|
||||
---@param loot_list LootList
|
||||
---@param softres GroupAwareSoftRes
|
||||
---@param auto_loot AutoLoot
|
||||
---@param config Config
|
||||
function M.process_dropped_items( loot_list, softres, auto_loot, config )
|
||||
local source_guid = loot_list.get_source_guid()
|
||||
local threshold = m.api.GetLootThreshold()
|
||||
local items = filter( loot_list.get_items(), function( item )
|
||||
if auto_loot.is_auto_looted( item ) and not config.auto_loot_announce() or item.id == 29434 then return false end
|
||||
|
||||
local quality = item.quality or 0
|
||||
|
||||
if item.bind == BindType.BindOnPickup and quality >= ItemQuality.Uncommon then
|
||||
return true
|
||||
end
|
||||
|
||||
return quality >= threshold
|
||||
end )
|
||||
|
||||
local summary = M.create_item_summary( items, softres )
|
||||
return source_guid or "unknown", items, M.create_item_announcements( summary )
|
||||
end
|
||||
|
||||
-- SoftResLootListDecorator?
|
||||
function M.create_item_summary( items, softres )
|
||||
local result = {}
|
||||
local distinct_items = distinct( items )
|
||||
|
||||
local function count_items( item_id )
|
||||
---@diagnostic disable-next-line: redefined-local
|
||||
local result = 0
|
||||
|
||||
for i = 1, getn( items ) do
|
||||
if items[ i ].id == item_id then result = result + 1 end
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
for i = 1, getn( distinct_items ) do
|
||||
local item = distinct_items[ i ]
|
||||
local item_count = count_items( item.id )
|
||||
local softressers = softres.get( item.id )
|
||||
local softres_count = getn( softressers )
|
||||
table.sort( softressers, function( l, r ) return l.name < r.name end )
|
||||
local hardressed = softres.is_item_hardressed( item.id )
|
||||
|
||||
|
||||
if hardressed then
|
||||
table.insert( result, { item = item, how_many_dropped = 1, softressers = {}, is_hardressed = hardressed } )
|
||||
item_count = item_count - 1
|
||||
end
|
||||
|
||||
if item_count > 0 then
|
||||
if item_count > softres_count and softres_count > 0 then
|
||||
table.insert( result, { item = item, how_many_dropped = softres_count, softressers = softressers, is_hardressed = false } )
|
||||
table.insert( result, { item = item, how_many_dropped = item_count - softres_count, softressers = {}, is_hardressed = false } )
|
||||
else
|
||||
table.insert( result, { item = item, how_many_dropped = item_count, softressers = softressers, is_hardressed = false } )
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
local function should_announce( i, item_count, announcement )
|
||||
if i < announce_limit then return true end
|
||||
if i == announce_limit and item_count == announce_limit then return true end
|
||||
|
||||
if announcement.entry.softres_count and announcement.entry.softres_count > 0 then
|
||||
return true
|
||||
end
|
||||
|
||||
if i == item_count then return true end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
---@class DroppedLootAnnounce
|
||||
---@field on_loot_opened fun()
|
||||
---@field reset fun()
|
||||
|
||||
---@param loot_list LootList
|
||||
---@param chat Chat
|
||||
---@param dropped_loot DroppedLoot
|
||||
---@param softres GroupAwareSoftRes
|
||||
---@param winner_tracker WinnerTracker
|
||||
---@param player_info PlayerInfo
|
||||
---@param config Config
|
||||
function M.new( loot_list, chat, dropped_loot, softres, winner_tracker, player_info, auto_loot, config )
|
||||
local announcing = false
|
||||
local announced_source_ids = {}
|
||||
|
||||
local function on_loot_opened()
|
||||
if not player_info.is_master_looter() or announcing then
|
||||
-- Wtf is this?
|
||||
if m.real_api then
|
||||
m.api = m.real_api
|
||||
m.real_api = nil
|
||||
end
|
||||
|
||||
return
|
||||
end
|
||||
|
||||
local source_guid, items, announcements = M.process_dropped_items( loot_list, softres, auto_loot, config )
|
||||
local was_announced = announced_source_ids[ source_guid ]
|
||||
if was_announced then return end
|
||||
|
||||
announcing = true
|
||||
local item_count = getn( items )
|
||||
|
||||
local target = m.api.UnitName( "target" )
|
||||
local target_msg = target and not m.api.UnitIsFriend( "player", "target" ) and string.format( "%s dropped ", target ) or ""
|
||||
|
||||
if item_count > 0 then
|
||||
chat.announce(
|
||||
string.format( "%s%s item%s%s", target_msg, item_count, item_count > 1 and "s" or "", target_msg == "" and " dropped:" or ":" ) )
|
||||
|
||||
for i = 1, item_count do
|
||||
local item = items[ i ]
|
||||
dropped_loot.add( item.id, item.name )
|
||||
end
|
||||
|
||||
local trimmed = false
|
||||
|
||||
for i, announcement in ipairs( announcements ) do
|
||||
if not trimmed and should_announce( i, item_count, announcement ) then
|
||||
chat.announce( announcement.text )
|
||||
|
||||
if announcement.entry.softres_count == 1 then
|
||||
winner_tracker.track( announcement.entry.softressers[ 1 ].name, announcement.entry.item_link, m.Types.RollType.SoftRes,
|
||||
nil, m.Types.RollingStrategy.SoftResRoll )
|
||||
end
|
||||
elseif not trimmed then
|
||||
if i > (announce_limit - 1) and item_count > announce_limit then
|
||||
local count = item_count - i + 1
|
||||
chat.announce( string.format( "and %s more item%s...", count, count > 1 and "s" or "" ) )
|
||||
trimmed = true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
announced_source_ids[ source_guid ] = true
|
||||
end
|
||||
|
||||
announcing = false
|
||||
end
|
||||
|
||||
local function reset()
|
||||
local former_size = m.count_elements( announced_source_ids )
|
||||
announced_source_ids = {}
|
||||
|
||||
if former_size > 0 then
|
||||
m.pretty_print( "Loot announcement has been reset." )
|
||||
end
|
||||
end
|
||||
|
||||
---@type DroppedLootAnnounce
|
||||
return {
|
||||
on_loot_opened = on_loot_opened,
|
||||
reset = reset
|
||||
}
|
||||
end
|
||||
|
||||
m.DroppedLootAnnounce = M
|
||||
return M
|
||||
38
src/EventBus.lua
Normal file
@ -0,0 +1,38 @@
|
||||
RollFor = RollFor or {}
|
||||
local m = RollFor
|
||||
|
||||
if m.EventBus then return end
|
||||
|
||||
local M = {}
|
||||
|
||||
---@class EventBus
|
||||
---@field subscribe fun( event_name: string, callback: function )
|
||||
---@field notify fun( event_name: string, data: any? )
|
||||
|
||||
function M.new()
|
||||
local subscribers = {}
|
||||
|
||||
---@param event_name string
|
||||
---@param callback fun()
|
||||
local function subscribe( event_name, callback )
|
||||
subscribers[ event_name ] = subscribers[ event_name ] or {}
|
||||
table.insert( subscribers[ event_name ], callback )
|
||||
end
|
||||
|
||||
---@param event_name string
|
||||
---@param data any
|
||||
local function notify( event_name, data )
|
||||
for _, callback in ipairs( subscribers[ event_name ] or {} ) do
|
||||
callback( data )
|
||||
end
|
||||
end
|
||||
|
||||
---@type EventBus
|
||||
return {
|
||||
subscribe = subscribe,
|
||||
notify = notify
|
||||
}
|
||||
end
|
||||
|
||||
m.EventBus = M
|
||||
return M
|
||||
59
src/EventFrame.lua
Normal file
@ -0,0 +1,59 @@
|
||||
RollFor = RollFor or {}
|
||||
local m = RollFor
|
||||
|
||||
if m.EventFrame then return end
|
||||
|
||||
local M = m.Module.new( "EventFrame" )
|
||||
---@diagnostic disable-next-line: undefined-field
|
||||
local lua50 = table.setn and true or false
|
||||
|
||||
function M.new( api )
|
||||
local frame = api.CreateFrame( "Frame" )
|
||||
local event_handlers = {}
|
||||
|
||||
local function subscribe( event_name, callback )
|
||||
if not event_name then error( "event_name was nil." ) end
|
||||
if not event_handlers[ event_name ] then
|
||||
frame:RegisterEvent( event_name )
|
||||
end
|
||||
|
||||
event_handlers[ event_name ] = event_handlers[ event_name ] or {}
|
||||
table.insert( event_handlers[ event_name ], callback )
|
||||
end
|
||||
|
||||
local function event_handler( event, arg1, arg2, arg3, arg4, arg5 )
|
||||
for event_name, handlers in pairs( event_handlers ) do
|
||||
if event_name == event then
|
||||
M.debug.add( event_name )
|
||||
|
||||
for _, handle_event in ipairs( handlers ) do
|
||||
handle_event( arg1, arg2, arg3, arg4, arg5 )
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
frame:SetScript( "OnEvent", function( _, _event, _arg1, _arg2, _arg3, _arg4, _arg5 )
|
||||
---@diagnostic disable-next-line: undefined-global
|
||||
local event = lua50 and event or _event
|
||||
---@diagnostic disable-next-line: undefined-global
|
||||
local arg1 = lua50 and arg1 or _arg1
|
||||
---@diagnostic disable-next-line: undefined-global
|
||||
local arg2 = lua50 and arg2 or _arg2
|
||||
---@diagnostic disable-next-line: undefined-global
|
||||
local arg3 = lua50 and arg3 or _arg3
|
||||
---@diagnostic disable-next-line: undefined-global
|
||||
local arg4 = lua50 and arg4 or _arg4
|
||||
---@diagnostic disable-next-line: undefined-global
|
||||
local arg5 = lua50 and arg5 or _arg5
|
||||
---@diagnostic disable-next-line: undefined-global
|
||||
event_handler( event, arg1, arg2, arg3, arg4, arg5 )
|
||||
end )
|
||||
|
||||
return {
|
||||
subscribe = subscribe
|
||||
}
|
||||
end
|
||||
|
||||
m.EventFrame = M
|
||||
return M
|
||||
121
src/EventHandler.lua
Normal file
@ -0,0 +1,121 @@
|
||||
RollFor = RollFor or {}
|
||||
local m = RollFor
|
||||
|
||||
if m.EventHandler then return end
|
||||
|
||||
local M = {}
|
||||
|
||||
---@diagnostic disable-next-line: undefined-field
|
||||
local lua50 = table.setn and true or false
|
||||
|
||||
function M.handle_events( main )
|
||||
local init = false
|
||||
local function event_handler( _, _event, _arg1, _arg2, _arg3, _arg4, _arg5 )
|
||||
---@diagnostic disable-next-line: undefined-global
|
||||
local event = lua50 and event or _event
|
||||
---@diagnostic disable-next-line: undefined-global
|
||||
local arg1 = lua50 and arg1 or _arg1
|
||||
---@diagnostic disable-next-line: undefined-global
|
||||
local arg2 = lua50 and arg2 or _arg2
|
||||
---@diagnostic disable-next-line: undefined-global
|
||||
local arg3 = lua50 and arg3 or _arg3
|
||||
---@diagnostic disable-next-line: undefined-global
|
||||
local arg4 = lua50 and arg4 or _arg4
|
||||
---@diagnostic disable-next-line: undefined-global
|
||||
local arg5 = lua50 and arg5 or _arg5
|
||||
|
||||
if event == "PLAYER_LOGIN" then
|
||||
main.on_player_login()
|
||||
init = true
|
||||
return
|
||||
end
|
||||
|
||||
if not init then return end
|
||||
|
||||
if event == "GROUP_ROSTER_UPDATE" or event == "PARTY_MEMBERS_CHANGED" then
|
||||
main.version_broadcast.on_group_changed()
|
||||
main.on_group_changed()
|
||||
main.new_group_event.on_group_changed()
|
||||
elseif event == "CHAT_MSG_PARTY" then
|
||||
main.roll_for_ad.on_chat_msg_party( arg1, arg2 )
|
||||
-- main.on_chat_msg_system( arg1, arg2, arg3, arg4, arg5 )
|
||||
elseif event == "CHAT_MSG_RAID" then
|
||||
main.roll_for_ad.on_chat_msg_raid( arg1, arg2 )
|
||||
elseif event == "CHAT_MSG_RAID_LEADER" then
|
||||
main.roll_for_ad.on_chat_msg_raid( arg1, arg2 )
|
||||
elseif event == "CHAT_MSG_WHISPER_INFORM" then
|
||||
main.roll_for_ad.on_chat_msg_whisper_inform( arg1, arg2 )
|
||||
elseif event == "CHAT_MSG_SYSTEM" then
|
||||
main.on_chat_msg_system( arg1, arg2, arg3, arg4, arg5 )
|
||||
elseif event == "CHAT_MSG_ADDON" then
|
||||
main.on_chat_msg_addon( arg1, arg2, arg3, arg4 )
|
||||
elseif event == "TRADE_SHOW" then
|
||||
main.trade_tracker.on_trade_show()
|
||||
elseif event == "TRADE_PLAYER_ITEM_CHANGED" then
|
||||
main.trade_tracker.on_trade_player_item_changed( arg1, arg2, arg3, arg4, arg5 )
|
||||
elseif event == "TRADE_TARGET_ITEM_CHANGED" then
|
||||
main.trade_tracker.on_trade_target_item_changed( arg1, arg2, arg3, arg4, arg5 )
|
||||
elseif event == "TRADE_CLOSED" then
|
||||
main.trade_tracker.on_trade_closed()
|
||||
elseif event == "TRADE_ACCEPT_UPDATE" then
|
||||
main.trade_tracker.on_trade_accept_update( arg1, arg2, arg3, arg4, arg5 )
|
||||
elseif event == "TRADE_REQUEST_CANCEL" then
|
||||
main.trade_tracker.on_trade_request_cancel()
|
||||
elseif event == "PLAYER_TARGET_CHANGED" then
|
||||
main.master_loot_warning.on_player_target_changed()
|
||||
main.auto_master_loot.on_player_target_changed( arg1 )
|
||||
elseif event == "UI_ERROR_MESSAGE" then
|
||||
local message = m.vanilla and arg1 or arg2
|
||||
|
||||
if message == "That player's inventory is full" then
|
||||
main.master_loot.on_recipient_inventory_full()
|
||||
main.roll_controller.player_has_full_bags()
|
||||
elseif message == "You are too far away to loot that corpse." then
|
||||
main.master_loot.on_player_is_too_far()
|
||||
elseif message == "Player has too many of that item already" then
|
||||
main.roll_controller.player_already_has_unique_item()
|
||||
elseif message == "Player not found" then
|
||||
main.roll_controller.player_not_found()
|
||||
elseif message == "Can't assign item to that player" then
|
||||
main.roll_controller.cant_assign_item_to_that_player()
|
||||
else
|
||||
main.master_loot.on_unknown_error_message( message )
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local frame = m.api.CreateFrame( "FRAME", "RollForFrame" )
|
||||
|
||||
frame:RegisterEvent( "PLAYER_LOGIN" )
|
||||
frame:RegisterEvent( "GROUP_JOINED" )
|
||||
frame:RegisterEvent( "GROUP_LEFT" )
|
||||
frame:RegisterEvent( "GROUP_FORMED" )
|
||||
frame:RegisterEvent( "CHAT_MSG_SYSTEM" )
|
||||
frame:RegisterEvent( "CHAT_MSG_ADDON" )
|
||||
frame:RegisterEvent( "CHAT_MSG_PARTY" )
|
||||
frame:RegisterEvent( "CHAT_MSG_RAID" )
|
||||
frame:RegisterEvent( "CHAT_MSG_RAID_LEADER" )
|
||||
frame:RegisterEvent( "CHAT_MSG_WHISPER_INFORM" )
|
||||
frame:RegisterEvent( "OPEN_MASTER_LOOT_LIST" )
|
||||
frame:RegisterEvent( "TRADE_SHOW" )
|
||||
frame:RegisterEvent( "TRADE_PLAYER_ITEM_CHANGED" )
|
||||
frame:RegisterEvent( "TRADE_TARGET_ITEM_CHANGED" )
|
||||
frame:RegisterEvent( "TRADE_CLOSED" )
|
||||
frame:RegisterEvent( "TRADE_ACCEPT_UPDATE" )
|
||||
frame:RegisterEvent( "TRADE_REQUEST_CANCEL" )
|
||||
frame:RegisterEvent( "UI_ERROR_MESSAGE" )
|
||||
frame:RegisterEvent( "PLAYER_TARGET_CHANGED" )
|
||||
frame:RegisterEvent( "ZONE_CHANGED" )
|
||||
frame:RegisterEvent( "ZONE_CHANGED_NEW_AREA" )
|
||||
|
||||
if m.vanilla then
|
||||
frame:RegisterEvent( "PARTY_MEMBERS_CHANGED" )
|
||||
else
|
||||
frame:RegisterEvent( "GROUP_ROSTER_UPDATE" )
|
||||
end
|
||||
|
||||
frame:SetScript( "OnEvent", event_handler )
|
||||
end
|
||||
|
||||
m.EventHandler = M
|
||||
return M
|
||||
626
src/FrameBuilder.lua
Normal file
@ -0,0 +1,626 @@
|
||||
RollFor = RollFor or {}
|
||||
local m = RollFor
|
||||
|
||||
if m.FrameBuilder then return end
|
||||
|
||||
local M = {}
|
||||
|
||||
local getn = m.getn
|
||||
|
||||
M.interface = {
|
||||
}
|
||||
|
||||
---@alias FrameStyle
|
||||
---| "Modern"
|
||||
---| "Classic"
|
||||
---| "None"
|
||||
|
||||
---@class Vector2
|
||||
---@field x number
|
||||
---@field y number
|
||||
|
||||
---@class FontString
|
||||
---@field SetFont fun( self: FontString, font: string, size: number, flags: string )
|
||||
---@field SetText fun( self: FontString, text: string )
|
||||
---@field SetTextColor fun( self: FontString, r: number, g: number, b: number, a: number )
|
||||
---@field SetJustifyH fun( self: FontString, justify_h: string )
|
||||
---@field SetWidth fun( self: FontString, width: number )
|
||||
---@field SetHeight fun( self: FontString, height: number )
|
||||
---@field SetPoint fun( self: FontString, point: string, relative_frame: Frame|Texture, relative_point: string, x: number, y: number )
|
||||
|
||||
---@class Texture
|
||||
---@field SetTexture fun( self: Texture, texture: string )
|
||||
---@field SetWidth fun( self: Texture, width: number )
|
||||
---@field SetHeight fun( self: Texture, height: number )
|
||||
---@field SetPoint fun( self: Texture, point: string, relative_frame: Frame|Texture, relative_point: string, x: number, y: number )
|
||||
---@field SetAllPoints fun( self: Texture, frame: Frame )
|
||||
---@field SetTexCoord fun( self: Texture, x1: number, x2: number, y1: number, y2: number )
|
||||
---@field SetBlendMode fun( self: Texture, blend_mode: string )
|
||||
|
||||
---@class Frame
|
||||
---@field add_line fun( line_type: string, modify_fn: function, padding: number ): table
|
||||
---@field clear fun()
|
||||
---@field border_color fun( _, r: number, g: number, b: number, a: number )
|
||||
---@field backdrop_color fun( _, r: number, g: number, b: number, a: number )
|
||||
---@field lock fun()
|
||||
---@field unlock fun()
|
||||
---@field position fun( self: Frame, point: table )
|
||||
---@field get_anchor_center fun(): Vector2
|
||||
---@field get_anchor_point fun(): Point
|
||||
---@field get_point fun(): Point
|
||||
---@field anchor fun( frame: Frame, point: string, relative_point: string, x: number, y: number )
|
||||
---@field Show fun( self )
|
||||
---@field Hide fun( self )
|
||||
---@field SetWidth fun( frame: Frame, width: number )
|
||||
---@field SetHeight fun( frame: Frame, height: number )
|
||||
---@field SetPoint fun( frame: Frame, point: string, relative_frame: Frame|string, relative_point: string, x: number, y: number )
|
||||
---@field GetScale fun(): number
|
||||
---@field GetWidth fun(): number
|
||||
---@field GetHeight fun(): number
|
||||
---@field StartSizing fun( self: Frame, resizePoint: string?, alwaysStartFromMouse: boolean? )
|
||||
---@field StopMovingOrSizing fun()
|
||||
---@field ClearAllPoints fun()
|
||||
---@field SetAllPoints fun( frame: Frame, relativeTo?: Frame|string, doResize?: boolean )
|
||||
---@field SetScript fun( frame: Frame, scriptTypeName: string, script: function|nil )
|
||||
---@field IsVisible fun( self ): boolean
|
||||
---@field GetName fun(): string?
|
||||
---@field SetFrameStrata fun( self: Frame, strata: string )
|
||||
---@field CreateTexture fun( self: Frame, name: string?, layer: string ): Texture
|
||||
---@field SetNormalTexture fun( self: Frame, texture: string )
|
||||
---@field SetPushedTexture fun( self: Frame, texture: string )
|
||||
---@field CreateFontString fun( self: Frame, name: string?, layer: string, font: string ): FontString
|
||||
---@field SetScript fun( self: Frame, event: string, callback: function )
|
||||
---@field GetTop fun(): number
|
||||
---@field GetBottom fun(): number
|
||||
---@field GetLeft fun(): number
|
||||
---@field GetRight fun(): number
|
||||
---@field SetPushedTexture fun( self: Frame, texture: string )
|
||||
---@field SetHighlightTexture fun( self: Frame, texture: string )
|
||||
|
||||
---@alias Anchor table
|
||||
|
||||
---@alias AnchorPoint
|
||||
---| "TOPLEFT"
|
||||
---| "TOPRIGHT"
|
||||
---| "BOTTOMLEFT"
|
||||
---| "BOTTOMRIGHT"
|
||||
---| "CENTER"
|
||||
---| "TOP"
|
||||
---| "BOTTOM"
|
||||
---| "LEFT"
|
||||
---| "RIGHT"
|
||||
|
||||
---@class Point
|
||||
---@field point AnchorPoint
|
||||
---@field relative_frame (Frame|string)?
|
||||
---@field relative_point AnchorPoint
|
||||
---@field x number?
|
||||
---@field y number?
|
||||
|
||||
---@alias FrameStrata
|
||||
---| "BACKGROUND"
|
||||
---| "LOW"
|
||||
---| "MEDIUM"
|
||||
---| "HIGH"
|
||||
---| "DIALOG"
|
||||
---| "FULLSCREEN"
|
||||
---| "FULLSCREEN_DIALOG"
|
||||
---| "TOOLTIP"
|
||||
|
||||
---@class FrameBuilder
|
||||
---@field parent fun( self: FrameBuilder, parent: Frame ): FrameBuilder
|
||||
---@field name fun( self: FrameBuilder, name: string ): FrameBuilder
|
||||
---@field type fun( self: FrameBuilder, name: string ): FrameBuilder
|
||||
---@field parent fun( self: FrameBuilder, parent: Frame ): FrameBuilder
|
||||
---@field height fun( self: FrameBuilder, height: number ): FrameBuilder
|
||||
---@field width fun( self: FrameBuilder, width: number ): FrameBuilder
|
||||
---@field point fun( self: FrameBuilder, p: Point ): FrameBuilder
|
||||
---@field sound fun( self: FrameBuilder ): FrameBuilder
|
||||
---@field frame_level fun( self: FrameBuilder, frame_level: number ): FrameBuilder
|
||||
---@field backdrop_color fun( self: FrameBuilder, r: number, g: number, b: number, a: number ): FrameBuilder
|
||||
---@field bg_file fun( self: FrameBuilder, bg_file: string ): FrameBuilder
|
||||
---@field edge_file fun( self: FrameBuilder, edge_file: string ): FrameBuilder
|
||||
---@field esc fun( self: FrameBuilder ): FrameBuilder
|
||||
---@field gui_elements fun( self: FrameBuilder, gui_elements: table ): FrameBuilder
|
||||
---@field frame_style fun( self: FrameBuilder, frame_style: FrameStyle ): FrameBuilder
|
||||
---@field on_drag_stop fun( self: FrameBuilder, callback: function ): FrameBuilder
|
||||
---@field movable fun( self: FrameBuilder ): FrameBuilder
|
||||
---@field resizable fun( self ): FrameBuilder
|
||||
---@field on_resize fun( self: FrameBuilder, callback: function ): FrameBuilder
|
||||
---@field enable_mouse fun( self: FrameBuilder ): FrameBuilder
|
||||
---@field border_size fun( self: FrameBuilder, border_size: number ): FrameBuilder
|
||||
---@field on_show fun( self: FrameBuilder, on_show: function ): FrameBuilder
|
||||
---@field on_hide fun( self: FrameBuilder, on_hide: function ): FrameBuilder
|
||||
---@field border_color fun( self: FrameBuilder, r: number, g: number, b: number, a: number ): FrameBuilder
|
||||
---@field self_centered_anchor fun( self: FrameBuilder ): FrameBuilder
|
||||
---@field scale fun( self: FrameBuilder, scale: number ): FrameBuilder
|
||||
---@field strata fun( self: FrameBuilder, strata: FrameStrata ): FrameBuilder
|
||||
---@field hidden fun( self: FrameBuilder ): FrameBuilder
|
||||
---@field build fun( self: FrameBuilder ): Frame
|
||||
|
||||
---@class FrameBuilderFactory
|
||||
---@field new fun(): FrameBuilder
|
||||
---@field button fun(): FrameBuilder
|
||||
---@field modern fun(): FrameBuilder
|
||||
---@field classic fun(): FrameBuilder
|
||||
|
||||
---@return FrameBuilder
|
||||
function M.new()
|
||||
local options = {}
|
||||
local frame_cache = {}
|
||||
local lines = {}
|
||||
local is_dragging
|
||||
|
||||
local function create_frame()
|
||||
local function create_anchor()
|
||||
local anchor = m.api.CreateFrame( "Frame", nil, m.api.UIParent )
|
||||
anchor:SetWidth( 1 )
|
||||
anchor:SetHeight( 1 )
|
||||
anchor:SetPoint( "CENTER", 0, 0 )
|
||||
anchor:EnableMouse( true )
|
||||
anchor:SetMovable( true )
|
||||
|
||||
return anchor
|
||||
end
|
||||
|
||||
local function create_main_frame( anchor )
|
||||
local type = options.type or "Frame"
|
||||
local frame = m.create_backdrop_frame( m.api, type, options.name, options.parent )
|
||||
|
||||
if options.hidden then
|
||||
frame:Hide()
|
||||
end
|
||||
|
||||
frame:SetWidth( options.width or 280 )
|
||||
frame:SetHeight( options.height or 100 )
|
||||
|
||||
if anchor then
|
||||
frame:SetPoint( "CENTER", anchor, "CENTER", 0, 0 )
|
||||
end
|
||||
|
||||
if options.point then
|
||||
local p = options.point
|
||||
local f = anchor or frame
|
||||
|
||||
f:SetPoint( p.point, p.relative_frame or m.api.UIParent, p.relative_point, p.x, p.y )
|
||||
else
|
||||
frame:SetPoint( "CENTER", anchor or m.api.UIParent, "CENTER", 0, 0 )
|
||||
end
|
||||
|
||||
if options.frame_level then
|
||||
frame:SetFrameLevel( options.frame_level )
|
||||
end
|
||||
|
||||
if options.strata then
|
||||
frame:SetFrameStrata( options.strata )
|
||||
else
|
||||
frame:SetFrameStrata( "DIALOG" )
|
||||
end
|
||||
|
||||
if options.frame_style == "Modern" then
|
||||
frame:SetBackdrop( {
|
||||
bgFile = options.bg_file or "Interface/Buttons/WHITE8x8",
|
||||
edgeFile = "Interface\\Buttons\\WHITE8X8",
|
||||
tile = false,
|
||||
tileSize = 0,
|
||||
edgeSize = 0.8,
|
||||
insets = { left = 0, right = 0, top = 0, bottom = 0 }
|
||||
} )
|
||||
elseif options.frame_style == "Classic" then
|
||||
frame:SetBackdrop( {
|
||||
bgFile = options.bg_file or "Interface/Buttons/WHITE8x8",
|
||||
edgeFile = options.edge_file or "Interface\\DialogFrame\\UI-DialogBox-Border",
|
||||
tile = true,
|
||||
tileSize = 22,
|
||||
edgeSize = options.border_size or 24,
|
||||
insets = { left = 5, right = 5, top = 5, bottom = 5 }
|
||||
} )
|
||||
end
|
||||
|
||||
if options.backdrop_color then
|
||||
local c = options.backdrop_color
|
||||
frame:SetBackdropColor( c.r, c.g, c.b, c.a or 1 )
|
||||
else
|
||||
frame:SetBackdropColor( 0, 0, 0, 0.7 )
|
||||
end
|
||||
|
||||
if options.border_color then
|
||||
local c = options.border_color
|
||||
frame:SetBackdropBorderColor( c.r, c.g, c.b, options.frame_style == "Classic" and 1 or c.a )
|
||||
end
|
||||
|
||||
return frame
|
||||
end
|
||||
|
||||
local function configure_main_frame( frame, anchor )
|
||||
if options.sound then
|
||||
local old_on_show = frame:GetScript( "OnShow" )
|
||||
|
||||
frame:SetScript( "OnShow", function()
|
||||
if m.vanilla then
|
||||
m.api.PlaySound( "igMainMenuOpen" )
|
||||
else
|
||||
m.api.PlaySound( m.api.SOUNDKIT.IG_MAINMENU_OPEN )
|
||||
end
|
||||
|
||||
if old_on_show then old_on_show() end
|
||||
if options.on_show then options.on_show() end
|
||||
end )
|
||||
|
||||
frame:SetScript( "OnHide", function()
|
||||
if is_dragging then
|
||||
local f = anchor or frame
|
||||
f:StopMovingOrSizing()
|
||||
end
|
||||
|
||||
if m.vanilla then
|
||||
m.api.PlaySound( "igMainMenuClose" )
|
||||
else
|
||||
m.api.PlaySound( m.api.SOUNDKIT.IG_MAINMENU_CLOSE )
|
||||
end
|
||||
|
||||
if options.on_hide then options.on_hide() end
|
||||
end )
|
||||
end
|
||||
|
||||
if options.enable_mouse then
|
||||
frame:EnableMouse( true )
|
||||
end
|
||||
|
||||
if options.movable then
|
||||
frame:SetMovable( true )
|
||||
-- frame:EnableMouse( true )
|
||||
frame:RegisterForDrag( "LeftButton" )
|
||||
frame:SetScript( "OnDragStart", function()
|
||||
if not frame:IsMovable() then return end
|
||||
is_dragging = true
|
||||
|
||||
local f = anchor or frame
|
||||
f:StartMoving()
|
||||
end )
|
||||
|
||||
frame:SetScript( "OnDragStop", function()
|
||||
is_dragging = false
|
||||
|
||||
local f = anchor or frame
|
||||
f:StopMovingOrSizing()
|
||||
|
||||
if options.on_drag_stop then
|
||||
options.on_drag_stop( frame )
|
||||
end
|
||||
|
||||
if anchor then
|
||||
frame:ClearAllPoints()
|
||||
frame:SetPoint( "CENTER", anchor, "CENTER", 0, 0 )
|
||||
end
|
||||
end )
|
||||
else
|
||||
frame:SetMovable( false )
|
||||
end
|
||||
|
||||
if options.resizable then
|
||||
frame:SetResizable( true )
|
||||
|
||||
if options.on_resize and frame:IsResizable() then
|
||||
frame:SetScript( "OnSizeChanged", function()
|
||||
options.on_resize( frame )
|
||||
end )
|
||||
end
|
||||
else
|
||||
frame:SetResizable( false )
|
||||
end
|
||||
|
||||
frame:EnableMouse( true )
|
||||
|
||||
if options.esc then
|
||||
m.api.tinsert( m.api.UISpecialFrames, frame:GetName() )
|
||||
end
|
||||
|
||||
if options.scale then
|
||||
frame:SetScale( options.scale )
|
||||
end
|
||||
end
|
||||
|
||||
local function get_from_cache( line_type )
|
||||
frame_cache[ line_type ] = frame_cache[ line_type ] or {}
|
||||
|
||||
for i = getn( frame_cache[ line_type ] ), 1, -1 do
|
||||
if not frame_cache[ line_type ][ i ].is_used then
|
||||
return frame_cache[ line_type ][ i ]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function add_api_to( frame, anchor )
|
||||
frame.add_line = function( line_type, modify_fn, padding )
|
||||
local line_frame = get_from_cache( line_type )
|
||||
|
||||
if not line_frame then
|
||||
local creator_fn = options.gui_elements and options.gui_elements[ line_type ] or nil
|
||||
if not creator_fn then return end
|
||||
|
||||
line_frame = creator_fn( frame )
|
||||
line_frame.is_used = true
|
||||
table.insert( frame_cache[ line_type ], line_frame )
|
||||
else
|
||||
line_frame.is_used = true
|
||||
line_frame:Show()
|
||||
end
|
||||
|
||||
modify_fn( line_type, line_frame, lines )
|
||||
local line = { line_type = line_type, padding = padding or 0, frame = line_frame }
|
||||
table.insert( lines, line )
|
||||
|
||||
if frame.resize then frame:resize( lines ) end
|
||||
|
||||
return line
|
||||
end
|
||||
|
||||
frame.clear = function()
|
||||
for _, line in ipairs( lines ) do
|
||||
line.frame:Hide()
|
||||
|
||||
line.frame.is_used = false
|
||||
end
|
||||
|
||||
m.clear_table( lines )
|
||||
if m.vanilla then lines.n = 0 end
|
||||
end
|
||||
|
||||
frame.backdrop_color = function( _, r, g, b, a )
|
||||
frame:SetBackdropColor( r, g, b, a )
|
||||
end
|
||||
|
||||
frame.border_color = function( _, r, g, b, a )
|
||||
frame:SetBackdropBorderColor( r, g, b, options.frame_style == "Classic" and 1 or a )
|
||||
end
|
||||
|
||||
frame.lock = function()
|
||||
frame:SetMovable( false )
|
||||
end
|
||||
|
||||
frame.unlock = function()
|
||||
frame:SetMovable( true )
|
||||
end
|
||||
|
||||
frame.position = function( _, point )
|
||||
local f = anchor or frame
|
||||
|
||||
f:ClearAllPoints()
|
||||
f:SetPoint( point.point, point.anchor or m.api.UIParent, point.relative_point, point.x, point.y )
|
||||
end
|
||||
|
||||
frame.get_anchor_center = function()
|
||||
local f = anchor or frame
|
||||
local x, y = f:GetCenter()
|
||||
|
||||
return { x = x, y = y }
|
||||
end
|
||||
|
||||
frame.get_anchor_point = function()
|
||||
local f = anchor or frame
|
||||
|
||||
local point, relative_frame, relative_point, x, y = f:GetPoint()
|
||||
return point and { point = point, relative_frame = relative_frame, relative_point = relative_point, x = x, y = y }
|
||||
end
|
||||
|
||||
frame.get_point = function()
|
||||
local f = frame
|
||||
|
||||
local point, relative_frame, relative_point, x, y = f:GetPoint()
|
||||
return point and { point = point, relative_frame = relative_frame, relative_point = relative_point, x = x, y = y }
|
||||
end
|
||||
|
||||
frame.anchor = function( _, source_frame, point, relative_point, x, y )
|
||||
if anchor then
|
||||
source_frame:ClearAllPoints()
|
||||
source_frame:SetPoint( point, anchor, relative_point, x, y )
|
||||
else
|
||||
source_frame:ClearAllPoints()
|
||||
source_frame:SetPoint( point, m.api.UIParent, relative_point, x, y )
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local self_centered_anchor = options.self_centered_anchor and create_anchor()
|
||||
local frame = create_main_frame( self_centered_anchor )
|
||||
configure_main_frame( frame, self_centered_anchor )
|
||||
add_api_to( frame, self_centered_anchor )
|
||||
|
||||
return frame, self_centered_anchor
|
||||
end
|
||||
|
||||
local function name( self, v )
|
||||
options.name = v
|
||||
return self
|
||||
end
|
||||
|
||||
local function type( self, v )
|
||||
options.type = v
|
||||
return self
|
||||
end
|
||||
|
||||
local function parent( self, v )
|
||||
options.parent = v
|
||||
return self
|
||||
end
|
||||
|
||||
local function height( self, v )
|
||||
options.height = v
|
||||
return self
|
||||
end
|
||||
|
||||
local function width( self, v )
|
||||
options.width = v
|
||||
return self
|
||||
end
|
||||
|
||||
local function point( self, p )
|
||||
options.point = { point = p.point, relative_frame = p.relative_frame or m.api.UIParent, relative_point = p.relative_point, x = p.x or 0, y = p.y or 0 }
|
||||
return self
|
||||
end
|
||||
|
||||
local function sound( self )
|
||||
options.sound = true
|
||||
return self
|
||||
end
|
||||
|
||||
local function frame_level( self, v )
|
||||
options.frame_level = v
|
||||
return self
|
||||
end
|
||||
|
||||
local function esc( self )
|
||||
options.esc = true
|
||||
return self
|
||||
end
|
||||
|
||||
---@return Frame
|
||||
---@return Anchor
|
||||
local function build()
|
||||
return create_frame()
|
||||
end
|
||||
|
||||
local function backdrop_color( self, r, g, b, a )
|
||||
options.backdrop_color = { r = r, g = g, b = b, a = a }
|
||||
return self
|
||||
end
|
||||
|
||||
local function bg_file( self, v )
|
||||
options.bg_file = v
|
||||
return self
|
||||
end
|
||||
|
||||
local function edge_file( self, v )
|
||||
options.edge_file = v
|
||||
return self
|
||||
end
|
||||
|
||||
local function gui_elements( self, t )
|
||||
options.gui_elements = t
|
||||
return self
|
||||
end
|
||||
|
||||
---@param self FrameBuilder
|
||||
---@param v FrameStyle
|
||||
local function frame_style( self, v )
|
||||
options.frame_style = v
|
||||
return self
|
||||
end
|
||||
|
||||
local function on_drag_stop( self, callback )
|
||||
options.on_drag_stop = callback
|
||||
return self
|
||||
end
|
||||
|
||||
local function movable( self )
|
||||
options.movable = true
|
||||
return self
|
||||
end
|
||||
|
||||
local function resizable( self )
|
||||
options.resizable = true
|
||||
return self
|
||||
end
|
||||
|
||||
local function on_resize( self, callback )
|
||||
options.on_resize = callback
|
||||
return self
|
||||
end
|
||||
|
||||
local function border_size( self, v )
|
||||
options.border_size = v
|
||||
return self
|
||||
end
|
||||
|
||||
local function on_show( self, f )
|
||||
options.on_show = f
|
||||
return self
|
||||
end
|
||||
|
||||
local function on_hide( self, f )
|
||||
options.on_hide = f
|
||||
return self
|
||||
end
|
||||
|
||||
local function border_color( self, r, g, b, a )
|
||||
options.border_color = { r = r, g = g, b = b, a = a }
|
||||
return self
|
||||
end
|
||||
|
||||
local function self_centered_anchor( self )
|
||||
options.self_centered_anchor = true
|
||||
return self
|
||||
end
|
||||
|
||||
local function scale( self, v )
|
||||
options.scale = v
|
||||
return self
|
||||
end
|
||||
|
||||
local function enable_mouse( self )
|
||||
options.enable_mouse = true
|
||||
return self
|
||||
end
|
||||
|
||||
local function strata( self, v )
|
||||
options.strata = v
|
||||
return self
|
||||
end
|
||||
|
||||
local function hidden( self )
|
||||
options.hidden = true
|
||||
return self
|
||||
end
|
||||
|
||||
---@type FrameBuilder
|
||||
return {
|
||||
name = name,
|
||||
type = type,
|
||||
parent = parent,
|
||||
height = height,
|
||||
width = width,
|
||||
point = point,
|
||||
sound = sound,
|
||||
frame_level = frame_level,
|
||||
backdrop_color = backdrop_color,
|
||||
bg_file = bg_file,
|
||||
edge_file = edge_file,
|
||||
esc = esc,
|
||||
gui_elements = gui_elements,
|
||||
frame_style = frame_style,
|
||||
on_drag_stop = on_drag_stop,
|
||||
movable = movable,
|
||||
resizable = resizable,
|
||||
on_resize = on_resize,
|
||||
border_size = border_size,
|
||||
on_show = on_show,
|
||||
on_hide = on_hide,
|
||||
border_color = border_color,
|
||||
self_centered_anchor = self_centered_anchor,
|
||||
scale = scale,
|
||||
enable_mouse = enable_mouse,
|
||||
strata = strata,
|
||||
hidden = hidden,
|
||||
build = build
|
||||
}
|
||||
end
|
||||
|
||||
function M.button()
|
||||
return M.new():type( "Button" )
|
||||
end
|
||||
|
||||
function M.modern()
|
||||
return M.new()
|
||||
:frame_style( "Modern" )
|
||||
end
|
||||
|
||||
function M.classic()
|
||||
return M.new()
|
||||
:frame_style( "Classic" )
|
||||
:border_size( 25 )
|
||||
end
|
||||
|
||||
m.FrameBuilder = M
|
||||
|
||||
return M
|
||||
121
src/GroupRoster.lua
Normal file
@ -0,0 +1,121 @@
|
||||
RollFor = RollFor or {}
|
||||
local m = RollFor
|
||||
|
||||
if m.GroupRoster then return end
|
||||
|
||||
local M = {}
|
||||
|
||||
---@type MakePlayerFn
|
||||
local make_player = m.Types.make_player
|
||||
|
||||
---@class GroupRosterApi
|
||||
---@field IsInParty fun(): number?
|
||||
---@field IsInRaid fun(): number?
|
||||
---@field IsInGroup fun(): number?
|
||||
---@field UnitName fun( unit: string ): string?
|
||||
---@field UnitClass fun( unit: string ): string?
|
||||
---@field UnitIsConnected fun( unit: string ): number?
|
||||
---@field GetRaidRosterInfo fun( index: number ): string?, string, number, number, PlayerClass, string, string
|
||||
|
||||
---@class GroupRoster
|
||||
---@field get_all_players_in_my_group fun( f: (fun( player: Player ): boolean)? ): Player[]
|
||||
---@field is_player_in_my_group fun( player_name: string ): boolean
|
||||
---@field am_i_in_group fun(): boolean
|
||||
---@field am_i_in_party fun(): boolean
|
||||
---@field am_i_in_raid fun(): boolean
|
||||
---@field find_player fun( player_name: string ): Player?
|
||||
|
||||
---@param api GroupRosterApi
|
||||
---@param player_info PlayerInfo
|
||||
function M.new( api, player_info )
|
||||
local function sort( candidates )
|
||||
table.sort( candidates, function( lhs, rhs )
|
||||
if lhs.class < rhs.class then
|
||||
return true
|
||||
elseif lhs.class > rhs.class then
|
||||
return false
|
||||
end
|
||||
|
||||
return lhs.name < rhs.name
|
||||
end )
|
||||
end
|
||||
|
||||
local function get_all_players_in_my_group( f )
|
||||
local result = {}
|
||||
|
||||
if not api.IsInGroup() then
|
||||
local name = player_info.get_name()
|
||||
local class = api.UnitClass( "player" )
|
||||
table.insert( result, { name = name, class = class } )
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
if api.IsInRaid() then
|
||||
for i = 1, 40 do
|
||||
local name, _, _, _, class, _, location = api.GetRaidRosterInfo( i )
|
||||
local player = { name = name, class = class, online = location ~= "Offline" and true or false }
|
||||
if name and (not f or f( player )) then table.insert( result, player ) end
|
||||
end
|
||||
|
||||
sort( result )
|
||||
return result
|
||||
end
|
||||
|
||||
local party = { "player", "party1", "party2", "party3", "party4" }
|
||||
|
||||
for _, v in ipairs( party ) do
|
||||
local name = api.UnitName( v )
|
||||
local class = api.UnitClass( v )
|
||||
local online = api.UnitIsConnected( v ) and true or false
|
||||
local player = name and class and make_player( name, class, online )
|
||||
if player and (not f or f( player )) then table.insert( result, player ) end
|
||||
end
|
||||
|
||||
sort( result )
|
||||
return result
|
||||
end
|
||||
|
||||
local function is_player_in_my_group( player_name )
|
||||
local players = get_all_players_in_my_group()
|
||||
|
||||
for _, player in pairs( players ) do
|
||||
if string.lower( player.name ) == string.lower( player_name ) then return true end
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
local function am_i_in_group()
|
||||
return api.IsInGroup()
|
||||
end
|
||||
|
||||
local function am_i_in_party()
|
||||
return api.IsInGroup() and not api.IsInRaid()
|
||||
end
|
||||
|
||||
local function am_i_in_raid()
|
||||
return api.IsInGroup() and api.IsInRaid()
|
||||
end
|
||||
|
||||
local function find_player( player_name )
|
||||
local players = get_all_players_in_my_group()
|
||||
|
||||
for _, player in pairs( players ) do
|
||||
if string.lower( player.name ) == string.lower( player_name ) then return player end
|
||||
end
|
||||
end
|
||||
|
||||
---@type GroupRoster
|
||||
return {
|
||||
get_all_players_in_my_group = get_all_players_in_my_group,
|
||||
is_player_in_my_group = is_player_in_my_group,
|
||||
am_i_in_group = am_i_in_group,
|
||||
am_i_in_party = am_i_in_party,
|
||||
am_i_in_raid = am_i_in_raid,
|
||||
find_player = find_player
|
||||
}
|
||||
end
|
||||
|
||||
m.GroupRoster = M
|
||||
return M
|
||||
584
src/GuiElements.lua
Normal file
@ -0,0 +1,584 @@
|
||||
RollFor = RollFor or {}
|
||||
local m = RollFor
|
||||
|
||||
if m.GuiElements then return end
|
||||
|
||||
local hl = m.colors.hl
|
||||
|
||||
---@class GuiElements
|
||||
---@field item_link fun( parent: Frame ): Frame
|
||||
---@field item_link_with_icon fun( parent: Frame, text: string ): Frame
|
||||
---@field text fun( parent: Frame, text: string ): Frame
|
||||
---@field icon fun( parent: Frame, show: boolean, width: number, height: number ): Frame
|
||||
---@field icon_text fun( parent: Frame, text: string ): Frame
|
||||
---@field roll fun( parent: Frame ): Frame
|
||||
---@field button fun( parent: Frame ): Frame
|
||||
---@field info fun( parent: Frame ): Frame
|
||||
---@field dropped_item fun( parent: Frame, text: string ): Frame
|
||||
---@field tiny_button fun( parent: Frame, text: string?, tooltip: string?, color: table?, font-size: number?):Frame
|
||||
---@field titlebar fun( parent: Frame, title: string, on_close: function )
|
||||
|
||||
local M = {}
|
||||
|
||||
function M.create_text_in_container( type, parent, container_width, alignment, text, inner_field, font_type )
|
||||
local container = m.create_backdrop_frame( m.api, type, nil, parent )
|
||||
container:SetWidth( container_width )
|
||||
local label = container:CreateFontString( nil, "ARTWORK", font_type or "GameFontNormalSmall" )
|
||||
|
||||
label:SetTextColor( 1, 1, 1 )
|
||||
if text then label:SetText( text ) end
|
||||
|
||||
if alignment then label:SetPoint( alignment, 0, 0 ) end
|
||||
container:SetHeight( label:GetHeight() )
|
||||
|
||||
if inner_field then
|
||||
container[ inner_field ] = label
|
||||
else
|
||||
container.inner = label
|
||||
end
|
||||
|
||||
return container
|
||||
end
|
||||
|
||||
function M.empty_line( parent )
|
||||
local result = m.api.CreateFrame( "Frame", nil, parent )
|
||||
result:SetWidth( 2 )
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
function M.item_link_with_icon( parent, text )
|
||||
local container = M.create_text_in_container( "Button", parent, 20, nil, nil, "text" )
|
||||
|
||||
local w = 14
|
||||
local h = 14
|
||||
local spacing = 10
|
||||
local count = 0
|
||||
local texture
|
||||
local tooltip_link
|
||||
|
||||
container:SetPoint( "TOP", 0, 0 )
|
||||
container.icon = M.icon( container, true, w, h )
|
||||
container.icon:SetPoint( "LEFT", 0, 0 )
|
||||
container.icon:SetTexCoord( 1 / w, (w - 1) / w, 1 / h, (h - 1) / h )
|
||||
container.count = M.text( container )
|
||||
container.text:SetTextColor( 1, 1, 1 )
|
||||
|
||||
if text then
|
||||
container.text:SetText( text )
|
||||
else
|
||||
container.text:SetText( "PrincessKenny" )
|
||||
end
|
||||
|
||||
container:SetHeight( container.text:GetHeight() )
|
||||
|
||||
local function resize()
|
||||
if texture then
|
||||
container.icon:Show()
|
||||
|
||||
local anchor = container.icon
|
||||
local padding = spacing
|
||||
local count_width = 0
|
||||
|
||||
if count > 1 then
|
||||
container.count:Show()
|
||||
container.count:ClearAllPoints()
|
||||
container.count:SetPoint( "LEFT", container.icon, "RIGHT", spacing, 0 )
|
||||
anchor = container.count
|
||||
padding = 0
|
||||
count_width = container.count:GetWidth()
|
||||
end
|
||||
|
||||
container.text:ClearAllPoints()
|
||||
container.text:SetPoint( "LEFT", anchor, "RIGHT", padding, 0 )
|
||||
container:SetWidth( container.text:GetWidth() + w + count_width + spacing )
|
||||
else
|
||||
local anchor = container
|
||||
local count_width = 0
|
||||
|
||||
if count > 1 then
|
||||
container.count:Show()
|
||||
container.count:ClearAllPoints()
|
||||
container.count:SetPoint( "LEFT", container.icon, "RIGHT", spacing, 0 )
|
||||
anchor = container.count
|
||||
count_width = container.count:GetWidth()
|
||||
end
|
||||
|
||||
container.icon:Hide()
|
||||
container.text:ClearAllPoints()
|
||||
container.text:SetPoint( "LEFT", anchor, 0, 0 )
|
||||
container:SetWidth( count_width + container.text:GetWidth() )
|
||||
end
|
||||
end
|
||||
|
||||
container.SetItem = function( _, i, tt_link )
|
||||
texture = i.texture
|
||||
count = i.count or 0
|
||||
tooltip_link = tt_link
|
||||
|
||||
container.text:SetText( i.link )
|
||||
container.icon:SetTexture( texture )
|
||||
container.count:SetText( count > 1 and hl( string.format( "%sx", count ) ) or nil )
|
||||
|
||||
resize()
|
||||
end
|
||||
|
||||
local function on_enter( self )
|
||||
if not tooltip_link then return end
|
||||
if m.vanilla then self = this end
|
||||
|
||||
m.api.GameTooltip:SetOwner( self, "ANCHOR_CURSOR" )
|
||||
m.api.GameTooltip:SetHyperlink( tooltip_link )
|
||||
m.api.GameTooltip:Show()
|
||||
end
|
||||
|
||||
local function on_leave()
|
||||
m.api.GameTooltip:Hide()
|
||||
end
|
||||
|
||||
container:SetScript( "OnEnter", on_enter )
|
||||
container:SetScript( "OnLeave", on_leave )
|
||||
container:SetScript( "OnClick", function()
|
||||
if not tooltip_link then return end
|
||||
|
||||
if m.is_ctrl_key_down() then
|
||||
m.api.DressUpItemLink( container.text:GetText() )
|
||||
return
|
||||
end
|
||||
|
||||
if m.is_shift_key_down() then
|
||||
m.link_item_in_chat( container.text:GetText() )
|
||||
end
|
||||
end )
|
||||
|
||||
return container
|
||||
end
|
||||
|
||||
function M.text( parent, text )
|
||||
local label = parent:CreateFontString( nil, "ARTWORK", "GameFontNormalSmall" )
|
||||
|
||||
label:SetTextColor( 1, 1, 1 )
|
||||
label:SetNonSpaceWrap( false )
|
||||
|
||||
if text then label:SetText( text ) end
|
||||
|
||||
return label
|
||||
end
|
||||
|
||||
function M.icon( parent, show, width, height )
|
||||
local icon = parent:CreateTexture( nil, "ARTWORK" )
|
||||
if not show then icon:Hide() end
|
||||
icon:SetWidth( width or 16 )
|
||||
icon:SetHeight( height or 16 )
|
||||
icon:SetTexture( "Interface\\AddOns\\RollFor\\assets\\icon-white2.tga" )
|
||||
|
||||
return icon
|
||||
end
|
||||
|
||||
function M.icon_text( parent, text )
|
||||
local container = M.create_text_in_container( "Button", parent, 20, nil, nil, "text" )
|
||||
|
||||
container:SetPoint( "CENTER", 0, 0 )
|
||||
container.icon = M.icon( container, true )
|
||||
container.icon:SetPoint( "LEFT", 0, 0 )
|
||||
container.text:SetPoint( "LEFT", container.icon, "RIGHT", 3, 0 )
|
||||
container.text:SetTextColor( 1, 1, 1 )
|
||||
|
||||
if text then container.text:SetText( text ) end
|
||||
|
||||
container.SetText = function( _, v )
|
||||
container.text:SetText( v )
|
||||
container:SetWidth( container.text:GetWidth() + 19 )
|
||||
end
|
||||
|
||||
return container
|
||||
end
|
||||
|
||||
function M.roll( parent )
|
||||
local frame = m.create_backdrop_frame( m.api, "Button", nil, parent )
|
||||
frame:SetWidth( 170 )
|
||||
frame:SetHeight( 14 )
|
||||
frame:SetFrameStrata( "DIALOG" )
|
||||
frame:SetFrameLevel( parent:GetFrameLevel() + 1 )
|
||||
frame:SetBackdrop( {
|
||||
bgFile = "Interface/Buttons/WHITE8x8",
|
||||
tile = true,
|
||||
tileSize = 22,
|
||||
} )
|
||||
|
||||
local function blue_hover( a )
|
||||
frame:SetBackdropColor( 0.125, 0.624, 0.976, a )
|
||||
end
|
||||
|
||||
local function hover()
|
||||
if frame.is_selected then
|
||||
return
|
||||
end
|
||||
|
||||
blue_hover( 0.2 )
|
||||
end
|
||||
|
||||
frame.select = function()
|
||||
blue_hover( 0.3 )
|
||||
frame.is_selected = true
|
||||
end
|
||||
|
||||
local function no_hover()
|
||||
if frame.is_selected then
|
||||
frame.select()
|
||||
else
|
||||
blue_hover( 0 )
|
||||
end
|
||||
end
|
||||
|
||||
frame.deselect = function()
|
||||
blue_hover( 0 )
|
||||
frame.is_selected = false
|
||||
end
|
||||
|
||||
frame:deselect()
|
||||
frame:SetScript( "OnEnter", function()
|
||||
hover()
|
||||
end )
|
||||
|
||||
frame:SetScript( "OnLeave", function()
|
||||
no_hover()
|
||||
end )
|
||||
|
||||
frame:EnableMouse( true )
|
||||
|
||||
local roll_container = M.create_text_in_container( "Button", frame, 35, "RIGHT" )
|
||||
roll_container:SetPoint( "LEFT", 0, 0 )
|
||||
frame.roll = roll_container.inner
|
||||
|
||||
local icon = M.icon( frame )
|
||||
icon:SetPoint( "LEFT", 22, 0 )
|
||||
frame.icon = icon
|
||||
|
||||
roll_container:SetPoint( "LEFT", 0, 0 )
|
||||
frame.roll = roll_container.inner
|
||||
|
||||
local player_name = M.text( frame )
|
||||
player_name:SetPoint( "CENTER", frame, "CENTER", 0, 0 )
|
||||
frame.player_name = player_name
|
||||
|
||||
local roll_type_container = M.create_text_in_container( "Button", frame, 37, "LEFT" )
|
||||
roll_type_container:SetPoint( "RIGHT", 0, 0 )
|
||||
frame.roll_type = roll_type_container.inner
|
||||
|
||||
return frame
|
||||
end
|
||||
|
||||
function M.button( parent )
|
||||
local template = m.vanilla and "StaticPopupButtonTemplate" or "UIPanelButtonTemplate"
|
||||
local height = m.vanilla and 20 or 21
|
||||
|
||||
local button = m.api.CreateFrame( "Button", nil, parent, template )
|
||||
button:SetWidth( 100 )
|
||||
button:SetHeight( height )
|
||||
button:SetText( "" )
|
||||
button:GetFontString():SetPoint( "CENTER", 0, -1 )
|
||||
|
||||
return button
|
||||
end
|
||||
|
||||
function M.award_button( parent )
|
||||
local template = m.vanilla and "StaticPopupButtonTemplate" or "UIPanelButtonTemplate"
|
||||
local height = m.vanilla and 20 or 21
|
||||
|
||||
local button = m.api.CreateFrame( "Button", nil, parent, template )
|
||||
button:SetWidth( 100 )
|
||||
button:SetHeight( height )
|
||||
button:SetText( "" )
|
||||
button:GetFontString():SetPoint( "CENTER", 0, -1 )
|
||||
|
||||
return button
|
||||
end
|
||||
|
||||
---@param parent Frame
|
||||
---@param text string?
|
||||
---@param tooltip string?
|
||||
---@param color string|table?
|
||||
---@param font_size number?
|
||||
function M.tiny_button( parent, text, tooltip, color, font_size )
|
||||
local font_x, font_y
|
||||
local button = m.api.CreateFrame( "Button", nil, parent )
|
||||
if not text then text = 'X' end
|
||||
|
||||
if type( color ) == "string" and color ~= "" then
|
||||
local str_color = color
|
||||
color = {}
|
||||
color.r, color.g, color.b, color.a = m.hex_to_rgba( str_color )
|
||||
end
|
||||
|
||||
if m.classic then
|
||||
if not color then color = { r = .9, g = .8, b = .25 } end
|
||||
button:SetWidth( 18 )
|
||||
button:SetHeight( 18 )
|
||||
|
||||
local highlight_texture = button:CreateTexture( nil, "HIGHLIGHT" )
|
||||
highlight_texture:SetTexture( "Interface\\Buttons\\UI-Panel-MinimizeButton-Highlight" )
|
||||
highlight_texture:SetTexCoord( .1875, .78125, .21875, .78125 )
|
||||
highlight_texture:SetBlendMode( "ADD" )
|
||||
highlight_texture:SetAllPoints( button )
|
||||
|
||||
if text == 'X' then
|
||||
button:SetNormalTexture( "Interface\\Buttons\\UI-Panel-MinimizeButton-Up" )
|
||||
button:SetPushedTexture( "Interface\\Buttons\\UI-Panel-MinimizeButton-Down" )
|
||||
else
|
||||
button:SetNormalTexture( "Interface\\AddOns\\RollFor\\assets\\tiny-button-up.tga" )
|
||||
button:SetPushedTexture( "Interface\\AddOns\\RollFor\\assets\\tiny-button-down.tga" )
|
||||
end
|
||||
button:GetNormalTexture():SetTexCoord( .1875, .78125, .21875, .78125 )
|
||||
button:GetPushedTexture():SetTexCoord( .1875, .78125, .21875, .78125 )
|
||||
|
||||
if text ~= 'X' then
|
||||
button:SetText( text )
|
||||
button:SetPushedTextOffset( -1.5, -1.5 )
|
||||
|
||||
if string.upper( text ) == text then
|
||||
font_x, font_y = 0, 0
|
||||
font_size = font_size or 13
|
||||
else
|
||||
font_x, font_y = -1, 2
|
||||
font_size = font_size or 15
|
||||
end
|
||||
end
|
||||
else
|
||||
if not color then color = { r = 1, g = .25, b = .25 } end
|
||||
button:SetBackdrop( {
|
||||
bgFile = "Interface/Tooltips/UI-Tooltip-Background",
|
||||
edgeFile = "Interface\\Buttons\\WHITE8X8",
|
||||
tile = false,
|
||||
tileSize = 0,
|
||||
edgeSize = 0.5,
|
||||
insets = { left = 0, right = 0, top = 0, bottom = 0 }
|
||||
} )
|
||||
button:SetBackdropColor( 0, 0, 0, 1 )
|
||||
button:SetBackdropBorderColor( .2, .2, .2, 1 )
|
||||
button:SetHeight( 13 )
|
||||
button:SetWidth( 13 )
|
||||
button:SetText( text )
|
||||
button:SetPushedTextOffset( 0, 0 )
|
||||
|
||||
if string.upper( text ) == text then
|
||||
font_x = text == "?" and -.5 or 0
|
||||
font_y = 0.5
|
||||
font_size = font_size or 10
|
||||
else
|
||||
font_x, font_y = -.5, 1.5
|
||||
font_size = font_size or 14
|
||||
end
|
||||
end
|
||||
|
||||
if not m.classic or text ~= "X" then
|
||||
button:GetFontString():SetFont( "FONTS\\FRIZQT__.TTF", font_size )
|
||||
button:GetFontString():SetTextColor( color.r, color.g, color.b, color.a or 1 )
|
||||
button:GetFontString():SetPoint( "CENTER", font_x, font_y )
|
||||
end
|
||||
|
||||
button:SetScript( "OnEnter", function()
|
||||
local self = button
|
||||
self:SetBackdropBorderColor( color.r, color.g, color.b, color.a or 1 )
|
||||
if tooltip then
|
||||
m.api.GameTooltip:SetOwner( button, "ANCHOR_RIGHT" )
|
||||
m.api.GameTooltip:SetText( tooltip )
|
||||
m.api.GameTooltip:SetScale( 0.8 )
|
||||
m.api.GameTooltip:Show()
|
||||
end
|
||||
end )
|
||||
button:SetScript( "OnLeave", function()
|
||||
local self = button
|
||||
if not self.active then
|
||||
self:SetBackdropBorderColor( .2, .2, .2, 1 )
|
||||
end
|
||||
if tooltip and m.api.GameTooltip:IsVisible() then
|
||||
m.api.GameTooltip:SetScale( 1 )
|
||||
m.api.GameTooltip:Hide()
|
||||
end
|
||||
end )
|
||||
|
||||
return button
|
||||
end
|
||||
|
||||
---@param parent Frame
|
||||
---@param on_start function
|
||||
---@param on_end function
|
||||
function M.resize_grip( parent, on_start, on_end )
|
||||
local button = m.api.CreateFrame( "Button", nil, parent )
|
||||
button:SetWidth( 16 )
|
||||
button:SetHeight( 16 )
|
||||
button:SetNormalTexture( "Interface\\AddOns\\RollFor\\assets\\resize-grip.tga", "ARTWORK" )
|
||||
button:GetNormalTexture():SetAllPoints( button )
|
||||
|
||||
button:SetScript( "OnEnter", function()
|
||||
button:GetNormalTexture():SetBlendMode( "ADD" )
|
||||
end )
|
||||
button:SetScript( "OnLeave", function()
|
||||
button:GetNormalTexture():SetBlendMode( "BLEND" )
|
||||
end )
|
||||
button:SetScript( "OnMouseDown", function()
|
||||
parent:StartSizing( "BOTTOMRIGHT" )
|
||||
if on_start then on_start( parent ) end
|
||||
end )
|
||||
button:SetScript( "OnMouseUp", function()
|
||||
parent:StopMovingOrSizing()
|
||||
if on_end then on_end( parent ) end
|
||||
end )
|
||||
|
||||
return button
|
||||
end
|
||||
|
||||
function M.checkbox( parent, text, on_change )
|
||||
local frame = m.api.CreateFrame( "Frame", nil, parent )
|
||||
frame:SetPoint( "LEFT", 5, 0 )
|
||||
frame:SetHeight( 14 )
|
||||
|
||||
local cb = m.api.CreateFrame( "CheckButton", nil, frame, "UICheckButtonTemplate" )
|
||||
cb:SetWidth( 14 )
|
||||
cb:SetHeight( 14 )
|
||||
cb:SetPoint( "LEFT", 2, 0 )
|
||||
cb:SetNormalTexture( nil )
|
||||
cb:SetPushedTexture( nil )
|
||||
cb:SetHighlightTexture( nil )
|
||||
cb:SetBackdrop( {
|
||||
bgFile = "Interface/Buttons/WHITE8x8",
|
||||
edgeFile = "Interface/Buttons/WHITE8x8",
|
||||
edgeSize = 0.5,
|
||||
insets = { left = 0, right = 0, top = 0, bottom = 0 }
|
||||
} )
|
||||
cb:SetBackdropColor( 0, 0, 0, 1 )
|
||||
cb:SetBackdropBorderColor( .2, .2, .2, 1 )
|
||||
cb:SetScript( "OnClick", function()
|
||||
if on_change then on_change( cb:GetChecked() ) end
|
||||
end )
|
||||
frame.checkbox = cb
|
||||
|
||||
local label = M.create_text_in_container( "Button", frame, 1, "LEFT", text )
|
||||
label.inner:SetJustifyH( "LEFT" )
|
||||
label:SetWidth( label.inner:GetWidth() )
|
||||
label:SetPoint( "LEFT", cb, "RIGHT", 5, 0 )
|
||||
label:SetScript( "OnClick", function()
|
||||
cb:SetChecked( not cb:GetChecked() )
|
||||
if on_change then on_change( cb:GetChecked() ) end
|
||||
end )
|
||||
|
||||
frame:SetWidth( cb:GetWidth() + label:GetWidth() + 5 )
|
||||
|
||||
return frame
|
||||
end
|
||||
|
||||
---@param parent Frame
|
||||
---@param title string
|
||||
---@param on_close function
|
||||
function M.titlebar( parent, title, on_close )
|
||||
local frame = m.api.CreateFrame( "Frame", nil, parent )
|
||||
frame:SetHeight( 32 )
|
||||
if not m.classic then
|
||||
frame:SetPoint( "TOPLEFT", 0, 5 )
|
||||
frame:SetPoint( "RIGHT", 0, 0 )
|
||||
else
|
||||
frame:SetPoint( "TOPLEFT", 3, 2 )
|
||||
frame:SetPoint( "RIGHT", -3, 2 )
|
||||
frame:SetBackdrop( {
|
||||
bgFile = "Interface\\AddOns\\RollFor\\assets\\titlebar-top.tga",
|
||||
tile = true,
|
||||
tileSize = 32,
|
||||
edgeSize = 0,
|
||||
insets = { left = 30, right = 30, top = 0, bottom = 0 }
|
||||
} )
|
||||
|
||||
local topLeft = frame:CreateTexture( nil, "BORDER" )
|
||||
topLeft:SetTexture( "Interface\\AddOns\\RollFor\\assets\\titlebar-topleft.tga" )
|
||||
topLeft:SetPoint( "TOPLEFT", frame, "TOPLEFT", 0, 0 )
|
||||
topLeft:SetWidth( 64 )
|
||||
topLeft:SetHeight( 32 )
|
||||
|
||||
local topRight = frame:CreateTexture( nil, "BORDER" )
|
||||
topRight:SetTexture( "Interface\\AddOns\\RollFor\\assets\\titlebar-topright.tga" )
|
||||
topRight:SetPoint( "TOPRIGHT", frame, "TOPRIGHT", 0, 0 )
|
||||
topRight:SetWidth( 64 )
|
||||
topRight:SetHeight( 32 )
|
||||
end
|
||||
|
||||
local label = frame:CreateFontString( nil, "ARTWORK", "GameFontNormalSmall" )
|
||||
label:SetPoint( "TOPLEFT", 10, -12 )
|
||||
label:SetPoint( "RIGHT", m.classic and -29 or 0, 0 )
|
||||
label:SetJustifyH( "CENTER" )
|
||||
label:SetTextColor( 1, 1, 1 )
|
||||
label:SetText( title )
|
||||
frame.title = label
|
||||
|
||||
local btn_close = M.tiny_button( parent, "X", "Close Window" )
|
||||
btn_close:SetPoint( "TOPRIGHT", m.classic and -7 or -5, -5 )
|
||||
btn_close:SetScript( "OnClick", function()
|
||||
if on_close then
|
||||
on_close()
|
||||
else
|
||||
if parent then parent:Hide() end
|
||||
end
|
||||
end )
|
||||
|
||||
return frame
|
||||
end
|
||||
|
||||
function M.info( parent )
|
||||
local frame = m.api.CreateFrame( "Frame", nil, parent )
|
||||
frame:SetWidth( 11 )
|
||||
frame:SetHeight( 11 )
|
||||
frame:SetFrameStrata( "DIALOG" )
|
||||
frame:SetFrameLevel( parent:GetFrameLevel() + 1 )
|
||||
frame:EnableMouse( true )
|
||||
|
||||
local icon = frame:CreateTexture( nil, "BACKGROUND" )
|
||||
icon:SetWidth( 11 )
|
||||
icon:SetHeight( 11 )
|
||||
icon:SetTexture( "Interface\\AddOns\\RollFor\\assets\\info.tga" )
|
||||
icon:SetPoint( "CENTER", 0, 0 )
|
||||
|
||||
frame:SetScript( "OnEnter", function( self )
|
||||
if m.vanilla then self = this end
|
||||
|
||||
self.tooltip_scale = m.api.GameTooltip:GetScale()
|
||||
m.api.GameTooltip:SetOwner( self, "ANCHOR_CURSOR" )
|
||||
m.api.GameTooltip:AddLine( frame.tooltip_info, 1, 1, 1 )
|
||||
m.api.GameTooltip:SetScale( 0.75 )
|
||||
m.api.GameTooltip:Show()
|
||||
end )
|
||||
|
||||
frame:SetScript( "OnLeave", function( self )
|
||||
if m.vanilla then self = this end
|
||||
|
||||
m.api.GameTooltip:Hide()
|
||||
m.api.GameTooltip:SetScale( self.tooltip_scale or 1 )
|
||||
end )
|
||||
|
||||
return frame
|
||||
end
|
||||
|
||||
function M.create_icon_in_container( type, parent, w, h, icon_zoom )
|
||||
local result = m.create_backdrop_frame( m.api, type or "Button", nil, parent )
|
||||
result:SetWidth( w + 1 )
|
||||
result:SetHeight( h )
|
||||
|
||||
result:SetBackdrop( {
|
||||
bgFile = "Interface/Tooltips/UI-Tooltip-Background",
|
||||
edgeFile = "Interface\\Buttons\\WHITE8X8",
|
||||
tile = false,
|
||||
tileSize = 0,
|
||||
edgeSize = 1,
|
||||
insets = { left = 0, right = 0, top = 0, bottom = 0 }
|
||||
} )
|
||||
|
||||
result:SetBackdropBorderColor( 0, 0, 0, 1 )
|
||||
result:SetBackdropColor( 0, 0, 0, 0 )
|
||||
|
||||
result.texture = M.icon( result, true, w, h )
|
||||
result.texture:SetPoint( "CENTER", 0, 0 )
|
||||
result.texture:SetTexCoord( icon_zoom / w, (w - icon_zoom) / w, icon_zoom / h, (h - icon_zoom) / h )
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
m.GuiElements = M
|
||||
return M
|
||||
87
src/InstaRaidRollRollingLogic.lua
Normal file
@ -0,0 +1,87 @@
|
||||
RollFor = RollFor or {}
|
||||
local m = RollFor
|
||||
|
||||
if m.InstaRaidRollRollingLogic then return end
|
||||
|
||||
local M = {}
|
||||
|
||||
local getn = m.getn
|
||||
local hl = m.colors.hl
|
||||
local strategy = m.Types.RollingStrategy.InstaRaidRoll
|
||||
local roll_type = m.Types.RollType.MainSpec
|
||||
local clear_table = m.clear_table
|
||||
|
||||
---@type MakeWinnerFn
|
||||
local make_winner = m.Types.make_winner
|
||||
|
||||
-- TODO: Lots of similarity with RaidRollRollingLogic. Perhaps refactor.
|
||||
|
||||
---@param chat Chat
|
||||
---@param item Item|MasterLootDistributableItem
|
||||
---@param item_count number
|
||||
---@param winner_tracker WinnerTracker
|
||||
---@param controller RollControllerFacade
|
||||
---@param candidates ItemCandidate[]|Player[]
|
||||
function M.new(
|
||||
chat,
|
||||
_,
|
||||
item,
|
||||
item_count,
|
||||
winner_tracker,
|
||||
controller,
|
||||
candidates
|
||||
)
|
||||
local m_winners = {}
|
||||
|
||||
local function clear_winners()
|
||||
clear_table( m_winners )
|
||||
if m.vanilla then m_winners.n = 0 end
|
||||
end
|
||||
|
||||
local function start_rolling()
|
||||
clear_winners()
|
||||
|
||||
for _ = 1, item_count do
|
||||
local roll = m.lua.math.random( 1, getn( candidates ) )
|
||||
table.insert( m_winners, candidates[ roll ] )
|
||||
end
|
||||
|
||||
local winners = m.map( m_winners,
|
||||
---@param player ItemCandidate|Player
|
||||
function( player )
|
||||
if type( player ) == "table" then -- Fucking lua50 and its n.
|
||||
local winner = make_winner( player.name, player.class, item, player.type == "ItemCandidate" or false, roll_type, nil )
|
||||
winner_tracker.track( winner.name, item.link, roll_type, nil, m.Types.RollingStrategy.InstaRaidRoll )
|
||||
return winner
|
||||
end
|
||||
end )
|
||||
|
||||
controller.winners_found( item, item_count, winners, strategy )
|
||||
controller.finish()
|
||||
end
|
||||
|
||||
local function show_sorted_rolls()
|
||||
if getn( m_winners ) == 0 then
|
||||
chat.info( "There is no winner yet.", nil, "RaidRoll" )
|
||||
return
|
||||
end
|
||||
|
||||
for _, winner in ipairs( m_winners ) do
|
||||
chat.info( string.format( "%s won %s.", hl( winner.name ), item.link ), nil, "InstaRaidRoll" )
|
||||
end
|
||||
end
|
||||
|
||||
---@type RollingStrategy
|
||||
return {
|
||||
start_rolling = start_rolling, -- This probably doesn't belong here either.
|
||||
on_roll = function() end,
|
||||
is_rolling = function() return false end,
|
||||
show_sorted_rolls = show_sorted_rolls,
|
||||
get_type = function() return m.Types.RollingStrategy.InstaRaidRoll end,
|
||||
stop_accepting_rolls = m.noop(),
|
||||
cancel_rolling = m.noop()
|
||||
}
|
||||
end
|
||||
|
||||
m.InstaRaidRollRollingLogic = M
|
||||
return M
|
||||
71
src/Interface.lua
Normal file
@ -0,0 +1,71 @@
|
||||
RollFor = RollFor or {}
|
||||
local m = RollFor
|
||||
|
||||
if m.Interface then return end
|
||||
|
||||
local M = {}
|
||||
---@diagnostic disable-next-line: undefined-global
|
||||
local debugstack = debugstack
|
||||
|
||||
---@param implementation table
|
||||
---@param i1 table
|
||||
---@param i2 table?
|
||||
---@param i3 table?
|
||||
---@param i4 table?
|
||||
---@param i5 table?
|
||||
function M.validate( implementation, i1, i2, i3, i4, i5, i6, i7, i8, i9 )
|
||||
assert( type( implementation ) == "table", "'implementation' must be a table." )
|
||||
|
||||
for _, interface in ipairs( { i1, i2, i3, i4, i5, i6, i7, i8, i9 } ) do
|
||||
for method_name, expected_type in pairs( interface ) do
|
||||
local v = implementation[ method_name ]
|
||||
|
||||
if type( v ) ~= expected_type then
|
||||
if debugstack then
|
||||
error( string.format( "'%s' must be a %s, got %s.", method_name, expected_type, type( v ) ) .. "\n" .. debugstack(), 2 )
|
||||
else
|
||||
error( string.format( "'%s' must be a %s, got %s.", method_name, expected_type, type( v ), debug.traceback() ), 2 )
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@param implementation table
|
||||
---@param n1 string
|
||||
---@param n2 string?
|
||||
---@param n3 string?
|
||||
---@param n4 string?
|
||||
---@param n5 string?
|
||||
---@param n6 string?
|
||||
---@param n7 string?
|
||||
---@param n8 string?
|
||||
---@param n9 string?
|
||||
function M.assert_members( implementation, n1, n2, n3, n4, n5, n6, n7, n8, n9 )
|
||||
assert( type( implementation ) == "table", "'implementation' must be a table." )
|
||||
|
||||
for _, member_name in ipairs( { n1, n2, n3, n4, n5, n6, n7, n8, n9 } ) do
|
||||
if implementation[ member_name ] == nil then
|
||||
if debugstack then
|
||||
error( string.format( "Member '%s' is not present.", member_name ) .. "\n" .. debugstack(), 2 )
|
||||
else
|
||||
error( string.format( "Member '%s' is not present: %s.", member_name, debug.traceback() ), 2 )
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function M.noop() end
|
||||
|
||||
function M.mock( interface )
|
||||
local result = {}
|
||||
|
||||
for k, v in pairs( interface ) do
|
||||
if v == "function" then result[ k ] = M.noop end
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
m.Interface = M
|
||||
return M
|
||||
306
src/ItemUtils.lua
Normal file
@ -0,0 +1,306 @@
|
||||
RollFor = RollFor or {}
|
||||
local m = RollFor
|
||||
|
||||
if m.ItemUtils then return end
|
||||
|
||||
local red, white = m.colors.red, m.colors.white
|
||||
|
||||
local M = {}
|
||||
|
||||
---@class LT
|
||||
---@field Item "Item"
|
||||
---@field SoftRessedItem "SoftRessedItem"
|
||||
---@field HardRessedItem "HardRessedItem"
|
||||
---@field Coin "Coin"
|
||||
---@field DroppedItem "DroppedItem"
|
||||
---@field SoftRessedDroppedItem "SoftRessedDroppedItem"
|
||||
---@field HardRessedDroppedItem "HardRessedDroppedItem"
|
||||
|
||||
---@type LT
|
||||
local LootType = {
|
||||
Item = "Item",
|
||||
SoftRessedItem = "SoftRessedItem",
|
||||
HardRessedItem = "HardRessedItem",
|
||||
Coin = "Coin",
|
||||
DroppedItem = "DroppedItem",
|
||||
SoftRessedDroppedItem = "SoftRessedDroppedItem",
|
||||
HardRessedDroppedItem = "HardRessedDroppedItem"
|
||||
}
|
||||
|
||||
M.LootType = LootType
|
||||
|
||||
---@alias LootType
|
||||
---| "Item"
|
||||
---| "SoftRessedItem"
|
||||
---| "HardRessedItem"
|
||||
---| DroppedLootType
|
||||
|
||||
---@alias DroppedLootType
|
||||
---| CoinType
|
||||
---| DroppedItemType
|
||||
|
||||
---@alias CoinType
|
||||
---| "Coin"
|
||||
|
||||
---@alias DroppedItemType
|
||||
---| "DroppedItem"
|
||||
---| "SoftRessedDroppedItem"
|
||||
---| "HardRessedDroppedItem"
|
||||
|
||||
---@class BT
|
||||
---@field BindOnPickup "BindOnPickup"
|
||||
---@field BindOnEquip "BindOnEquip"
|
||||
---@field Soulbound "Soulbound"
|
||||
---@field Quest "Quest"
|
||||
---@field None "None"
|
||||
|
||||
---@type BT
|
||||
local BindType = {
|
||||
BindOnPickup = "BindOnPickup",
|
||||
BindOnEquip = "BindOnEquip",
|
||||
Soulbound = "Soulbound",
|
||||
Quest = "Quest",
|
||||
None = "None"
|
||||
}
|
||||
|
||||
M.BindType = BindType
|
||||
|
||||
---@alias BindType
|
||||
---| "BindOnPickup"
|
||||
---| "BindOnEquip"
|
||||
---| "Soulbound"
|
||||
---| "Quest"
|
||||
---| "None"
|
||||
|
||||
---@alias ItemQuality
|
||||
---| 0 -- Poor
|
||||
---| 1 -- Common
|
||||
---| 2 -- Uncommon
|
||||
---| 3 -- Rare
|
||||
---| 4 -- Epic
|
||||
---| 5 -- Legendary
|
||||
|
||||
---@alias ItemLink string
|
||||
---@alias TooltipItemLink string
|
||||
---@alias ItemTexture string
|
||||
|
||||
---@class Item
|
||||
---@field id number
|
||||
---@field name string
|
||||
---@field link ItemLink
|
||||
---@field quality ItemQuality?
|
||||
---@field texture string?
|
||||
---@field classes table<number, PlayerClass>?
|
||||
---@field is_boss_loot boolean?
|
||||
---@field type "Item"
|
||||
|
||||
---@class DroppedItem : Item
|
||||
---@field tooltip_link TooltipItemLink
|
||||
---@field quantity number
|
||||
---@field bind BindType?
|
||||
---@field type "DroppedItem"
|
||||
|
||||
---@class HardRessedDroppedItem : DroppedItem
|
||||
---@field type "HardRessedDroppedItem"
|
||||
|
||||
---@class SoftRessedDroppedItem : DroppedItem
|
||||
---@field sr_players RollingPlayer[]
|
||||
---@field type "SoftRessedDroppedItem"
|
||||
|
||||
---@class Coin
|
||||
---@field texture string
|
||||
---@field amount_text string
|
||||
---@field type "Coin"
|
||||
|
||||
---@alias MasterLootDistributableItem DroppedItem|HardRessedDroppedItem|SoftRessedDroppedItem
|
||||
|
||||
---@alias MakeItemFn fun(
|
||||
--- id: number,
|
||||
--- name: string,
|
||||
--- link: ItemLink,
|
||||
--- quality: ItemQuality,
|
||||
--- texture: string ): Item
|
||||
|
||||
---@alias MakeDroppedItemFn fun(
|
||||
--- id: number,
|
||||
--- name: string,
|
||||
--- link: ItemLink,
|
||||
--- tooltip_link: TooltipItemLink,
|
||||
--- quality: ItemQuality,
|
||||
--- quantity: number,
|
||||
--- texture: string,
|
||||
--- bind: BindType,
|
||||
--- classes: table<number, PlayerClass>|nil,
|
||||
--- is_boss_loot: boolean ): DroppedItem
|
||||
|
||||
---@alias MakeSoftRessedDroppedItemFn fun(
|
||||
--- item: DroppedItem,
|
||||
--- sr_players: RollingPlayer[] ): SoftRessedDroppedItem
|
||||
|
||||
---@alias MakeHardRessedDroppedItemFn fun(
|
||||
--- item: DroppedItem ): HardRessedDroppedItem
|
||||
|
||||
---@class ItemUtils
|
||||
---@field get_item_id fun( item_link: ItemLink ): number?
|
||||
---@field get_item_name fun( item_link: ItemLink ): string
|
||||
---@field parse_link fun( item_link: string ): ItemLink? -- Sometimes we need to parse the link from the "[Item Name]x4." string.
|
||||
---@field parse_all_links fun( item_links: string ): ItemLink[]
|
||||
---@field get_tooltip_link fun( item_link: ItemLink ): TooltipItemLink
|
||||
---@field bind_abbrev fun( bind: BindType ): string?
|
||||
---@field make_item MakeItemFn
|
||||
---@field make_dropped_item MakeDroppedItemFn
|
||||
---@field make_softres_dropped_item MakeSoftRessedDroppedItemFn
|
||||
---@field make_hardres_dropped_item MakeHardRessedDroppedItemFn
|
||||
---@field make_coin fun( texture: string, amount_text: string ): Coin
|
||||
|
||||
---@param item_link ItemLink
|
||||
---@return number?
|
||||
function M.get_item_id( item_link )
|
||||
for item_id in string.gmatch( item_link, "|c%x%x%x%x%x%x%x%x|Hitem:(%d+):.+|r" ) do
|
||||
return tonumber( item_id )
|
||||
end
|
||||
end
|
||||
|
||||
---@param item_link ItemLink
|
||||
---@return string
|
||||
function M.get_item_name( item_link )
|
||||
local result = string.gsub( item_link, "|c%x%x%x%x%x%x%x%x|Hitem:%d+.*|h%[(.*)%]|h|r", "%1" )
|
||||
return result
|
||||
end
|
||||
|
||||
---@param item_link string
|
||||
---@return string?
|
||||
function M.parse_link( item_link )
|
||||
if not item_link then return end
|
||||
|
||||
for link in string.gmatch( item_link, "|c%x%x%x%x%x%x%x%x|Hitem:%d+.-|h%[.-%]|h|r" ) do
|
||||
return link
|
||||
end
|
||||
end
|
||||
|
||||
---@param item_links string
|
||||
---@return ItemLink[]
|
||||
function M.parse_all_links( item_links )
|
||||
local result = {}
|
||||
if not item_links then return result end
|
||||
|
||||
for item_link in string.gmatch( item_links, "|c%x%x%x%x%x%x%x%x|Hitem:[^%]]+%]|h|r" ) do
|
||||
table.insert( result, item_link )
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
---@param item_link ItemLink
|
||||
---@return TooltipItemLink
|
||||
function M.get_tooltip_link( item_link )
|
||||
return string.match( item_link, "|H(item:[^|]+)|h" )
|
||||
end
|
||||
|
||||
---@param bind BindType
|
||||
---@return string?
|
||||
function M.bind_abbrev( bind )
|
||||
if bind == BindType.BindOnPickup or bind == BindType.Soulbound or bind == BindType.Quest then
|
||||
return red( "BoP" )
|
||||
elseif bind == BindType.BindOnEquip then
|
||||
return white( "BoE" )
|
||||
end
|
||||
end
|
||||
|
||||
---@param id number
|
||||
---@param name string
|
||||
---@param link ItemLink
|
||||
---@param quality ItemQuality?
|
||||
---@param texture string?
|
||||
---@return Item
|
||||
function M.make_item( id, name, link, quality, texture )
|
||||
return {
|
||||
id = id,
|
||||
name = name,
|
||||
link = link,
|
||||
quality = quality,
|
||||
texture = texture,
|
||||
type = LootType.Item
|
||||
}
|
||||
end
|
||||
|
||||
---@param id number
|
||||
---@param name string
|
||||
---@param link ItemLink
|
||||
---@param tooltip_link TooltipItemLink
|
||||
---@param quality ItemQuality?
|
||||
---@param quantity number?
|
||||
---@param texture string?
|
||||
---@param bind BindType?
|
||||
---@param classes table<number, PlayerClass>?
|
||||
---@param is_boss_loot boolean?
|
||||
---@return DroppedItem
|
||||
function M.make_dropped_item( id, name, link, tooltip_link, quality, quantity, texture, bind, classes, is_boss_loot )
|
||||
return {
|
||||
id = id,
|
||||
name = name,
|
||||
link = link,
|
||||
tooltip_link = tooltip_link,
|
||||
quality = quality,
|
||||
quantity = quantity,
|
||||
texture = texture,
|
||||
bind = bind or BindType.None,
|
||||
classes = classes,
|
||||
is_boss_loot = is_boss_loot,
|
||||
type = LootType.DroppedItem
|
||||
}
|
||||
end
|
||||
|
||||
---@param item DroppedItem
|
||||
---@param sr_players RollingPlayer[]
|
||||
---@return SoftRessedDroppedItem
|
||||
function M.make_softres_dropped_item( item, sr_players )
|
||||
---@param a RollingPlayer
|
||||
---@param b RollingPlayer
|
||||
local function sort( a, b ) return a.name < b.name end
|
||||
local players = sr_players or {}
|
||||
table.sort( players, sort )
|
||||
|
||||
return {
|
||||
id = item.id,
|
||||
name = item.name,
|
||||
link = item.link,
|
||||
tooltip_link = item.tooltip_link,
|
||||
quality = item.quality,
|
||||
quantity = item.quantity,
|
||||
texture = item.texture,
|
||||
bind = item.bind,
|
||||
sr_players = players,
|
||||
type = LootType.SoftRessedDroppedItem
|
||||
}
|
||||
end
|
||||
|
||||
---@param item DroppedItem
|
||||
---@return HardRessedDroppedItem
|
||||
function M.make_hardres_dropped_item( item )
|
||||
return {
|
||||
id = item.id,
|
||||
name = item.name,
|
||||
link = item.link,
|
||||
tooltip_link = item.tooltip_link,
|
||||
quality = item.quality,
|
||||
quantity = item.quantity,
|
||||
texture = item.texture,
|
||||
bind = item.bind,
|
||||
type = LootType.HardRessedDroppedItem
|
||||
}
|
||||
end
|
||||
|
||||
---@param texture string
|
||||
---@param amount_text string
|
||||
---@return Coin
|
||||
function M.make_coin( texture, amount_text )
|
||||
return {
|
||||
texture = texture,
|
||||
amount_text = amount_text,
|
||||
type = LootType.Coin
|
||||
}
|
||||
end
|
||||
|
||||
m.ItemUtils = M
|
||||
return M
|
||||
90
src/LootAutoProcess.lua
Normal file
@ -0,0 +1,90 @@
|
||||
RollFor = RollFor or {}
|
||||
local m = RollFor
|
||||
|
||||
if m.LootAutoProcess then return end
|
||||
|
||||
local M = {}
|
||||
|
||||
local getn = m.getn
|
||||
---@type LT
|
||||
local LT = m.ItemUtils.LootType
|
||||
local clear_table = m.clear_table
|
||||
|
||||
---@class LootAutoProcess
|
||||
---@field on_loot_opened fun()
|
||||
---@field on_loot_slot_cleared fun( slot: number )
|
||||
---@field on_loot_closed fun()
|
||||
|
||||
---@param config Config
|
||||
---@param roll_tracker RollTracker
|
||||
---@param loot_list LootList
|
||||
---@param roll_controller RollController
|
||||
---@param player_info PlayerInfo
|
||||
---@return LootAutoProcess
|
||||
function M.new( config, roll_tracker, loot_list, roll_controller, player_info )
|
||||
local loot_cache = {}
|
||||
-- local selected_loot_list_item
|
||||
|
||||
local function process_next_item()
|
||||
local threshold = m.api.GetLootThreshold()
|
||||
local data = roll_tracker.get()
|
||||
local items = loot_list.get_items()
|
||||
local item_count = getn( items )
|
||||
|
||||
if item_count == 0 then return end
|
||||
|
||||
local is_coin = items[ 1 ].type == LT.Coin
|
||||
local first_item = not is_coin and items[ 1 ]
|
||||
|
||||
if first_item and first_item.quality >= threshold and not data.status then
|
||||
local count = loot_list.count( first_item.id )
|
||||
roll_controller.preview( first_item, count )
|
||||
end
|
||||
end
|
||||
|
||||
local function on_loot_slot_cleared( slot )
|
||||
loot_cache[ slot ] = nil
|
||||
end
|
||||
|
||||
local function on_loot_opened()
|
||||
for _, item in ipairs( loot_list.get_items() ) do
|
||||
local slot = loot_list.get_slot( item.id )
|
||||
|
||||
if slot then
|
||||
loot_cache[ slot ] = item
|
||||
end
|
||||
end
|
||||
|
||||
if not config.auto_process_loot() or not player_info.is_master_looter() then return end
|
||||
|
||||
if config.autostart_loot_process() then
|
||||
process_next_item()
|
||||
end
|
||||
end
|
||||
|
||||
local function on_loot_closed()
|
||||
clear_table( loot_cache )
|
||||
if m.vanilla then loot_cache.n = 0 end
|
||||
end
|
||||
|
||||
-- local function on_loot_list_item_selected( selected_item )
|
||||
-- selected_loot_list_item = selected_item
|
||||
-- end
|
||||
--
|
||||
-- local function on_loot_list_item_deselected()
|
||||
-- selected_loot_list_item = nil
|
||||
-- end
|
||||
|
||||
roll_controller.subscribe( "process_next_item", process_next_item )
|
||||
-- roll_controller.subscribe( "loot_list_item_selected", on_loot_list_item_selected )
|
||||
-- roll_controller.subscribe( "loot_list_item_deselected", on_loot_list_item_deselected )
|
||||
|
||||
return {
|
||||
on_loot_opened = on_loot_opened,
|
||||
on_loot_slot_cleared = on_loot_slot_cleared,
|
||||
on_loot_closed = on_loot_closed
|
||||
}
|
||||
end
|
||||
|
||||
m.LootAutoProcess = M
|
||||
return M
|
||||
74
src/LootAwardCallback.lua
Normal file
@ -0,0 +1,74 @@
|
||||
RollFor = RollFor or {}
|
||||
local m = RollFor
|
||||
|
||||
if m.LootAwardCallback then return end
|
||||
|
||||
local getn = m.getn
|
||||
|
||||
local M = m.Module.new( "LootAwardCallback" )
|
||||
|
||||
---@class LootAwardCallback
|
||||
---@field on_loot_awarded fun( item_id: number, item_link: string, player_name: string, player_class: string?, is_trade: boolean? )
|
||||
|
||||
---@param awarded_loot AwardedLoot
|
||||
---@param roll_controller RollController
|
||||
---@param winner_tracker WinnerTracker
|
||||
---@param group_roster GroupRoster
|
||||
---@param softres GroupAwareSoftRes
|
||||
function M.new( awarded_loot, roll_controller, winner_tracker, group_roster, softres )
|
||||
---@param item_id number
|
||||
---@param item_link string
|
||||
---@param player_name string
|
||||
---@param player_class PlayerClass?
|
||||
local function on_loot_awarded( item_id, item_link, player_name, player_class, is_trade )
|
||||
M.debug.add( string.format( "on_loot_awarded( %s, %s, %s, %s )", item_id, item_link, player_name, player_class or "nil" ) )
|
||||
local roll_tracker = roll_controller.get_roll_tracker( item_id )
|
||||
local _, current_iteration = roll_tracker.get()
|
||||
local roll_data = m.find( player_name, current_iteration.rolls, 'player_name' )
|
||||
local sr_players = softres.get( item_id )
|
||||
local sr_player = m.find( player_name, sr_players, 'name' )
|
||||
local rolling_strategy
|
||||
local class
|
||||
|
||||
if roll_data then
|
||||
rolling_strategy = current_iteration.rolling_strategy
|
||||
else
|
||||
local winners = winner_tracker.find_winners( item_link )
|
||||
local winner = m.find( player_name, winners, 'winner_name' )
|
||||
rolling_strategy = winner and winner.rolling_strategy
|
||||
end
|
||||
|
||||
if not player_class then
|
||||
local player = group_roster.find_player( player_name )
|
||||
class = player and player.class or nil
|
||||
end
|
||||
|
||||
awarded_loot.award(
|
||||
player_name,
|
||||
item_id,
|
||||
roll_data,
|
||||
rolling_strategy,
|
||||
item_link,
|
||||
player_class or class,
|
||||
sr_player and sr_player.sr_plus
|
||||
)
|
||||
|
||||
if is_trade then return end
|
||||
|
||||
if player_class then
|
||||
roll_controller.loot_awarded( item_id, item_link, player_name, player_class )
|
||||
else
|
||||
roll_controller.loot_awarded( item_id, item_link, player_name, class )
|
||||
end
|
||||
|
||||
winner_tracker.untrack( player_name, item_link )
|
||||
end
|
||||
|
||||
---@type LootAwardCallback
|
||||
return {
|
||||
on_loot_awarded = on_loot_awarded,
|
||||
}
|
||||
end
|
||||
|
||||
m.LootAwardCallback = M
|
||||
return M
|
||||
224
src/LootAwardPopup.lua
Normal file
@ -0,0 +1,224 @@
|
||||
RollFor = RollFor or {}
|
||||
local m = RollFor
|
||||
|
||||
if m.LootAwardPopup then return end
|
||||
|
||||
local M = m.Module.new( "LootAwardPopup" )
|
||||
local getn = m.getn
|
||||
|
||||
local RS = m.Types.RollingStrategy
|
||||
local RT = m.Types.RollType
|
||||
local LAE = m.Types.LootAwardError
|
||||
local red = m.colors.red
|
||||
local blue = m.colors.blue
|
||||
local c = m.colorize_player_by_class
|
||||
local r = m.roll_type_color
|
||||
local possesive_case = m.possesive_case
|
||||
local article = m.article
|
||||
|
||||
local button_defaults = {
|
||||
width = 80,
|
||||
height = 24,
|
||||
scale = 0.76
|
||||
}
|
||||
|
||||
---@class LootAwardPopup
|
||||
---@field show fun( data: MasterLootConfirmationData )
|
||||
---@field hide fun()
|
||||
|
||||
---@param popup_builder PopupBuilder
|
||||
---@param config Config
|
||||
---@param rolling_popup RollingPopup
|
||||
function M.new( popup_builder, config, rolling_popup )
|
||||
local popup
|
||||
local top_padding = config.classic_look() and 18 or 14
|
||||
local on_hide ---@type fun()?
|
||||
|
||||
local function create_popup()
|
||||
local builder = popup_builder
|
||||
:name( "RollForLootAssignmentFrame" )
|
||||
:width( 280 )
|
||||
:height( 100 )
|
||||
:sound()
|
||||
:esc()
|
||||
:gui_elements( m.GuiElements )
|
||||
:on_hide( function()
|
||||
if on_hide then
|
||||
on_hide()
|
||||
end
|
||||
end )
|
||||
:self_centered_anchor()
|
||||
:strata( "DIALOG" )
|
||||
|
||||
local anchor = rolling_popup.get_anchor_point()
|
||||
|
||||
if anchor then
|
||||
builder = builder:point( anchor )
|
||||
end
|
||||
|
||||
local frame = builder:build()
|
||||
|
||||
return frame
|
||||
end
|
||||
|
||||
local function border_color( item_id )
|
||||
local _, _, quality = m.api.GetItemInfo( string.format( "item:%s:0:0:0", item_id ) )
|
||||
local color = m.get_popup_border_color( quality or 0 )
|
||||
local col = config.classic_look() and m.brighten( color, 0.5 ) or color
|
||||
|
||||
popup:border_color( col.r, col.g, col.b, col.a )
|
||||
end
|
||||
|
||||
---@param content table
|
||||
---@param winners Winner[]
|
||||
---@param receiver ItemCandidate
|
||||
---@diagnostic disable-next-line: unused-local
|
||||
local function add_raid_roll_winners( content, winners, receiver ) -- TODO: To fix the popup display.
|
||||
for i, winner in ipairs( winners ) do
|
||||
local padding = i > 1 and 2 or 8
|
||||
local player = c( winner.name, winner.class )
|
||||
table.insert( content, { type = "text", value = string.format( "%s wins the %s.", player, blue( "raid-roll" ) ), padding = padding } )
|
||||
end
|
||||
end
|
||||
|
||||
---@param winner Winner
|
||||
---@param padding number?
|
||||
local function sr_content( winner, padding )
|
||||
M.debug.add( "sr_content" )
|
||||
local player = c( winner.name, winner.class )
|
||||
local soft_ressed = r( RT.MainSpec, "soft-ressed" )
|
||||
return { type = "text", value = string.format( "%s %s this item.", player, soft_ressed ), padding = padding or top_padding }
|
||||
end
|
||||
|
||||
---@param content table
|
||||
---@param winners Winner[]
|
||||
---@param strategy_type RollingStrategyType
|
||||
local function add_roll_winners( content, winners, strategy_type )
|
||||
local last_award_button_visible = false
|
||||
|
||||
for i, winner in ipairs( winners ) do
|
||||
local player = c( winner.name, winner.class )
|
||||
local roll_type = winner.roll_type and r( winner.roll_type )
|
||||
local roll = winner.winning_roll and blue( winner.winning_roll )
|
||||
local padding = last_award_button_visible and 8 or i == 1 and top_padding or (top_padding - 6)
|
||||
|
||||
if roll then
|
||||
table.insert( content,
|
||||
{ type = "text", value = string.format( "%s wins the %s roll with %s %s.", player, roll_type, article( winner.winning_roll ), roll ), padding = padding } )
|
||||
elseif strategy_type == RS.SoftResRoll then
|
||||
table.insert( content, sr_content( winner, padding ) )
|
||||
else
|
||||
table.insert( content, { type = "text", value = string.format( "%s %s win the roll.", player, red( "did not" ) ), padding = padding } )
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@param content table
|
||||
---@param data MasterLootConfirmationData
|
||||
local function add_winners( content, data )
|
||||
if data.strategy_type == RS.InstaRaidRoll or data.strategy_type == RS.RaidRoll then
|
||||
add_raid_roll_winners( content, data.winners, data.receiver )
|
||||
else
|
||||
add_roll_winners( content, data.winners, data.strategy_type )
|
||||
end
|
||||
end
|
||||
|
||||
---@param data MasterLootConfirmationData
|
||||
local function make_content( data )
|
||||
local content = { { type = "item_link_with_icon", link = data.item.link, texture = data.item.texture } }
|
||||
local winner_count = getn( data.winners )
|
||||
|
||||
if winner_count > 0 then
|
||||
add_winners( content, data )
|
||||
end
|
||||
|
||||
local name = c( data.receiver.name, data.receiver.class )
|
||||
-- TODO: check if receiver is a winner and add a warning if not.
|
||||
table.insert( content, { type = "text", value = string.format( "Award this item to %s?", name ), padding = 6 } )
|
||||
|
||||
if data.error then
|
||||
local message = data.error == LAE.FullBags and string.format( "%s%s %s", name, red( possesive_case( data.receiver.name ) ), red( "bags are full." ) ) or
|
||||
data.error == LAE.AlreadyOwnsUniqueItem and string.format( "%s %s", name, red( "already owns this unique item." ) ) or
|
||||
data.error == LAE.PlayerNotFound and string.format( "%s %s", name, red( "cannot be found." ) ) or
|
||||
data.error == LAE.CantAssignItemToThatPlayer and string.format( "%s %s.", red( "Can't assign this item to" ), name ) or nil
|
||||
|
||||
if message then
|
||||
table.insert( content, { type = "text", value = message, padding = 7 } )
|
||||
end
|
||||
end
|
||||
|
||||
table.insert( content, { type = "button", label = "Yes", width = 80, on_click = data.confirm_fn } )
|
||||
table.insert( content, {
|
||||
type = "button",
|
||||
label = "No",
|
||||
width = 80,
|
||||
on_click = function()
|
||||
on_hide = nil
|
||||
data.abort_fn()
|
||||
end
|
||||
} )
|
||||
|
||||
return content
|
||||
end
|
||||
|
||||
---@param data MasterLootConfirmationData
|
||||
local function show( data )
|
||||
if not popup then popup = create_popup() end
|
||||
popup:clear()
|
||||
on_hide = data.abort_fn
|
||||
|
||||
for _, v in ipairs( make_content( data ) ) do
|
||||
popup.add_line( v.type, function( type, frame, lines )
|
||||
if type == "item_link_with_icon" then
|
||||
frame:SetItem( v, v.link and m.ItemUtils.get_tooltip_link( v.link ) )
|
||||
elseif type == "text" then
|
||||
frame:SetText( v.value )
|
||||
elseif type == "button" then
|
||||
frame:SetWidth( v.width or button_defaults.width )
|
||||
frame:SetHeight( v.height or button_defaults.height )
|
||||
frame:SetText( v.label or "" )
|
||||
frame:SetScale( v.scale or button_defaults.scale )
|
||||
frame:SetScript( "OnClick", v.on_click or function() end )
|
||||
frame:SetFrameLevel( popup:GetFrameLevel() + 1 )
|
||||
end
|
||||
|
||||
if type ~= "button" then
|
||||
local count = getn( lines )
|
||||
|
||||
if count == 0 then
|
||||
local y = -top_padding - (v.padding or 0)
|
||||
frame:ClearAllPoints()
|
||||
frame:SetPoint( "TOP", popup, "TOP", 0, y )
|
||||
else
|
||||
local line_anchor = lines[ count ].frame
|
||||
frame:ClearAllPoints()
|
||||
frame:SetPoint( "TOP", line_anchor, "BOTTOM", 0, v.padding and -v.padding or 0 )
|
||||
end
|
||||
end
|
||||
end, v.padding )
|
||||
end
|
||||
|
||||
border_color( data.item.id )
|
||||
|
||||
popup:ClearAllPoints()
|
||||
popup:SetPoint( "CENTER", rolling_popup.get_frame(), "CENTER", 0, 0 )
|
||||
|
||||
popup:Show()
|
||||
end
|
||||
|
||||
local function hide()
|
||||
if popup then
|
||||
on_hide = nil
|
||||
popup:Hide()
|
||||
end
|
||||
end
|
||||
|
||||
---@type LootAwardPopup
|
||||
return {
|
||||
show = show,
|
||||
hide = hide
|
||||
}
|
||||
end
|
||||
|
||||
m.LootAwardPopup = M
|
||||
return M
|
||||
344
src/LootController.lua
Normal file
@ -0,0 +1,344 @@
|
||||
RollFor = RollFor or {}
|
||||
local m = RollFor
|
||||
|
||||
if m.LootController then return end
|
||||
|
||||
local M = m.Module.new( "LootController" )
|
||||
|
||||
local getn = m.getn
|
||||
local red, orange, hl = m.colors.red, m.colors.orange, m.colors.hl
|
||||
local item_utils = m.ItemUtils
|
||||
|
||||
---@alias SelectedItem { item_id: number, comment: string? }
|
||||
|
||||
---@param player_info PlayerInfo
|
||||
---@param loot_facade LootFacade
|
||||
---@param loot_list LootList
|
||||
---@param loot_frame LootFrame
|
||||
---@param roll_controller RollController
|
||||
---@param softres GroupAwareSoftRes
|
||||
---@param rolling_logic RollingLogic
|
||||
---@param chat Chat
|
||||
function M.new( player_info, loot_facade, loot_list, loot_frame, roll_controller, softres, rolling_logic, chat )
|
||||
-- This will store which items were selected, because we'll lost that info when the loot is closed.
|
||||
-- Upon loot opening, we'll check it here and reselect if appropriate.
|
||||
local item_selection_cache = {}
|
||||
local selected_item = nil ---@type SelectedItem?
|
||||
|
||||
local function show()
|
||||
M.debug.add( "show" )
|
||||
loot_frame.show()
|
||||
end
|
||||
|
||||
---@param item SelectedItem
|
||||
local function make_cache_key( item )
|
||||
return string.format( "%s|%s", item.item_id, item.comment or "" )
|
||||
end
|
||||
|
||||
---@param entries LootListEntry[]
|
||||
---@param item_id number
|
||||
---@param comment string?
|
||||
local function count_selected_items( entries, item_id, comment )
|
||||
local result = 0
|
||||
|
||||
for _, entry in ipairs( entries ) do
|
||||
if entry.item.id == item_id and entry.comment == comment then
|
||||
result = result + 1
|
||||
end
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
---@param item_id number
|
||||
---@param entries LootListEntry[]
|
||||
local function find_top_priority_comment( item_id, entries ) -- HR > SR > none
|
||||
for _, entry in ipairs( entries ) do
|
||||
if entry.item.id == item_id and entry.hard_ressed then
|
||||
return entry.comment
|
||||
end
|
||||
|
||||
if entry.item.id == item_id and entry.soft_ressed then
|
||||
return entry.comment
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@param entries LootListEntry[]
|
||||
---@param item DroppedItem
|
||||
---@param hard_ressed boolean?
|
||||
---@param soft_ressed boolean?
|
||||
local function select_item( entries, item, hard_ressed, soft_ressed )
|
||||
M.debug.add( string.format( "select_item( %s, %s )", item.id, hard_ressed and "hr" or soft_ressed and "sr" or "free roll" ) )
|
||||
local c = find_top_priority_comment( item.id, entries )
|
||||
local count = count_selected_items( entries, item.id, c )
|
||||
|
||||
selected_item = { item_id = item.id, comment = c }
|
||||
local key = make_cache_key( selected_item )
|
||||
item_selection_cache[ key ] = selected_item
|
||||
|
||||
roll_controller.preview( item, count )
|
||||
end
|
||||
|
||||
---@param items (DroppedItem|Coin)[]
|
||||
local function make_sr_player_map( items )
|
||||
local result = {}
|
||||
|
||||
for _, item in ipairs( items ) do
|
||||
if item.type ~= "Coin" then
|
||||
result[ item.id ] = softres.get( item.id )
|
||||
end
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
---@return RollingPlayer?
|
||||
local function pop_first_item_from_a_table( t )
|
||||
if getn( t ) == 0 then return nil end
|
||||
|
||||
local result = t[ 1 ]
|
||||
table.remove( t, 1 )
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
---@param sr_players RollingPlayer[]
|
||||
local function make_comment_tooltip( sr_players )
|
||||
local result = { orange( "Soft-ressed by" ) }
|
||||
|
||||
if getn( sr_players ) == 1 then
|
||||
local player = sr_players[ 1 ]
|
||||
table.insert( result, m.colorize_player_by_class( player.name, player.class ) )
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
for _, player in ipairs( sr_players ) do
|
||||
local rolls = player.rolls and player.rolls > 1 and hl( string.format( " [%s rolls]", player.rolls ) ) or ""
|
||||
table.insert( result, string.format( "%s%s", m.colorize_player_by_class( player.name, player.class ), rolls ) )
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
---@alias LootListEntry {
|
||||
--- item: DroppedItem|Coin,
|
||||
--- comment: string?,
|
||||
--- comment_tooltip: string?,
|
||||
--- hard_ressed: boolean?,
|
||||
--- soft_ressed: boolean? }
|
||||
|
||||
---@param item_id number
|
||||
---@param entries LootListEntry[]
|
||||
local function is_already_hr( item_id, entries )
|
||||
for _, entry in ipairs( entries ) do
|
||||
if entry.item.id == item_id and entry.hard_ressed then return true end
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
---@param items (DroppedItem|Coin)[]
|
||||
---@return LootListEntry[]
|
||||
local function get_entries( items )
|
||||
local result = {}
|
||||
local sr_player_map = make_sr_player_map( items )
|
||||
|
||||
for _, item in ipairs( items ) do
|
||||
if item.type == "Coin" then
|
||||
table.insert( result, { item = item } )
|
||||
elseif softres.is_item_hardressed( item.id ) and not is_already_hr( item.id, result ) then
|
||||
table.insert( result, { item = item, comment = red( "HR" ), hard_ressed = true } )
|
||||
else
|
||||
local sr_players = softres.get( item.id )
|
||||
local sr_player_count = getn( sr_players )
|
||||
local item_count = loot_list.count( item.id )
|
||||
|
||||
if is_already_hr( item.id, result ) then item_count = item_count - 1 end
|
||||
|
||||
if item_count > 0 then
|
||||
if sr_player_count > 0 then
|
||||
if sr_player_count > item_count then
|
||||
table.insert( result, { item = item, comment = orange( "SR" ), comment_tooltip = make_comment_tooltip( sr_players ), soft_ressed = true } )
|
||||
else
|
||||
local sr_player = pop_first_item_from_a_table( sr_player_map[ item.id ] )
|
||||
|
||||
if sr_player then
|
||||
table.insert( result, { item = item, comment = orange( "SR" ), comment_tooltip = make_comment_tooltip( { sr_player } ), soft_ressed = true } )
|
||||
else
|
||||
table.insert( result, { item = item } )
|
||||
end
|
||||
end
|
||||
else
|
||||
table.insert( result, { item = item } )
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
---@param item_id number
|
||||
---@param comment string?
|
||||
local function should_be_selected( item_id, comment )
|
||||
if selected_item and selected_item.item_id == item_id and selected_item.comment == comment then
|
||||
return true
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
---@param entries LootListEntry[]
|
||||
local function find_selected_item( entries )
|
||||
for _, entry in ipairs( entries ) do
|
||||
if entry.item.type ~= "Coin" then
|
||||
local key = make_cache_key( { item_id = entry.item.id, comment = entry.comment } )
|
||||
|
||||
if item_selection_cache[ key ] then
|
||||
return item_selection_cache[ key ]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function update()
|
||||
M.debug.add( "update" )
|
||||
|
||||
local items = loot_list.get_items() ---@type (DroppedItem|Coin)[]
|
||||
local entries = get_entries( items )
|
||||
|
||||
selected_item = find_selected_item( entries )
|
||||
|
||||
---@type LootFrameItem[]
|
||||
local result = {}
|
||||
|
||||
for index, entry in ipairs( entries ) do
|
||||
local item = entry.item
|
||||
local is_coin = item.type == "Coin"
|
||||
local selected = should_be_selected( item.id, entry.comment )
|
||||
local selected_entry = entry -- Fucking lua50 and its broken closures.
|
||||
|
||||
---@type LootFrameItem
|
||||
table.insert( result, {
|
||||
index = index,
|
||||
texture = item.texture,
|
||||
name = is_coin and m.one_line_coin_name( item.amount_text ) or item.name,
|
||||
quality = item.quality or 0,
|
||||
quantity = item.quantity,
|
||||
link = item.link,
|
||||
click_fn = function()
|
||||
if m.is_ctrl_key_down() then
|
||||
m.api.DressUpItemLink( item.link )
|
||||
return
|
||||
end
|
||||
|
||||
if m.is_shift_key_down() then
|
||||
m.link_item_in_chat( item.link )
|
||||
return
|
||||
end
|
||||
|
||||
if m.bcc and (is_coin or item.quality < 2) then
|
||||
local slot = loot_list.get_slot( item.id )
|
||||
if slot then loot_facade.loot_slot( slot ) end
|
||||
return
|
||||
end
|
||||
|
||||
if rolling_logic.is_rolling() then
|
||||
chat.info( "Cannot select item while rolling is in progress.", m.colors.red )
|
||||
return
|
||||
end
|
||||
|
||||
local master_loot = m.is_master_loot()
|
||||
|
||||
if is_coin or selected or not master_loot then return end
|
||||
|
||||
if master_loot and not player_info.is_master_looter() then
|
||||
chat.info( "You are not the master looter.", m.colors.red )
|
||||
return
|
||||
end
|
||||
|
||||
select_item( entries, selected_entry.item --[[@as DroppedItem]], selected_entry.hard_ressed, selected_entry.soft_ressed ); update()
|
||||
end,
|
||||
is_selected = selected or false,
|
||||
is_enabled = selected or not selected_item or false,
|
||||
slot = loot_list.get_slot( is_coin and "Coin" or item.id ),
|
||||
tooltip_link = item.tooltip_link,
|
||||
comment = entry.comment,
|
||||
comment_tooltip = entry.comment_tooltip,
|
||||
bind = item_utils.bind_abbrev( item.bind )
|
||||
} )
|
||||
end
|
||||
|
||||
loot_frame.update( result )
|
||||
end
|
||||
|
||||
local function hide()
|
||||
M.debug.add( "hide" )
|
||||
loot_frame.hide()
|
||||
end
|
||||
|
||||
---@class LootFrameDeselectData
|
||||
---@field item_id number?
|
||||
|
||||
---@param data LootFrameDeselectData
|
||||
local function deselect( data )
|
||||
if data.item_id then
|
||||
local key = string.format( "^%s", data.item_id )
|
||||
|
||||
for k, _ in pairs( item_selection_cache ) do
|
||||
if string.find( k, key ) then item_selection_cache[ k ] = nil end
|
||||
end
|
||||
|
||||
update()
|
||||
return
|
||||
end
|
||||
|
||||
if not selected_item then return end
|
||||
|
||||
M.debug.add( "deselect" )
|
||||
local key = make_cache_key( selected_item )
|
||||
item_selection_cache[ key ] = nil
|
||||
selected_item = nil
|
||||
|
||||
update()
|
||||
end
|
||||
|
||||
local function on_loot_opened()
|
||||
M.debug.add( "loot_opened" )
|
||||
show()
|
||||
update()
|
||||
|
||||
if selected_item then
|
||||
roll_controller.update( selected_item.item_id )
|
||||
end
|
||||
end
|
||||
|
||||
local function on_loot_slot_cleared( slot )
|
||||
M.debug.add( string.format( "loot_slot_cleared( %s )", slot ) )
|
||||
update()
|
||||
end
|
||||
|
||||
local function on_loot_closed()
|
||||
M.debug.add( "loot_closed" )
|
||||
hide()
|
||||
end
|
||||
|
||||
---@param item_id number
|
||||
local function clear_selection_cache( item_id )
|
||||
for k, v in pairs( item_selection_cache ) do
|
||||
if v.item_id == item_id then item_selection_cache[ k ] = nil end
|
||||
end
|
||||
end
|
||||
|
||||
loot_facade.subscribe( "LootOpened", on_loot_opened )
|
||||
loot_facade.subscribe( "LootClosed", on_loot_closed )
|
||||
loot_facade.subscribe( "LootSlotCleared", on_loot_slot_cleared )
|
||||
roll_controller.subscribe( "LootFrameDeselect", deselect )
|
||||
roll_controller.subscribe( "LootFrameClearSelectionCache", clear_selection_cache )
|
||||
roll_controller.subscribe( "LootFrameUpdate", update )
|
||||
end
|
||||
|
||||
m.LootController = M
|
||||
return M
|
||||
132
src/LootFacade.lua
Normal file
@ -0,0 +1,132 @@
|
||||
RollFor = RollFor or {}
|
||||
local m = RollFor
|
||||
|
||||
if m.LootFacade then return end
|
||||
|
||||
local M = {}
|
||||
local interface = m.Interface
|
||||
|
||||
M.interface = {
|
||||
subscribe = "function",
|
||||
get_item_count = "function",
|
||||
get_source_guid = "function",
|
||||
get_link = "function",
|
||||
get_info = "function",
|
||||
is_item = "function",
|
||||
is_coin = "function",
|
||||
loot_slot = "function"
|
||||
}
|
||||
|
||||
---@class LootSlotInfo
|
||||
---@field texture string
|
||||
---@field name string
|
||||
---@field quantity number
|
||||
---@field quality number
|
||||
|
||||
---@class LootFacade
|
||||
---@field subscribe fun( event_name: LootEventName, callback: fun( arg: any? ) )
|
||||
---@field get_item_count fun(): number
|
||||
---@field get_source_guid fun(): string
|
||||
---@field get_link fun( slot: number ): ItemLink
|
||||
---@field get_info fun( slot: number ): LootSlotInfo
|
||||
---@field is_item fun( slot: number ): boolean
|
||||
---@field is_coin fun( slot: number ): boolean
|
||||
---@field loot_slot fun( slot: number )
|
||||
|
||||
---@alias LootEventName
|
||||
---| "LootOpened"
|
||||
---| "LootClosed"
|
||||
---| "LootSlotCleared"
|
||||
---| "ChatMsgLoot"
|
||||
|
||||
function M.new( event_frame, api )
|
||||
interface.validate( api, m.WowApi.LootInterface )
|
||||
|
||||
---@param event_name LootEventName
|
||||
---@param callback fun()
|
||||
local function subscribe( event_name, callback )
|
||||
local blizz_event =
|
||||
event_name == "LootOpened" and "LOOT_OPENED" or
|
||||
event_name == "LootClosed" and "LOOT_CLOSED" or
|
||||
event_name == "LootSlotCleared" and "LOOT_SLOT_CLEARED" or
|
||||
event_name == "ChatMsgLoot" and "CHAT_MSG_LOOT"
|
||||
|
||||
if blizz_event then
|
||||
event_frame.subscribe( blizz_event, callback )
|
||||
end
|
||||
end
|
||||
|
||||
---@return number
|
||||
local function get_item_count()
|
||||
return api.GetNumLootItems()
|
||||
end
|
||||
|
||||
---@return string?
|
||||
local function get_source_guid()
|
||||
return m.UnitGUID( api, "target" )
|
||||
end
|
||||
|
||||
---@param slot number
|
||||
---@return ItemLink?
|
||||
local function get_link( slot )
|
||||
return api.GetLootSlotLink( slot )
|
||||
end
|
||||
|
||||
---@param slot number
|
||||
---@return LootSlotInfo?
|
||||
local function get_info( slot )
|
||||
if m.vanilla then
|
||||
local texture, name, quantity, quality = api.GetLootSlotInfo( slot )
|
||||
|
||||
return texture and {
|
||||
texture = texture,
|
||||
name = name,
|
||||
quantity = quantity,
|
||||
quality = quality
|
||||
} or nil
|
||||
else
|
||||
local texture, name, quantity, _, quality = api.GetLootSlotInfo( slot )
|
||||
|
||||
return texture and {
|
||||
texture = texture,
|
||||
name = name,
|
||||
quantity = quantity,
|
||||
quality = quality
|
||||
} or nil
|
||||
end
|
||||
end
|
||||
|
||||
---@param slot number
|
||||
---@return boolean
|
||||
local function is_item( slot )
|
||||
local slot_type = api.GetLootSlotType( slot )
|
||||
return slot_type == api.LOOT_SLOT_ITEM
|
||||
end
|
||||
|
||||
---@param slot number
|
||||
---@return boolean
|
||||
local function is_coin( slot )
|
||||
local slot_type = api.GetLootSlotType( slot )
|
||||
return slot_type == api.LOOT_SLOT_MONEY
|
||||
end
|
||||
|
||||
---@param slot number
|
||||
local function loot_slot( slot )
|
||||
api.LootSlot( slot )
|
||||
end
|
||||
|
||||
---@type LootFacade
|
||||
return {
|
||||
subscribe = subscribe,
|
||||
get_item_count = get_item_count,
|
||||
get_source_guid = get_source_guid,
|
||||
get_link = get_link,
|
||||
get_info = get_info,
|
||||
is_item = is_item,
|
||||
is_coin = is_coin,
|
||||
loot_slot = loot_slot
|
||||
}
|
||||
end
|
||||
|
||||
m.LootFacade = M
|
||||
return M
|
||||
76
src/LootFacadeListener.lua
Normal file
@ -0,0 +1,76 @@
|
||||
RollFor = RollFor or {}
|
||||
local m = RollFor
|
||||
|
||||
if m.LootFacadeListener then return end
|
||||
|
||||
local IU = m.ItemUtils
|
||||
|
||||
local M = {}
|
||||
|
||||
---@param loot_facade LootFacade
|
||||
---@param auto_loot AutoLoot
|
||||
---@param dropped_loot_announce DroppedLootAnnounce
|
||||
---@param master_loot MasterLoot
|
||||
---@param auto_group_loot AutoGroupLoot
|
||||
---@param roll_controller RollController
|
||||
---@param player_info PlayerInfo
|
||||
function M.new(
|
||||
loot_facade,
|
||||
auto_loot,
|
||||
dropped_loot_announce,
|
||||
master_loot,
|
||||
auto_group_loot,
|
||||
roll_controller,
|
||||
player_info
|
||||
)
|
||||
loot_facade.subscribe( "LootOpened", function()
|
||||
auto_loot.on_loot_opened()
|
||||
dropped_loot_announce.on_loot_opened()
|
||||
master_loot.on_loot_opened()
|
||||
auto_group_loot.on_loot_opened()
|
||||
roll_controller.loot_opened()
|
||||
end )
|
||||
|
||||
loot_facade.subscribe( "LootClosed", function()
|
||||
roll_controller.loot_closed()
|
||||
end )
|
||||
|
||||
loot_facade.subscribe( "LootSlotCleared", function( slot )
|
||||
master_loot.on_loot_slot_cleared( slot )
|
||||
auto_group_loot.on_loot_slot_cleared()
|
||||
end )
|
||||
|
||||
-- This covers the scenario where the master looter assigns the loot and then moves immediately,
|
||||
-- causing the loot frame to close. In normal circumstances, when the last item gets assigned,
|
||||
-- the LOOT_SLOT_CLEARED fires and then LOOT_CLOSED event follows. In this case, however,
|
||||
-- LOOT_CLOSED fires first, because of the player movement and the LOOT_SLOT_CLEARED doesn't
|
||||
-- (because we're not looting anymore).
|
||||
local function on_chat_msg_loot( message )
|
||||
for player_name, link_with_optional_quantity in string.gmatch( message, "(.-) receives loot: (.*)" ) do
|
||||
local item_link = IU.parse_link( link_with_optional_quantity )
|
||||
local item_id = item_link and IU.get_item_id( item_link )
|
||||
|
||||
if item_id and item_link then
|
||||
master_loot.on_loot_received( player_name, item_id, item_link )
|
||||
end
|
||||
|
||||
return
|
||||
end
|
||||
|
||||
for link_with_optional_quantity in string.gmatch( message, "You receive loot: (.*)" ) do
|
||||
local item_link = IU.parse_link( link_with_optional_quantity )
|
||||
local item_id = item_link and IU.get_item_id( item_link )
|
||||
|
||||
if item_id and item_link then
|
||||
master_loot.on_loot_received( player_info.get_name(), item_id, item_link )
|
||||
end
|
||||
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
loot_facade.subscribe( "ChatMsgLoot", on_chat_msg_loot )
|
||||
end
|
||||
|
||||
m.LootFacadeListener = M
|
||||
return M
|
||||
219
src/LootFrame.lua
Normal file
@ -0,0 +1,219 @@
|
||||
RollFor = RollFor or {}
|
||||
local m = RollFor
|
||||
if m.LootFrame then return end
|
||||
|
||||
local M = m.Module.new( "LootFrame" )
|
||||
|
||||
---@class LootFrame
|
||||
---@field show fun()
|
||||
---@field update fun( items: LootFrameItem[] )
|
||||
---@field hide fun()
|
||||
---@field get_frame fun(): Frame
|
||||
|
||||
M.center_point = { point = "CENTER", relative_point = "CENTER", x = -260, y = 220 }
|
||||
|
||||
---@class LootFrameSkin
|
||||
---@field header fun( on_drag_stop: function, on_show: function, on_hide: function ): Frame
|
||||
---@field body fun( parent: Frame ): Frame
|
||||
---@field dropped_item fun(): Frame
|
||||
---@field footer fun( parent: Frame ): Frame?
|
||||
---@field get_item_height fun(): number
|
||||
|
||||
---@param loot_frame_skin LootFrameSkin
|
||||
---@param db table
|
||||
---@param config Config
|
||||
function M.new( loot_frame_skin, db, config )
|
||||
---@type Frame
|
||||
local header_frame
|
||||
---@type Frame
|
||||
local body_frame
|
||||
---@type Frame?
|
||||
local footer_frame
|
||||
|
||||
local boss_name_width = 0
|
||||
local max_frame_width
|
||||
local max_item_count
|
||||
|
||||
local function on_drag_stop( frame )
|
||||
local point, _, relative_point, x, y = frame:GetPoint()
|
||||
|
||||
if m.is_frame_out_of_bounds( frame ) then
|
||||
db.point = M.center_point
|
||||
frame:position( M.center_point )
|
||||
|
||||
return
|
||||
end
|
||||
|
||||
db.point = { point = point, relative_point = relative_point, x = x, y = y }
|
||||
end
|
||||
|
||||
local function create_header_frame()
|
||||
local function on_show()
|
||||
body_frame:Show()
|
||||
if footer_frame then footer_frame:Show() end
|
||||
end
|
||||
|
||||
local function on_hide()
|
||||
body_frame:Hide()
|
||||
if footer_frame then footer_frame:Hide() end
|
||||
end
|
||||
|
||||
local frame = loot_frame_skin.header( on_drag_stop, on_show, on_hide )
|
||||
frame:ClearAllPoints()
|
||||
|
||||
if db.point then
|
||||
local p = db.point
|
||||
---@diagnostic disable-next-line: undefined-global
|
||||
frame:SetPoint( p.point, UIParent, p.relative_point, p.x, p.y )
|
||||
else
|
||||
frame:position( M.center_point )
|
||||
end
|
||||
|
||||
return frame
|
||||
end
|
||||
|
||||
local function update_boss_name_frame()
|
||||
header_frame.clear()
|
||||
header_frame.add_line( "text", function( type, frame )
|
||||
if type == "text" then
|
||||
frame:ClearAllPoints()
|
||||
frame:SetHeight( 16 )
|
||||
frame:SetPoint( "CENTER", 1, 0 )
|
||||
frame:SetTextColor( 0.125, 0.624, 0.976 )
|
||||
|
||||
local name = m.api.UnitName( "target" )
|
||||
|
||||
if not name then
|
||||
frame:SetText( "Loot" )
|
||||
else
|
||||
frame:SetText( string.format( "%s%s Loot", name, m.possesive_case( name ) ) )
|
||||
end
|
||||
|
||||
boss_name_width = frame:GetStringWidth() + 30
|
||||
end
|
||||
end, 0 )
|
||||
end
|
||||
|
||||
local function show()
|
||||
M.debug.add( "show" )
|
||||
update_boss_name_frame()
|
||||
max_frame_width = nil
|
||||
max_item_count = nil
|
||||
header_frame:Show()
|
||||
end
|
||||
|
||||
local function hide()
|
||||
if header_frame then
|
||||
M.debug.add( "hide" )
|
||||
header_frame:Hide()
|
||||
end
|
||||
end
|
||||
|
||||
---@class LootFrameItem
|
||||
---@field index number
|
||||
---@field texture ItemTexture
|
||||
---@field name string
|
||||
---@field quality ItemQuality
|
||||
---@field quantity number
|
||||
---@field link ItemLink
|
||||
---@field click_fn fun()
|
||||
---@field is_selected boolean
|
||||
---@field is_enabled boolean
|
||||
---@field slot number?
|
||||
---@field tooltip_link TooltipItemLink?
|
||||
---@field comment string?
|
||||
---@field comment_tooltip string[]?
|
||||
---@field bind string?
|
||||
|
||||
---@param items LootFrameItem[]
|
||||
local function update( items )
|
||||
M.debug.add( "update" )
|
||||
body_frame.clear()
|
||||
|
||||
local content = {}
|
||||
|
||||
for _, item in ipairs( items ) do
|
||||
table.insert( content, {
|
||||
type = "dropped_item",
|
||||
item = item
|
||||
} )
|
||||
end
|
||||
|
||||
local max_width = 0
|
||||
local anchor
|
||||
local item_count = 0
|
||||
local frames = {}
|
||||
|
||||
for _, v in ipairs( content ) do
|
||||
body_frame.add_line( v.type, function( type, frame )
|
||||
if type == "dropped_item" then
|
||||
local item = v.item ---@type LootFrameItem
|
||||
frame:SetItem( item )
|
||||
frame:ClearAllPoints()
|
||||
|
||||
if max_frame_width then
|
||||
frame:SetWidth( max_frame_width - 2 )
|
||||
end
|
||||
|
||||
if not anchor then
|
||||
frame:SetPoint( "TOPLEFT", body_frame, "TOPLEFT", 0, 0 )
|
||||
frame:SetPoint( "TOPRIGHT", body_frame, "TOPRIGHT", 0, 0 )
|
||||
else
|
||||
frame:SetPoint( "TOPLEFT", anchor, "BOTTOMLEFT", 0, 0 )
|
||||
frame:SetPoint( "TOPRIGHT", anchor, "BOTTOMRIGHT", 0, 0 )
|
||||
end
|
||||
|
||||
anchor = frame
|
||||
|
||||
local w = frame:GetWidth() + 2
|
||||
if w > max_width then max_width = w end
|
||||
item_count = item_count + 1
|
||||
|
||||
table.insert( frames, frame )
|
||||
end
|
||||
end, 0 )
|
||||
end
|
||||
|
||||
max_frame_width = m.lua.math.max( boss_name_width, max_width )
|
||||
max_item_count = max_item_count or item_count
|
||||
|
||||
header_frame:SetWidth( max_frame_width )
|
||||
body_frame:SetWidth( max_frame_width )
|
||||
body_frame:SetHeight( item_count * loot_frame_skin.get_item_height() + 1 )
|
||||
|
||||
if footer_frame then
|
||||
footer_frame:ClearAllPoints()
|
||||
footer_frame:SetWidth( max_frame_width )
|
||||
footer_frame:SetPoint( "TOP", body_frame, "BOTTOM", 0, 2 )
|
||||
end
|
||||
|
||||
for _, frame in ipairs( frames ) do
|
||||
frame:SetWidth( max_frame_width - 2 )
|
||||
end
|
||||
|
||||
if config.loot_frame_cursor() and item_count == max_item_count then
|
||||
local uiScale, x, y = m.api.UIParent:GetEffectiveScale(), m.api.GetCursorPosition()
|
||||
header_frame:SetPoint( "TOPLEFT", m.api.UIParent, "BOTTOMLEFT", (x / uiScale) -10, (y / uiScale) + 30 )
|
||||
end
|
||||
end
|
||||
|
||||
config.subscribe( "reset_loot_frame", function()
|
||||
db.point = nil
|
||||
if header_frame then header_frame:position( M.center_point ) end
|
||||
end )
|
||||
|
||||
header_frame = create_header_frame()
|
||||
body_frame = loot_frame_skin.body( header_frame )
|
||||
footer_frame = loot_frame_skin.footer( header_frame )
|
||||
|
||||
---@type LootFrame
|
||||
return {
|
||||
show = show,
|
||||
update = update,
|
||||
hide = hide,
|
||||
get_frame = function() return header_frame end
|
||||
}
|
||||
end
|
||||
|
||||
m.LootFrame = M
|
||||
return M
|
||||
180
src/LootList.lua
Normal file
@ -0,0 +1,180 @@
|
||||
RollFor = RollFor or {}
|
||||
local m = RollFor
|
||||
if m.LootList then return end
|
||||
|
||||
local M = m.Module.new( "LootList" )
|
||||
|
||||
local getn = m.getn
|
||||
local interface = m.Interface
|
||||
local clear = m.clear_table
|
||||
|
||||
---@class LootList
|
||||
---@field get_items fun(): DroppedItem[]
|
||||
---@field get_source_guid fun(): string
|
||||
---@field get_slot fun( item_id: number|"Coin" ): number?
|
||||
---@field is_looting fun(): boolean
|
||||
---@field count fun( item_id: number ): number
|
||||
---@field size fun(): number
|
||||
|
||||
---@param loot_facade LootFacade
|
||||
---@param item_utils ItemUtils
|
||||
---@param tooltip_reader TooltipReader
|
||||
---@param boss_list BossList
|
||||
---@return LootList
|
||||
function M.new( loot_facade, item_utils, tooltip_reader, boss_list, dummy_items_fn )
|
||||
interface.validate( loot_facade, m.LootFacade.interface )
|
||||
interface.validate( item_utils, m.ItemUtils.interface )
|
||||
|
||||
---@alias Slot number
|
||||
---@type table<Slot, Coin|DroppedItem>
|
||||
local items = {}
|
||||
local lf = loot_facade
|
||||
local looting = false
|
||||
local source_guid
|
||||
|
||||
local function clear_items()
|
||||
clear( items )
|
||||
source_guid = nil
|
||||
end
|
||||
|
||||
local function add_item( slot, item, item_count )
|
||||
local dummy_items = dummy_items_fn and dummy_items_fn() or {}
|
||||
local dummy_item_count = getn( dummy_items )
|
||||
local new_item = item_count > dummy_item_count and item or dummy_items[ item_count ]
|
||||
|
||||
items[ slot ] = new_item
|
||||
end
|
||||
|
||||
local function on_loot_opened()
|
||||
M.debug.add( "loot_opened" )
|
||||
clear_items()
|
||||
looting = true
|
||||
source_guid = lf.get_source_guid()
|
||||
|
||||
local item_count = 1
|
||||
|
||||
for slot = 1, lf.get_item_count() do
|
||||
if lf.is_coin( slot ) then
|
||||
local info = lf.get_info( slot )
|
||||
|
||||
if info then
|
||||
items[ slot ] = item_utils.make_coin( info.texture, info.name )
|
||||
end
|
||||
else
|
||||
local link = lf.get_link( slot )
|
||||
local info = lf.get_info( slot )
|
||||
local item_id = link and item_utils.get_item_id( link )
|
||||
local item_name = link and item_utils.get_item_name( link )
|
||||
local tooltip_link = link and item_utils.get_tooltip_link( link )
|
||||
local bind_type = tooltip_reader.get_slot_bind_type( slot )
|
||||
local classes = tooltip_reader.get_slot_classes( slot )
|
||||
|
||||
local is_boss_loot = false
|
||||
if m.api.UnitName then -- workaround to make tests work
|
||||
local target_name = m.target_name()
|
||||
if target_name and m.target_dead() then
|
||||
local zone_name = m.api.GetRealZoneText()
|
||||
local bosses = boss_list[ zone_name ] or {}
|
||||
is_boss_loot = m.table_contains_value( bosses, target_name )
|
||||
end
|
||||
end
|
||||
|
||||
if item_id and item_name then
|
||||
add_item( slot,
|
||||
item_utils.make_dropped_item(
|
||||
item_id,
|
||||
item_name,
|
||||
link,
|
||||
tooltip_link,
|
||||
info and info.quality,
|
||||
info and info.quantity,
|
||||
info and info.texture,
|
||||
bind_type,
|
||||
classes,
|
||||
is_boss_loot
|
||||
), item_count )
|
||||
|
||||
item_count = item_count + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function on_loot_closed()
|
||||
M.debug.add( "loot_closed" )
|
||||
clear_items()
|
||||
looting = false
|
||||
end
|
||||
|
||||
local function on_loot_slot_cleared( slot )
|
||||
M.debug.add( "loot_slot_cleared" )
|
||||
items[ slot ] = nil
|
||||
end
|
||||
|
||||
local function get_items()
|
||||
local result = {}
|
||||
|
||||
for _, item in pairs( items ) do
|
||||
table.insert( result, item )
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
loot_facade.subscribe( "LootOpened", on_loot_opened )
|
||||
loot_facade.subscribe( "LootClosed", on_loot_closed )
|
||||
loot_facade.subscribe( "LootSlotCleared", on_loot_slot_cleared )
|
||||
|
||||
---@param item_id number|"Coin"
|
||||
---@return number?
|
||||
local function get_slot( item_id )
|
||||
for slot, item in pairs( items ) do
|
||||
if item_id == "Coin" and item.type == "Coin" then
|
||||
return slot
|
||||
end
|
||||
|
||||
if item.id == item_id then
|
||||
return slot
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function is_looting()
|
||||
return looting
|
||||
end
|
||||
|
||||
local function count( item_id )
|
||||
local result = 0
|
||||
|
||||
for _, item in pairs( items ) do
|
||||
if item.id == item_id then
|
||||
result = result + 1
|
||||
end
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
local function size()
|
||||
local result = 0
|
||||
|
||||
for _ in pairs( items ) do
|
||||
result = result + 1
|
||||
end
|
||||
|
||||
return 0
|
||||
end
|
||||
|
||||
---@type LootList
|
||||
return {
|
||||
get_items = get_items,
|
||||
get_source_guid = function() return source_guid end,
|
||||
get_slot = get_slot,
|
||||
is_looting = is_looting,
|
||||
count = count,
|
||||
size = size
|
||||
}
|
||||
end
|
||||
|
||||
m.LootList = M
|
||||
return M
|
||||
139
src/MasterLoot.lua
Normal file
@ -0,0 +1,139 @@
|
||||
RollFor = RollFor or {}
|
||||
local m = RollFor
|
||||
|
||||
if m.MasterLoot then return end
|
||||
|
||||
local M = m.Module.new( "MasterLoot" )
|
||||
local pretty_print = m.pretty_print
|
||||
local hl = m.colors.hl
|
||||
local clear_table = m.clear_table
|
||||
local err = m.err
|
||||
|
||||
---@class MasterLoot
|
||||
---@field on_loot_opened fun()
|
||||
---@field on_recipient_inventory_full fun()
|
||||
---@field on_player_is_too_far fun()
|
||||
---@field on_unknown_error_message fun( message: string )
|
||||
---@field on_loot_slot_cleared fun( slot: number )
|
||||
---@field on_loot_received fun( player_name: string, item_id: number, item_link: string )
|
||||
|
||||
---@param master_loot_candidates MasterLootCandidates
|
||||
---@param loot_award_callback LootAwardCallback
|
||||
---@param loot_list LootList
|
||||
---@param roll_controller RollController
|
||||
function M.new( master_loot_candidates, loot_award_callback, loot_list, roll_controller )
|
||||
---@type { player: ItemCandidate|Winner, item: Item }?
|
||||
local m_confirmed = nil
|
||||
local m_slot_cache = {}
|
||||
|
||||
local function reset_confirmation()
|
||||
M.debug.add( "reset_confirmation" )
|
||||
m_confirmed = nil
|
||||
end
|
||||
|
||||
-- We are storing the item in the slot cache (m_slot_cache) and ML confirmation (m_confirmed).
|
||||
-- This is to correlate the loot award event which we have to do using LOOT_SLOT_CLEARED,
|
||||
-- because CHAT_MSG_LOOT doesn't seem to be synced with LOOT_ events.
|
||||
-- Normally one would expect CHAT_MSG_LOOT to happen before LOOT_SLOT_CLEARED, or at least
|
||||
-- before LOOT_CLOSED, but this is what happened once:
|
||||
-- LOOT_OPENED -> LOOT_SLOT_CLEARED -> LOOT_CLOSED -> CHAT_MSG_LOOT.
|
||||
-- It's safer and simpler to just rely on LOOT_ events.
|
||||
local function on_loot_slot_cleared( slot )
|
||||
M.debug.add( string.format( "on_loot_slot_cleared( %s )", slot or nil ) )
|
||||
if not m_slot_cache[ slot ] or not m_confirmed then return end
|
||||
|
||||
local cached_item = m_slot_cache[ slot ]
|
||||
|
||||
if cached_item.id == m_confirmed.item.id then
|
||||
loot_award_callback.on_loot_awarded( m_confirmed.item.id, m_confirmed.item.link, m_confirmed.player.name, m_confirmed.player.class )
|
||||
reset_confirmation()
|
||||
end
|
||||
|
||||
m_slot_cache[ slot ] = nil
|
||||
end
|
||||
|
||||
---@param data AwardConfirmedData
|
||||
local function on_confirm( data )
|
||||
local player = data.player
|
||||
local item = data.item
|
||||
|
||||
M.debug.add( string.format( "on_confirm( %s [%s], %s )", player and player.name or "nil", player and player.type or "nil", item and item.id or "nil" ) )
|
||||
local slot = loot_list.get_slot( item.id )
|
||||
if not slot then return end
|
||||
|
||||
if player.type ~= "ItemCandidate" and not (player.type == "Winner" and player.is_on_master_loot_candidate_list) then
|
||||
err( "Player is not eligible for this item." )
|
||||
return
|
||||
end
|
||||
|
||||
m_confirmed = { item = item, player = player }
|
||||
m_slot_cache[ slot ] = item
|
||||
|
||||
local index = master_loot_candidates.get_index( slot, player.name )
|
||||
|
||||
if not index then
|
||||
err( "Player is not in the loot candidates list." )
|
||||
return
|
||||
end
|
||||
|
||||
m.api.GiveMasterLoot( slot, index )
|
||||
end
|
||||
|
||||
local function on_loot_opened()
|
||||
M.debug.add( "on_loot_opened" )
|
||||
clear_table( m_slot_cache )
|
||||
reset_confirmation()
|
||||
end
|
||||
|
||||
local function on_loot_received( player_name, item_id, item_link )
|
||||
M.debug.add( string.format( "on_loot_received( %s, %s, %s )", player_name or "nil", item_id or "nil", item_link or "nil" ) )
|
||||
local is_looting = loot_list.is_looting()
|
||||
if m_confirmed and is_looting then return end
|
||||
if not m_confirmed then return end
|
||||
|
||||
-- This isn't tested, because it's hard to reproduce. Not sure if it can happen. Let's keep it here to be safe.
|
||||
if m_confirmed.item.id ~= item_id then return end
|
||||
|
||||
loot_award_callback.on_loot_awarded( item_id, item_link, player_name )
|
||||
reset_confirmation()
|
||||
end
|
||||
|
||||
local function on_recipient_inventory_full()
|
||||
if m_confirmed then
|
||||
pretty_print( string.format( "%s%s bags are full.", hl( m_confirmed.player.name ), m.possesive_case( m_confirmed.player.name ) ), "red" )
|
||||
reset_confirmation()
|
||||
end
|
||||
end
|
||||
|
||||
local function on_player_is_too_far()
|
||||
if m_confirmed then
|
||||
pretty_print( string.format( "%s is too far to receive the item.", hl( m_confirmed.player.name ) ), "red" )
|
||||
reset_confirmation()
|
||||
end
|
||||
end
|
||||
|
||||
local function on_unknown_error_message( message )
|
||||
if m_confirmed then
|
||||
if message ~= "You are too far away!" and message ~= "You must be in a raid group to enter this instance" then
|
||||
pretty_print( message, "red" )
|
||||
end
|
||||
|
||||
reset_confirmation()
|
||||
end
|
||||
end
|
||||
|
||||
roll_controller.subscribe( "award_confirmed", on_confirm )
|
||||
|
||||
---@type MasterLoot
|
||||
return {
|
||||
on_loot_opened = on_loot_opened,
|
||||
on_recipient_inventory_full = on_recipient_inventory_full,
|
||||
on_player_is_too_far = on_player_is_too_far,
|
||||
on_unknown_error_message = on_unknown_error_message,
|
||||
on_loot_slot_cleared = on_loot_slot_cleared,
|
||||
on_loot_received = on_loot_received
|
||||
}
|
||||
end
|
||||
|
||||
m.MasterLoot = M
|
||||
return M
|
||||
260
src/MasterLootCandidateSelectionFrame.lua
Normal file
@ -0,0 +1,260 @@
|
||||
RollFor = RollFor or {}
|
||||
local m = RollFor
|
||||
|
||||
if m.MasterLootCandidateSelectionFrame then return end
|
||||
|
||||
local M = {}
|
||||
|
||||
local icon_width = 16
|
||||
local button_width = 85 + icon_width
|
||||
local button_height = 16
|
||||
local vertical_margin = 5
|
||||
local horizontal_margin = 5
|
||||
local horizontal_padding = 3
|
||||
local vertical_padding = 5
|
||||
|
||||
local mod, getn = m.mod, m.getn
|
||||
|
||||
local function highlight( frame )
|
||||
frame:SetBackdropColor( frame.color.r, frame.color.g, frame.color.b, 0.3 )
|
||||
end
|
||||
|
||||
local function dim( frame )
|
||||
frame:SetBackdropColor( 0.5, 0.5, 0.5, 0.1 )
|
||||
end
|
||||
|
||||
local function press( frame )
|
||||
frame:SetBackdropColor( frame.color.r, frame.color.g, frame.color.b, 0.7 )
|
||||
end
|
||||
|
||||
---@param frame_builder FrameBuilderFactory
|
||||
---@param config Config
|
||||
local function create_main_frame( frame_builder, config )
|
||||
local builder = config.classic_look() and
|
||||
frame_builder.classic() or
|
||||
frame_builder.modern()
|
||||
:backdrop_color( 0, 0, 0, 0.8 )
|
||||
:border_color( 0.851, 0.553, 0.341, 0.3 )
|
||||
|
||||
builder = builder
|
||||
:name( "RollForPlayerSelectionFrame" )
|
||||
:width( 100 )
|
||||
:height( 100 )
|
||||
:point( { point = "CENTER", relative_frame = m.api.UIParent, relative_point = "CENTER" } )
|
||||
:enable_mouse()
|
||||
:strata( "DIALOG" )
|
||||
:hidden()
|
||||
|
||||
if config.classic_look() then
|
||||
vertical_margin = 9
|
||||
horizontal_margin = 10
|
||||
end
|
||||
|
||||
local frame = builder:build()
|
||||
|
||||
frame:SetScript( "OnLeave",
|
||||
function( self )
|
||||
if m.vanilla then self = this end
|
||||
|
||||
local mouse_x, mouse_y = m.api.GetCursorPosition()
|
||||
local x, y = self:GetCenter()
|
||||
local width = self:GetWidth()
|
||||
local height = self:GetHeight()
|
||||
local half_width = width / 2
|
||||
local half_height = height / 2
|
||||
local left = x - half_width
|
||||
local right = x + half_width
|
||||
local top = y + half_height
|
||||
local bottom = y - half_height
|
||||
local is_over = mouse_x >= left and mouse_x <= right and mouse_y >= bottom and mouse_y <= top
|
||||
|
||||
if not is_over then self:Hide() end
|
||||
end )
|
||||
|
||||
return frame
|
||||
end
|
||||
|
||||
local function position_button( button, parent, index, rows )
|
||||
local width = horizontal_margin + horizontal_padding + m.api.math.floor( (index - 1) / rows ) * (button_width + horizontal_padding)
|
||||
local height = (-vertical_margin) - vertical_padding - (mod( index - 1, rows ) * (button_height + vertical_padding))
|
||||
button:ClearAllPoints()
|
||||
button:SetPoint( "TOPLEFT", parent, "TOPLEFT", width, height )
|
||||
end
|
||||
|
||||
local function create_button( parent, index, rows )
|
||||
local frame = m.create_backdrop_frame( m.api, "Button", nil, parent )
|
||||
|
||||
frame:SetWidth( button_width )
|
||||
frame:SetHeight( button_height )
|
||||
position_button( frame, parent, index, rows )
|
||||
frame:SetBackdrop( { bgFile = "Interface\\Buttons\\WHITE8x8" } )
|
||||
frame:SetNormalTexture( "" )
|
||||
frame.parent = parent
|
||||
|
||||
local text = frame:CreateFontString( nil, "OVERLAY", "GameFontNormalSmall" )
|
||||
text:SetPoint( "CENTER", frame, "CENTER" )
|
||||
text:SetText( "" )
|
||||
frame.text = text
|
||||
|
||||
local icon = frame:CreateTexture( nil, "ARTWORK" )
|
||||
icon:SetPoint( "LEFT", text, "RIGHT", 2, 0 )
|
||||
icon:SetWidth( 13 )
|
||||
icon:SetHeight( 12 )
|
||||
icon:SetTexture( string.format( "Interface\\AddOns\\RollFor\\assets\\star-%s.tga", "gold" ) )
|
||||
icon:Hide()
|
||||
frame.icon = icon
|
||||
|
||||
frame:SetScript( "OnEnter", function( self )
|
||||
if m.vanilla then self = this end
|
||||
|
||||
highlight( self )
|
||||
end )
|
||||
|
||||
frame:SetScript( "OnLeave", function( self )
|
||||
if m.vanilla then self = this end
|
||||
|
||||
dim( self )
|
||||
end )
|
||||
|
||||
frame:SetScript( "OnMouseDown", function( self, button )
|
||||
if m.vanilla then
|
||||
self = this
|
||||
button = arg1
|
||||
end
|
||||
|
||||
if button == "LeftButton" then press( self ) end
|
||||
end )
|
||||
|
||||
frame:SetScript( "OnMouseUp", function( self, button )
|
||||
if m.vanilla then
|
||||
self = this
|
||||
button = arg1
|
||||
end
|
||||
|
||||
if button == "LeftButton" then
|
||||
if m.api.MouseIsOver( self ) then
|
||||
highlight( self )
|
||||
else
|
||||
dim( self )
|
||||
end
|
||||
end
|
||||
end )
|
||||
|
||||
frame.unmark_winner = function()
|
||||
frame.text:SetPoint( "CENTER", frame, "CENTER" )
|
||||
frame.icon:Hide()
|
||||
end
|
||||
|
||||
frame.mark_winner = function()
|
||||
frame.text:SetPoint( "CENTER", frame, "CENTER", 2 - icon_width / 2, 0 )
|
||||
frame.icon:Show()
|
||||
end
|
||||
|
||||
return frame
|
||||
end
|
||||
|
||||
---@class MasterLootCandidateSelectionFrame
|
||||
---@field show fun( candidates: MasterLootCandidate[] )
|
||||
---@field hide fun()
|
||||
---@field get_frame fun(): Frame
|
||||
|
||||
---@param frame_builder FrameBuilderFactory
|
||||
---@param config Config
|
||||
function M.new( frame_builder, config )
|
||||
local m_frame
|
||||
local m_buttons = {}
|
||||
|
||||
local function resize_frame( total, rows )
|
||||
local columns = m.api.math.ceil( total / rows )
|
||||
local total_rows = total < 5 and total or rows
|
||||
|
||||
m_frame:SetWidth( (button_width + horizontal_padding) * columns + horizontal_padding + horizontal_margin * 2 )
|
||||
m_frame:SetHeight( (button_height + vertical_padding) * total_rows + vertical_padding + vertical_margin * 2 )
|
||||
end
|
||||
|
||||
---@param candidates MasterLootCandidate[]
|
||||
local function create_candidate_frames( candidates )
|
||||
local total = getn( candidates )
|
||||
local rows = config.master_loot_frame_rows()
|
||||
|
||||
resize_frame( total, rows )
|
||||
|
||||
local function loop( i )
|
||||
if i > total then
|
||||
if m_buttons[ i ] then m_buttons[ i ]:Hide() end
|
||||
return
|
||||
end
|
||||
|
||||
local candidate = candidates[ i ]
|
||||
|
||||
if not m_buttons[ i ] then
|
||||
m_buttons[ i ] = create_button( m_frame, i, rows )
|
||||
end
|
||||
|
||||
local button = m_buttons[ i ]
|
||||
button.text:SetText( candidate.name )
|
||||
local color = m.api.RAID_CLASS_COLORS[ string.upper( candidate.class ) ]
|
||||
button.color = color
|
||||
button.player = candidate
|
||||
|
||||
if color then
|
||||
button.text:SetTextColor( color.r, color.g, color.b )
|
||||
dim( button )
|
||||
else
|
||||
button.text:SetTextColor( 1, 1, 1 )
|
||||
end
|
||||
|
||||
button:SetScript( "OnClick", candidate.confirm_fn )
|
||||
|
||||
if candidate.is_winner then
|
||||
button.mark_winner()
|
||||
else
|
||||
button.unmark_winner()
|
||||
end
|
||||
|
||||
button:Show()
|
||||
end
|
||||
|
||||
for i = 1, 40 do
|
||||
loop( i )
|
||||
end
|
||||
end
|
||||
|
||||
---@param candidates MasterLootCandidate[]
|
||||
local function show( candidates )
|
||||
if not m_frame then m_frame = create_main_frame( frame_builder, config ) end
|
||||
|
||||
create_candidate_frames( candidates )
|
||||
m_frame:Show()
|
||||
end
|
||||
|
||||
local function hide()
|
||||
if m_frame then m_frame:Hide() end
|
||||
end
|
||||
|
||||
config.subscribe( "master_loot_frame_rows", function()
|
||||
if not m_frame then return end
|
||||
|
||||
local total = 0
|
||||
local rows = config.master_loot_frame_rows()
|
||||
|
||||
for i = 1, 40 do
|
||||
if m_buttons[ i ] then
|
||||
total = total + 1
|
||||
position_button( m_buttons[ i ], m_frame, i, rows )
|
||||
end
|
||||
end
|
||||
|
||||
resize_frame( total, rows )
|
||||
end )
|
||||
|
||||
---@type MasterLootCandidateSelectionFrame
|
||||
return {
|
||||
show = show,
|
||||
hide = hide,
|
||||
get_frame = function() return m_frame end
|
||||
}
|
||||
end
|
||||
|
||||
m.MasterLootCandidateSelectionFrame = M
|
||||
return M
|
||||
118
src/MasterLootCandidates.lua
Normal file
@ -0,0 +1,118 @@
|
||||
RollFor = RollFor or {}
|
||||
local m = RollFor
|
||||
|
||||
if m.MasterLootCandidates then return end
|
||||
|
||||
local M = {}
|
||||
|
||||
---@type MakeItemCandidateFn
|
||||
local make_item_candidate = m.Types.make_item_candidate
|
||||
---@type MakeWinnerFn
|
||||
local make_winner = m.Types.make_winner
|
||||
|
||||
local function get_dummy_candidates()
|
||||
return {
|
||||
{ name = "Ohhaimark", class = "Warrior", value = 1 },
|
||||
{ name = "Obszczymucha", class = "Druid", value = 2 },
|
||||
{ name = "Jogobobek", class = "Hunter", value = 3 },
|
||||
{ name = "Xiaorotflmao", class = "Shaman", value = 4 },
|
||||
{ name = "Kacprawcze", class = "Priest", value = 5 },
|
||||
{ name = "Psikutas", class = "Paladin", value = 6 },
|
||||
{ name = "Motoko", class = "Rogue", value = 7 },
|
||||
{ name = "Blanchot", class = "Warrior", value = 8 },
|
||||
{ name = "Adamsandler", class = "Druid", value = 9 },
|
||||
{ name = "Johnstamos", class = "Hunter", value = 10 },
|
||||
{ name = "Xiaolmao", class = "Shaman", value = 11 },
|
||||
{ name = "Ronaldtramp", class = "Priest", value = 12 },
|
||||
{ name = "Psikuta", class = "Paladin", value = 13 },
|
||||
{ name = "Kusanagi", class = "Rogue", value = 14 },
|
||||
{ name = "Chuj", class = "Priest", value = 15 },
|
||||
}
|
||||
end
|
||||
|
||||
---@class MasterLootCandidatesApi
|
||||
---@field GetMasterLootCandidate fun( slot: number, index: number ): string
|
||||
|
||||
---@class MasterLootCandidates
|
||||
---@field get fun( slot: number ): ItemCandidate[]
|
||||
---@field find fun( slot: number, player_name: string ): ItemCandidate?
|
||||
---@field get_index fun( slot: number, player_name: string ): number?
|
||||
---@field transform_to_winner fun( player: RollingPlayer, item: Item|MasterLootDistributableItem, roll_type: RollType, winning_roll: number?, rerolling: boolean? ): Winner
|
||||
|
||||
---@param api MasterLootCandidatesApi
|
||||
---@param group_roster GroupRoster
|
||||
---@param loot_list LootList
|
||||
function M.new( api, group_roster, loot_list )
|
||||
local function get( slot )
|
||||
if not group_roster then return get_dummy_candidates() end
|
||||
|
||||
local result = {}
|
||||
local players = group_roster.get_all_players_in_my_group()
|
||||
|
||||
for i = 1, 40 do
|
||||
-- There's probably a better way of separating the APIs. For now I'm leaving it like this.
|
||||
if m.vanilla then
|
||||
---@diagnostic disable-next-line: missing-parameter
|
||||
local name = api.GetMasterLootCandidate( i )
|
||||
|
||||
for _, p in ipairs( players ) do
|
||||
if name == p.name then
|
||||
table.insert( result, make_item_candidate( name, p.class, p.online ) )
|
||||
end
|
||||
end
|
||||
else
|
||||
local name = api.GetMasterLootCandidate( slot, i )
|
||||
|
||||
for _, p in ipairs( players ) do
|
||||
if name == p.name then
|
||||
table.insert( result, make_item_candidate( name, p.class, p.online ) )
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
local function find( slot, player_name )
|
||||
local candidates = get( slot )
|
||||
|
||||
return m.find_value_in_table( candidates, player_name, function( v ) return v.name end )
|
||||
end
|
||||
|
||||
---@param player RollingPlayer
|
||||
---@param item Item|MasterLootDistributableItem
|
||||
---@param roll_type RollType
|
||||
---@param winning_roll number?
|
||||
---@param rerolling boolean?
|
||||
---@return Winner
|
||||
local function transform_to_winner( player, item, roll_type, winning_roll, rerolling )
|
||||
local slot = loot_list.get_slot( item.id )
|
||||
local candidate = slot and find( slot, player.name )
|
||||
return make_winner( player.name, player.class, item, candidate and true or false, roll_type, winning_roll and winning_roll, rerolling )
|
||||
end
|
||||
|
||||
local function get_index( slot, player_name )
|
||||
for i = 1, 40 do
|
||||
if m.vanilla then
|
||||
---@diagnostic disable-next-line: missing-parameter
|
||||
local name = api.GetMasterLootCandidate( i )
|
||||
if name == player_name then return i end
|
||||
else
|
||||
local name = api.GetMasterLootCandidate( slot, i )
|
||||
if name == player_name then return i end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@type MasterLootCandidates
|
||||
return {
|
||||
get = get,
|
||||
find = find,
|
||||
get_index = get_index,
|
||||
transform_to_winner = transform_to_winner
|
||||
}
|
||||
end
|
||||
|
||||
m.MasterLootCandidates = M
|
||||
return M
|
||||
95
src/MasterLootWarning.lua
Normal file
@ -0,0 +1,95 @@
|
||||
RollFor = RollFor or {}
|
||||
local m = RollFor
|
||||
|
||||
if m.MasterLootWarning then return end
|
||||
|
||||
local M = {}
|
||||
local red = m.colors.red
|
||||
local blue = m.colors.blue
|
||||
local grey = m.colors.grey
|
||||
local table_contains_value = m.table_contains_value
|
||||
|
||||
---@diagnostic disable-next-line: undefined-global
|
||||
local UIParent = UIParent
|
||||
|
||||
local function create_frame( api )
|
||||
local frame = api().CreateFrame( "FRAME", "RollForMasterLootWarning", UIParent )
|
||||
frame:Hide()
|
||||
|
||||
local label = frame:CreateFontString( nil, "OVERLAY" )
|
||||
label:SetFont( "FONTS\\FRIZQT__.TTF", 24, "OUTLINE" )
|
||||
label:SetPoint( "CENTER", 0, 0 )
|
||||
label:SetText( string.format( "No %s!", red( "Master Loot" ) ) )
|
||||
|
||||
local label2 = frame:CreateFontString( nil, "OVERLAY" )
|
||||
label2:SetFont( "FONTS\\FRIZQT__.TTF", 16, "OUTLINE" )
|
||||
label2:SetPoint( "CENTER", 0, -20 )
|
||||
label2:SetText( string.format( "Enable %s or type %s to disable this message.", grey( "Master Loot" ), blue( "/rf ml" ) ) )
|
||||
|
||||
frame:SetWidth( label:GetWidth() )
|
||||
frame:SetHeight( label:GetHeight() )
|
||||
frame:SetPoint( "CENTER", UIParent, "CENTER", 0, 140 )
|
||||
|
||||
return frame
|
||||
end
|
||||
|
||||
---@param api table
|
||||
---@param config Config
|
||||
---@param boss_list BossList
|
||||
---@param player_info PlayerInfo
|
||||
function M.new( api, config, boss_list, player_info )
|
||||
local frame
|
||||
local is_visible = false
|
||||
|
||||
local function show()
|
||||
if not frame or is_visible or not config.show_ml_warning() then return end
|
||||
|
||||
api().UIFrameFadeRemoveFrame( frame )
|
||||
frame:SetAlpha( 1 )
|
||||
frame:Show()
|
||||
is_visible = true
|
||||
frame.fading_out = nil
|
||||
end
|
||||
|
||||
local function hide()
|
||||
if not frame or frame.fading_out or not frame:IsVisible() then return end
|
||||
|
||||
frame.fading_out = true
|
||||
is_visible = false
|
||||
api().UIFrameFadeOut( frame, 2, 1, 0 )
|
||||
frame.fadeInfo.finishedFunc = function()
|
||||
frame.fading_out = nil
|
||||
frame:Hide()
|
||||
end
|
||||
end
|
||||
|
||||
local function toggle()
|
||||
if not player_info.is_master_looter() and not player_info.is_leader() or config.auto_master_loot() then
|
||||
if is_visible then hide() end
|
||||
return
|
||||
end
|
||||
|
||||
local master_loot = m.is_master_loot()
|
||||
local zone_name = api().GetRealZoneText()
|
||||
local target_name = api().UnitName( "target" )
|
||||
local zone = boss_list[ zone_name ] or {}
|
||||
local dead = api().UnitIsDead( "target" )
|
||||
|
||||
if not zone or master_loot or not table_contains_value( zone, target_name ) or dead then
|
||||
if is_visible then hide() end
|
||||
return
|
||||
end
|
||||
|
||||
if not frame then frame = create_frame( api ) end
|
||||
show()
|
||||
end
|
||||
|
||||
return {
|
||||
on_player_target_changed = toggle,
|
||||
on_party_loot_method_changed = toggle,
|
||||
hide = hide
|
||||
}
|
||||
end
|
||||
|
||||
m.MasterLootWarning = M
|
||||
return M
|
||||
316
src/MinimapButton.lua
Normal file
@ -0,0 +1,316 @@
|
||||
RollFor = RollFor or {}
|
||||
local m = RollFor
|
||||
|
||||
if m.MinimapButton then return end
|
||||
|
||||
local M = {}
|
||||
local hl = m.colors.hl
|
||||
local blue = m.colors.blue
|
||||
local grey = m.colors.grey
|
||||
local white = m.colors.white
|
||||
local green = m.colors.green
|
||||
local red = m.colors.red
|
||||
local pretty_print = m.pretty_print
|
||||
local class_color = m.colorize_player_by_class
|
||||
|
||||
local ColorType = {
|
||||
White = "White",
|
||||
Green = "Green",
|
||||
Orange = "Orange",
|
||||
Red = "Red"
|
||||
}
|
||||
|
||||
function M.new( api, db, manage_softres_fn, winners_popup_fn, softres_check, config )
|
||||
local icon_color
|
||||
|
||||
local function persist_angle( angle )
|
||||
db.angle = angle
|
||||
end
|
||||
|
||||
local function get_angle()
|
||||
return db.angle
|
||||
end
|
||||
|
||||
local function is_locked()
|
||||
return config.minimap_button_locked()
|
||||
end
|
||||
|
||||
local function is_hidden()
|
||||
return config.minimap_button_hidden()
|
||||
end
|
||||
|
||||
local function print_players_who_did_not_softres( tooltip )
|
||||
local result, players = softres_check.check_softres( true )
|
||||
|
||||
if result == softres_check.ResultType.SomeoneIsNotSoftRessing then
|
||||
tooltip:AddLine( white( "Missing softres:" ) )
|
||||
|
||||
for _, player in pairs( players ) do
|
||||
tooltip:AddLine( class_color( player.name, player.class ) )
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function create()
|
||||
local frame = api().CreateFrame( "Button", "RollForMinimapButton", api().Minimap )
|
||||
local was_dragging = false
|
||||
|
||||
function frame.OnClick( self )
|
||||
if m.vanilla then self = this end
|
||||
|
||||
if m.is_shift_key_down() then
|
||||
winners_popup_fn()
|
||||
else
|
||||
manage_softres_fn()
|
||||
end
|
||||
self:OnEnter()
|
||||
api().GameTooltip:Hide()
|
||||
end
|
||||
|
||||
function frame.OnMouseDown( self )
|
||||
if m.vanilla then self = this end
|
||||
|
||||
self.icon:SetTexCoord( 0, 1, 0, 1 )
|
||||
was_dragging = false
|
||||
end
|
||||
|
||||
function frame.OnMouseUp( self )
|
||||
if m.vanilla then self = this end
|
||||
|
||||
self.icon:SetTexCoord( 0.05, 0.95, 0.05, 0.95 )
|
||||
if m.vanilla and not was_dragging then self:OnClick() end
|
||||
end
|
||||
|
||||
function frame.OnEnter( self )
|
||||
if m.vanilla then self = this end
|
||||
|
||||
if not self.dragging then
|
||||
api().GameTooltip:SetOwner( self, "ANCHOR_LEFT" )
|
||||
api().GameTooltip:SetText( blue( "RollFor" ) )
|
||||
|
||||
api().GameTooltip:AddLine( " " )
|
||||
api().GameTooltip:AddLine( string.format( "%s - %s", hl( "/htr" ), white( "show how to roll" ) ) )
|
||||
api().GameTooltip:AddLine( string.format( "%s %s - %s", hl( "/rf" ), grey( "<item>" ), white( "roll for" ) ) )
|
||||
api().GameTooltip:AddLine( string.format( "%s %s - %s", hl( "/rr" ), grey( "<item>" ), white( "raid-roll" ) ) )
|
||||
api().GameTooltip:AddLine( string.format( "%s %s - %s", hl( "/irr" ), grey( "<item>" ), white( "insta raid-roll" ) ) )
|
||||
api().GameTooltip:AddLine( string.format( "%s %s - %s", hl( "/arf" ), grey( "<item>" ), white( "roll for (ignore SR)" ) ) )
|
||||
api().GameTooltip:AddLine( string.format( "%s %s %s - %s", hl( "/rf" ), grey( "<item>" ), grey( "<seconds>" ), white( "roll with custom time" ) ) )
|
||||
api().GameTooltip:AddLine( string.format( "%s - %s", hl( "/sr" ), white( "manage softres" ) ) )
|
||||
api().GameTooltip:AddLine( string.format( "%s - %s", hl( "/sro" ), white( "fix player softres name" ) ) )
|
||||
api().GameTooltip:AddLine( string.format( "%s - %s", hl( "/src" ), white( "check softres status" ) ) )
|
||||
api().GameTooltip:AddLine( string.format( "%s - %s", hl( "/srs" ), white( "show softres items" ) ) )
|
||||
api().GameTooltip:AddLine( string.format( "%s - %s", hl( "/rfr" ), white( "reset loot announce" ) ) )
|
||||
api().GameTooltip:AddLine( string.format( "%s - %s", hl( "/cr" ), white( "cancel rolling in progress" ) ) )
|
||||
api().GameTooltip:AddLine( string.format( "%s - %s", hl( "/fr" ), white( "finish rolling early" ) ) )
|
||||
api().GameTooltip:AddLine( string.format( "%s - %s", hl( "/rf config" ), white( "show configuration" ) ) )
|
||||
api().GameTooltip:AddLine( string.format( "%s - %s", hl( "/rf config help" ), white( "show configuration help" ) ) )
|
||||
api().GameTooltip:AddLine( " " )
|
||||
api().GameTooltip:AddLine( "Click to manage softres." )
|
||||
|
||||
if icon_color == ColorType.Green then
|
||||
api().GameTooltip:AddLine( " " )
|
||||
api().GameTooltip:AddLine( string.format( "%s %s", white( "Softres status:" ), green( "OK" ) ) )
|
||||
elseif icon_color == ColorType.Orange then
|
||||
api().GameTooltip:AddLine( " " )
|
||||
print_players_who_did_not_softres( api().GameTooltip )
|
||||
elseif icon_color == ColorType.Red then
|
||||
api().GameTooltip:AddLine( " " )
|
||||
api().GameTooltip:AddLine( white( "Softres status:" ) )
|
||||
api().GameTooltip:AddLine( red( "Found outdated softres data!" ) )
|
||||
end
|
||||
|
||||
api().GameTooltip:Show()
|
||||
end
|
||||
end
|
||||
|
||||
function frame.OnLeave()
|
||||
api().GameTooltip:Hide()
|
||||
end
|
||||
|
||||
function frame.OnDragStart( self )
|
||||
if m.vanilla then self = this end
|
||||
|
||||
self.dragging = true
|
||||
self:LockHighlight()
|
||||
self.icon:SetTexCoord( 0, 1, 0, 1 )
|
||||
self:SetScript( "OnUpdate", self.OnUpdate )
|
||||
api().GameTooltip:Hide()
|
||||
was_dragging = true
|
||||
end
|
||||
|
||||
function frame.OnDragStop( self )
|
||||
if m.vanilla then self = this end
|
||||
|
||||
self.dragging = nil
|
||||
self:SetScript( "OnUpdate", nil )
|
||||
self.icon:SetTexCoord( 0.05, 0.95, 0.05, 0.95 )
|
||||
self:UnlockHighlight()
|
||||
end
|
||||
|
||||
function frame.OnUpdate( self )
|
||||
if m.vanilla then self = this end
|
||||
|
||||
local mx, my = api().Minimap:GetCenter()
|
||||
local px, py = api().GetCursorPosition()
|
||||
local scale = api().Minimap:GetEffectiveScale()
|
||||
|
||||
px, py = px / scale, py / scale
|
||||
|
||||
persist_angle( m.mod( math.deg( math.atan2( py - my, px - mx ) ), 360 ) )
|
||||
self:UpdatePosition()
|
||||
end
|
||||
|
||||
-- Copy pasted from Bongos.
|
||||
--magic fubar code for updating the minimap button"s position
|
||||
--I suck at trig, so I"m not going to bother figuring it out
|
||||
---@diagnostic disable-next-line: redefined-local
|
||||
function frame.UpdatePosition( self )
|
||||
if m.vanilla then self = this end
|
||||
|
||||
local angle = math.rad( get_angle() or m.lua.random( 0, 360 ) )
|
||||
local cos = math.cos( angle )
|
||||
local sin = math.sin( angle )
|
||||
local minimapShape = api().GetMinimapShape and api().GetMinimapShape() or "ROUND"
|
||||
|
||||
local round = false
|
||||
if minimapShape == "ROUND" then
|
||||
round = true
|
||||
elseif minimapShape == "SQUARE" then
|
||||
round = false
|
||||
elseif minimapShape == "CORNER-TOPRIGHT" then
|
||||
round = not (cos < 0 or sin < 0)
|
||||
elseif minimapShape == "CORNER-TOPLEFT" then
|
||||
round = not (cos > 0 or sin < 0)
|
||||
elseif minimapShape == "CORNER-BOTTOMRIGHT" then
|
||||
round = not (cos < 0 or sin > 0)
|
||||
elseif minimapShape == "CORNER-BOTTOMLEFT" then
|
||||
round = not (cos > 0 or sin > 0)
|
||||
elseif minimapShape == "SIDE-LEFT" then
|
||||
round = cos <= 0
|
||||
elseif minimapShape == "SIDE-RIGHT" then
|
||||
round = cos >= 0
|
||||
elseif minimapShape == "SIDE-TOP" then
|
||||
round = sin <= 0
|
||||
elseif minimapShape == "SIDE-BOTTOM" then
|
||||
round = sin >= 0
|
||||
elseif minimapShape == "TRICORNER-TOPRIGHT" then
|
||||
round = not (cos < 0 and sin > 0)
|
||||
elseif minimapShape == "TRICORNER-TOPLEFT" then
|
||||
round = not (cos > 0 and sin > 0)
|
||||
elseif minimapShape == "TRICORNER-BOTTOMRIGHT" then
|
||||
round = not (cos < 0 and sin < 0)
|
||||
elseif minimapShape == "TRICORNER-BOTTOMLEFT" then
|
||||
round = not (cos > 0 and sin < 0)
|
||||
end
|
||||
|
||||
local x, y
|
||||
if round then
|
||||
x = cos * 80
|
||||
y = sin * 80
|
||||
else
|
||||
x = math.max( -82, math.min( 110 * cos, 84 ) )
|
||||
y = math.max( -86, math.min( 110 * sin, 82 ) )
|
||||
end
|
||||
|
||||
self:ClearAllPoints()
|
||||
self:SetPoint( "CENTER", x, y )
|
||||
end
|
||||
|
||||
frame:SetFrameStrata( "MEDIUM" )
|
||||
frame:SetWidth( 31 )
|
||||
frame:SetHeight( 31 )
|
||||
frame:SetFrameLevel( 8 )
|
||||
frame:RegisterForClicks( "anyUp" )
|
||||
frame:RegisterForDrag( "LeftButton" )
|
||||
frame:SetHighlightTexture( "Interface\\Minimap\\UI-Minimap-ZoomButton-Highlight" )
|
||||
|
||||
local overlay = frame:CreateTexture( nil, "OVERLAY" )
|
||||
overlay:SetWidth( 53 )
|
||||
overlay:SetHeight( 53 )
|
||||
overlay:SetTexture( "Interface\\Minimap\\MiniMap-TrackingBorder" )
|
||||
overlay:SetPoint( "TOPLEFT", 0, 0 )
|
||||
|
||||
local icon = frame:CreateTexture( nil, "BACKGROUND" )
|
||||
icon:SetWidth( 20 )
|
||||
icon:SetHeight( 20 )
|
||||
icon:SetTexCoord( 0.05, 0.95, 0.05, 0.95 )
|
||||
icon:SetPoint( "TOPLEFT", 7, -5 )
|
||||
frame.icon = icon
|
||||
|
||||
frame:SetScript( "OnEnter", frame.OnEnter )
|
||||
frame:SetScript( "OnLeave", frame.OnLeave )
|
||||
frame:SetScript( "OnClick", frame.OnClick )
|
||||
|
||||
frame:SetScript( "OnMouseDown", frame.OnMouseDown )
|
||||
frame:SetScript( "OnMouseUp", frame.OnMouseUp )
|
||||
|
||||
frame:UpdatePosition()
|
||||
|
||||
frame:SetScript( "OnEvent", function() frame:UpdatePosition() end )
|
||||
frame:RegisterEvent( "PLAYER_ENTERING_WORLD" )
|
||||
|
||||
return frame
|
||||
end
|
||||
|
||||
local frame = create()
|
||||
|
||||
local function show()
|
||||
if is_hidden() then
|
||||
frame:Hide()
|
||||
pretty_print( string.format( "Minimap button is hidden. Type %s to show.", hl( "/rf config minimap" ) ) )
|
||||
else
|
||||
frame:Show()
|
||||
end
|
||||
end
|
||||
|
||||
local function lock()
|
||||
if is_locked() then
|
||||
frame:SetScript( "OnDragStart", nil )
|
||||
frame:SetScript( "OnDragStop", nil )
|
||||
else
|
||||
frame:SetScript( "OnDragStart", frame.OnDragStart )
|
||||
frame:SetScript( "OnDragStop", frame.OnDragStop )
|
||||
end
|
||||
end
|
||||
|
||||
local function set_icon( color )
|
||||
frame.icon:SetTexture( string.format( "Interface\\AddOns\\RollFor\\assets\\icon-%s.tga", string.lower( color ) ) )
|
||||
icon_color = color
|
||||
end
|
||||
|
||||
show()
|
||||
lock()
|
||||
set_icon( ColorType.Red )
|
||||
|
||||
local function toggle()
|
||||
if is_hidden() then
|
||||
config.show_minimap_button()
|
||||
else
|
||||
config.hide_minimap_button()
|
||||
end
|
||||
|
||||
show()
|
||||
end
|
||||
|
||||
local function toggle_lock()
|
||||
if is_locked() then
|
||||
config.unlock_minimap_button()
|
||||
else
|
||||
config.lock_minimap_button()
|
||||
end
|
||||
|
||||
lock()
|
||||
end
|
||||
|
||||
config.subscribe( "minimap_button_hidden", show )
|
||||
|
||||
return {
|
||||
toggle = toggle,
|
||||
toggle_lock = toggle_lock,
|
||||
set_icon = set_icon,
|
||||
ColorType = ColorType
|
||||
}
|
||||
end
|
||||
|
||||
m.MinimapButton = M
|
||||
return M
|
||||
330
src/ModernLootFrameSkin.lua
Normal file
@ -0,0 +1,330 @@
|
||||
RollFor = RollFor or {}
|
||||
local m = RollFor
|
||||
|
||||
if m.ModernLootFrameSkin then return end
|
||||
|
||||
local M = {}
|
||||
|
||||
local gui = m.GuiElements
|
||||
|
||||
local item_height = 22
|
||||
local footer_height = 0
|
||||
|
||||
---@param frame_builder FrameBuilderFactory
|
||||
function M.new( frame_builder )
|
||||
---@param parent Frame
|
||||
local function dropped_item( parent )
|
||||
local container = m.create_loot_button( m.api, parent )
|
||||
|
||||
local w = 22
|
||||
local h = 22
|
||||
local spacing = 6
|
||||
local bind_spacing = 3
|
||||
local mouse_down = false
|
||||
local icon_zoom = 2
|
||||
|
||||
local item
|
||||
|
||||
container:SetHeight( h )
|
||||
container.name = gui.create_text_in_container( "Frame", container, 20, "LEFT", nil, "text" )
|
||||
container.name.text:SetJustifyH( "LEFT" )
|
||||
container.name.text:SetTextColor( 1, 1, 1 )
|
||||
container.index = gui.create_text_in_container( "Frame", container, 20, "CENTER", nil, "text" )
|
||||
container.index:SetPoint( "LEFT", 1, 0 )
|
||||
container.index:SetWidth( 16 )
|
||||
container.index:SetHeight( h )
|
||||
container.icon = gui.create_icon_in_container( "Button", container, w, h, icon_zoom )
|
||||
container.icon:SetPoint( "LEFT", container.index, "RIGHT", 2, 0 )
|
||||
container.icon:EnableMouse( false )
|
||||
container.quantity = gui.create_text_in_container( "Frame", container.icon, 20, "CENTER", nil, "text", "NumberFontNormalSmall" )
|
||||
container.quantity:SetPoint( "BOTTOMRIGHT", 1, -2 )
|
||||
container.quantity:SetHeight( 16 )
|
||||
container.bind = gui.create_text_in_container( "Frame", container, 15, "LEFT", nil, "text" )
|
||||
container.bind:SetPoint( "LEFT", container.icon, "RIGHT", 5, 0 )
|
||||
container.comment = gui.create_text_in_container( "Button", container, 20, "CENTER", nil, "text" )
|
||||
container.comment:SetPoint( "RIGHT", -4, 0 )
|
||||
container.comment:SetHeight( 16 )
|
||||
|
||||
local function resize()
|
||||
container.icon:Show()
|
||||
|
||||
local index_width = container.index:GetWidth() + 1
|
||||
local icon_width = container.icon:GetWidth() + spacing
|
||||
local bind_width = item.bind and (container.bind:GetWidth() + bind_spacing) or 0
|
||||
local text_width = container.name.text:GetStringWidth() + spacing + 1
|
||||
local comment_width = container.comment:IsVisible() and container.comment:GetWidth() + spacing or 0
|
||||
|
||||
local total_width = index_width + icon_width + bind_width + text_width + comment_width
|
||||
|
||||
container:SetWidth( total_width )
|
||||
container:SetPoint( "LEFT", 0, 0 )
|
||||
container:SetPoint( "RIGHT", 0, 0 )
|
||||
end
|
||||
|
||||
local function get_color( multiplier )
|
||||
local mult = multiplier or 1
|
||||
local color = m.api.ITEM_QUALITY_COLORS[ item.quality or 0 ]
|
||||
return color.r * mult, color.g * mult, color.b * mult
|
||||
end
|
||||
|
||||
local function hovered_color()
|
||||
if not item then return end
|
||||
if item.is_selected then return end
|
||||
local r, g, b = get_color()
|
||||
container:SetBackdropColor( r, g, b, 0.3 )
|
||||
end
|
||||
|
||||
local function clicked_color()
|
||||
local r, g, b = get_color()
|
||||
container:SetBackdropColor( r, g, b, 0.4 )
|
||||
end
|
||||
|
||||
local function selected_color()
|
||||
if not item then return end
|
||||
local r, g, b = get_color()
|
||||
container:SetBackdropColor( r, g, b, 0.3 )
|
||||
end
|
||||
|
||||
local function not_hovered_color()
|
||||
if not item or item.is_selected then return end
|
||||
container:SetBackdropColor( 0, 0, 0, 0.1 )
|
||||
end
|
||||
|
||||
local function update()
|
||||
if not item then return end
|
||||
|
||||
if not item.is_enabled then
|
||||
container:SetAlpha( 0.6 )
|
||||
return
|
||||
end
|
||||
|
||||
if item.is_selected then
|
||||
selected_color()
|
||||
else
|
||||
not_hovered_color()
|
||||
end
|
||||
|
||||
container:SetAlpha( 1 )
|
||||
end
|
||||
|
||||
---@param v LootFrameItem
|
||||
container.SetItem = function( _, v )
|
||||
item = v
|
||||
container.index.text:SetText( v.index )
|
||||
container.icon.texture:SetTexture( v.texture )
|
||||
container.name.text:SetText( m.colorize_item_by_quality( v.name, v.quality ) )
|
||||
|
||||
if v.bind then
|
||||
container.bind.text:SetText( v.bind )
|
||||
container.bind:SetWidth( container.bind.text:GetStringWidth() )
|
||||
container.bind:Show()
|
||||
container.name:SetPoint( "LEFT", container.bind, "RIGHT", bind_spacing, 0 )
|
||||
else
|
||||
container.bind:Hide()
|
||||
container.name:SetPoint( "LEFT", container.icon, "RIGHT", spacing, 0 )
|
||||
end
|
||||
|
||||
if v.comment then
|
||||
container.comment.text:SetText( v.comment )
|
||||
container.comment:Show()
|
||||
container.name:SetPoint( "RIGHT", container.comment, "LEFT", 0, 0 )
|
||||
else
|
||||
container.comment:Hide()
|
||||
container.name:SetPoint( "RIGHT", container, "RIGHT", 0, 0 )
|
||||
end
|
||||
|
||||
if v.quantity and v.quantity > 1 then
|
||||
container.quantity:Show()
|
||||
container.quantity.text:SetText( v.quantity )
|
||||
container.quantity:SetWidth( container.quantity.text:GetStringWidth() )
|
||||
else
|
||||
container.quantity:Hide()
|
||||
end
|
||||
|
||||
local function modifier_fn()
|
||||
if m.is_ctrl_key_down() then
|
||||
m.api.DressUpItemLink( v.link )
|
||||
return
|
||||
end
|
||||
|
||||
if m.is_shift_key_down() then
|
||||
m.link_item_in_chat( v.link )
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
container:SetScript( "OnClick", v.is_enabled and not v.is_selected and v.click_fn or modifier_fn )
|
||||
container.comment:SetScript( "OnClick", v.is_enabled and not v.is_selected and v.click_fn or modifier_fn )
|
||||
|
||||
if m.vanilla then
|
||||
-- Fucking hell this took forever to figure out. Fuck you Blizzard.
|
||||
-- For looting to work in vanilla, the frame must be of a "LootButton" type and
|
||||
-- then it comes with the SetSlot function that we need to use to set the slot.
|
||||
-- This will probably be a pain in the ass when porting.
|
||||
container:SetSlot( v.slot or 0 )
|
||||
end
|
||||
|
||||
update()
|
||||
resize()
|
||||
end
|
||||
|
||||
local function on_enter( self )
|
||||
if m.vanilla then self = this end
|
||||
|
||||
if not item then return end
|
||||
if item.tooltip_link then
|
||||
m.api.GameTooltip:SetOwner( self, "ANCHOR_RIGHT" )
|
||||
m.api.GameTooltip:SetHyperlink( item.tooltip_link )
|
||||
m.api.GameTooltip:Show()
|
||||
end
|
||||
|
||||
if not item.is_enabled then return end
|
||||
hovered_color()
|
||||
end
|
||||
|
||||
container:SetBackdrop( {
|
||||
bgFile = "Interface/Buttons/WHITE8x8",
|
||||
tile = false,
|
||||
tileSize = 0,
|
||||
} )
|
||||
|
||||
not_hovered_color()
|
||||
|
||||
local function on_leave()
|
||||
m.api.GameTooltip:Hide()
|
||||
mouse_down = false
|
||||
not_hovered_color()
|
||||
end
|
||||
|
||||
container.comment:SetScript( "OnEnter", function( self )
|
||||
if not item then return end
|
||||
if item.comment_tooltip then
|
||||
if m.vanilla then self = this end
|
||||
|
||||
self.tooltip_scale = m.api.GameTooltip:GetScale()
|
||||
m.api.GameTooltip:SetOwner( self, "ANCHOR_RIGHT" )
|
||||
|
||||
local result = ""
|
||||
|
||||
for _, line in ipairs( item.comment_tooltip ) do
|
||||
if result ~= "" then result = result .. "\n" end
|
||||
result = result .. line
|
||||
end
|
||||
|
||||
m.api.GameTooltip:AddLine( result, 1, 1, 1 )
|
||||
m.api.GameTooltip:SetScale( 0.9 )
|
||||
m.api.GameTooltip:Show()
|
||||
end
|
||||
|
||||
if not item.is_enabled then return end
|
||||
hovered_color()
|
||||
end )
|
||||
|
||||
container.comment:SetScript( "OnLeave", function( self )
|
||||
if m.vanilla then self = this end
|
||||
|
||||
m.api.GameTooltip:Hide()
|
||||
m.api.GameTooltip:SetScale( self.tooltip_scale or 1 )
|
||||
mouse_down = false
|
||||
|
||||
not_hovered_color()
|
||||
end )
|
||||
|
||||
container:SetScript( "OnEnter", on_enter )
|
||||
container:SetScript( "OnLeave", on_leave )
|
||||
|
||||
local function on_mouse_down()
|
||||
if not item then return end
|
||||
if not item.is_enabled or item.is_selected then return end
|
||||
|
||||
mouse_down = true
|
||||
clicked_color()
|
||||
end
|
||||
|
||||
local function on_mouse_up()
|
||||
if not item then return end
|
||||
if not item.is_enabled or item.is_selected then return end
|
||||
|
||||
if not mouse_down then return end
|
||||
hovered_color()
|
||||
end
|
||||
|
||||
container:SetScript( "OnMouseUp", on_mouse_up )
|
||||
container:SetScript( "OnMouseDown", on_mouse_down )
|
||||
|
||||
container:SetScript( "OnShow", function()
|
||||
mouse_down = false
|
||||
end )
|
||||
|
||||
return container
|
||||
end
|
||||
|
||||
---@param on_drag_stop function
|
||||
---@param on_show function
|
||||
---@param on_hide function
|
||||
local function header( on_drag_stop, on_show, on_hide )
|
||||
return frame_builder.new()
|
||||
:name( "RollForLootFrameHeader" )
|
||||
:parent( m.api.UIParent )
|
||||
:width( 380 )
|
||||
:height( 24 )
|
||||
:sound()
|
||||
:gui_elements( gui )
|
||||
:frame_style( "Modern" )
|
||||
:backdrop_color( 0, 0.501, 1, 0.3 )
|
||||
:border_color( 0, 0, 0, 0.9 )
|
||||
:movable()
|
||||
:gui_elements( m.GuiElements )
|
||||
:bg_file( "Interface/Buttons/WHITE8x8" )
|
||||
:on_show( on_show )
|
||||
:on_hide( on_hide )
|
||||
:on_drag_stop( on_drag_stop )
|
||||
:hidden()
|
||||
:build()
|
||||
end
|
||||
|
||||
---@param parent Frame
|
||||
local function body( parent )
|
||||
local frame = frame_builder.new()
|
||||
:name( "RollForLootFrame" )
|
||||
:parent( parent )
|
||||
:width( 280 )
|
||||
:height( 100 )
|
||||
:gui_elements( { dropped_item = dropped_item } )
|
||||
:frame_style( "Modern" )
|
||||
:backdrop_color( 0, 0, 0, 0.5 )
|
||||
:border_color( 0, 0, 0, 0.9 )
|
||||
:movable()
|
||||
:bg_file( "Interface/Buttons/WHITE8x8" )
|
||||
:build()
|
||||
|
||||
frame:ClearAllPoints()
|
||||
frame:SetPoint( "TOP", parent, "BOTTOM", 0, 0 )
|
||||
|
||||
return frame
|
||||
end
|
||||
|
||||
local function footer()
|
||||
end
|
||||
|
||||
local function get_item_height()
|
||||
return item_height
|
||||
end
|
||||
|
||||
local function get_footer_height()
|
||||
return footer_height
|
||||
end
|
||||
|
||||
---@type LootFrameSkin
|
||||
return {
|
||||
header = header,
|
||||
body = body,
|
||||
dropped_item = dropped_item,
|
||||
footer = footer,
|
||||
get_item_height = get_item_height,
|
||||
get_footer_height = get_footer_height
|
||||
}
|
||||
end
|
||||
|
||||
m.ModernLootFrameSkin = M
|
||||
return M
|
||||
22
src/Module.lua
Normal file
@ -0,0 +1,22 @@
|
||||
RollFor = RollFor or {}
|
||||
local m = RollFor
|
||||
|
||||
if m.Module then return end
|
||||
|
||||
---@class RollForModule
|
||||
---@field debug DebugBuffer
|
||||
|
||||
local M = {}
|
||||
|
||||
---@return RollForModule
|
||||
function M.new( module_name, debug_size )
|
||||
---@type DebugBuffer
|
||||
local debug_buffer = m.DebugBuffer.new( module_name, debug_size or 20 )
|
||||
|
||||
return {
|
||||
debug = debug_buffer
|
||||
}
|
||||
end
|
||||
|
||||
m.Module = M
|
||||
return M
|
||||
262
src/NameAutoMatcher.lua
Normal file
@ -0,0 +1,262 @@
|
||||
RollFor = RollFor or {}
|
||||
local rf = RollFor
|
||||
|
||||
if rf.NameAutoMatcher then return end
|
||||
|
||||
local M = {}
|
||||
|
||||
local getn = rf.getn
|
||||
local count = rf.count_elements
|
||||
local map = rf.map
|
||||
|
||||
local function to_map( t )
|
||||
local result = {}
|
||||
|
||||
for _, v in pairs( t ) do
|
||||
result[ v ] = 1
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
-- Returns the values that are in the left table, but not in the right table.
|
||||
local function is_in_left_but_not_in_right( left, right )
|
||||
local softres_player_map = to_map( right )
|
||||
local result = {}
|
||||
|
||||
for _, player_name in pairs( left ) do
|
||||
if not softres_player_map[ player_name ] then
|
||||
table.insert( result, player_name )
|
||||
end
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
local function string_similarity( s1, s2 )
|
||||
local n = string.len( s1 )
|
||||
local m = string.len( s2 )
|
||||
local ssnc = 0
|
||||
|
||||
if n > m then
|
||||
s1, s2 = s2, s1
|
||||
n, m = m, n
|
||||
end
|
||||
|
||||
for i = n, 1, -1 do
|
||||
if i <= string.len( s1 ) then
|
||||
for j = 1, n - i + 1, 1 do
|
||||
local pattern = string.sub( s1, j, j + i - 1 )
|
||||
if string.len( pattern ) == 0 then break end
|
||||
local foundAt = string.find( s2, pattern )
|
||||
|
||||
if foundAt ~= nil then
|
||||
ssnc = ssnc + (2 * i) ^ 2
|
||||
s1 = string.sub( s1, 0, j - 1 ) .. string.sub( s1, j + i )
|
||||
s2 = string.sub( s2, 0, foundAt - 1 ) .. string.sub( s2, foundAt + i )
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return (ssnc / ((n + m) ^ 2)) ^ (1 / 2)
|
||||
end
|
||||
|
||||
local function get_levenshtein( s1, s2 )
|
||||
local len1 = string.len( s1 )
|
||||
local len2 = string.len( s2 )
|
||||
local matrix = {}
|
||||
local cost = 1
|
||||
local min = math.min;
|
||||
|
||||
-- quick cut-offs to save time
|
||||
if (len1 == 0) then
|
||||
return len2
|
||||
elseif (len2 == 0) then
|
||||
return len1
|
||||
elseif (s1 == s2) then
|
||||
return 0
|
||||
end
|
||||
|
||||
-- initialise the base matrix values
|
||||
for i = 0, len1, 1 do
|
||||
matrix[ i ] = {}
|
||||
matrix[ i ][ 0 ] = i
|
||||
end
|
||||
for j = 0, len2, 1 do
|
||||
matrix[ 0 ][ j ] = j
|
||||
end
|
||||
|
||||
-- actual Levenshtein algorithm
|
||||
for i = 1, len1, 1 do
|
||||
for j = 1, len2, 1 do
|
||||
if (string.byte( s1, i ) == string.byte( s2, j )) then
|
||||
cost = 0
|
||||
end
|
||||
|
||||
matrix[ i ][ j ] = min( matrix[ i - 1 ][ j ] + 1, matrix[ i ][ j - 1 ] + 1, matrix[ i - 1 ][ j - 1 ] + cost )
|
||||
end
|
||||
end
|
||||
|
||||
-- return the last value - this is the Levenshtein distance
|
||||
return matrix[ len1 ][ len2 ]
|
||||
end
|
||||
|
||||
local function get_similarity_predictions( present_players_who_did_not_softres, absent_players_who_did_softres, sort )
|
||||
local result = {}
|
||||
|
||||
for _, player in pairs( present_players_who_did_not_softres ) do
|
||||
local predictions = {}
|
||||
|
||||
for _, candidate in pairs( absent_players_who_did_softres ) do
|
||||
local prediction = {
|
||||
[ "candidate" ] = candidate,
|
||||
[ "similarity" ] = string_similarity( player, candidate ),
|
||||
[ "levenshtein" ] = get_levenshtein( player, candidate )
|
||||
}
|
||||
table.insert( predictions, prediction )
|
||||
end
|
||||
|
||||
table.sort( predictions, sort )
|
||||
result[ player ] = predictions
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
local function improved_descending( l, r )
|
||||
return l[ "levenshtein" ] < r[ "levenshtein" ] or
|
||||
l[ "levenshtein" ] == r[ "levenshtein" ] and l[ "similarity" ] > r[ "similarity" ]
|
||||
end
|
||||
|
||||
---@diagnostic disable-next-line: unused-function
|
||||
local function ends_with( str, ending )
|
||||
return ending == "" or string.sub( str, -string.len( ending ) ) == ending
|
||||
end
|
||||
|
||||
---@diagnostic disable-next-line: unused-function, unused-local
|
||||
local function format_percent( value )
|
||||
local result = string.format( "%.2f", value * 100 )
|
||||
|
||||
if ends_with( result, "0" ) then
|
||||
result = string.sub( result, 0, string.len( result ) - 1 )
|
||||
end
|
||||
|
||||
if ends_with( result, "0" ) then
|
||||
result = string.sub( result, 0, string.len( result ) - 1 )
|
||||
end
|
||||
|
||||
if ends_with( result, "." ) then
|
||||
result = string.sub( result, 0, string.len( result ) - 1 )
|
||||
end
|
||||
|
||||
return string.format( "%s%%", result )
|
||||
end
|
||||
|
||||
local function assign_predictions( predictions, top_threshold, bottom_threshold )
|
||||
local function format_4( value ) return string.format( "%.4f", value ) end
|
||||
|
||||
local results = {}
|
||||
local results_below_threshold = {}
|
||||
|
||||
for player, prediction in pairs( predictions ) do
|
||||
local top_candidate = prediction[ 1 ]
|
||||
local similarity = top_candidate[ "similarity" ]
|
||||
local levenshtein = top_candidate[ "levenshtein" ]
|
||||
|
||||
local match = {
|
||||
[ "matched_name" ] = top_candidate[ "candidate" ],
|
||||
[ "similarity" ] = format_4( similarity ),
|
||||
[ "levenshtein" ] = levenshtein
|
||||
}
|
||||
|
||||
if similarity >= (top_threshold or 0.57) then
|
||||
results[ player ] = match
|
||||
elseif similarity >= (bottom_threshold or 0.4) then
|
||||
results_below_threshold[ player ] = match
|
||||
end
|
||||
end
|
||||
|
||||
return results, results_below_threshold
|
||||
end
|
||||
|
||||
function M.new( group_roster, softres, top_threshold, bottom_threshold )
|
||||
local matched_names = {}
|
||||
local matched_names_below_threshold = {}
|
||||
|
||||
local function auto_match()
|
||||
matched_names = {}
|
||||
matched_names_below_threshold = {}
|
||||
|
||||
---@param p Player | Roller
|
||||
local function get_name( p ) return p.name end
|
||||
local player_names = map( group_roster.get_all_players_in_my_group(), get_name )
|
||||
local roller_names = map( softres.get_all_rollers(), get_name )
|
||||
|
||||
local present_players_who_did_not_softres = is_in_left_but_not_in_right( player_names, roller_names )
|
||||
if getn( present_players_who_did_not_softres ) == 0 then return end
|
||||
|
||||
local absent_players_who_did_softres = is_in_left_but_not_in_right( roller_names, player_names )
|
||||
if getn( absent_players_who_did_softres ) == 0 then return end
|
||||
|
||||
local predictions = get_similarity_predictions( present_players_who_did_not_softres, absent_players_who_did_softres, improved_descending )
|
||||
local matched, matched_below_threshold = assign_predictions( predictions, top_threshold, bottom_threshold )
|
||||
|
||||
for player, match_result in pairs( matched ) do
|
||||
local matched_name = match_result[ "matched_name" ]
|
||||
local similarity = match_result[ "similarity" ]
|
||||
matched_names[ matched_name ] = { [ "matched_name" ] = player, [ "similarity" ] = similarity }
|
||||
end
|
||||
|
||||
for player, match_result in pairs( matched_below_threshold ) do
|
||||
local matched_name = match_result[ "matched_name" ]
|
||||
local similarity = match_result[ "similarity" ]
|
||||
matched_names_below_threshold[ matched_name ] = { [ "matched_name" ] = player, [ "similarity" ] = similarity }
|
||||
end
|
||||
end
|
||||
|
||||
local function get_softres_name( matched_name )
|
||||
for softres_name, match in pairs( matched_names ) do
|
||||
if match.matched_name == matched_name then return softres_name end
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
local function get_matched_name( softres_name )
|
||||
return matched_names[ softres_name ] and matched_names[ softres_name ].matched_name or nil
|
||||
end
|
||||
|
||||
local function get_matches()
|
||||
if count( matched_names ) == 0 and count( matched_names_below_threshold ) == 0 then return {}, {} end
|
||||
|
||||
local matches = {}
|
||||
local not_matches = {}
|
||||
|
||||
for softres_name, match in pairs( matched_names ) do
|
||||
table.insert( matches, { softres_name = softres_name, matched_name = match.matched_name, similarity = match.similarity } )
|
||||
end
|
||||
|
||||
for softres_name, match in pairs( matched_names_below_threshold ) do
|
||||
table.insert( not_matches, { softres_name = softres_name, matched_name = match.matched_name, similarity = match.similarity } )
|
||||
end
|
||||
|
||||
return matches, not_matches
|
||||
end
|
||||
|
||||
local function is_matched( softres_name )
|
||||
return get_matched_name( softres_name ) or false
|
||||
end
|
||||
|
||||
return {
|
||||
auto_match = auto_match,
|
||||
get_softres_name = get_softres_name,
|
||||
get_matched_name = get_matched_name,
|
||||
is_matched = is_matched,
|
||||
get_matches = get_matches
|
||||
}
|
||||
end
|
||||
|
||||
rf.NameAutoMatcher = M
|
||||
return M
|
||||
160
src/NameManualMatcher.lua
Normal file
@ -0,0 +1,160 @@
|
||||
RollFor = RollFor or {}
|
||||
local m = RollFor
|
||||
|
||||
if m.NameManualMatcher then return end
|
||||
|
||||
local M = {}
|
||||
|
||||
local getn = m.getn
|
||||
local clone = m.clone
|
||||
local negate = m.negate
|
||||
local filter = m.filter
|
||||
local keys = m.keys
|
||||
local merge = m.merge
|
||||
local colors = m.colors
|
||||
local p = m.pretty_print
|
||||
local map = m.map
|
||||
|
||||
function M.new( db, api, absent_unfiltered_softres, name_matcher, softres_status_changed )
|
||||
db.manual_matches = db.manual_matches or {}
|
||||
local manual_match_options = nil
|
||||
|
||||
local function show_manual_matches( matches, absent_players )
|
||||
if getn( matches ) == 0 and getn( absent_players ) == 0 then
|
||||
p( "There are no players that can be manually matched." )
|
||||
return
|
||||
end
|
||||
|
||||
local index = 1
|
||||
|
||||
if getn( matches ) > 0 then
|
||||
p( string.format( "To unmatch, clear your target and type: %s", colors.hl( "/sro <number>" ) ) )
|
||||
|
||||
for i = 1, getn( matches ) do
|
||||
p( string.format( "[%s]: %s (manually matched with %s)", colors.green( index ), colors.hl( matches[ i ] ), colors.hl( db.manual_matches[ matches[ i ] ] ) ) )
|
||||
index = index + 1
|
||||
end
|
||||
end
|
||||
|
||||
if getn( absent_players ) > 0 then
|
||||
p( string.format( "To match, target a player and type: %s", colors.hl( "/sro <number>" ) ) )
|
||||
|
||||
for i = 1, getn( absent_players ) do
|
||||
p( string.format( "[%s]: %s", colors.green( index ), colors.red( absent_players[ i ] ) ) )
|
||||
index = index + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local parse_number = function( args )
|
||||
for i in string.gmatch( args, "(%d+)" ) do
|
||||
return tonumber( i )
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
local function is_matched( player )
|
||||
return db.manual_matches[ player.name ] or name_matcher.is_matched( player.name )
|
||||
end
|
||||
|
||||
local function create_matches_and_show()
|
||||
local absent_players = map( filter( absent_unfiltered_softres.get_all_rollers(), negate( is_matched ) ), function( v ) return v.name end )
|
||||
local manually_matched = keys( db.manual_matches )
|
||||
manual_match_options = merge( {}, manually_matched, absent_players )
|
||||
show_manual_matches( manually_matched, absent_players )
|
||||
end
|
||||
|
||||
local function manual_match( args )
|
||||
if not manual_match_options or not args or args == "" then
|
||||
create_matches_and_show()
|
||||
return
|
||||
end
|
||||
|
||||
local count = getn( manual_match_options )
|
||||
local target = api().UnitName( "target" )
|
||||
local index = parse_number( args )
|
||||
|
||||
if not index or index < 0 or index > count then
|
||||
p( "Invalid player number." )
|
||||
create_matches_and_show()
|
||||
|
||||
return
|
||||
end
|
||||
|
||||
local softres_name = manual_match_options[ index ]
|
||||
local already_matched_name = db.manual_matches[ softres_name ]
|
||||
|
||||
if target and already_matched_name then
|
||||
p( string.format( "%s is already matched to %s.", colors.hl( softres_name ), colors.hl( already_matched_name ) ) )
|
||||
create_matches_and_show()
|
||||
elseif target and not already_matched_name then
|
||||
manual_match_options = nil
|
||||
db.manual_matches[ softres_name ] = target
|
||||
p( string.format( "%s is now soft-ressing as %s.", colors.hl( target ), colors.hl( softres_name ) ) )
|
||||
softres_status_changed()
|
||||
elseif not target and already_matched_name then
|
||||
manual_match_options = nil
|
||||
db.manual_matches[ softres_name ] = nil
|
||||
p( string.format( "Unmatched %s.", colors.hl( softres_name ) ) )
|
||||
softres_status_changed()
|
||||
else
|
||||
p( string.format( "To match a player, target them first." ) )
|
||||
create_matches_and_show()
|
||||
end
|
||||
end
|
||||
|
||||
local function get_matched_name( softres_name )
|
||||
return db.manual_matches[ softres_name ] or name_matcher.get_matched_name( softres_name )
|
||||
end
|
||||
|
||||
local function get_softres_name( matched_name )
|
||||
for softres_name, name in pairs( db.manual_matches ) do
|
||||
if name == matched_name then return softres_name end
|
||||
end
|
||||
|
||||
return name_matcher.get_softres_name( matched_name )
|
||||
end
|
||||
|
||||
local function clear( report )
|
||||
if not db.manual_matches or m.count_elements( db.manual_matches ) == 0 then return end
|
||||
m.clear_table( db.manual_matches )
|
||||
if report then p( "Cleared manual matches." ) end
|
||||
end
|
||||
|
||||
local function remove_duplicates( source, duplicates )
|
||||
local result = {}
|
||||
|
||||
for _, v in pairs( source ) do
|
||||
if not m.find( v.softres_name, duplicates, "softres_name" ) then
|
||||
table.insert( result, v )
|
||||
end
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
local function get_matches()
|
||||
local matches = {}
|
||||
|
||||
for softres_name, match in pairs( db.manual_matches ) do
|
||||
table.insert( matches, { softres_name = softres_name, matched_name = match } )
|
||||
end
|
||||
|
||||
local auto_matches, auto_not_matches = name_matcher.get_matches()
|
||||
return remove_duplicates( auto_matches, matches ), remove_duplicates( remove_duplicates( auto_not_matches, matches ), auto_matches ), matches
|
||||
end
|
||||
|
||||
local decorator = clone( name_matcher )
|
||||
decorator.manual_match = manual_match
|
||||
decorator.is_matched = is_matched
|
||||
decorator.get_matched_name = get_matched_name
|
||||
decorator.get_softres_name = get_softres_name
|
||||
decorator.clear = clear
|
||||
decorator.get_matches = get_matches
|
||||
|
||||
return decorator
|
||||
end
|
||||
|
||||
m.NameManualMatcher = M
|
||||
return M
|
||||
32
src/NameMatchReport.lua
Normal file
@ -0,0 +1,32 @@
|
||||
RollFor = RollFor or {}
|
||||
local m = RollFor
|
||||
|
||||
if m.SoftResCheck then return end
|
||||
|
||||
local M = {}
|
||||
|
||||
local hl = m.colors.hl
|
||||
local grey = m.colors.grey
|
||||
local red = m.colors.red
|
||||
local p = m.pretty_print
|
||||
|
||||
function M.report( name_matcher )
|
||||
local auto_matched, auto_not_matched, manually_matched = name_matcher.get_matches()
|
||||
|
||||
if manually_matched then
|
||||
for _, match in pairs( manually_matched ) do
|
||||
p( string.format( "%s is manually matched with %s.", hl( match.softres_name ), hl( match.matched_name ) ), grey )
|
||||
end
|
||||
end
|
||||
|
||||
for _, match in pairs( auto_matched ) do
|
||||
p( string.format( "%s is auto-matched with %s.", hl( match.softres_name ), hl( match.matched_name ) ), grey )
|
||||
end
|
||||
|
||||
for _, match in pairs( auto_not_matched ) do
|
||||
p( string.format( "%s could not be auto-matched. Top candidate: %s.", hl( match.softres_name ), hl( match.matched_name ) ), red )
|
||||
end
|
||||
end
|
||||
|
||||
m.NameMatchReport = M
|
||||
return M
|
||||
48
src/NewGroupEvent.lua
Normal file
@ -0,0 +1,48 @@
|
||||
RollFor = RollFor or {}
|
||||
local m = RollFor
|
||||
|
||||
if m.NewGroupEvent then return end
|
||||
|
||||
local M = {}
|
||||
|
||||
---@class NewGroupEvent
|
||||
---@field on_group_changed fun()
|
||||
---@field subscribe fun( callback: fun() )
|
||||
|
||||
---@param group_roster GroupRoster
|
||||
function M.new( group_roster )
|
||||
local m_subscribers = {}
|
||||
local group = group_roster.am_i_in_group()
|
||||
|
||||
local function notify_subscribers()
|
||||
for _, subscriber in ipairs( m_subscribers ) do
|
||||
subscriber()
|
||||
end
|
||||
end
|
||||
|
||||
local function on_group_changed()
|
||||
local in_group_now = group_roster.am_i_in_group()
|
||||
|
||||
if not group and in_group_now then
|
||||
group = true
|
||||
notify_subscribers()
|
||||
return
|
||||
end
|
||||
|
||||
if group and not in_group_now then
|
||||
group = false
|
||||
end
|
||||
end
|
||||
|
||||
local function subscribe( callback )
|
||||
table.insert( m_subscribers, callback )
|
||||
end
|
||||
|
||||
return {
|
||||
on_group_changed = on_group_changed,
|
||||
subscribe = subscribe
|
||||
}
|
||||
end
|
||||
|
||||
m.NewGroupEvent = M
|
||||
return M
|
||||
288
src/NonSoftResRollingLogic.lua
Normal file
@ -0,0 +1,288 @@
|
||||
RollFor = RollFor or {}
|
||||
local m = RollFor
|
||||
|
||||
if m.NonSoftResRollingLogic then return end
|
||||
|
||||
local M = m.Module.new( "NonSoftResRollingLogic" )
|
||||
|
||||
local getn = m.getn
|
||||
local count_elements = m.count_elements
|
||||
local merge = m.merge
|
||||
local take = m.take
|
||||
local rlu = m.RollingLogicUtils
|
||||
local RollType = m.Types.RollType
|
||||
local hl = m.colors.hl
|
||||
|
||||
---@type MakeRollFn
|
||||
local make_roll = m.Types.make_roll
|
||||
|
||||
---@param players RollingPlayer[]
|
||||
local function have_all_players_rolled( players )
|
||||
for _, v in ipairs( players ) do
|
||||
if v.rolls > 0 then return false end
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
---@param chat Chat
|
||||
---@param ace_timer AceTimer
|
||||
---@param players RollingPlayer[]
|
||||
---@param item Item
|
||||
---@param item_count number
|
||||
---@param info string?
|
||||
---@param seconds number
|
||||
---@param on_rolling_finished RollingFinishedCallback
|
||||
---@param config Config
|
||||
---@param controller RollControllerFacade
|
||||
function M.new(
|
||||
chat,
|
||||
ace_timer,
|
||||
players,
|
||||
item,
|
||||
item_count,
|
||||
info,
|
||||
seconds,
|
||||
on_rolling_finished,
|
||||
config,
|
||||
controller
|
||||
)
|
||||
---@type RollingPlayer[], Roll[]
|
||||
local mainspec_rollers, mainspec_rolls = players, {}
|
||||
---@type RollingPlayer[], Roll[]
|
||||
local offspec_rollers, offspec_rolls = rlu.copy_rollers( mainspec_rollers ), {}
|
||||
---@type RollingPlayer[], Roll[]
|
||||
local tmog_rollers, tmog_rolls = rlu.copy_rollers( mainspec_rollers ), {}
|
||||
|
||||
local rolling = false
|
||||
local seconds_left = seconds
|
||||
local timer
|
||||
|
||||
local ms_threshold = config.ms_roll_threshold()
|
||||
local os_threshold = config.os_roll_threshold()
|
||||
local tmog_threshold = config.tmog_roll_threshold()
|
||||
local tmog_rolling_enabled = config.tmog_rolling_enabled()
|
||||
tmog_rolling_enabled = tmog_rolling_enabled and (item.is_boss_loot or not config.auto_tmog_disable() )
|
||||
|
||||
local function sort_rolls()
|
||||
local f = function( a, b )
|
||||
if a.roll == b.roll then
|
||||
return a.player.name < b.player.name
|
||||
else
|
||||
return a.roll > b.roll
|
||||
end
|
||||
end
|
||||
|
||||
table.sort( mainspec_rolls, f )
|
||||
table.sort( offspec_rolls, f )
|
||||
table.sort( tmog_rolls, f )
|
||||
end
|
||||
|
||||
local function have_all_rolls_been_exhausted()
|
||||
local mainspec_roll_count = getn( mainspec_rolls )
|
||||
local offspec_roll_count = getn( offspec_rolls )
|
||||
local tmog_roll_count = getn( tmog_rolls )
|
||||
local total_roll_count = mainspec_roll_count + offspec_roll_count + tmog_roll_count
|
||||
|
||||
if item_count == getn( tmog_rollers ) and have_all_players_rolled( tmog_rollers ) or
|
||||
item_count == getn( offspec_rollers ) and have_all_players_rolled( offspec_rollers ) or
|
||||
item_count == getn( mainspec_rollers ) and total_roll_count == getn( mainspec_rollers ) then
|
||||
return true
|
||||
end
|
||||
|
||||
return have_all_players_rolled( mainspec_rollers )
|
||||
end
|
||||
|
||||
---@param player_name string
|
||||
---@param rollers RollingPlayer[]
|
||||
local function find_player( player_name, rollers )
|
||||
for _, player in ipairs( rollers ) do
|
||||
if player.name == player_name then return player end
|
||||
end
|
||||
end
|
||||
|
||||
local function stop_listening()
|
||||
rolling = false
|
||||
|
||||
if timer then
|
||||
ace_timer:CancelTimer( timer )
|
||||
timer = nil
|
||||
end
|
||||
end
|
||||
|
||||
local function find_winner()
|
||||
stop_listening()
|
||||
|
||||
local mainspec_roll_count = count_elements( mainspec_rolls )
|
||||
local offspec_roll_count = count_elements( offspec_rolls )
|
||||
local tmog_roll_count = count_elements( tmog_rolls )
|
||||
|
||||
if mainspec_roll_count == 0 and offspec_roll_count == 0 and tmog_roll_count == 0 then
|
||||
on_rolling_finished( item, item_count, {} )
|
||||
return
|
||||
end
|
||||
|
||||
sort_rolls()
|
||||
|
||||
---@type Roll[]
|
||||
local all_rolls = merge( {}, mainspec_rolls, offspec_rolls, tmog_rolls )
|
||||
local roll_count = getn( all_rolls )
|
||||
|
||||
local function count_top_roll_winners()
|
||||
if roll_count == 0 then return 0 end
|
||||
|
||||
local function split_by_roll_and_type()
|
||||
local result = {}
|
||||
local last_roll
|
||||
local last_type
|
||||
|
||||
for _, roll in ipairs( all_rolls ) do
|
||||
if not last_roll or last_roll ~= roll.roll or last_type ~= roll.roll_type then
|
||||
table.insert( result, { roll } )
|
||||
last_roll = roll.roll
|
||||
last_type = roll.roll_type
|
||||
else
|
||||
table.insert( result[ getn( result ) ], roll )
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
local result = 0
|
||||
|
||||
for _, rolls in ipairs( split_by_roll_and_type() ) do
|
||||
result = result + getn( rolls )
|
||||
if result >= item_count then return result end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
local top_roll_winner_count = count_top_roll_winners()
|
||||
local winner_rolls = take( all_rolls, top_roll_winner_count > item_count and top_roll_winner_count or item_count )
|
||||
|
||||
on_rolling_finished( item, item_count, winner_rolls )
|
||||
end
|
||||
|
||||
---@param roller Player
|
||||
---@param roll number
|
||||
---@param min number
|
||||
---@param max number
|
||||
local function on_roll( roller, roll, min, max )
|
||||
if not rolling or min ~= 1 or (max ~= tmog_threshold and max ~= os_threshold and max ~= ms_threshold) then return end
|
||||
if max == tmog_threshold and not tmog_rolling_enabled then return end
|
||||
|
||||
local ms_roll = max == ms_threshold
|
||||
local os_roll = max == os_threshold
|
||||
local roll_type = ms_roll and RollType.MainSpec or os_roll and RollType.OffSpec or RollType.Transmog
|
||||
local rollers = ms_roll and mainspec_rollers or os_roll and offspec_rollers or tmog_rollers
|
||||
local player = find_player( roller.name, rollers ) ---@type RollingPlayer
|
||||
|
||||
if player.rolls == 0 then
|
||||
chat.info( m.msg.rolls_exhausted( player.name, player.class, roll ) )
|
||||
controller.roll_was_ignored( roller.name, player.class, roll_type, roll, "Rolled too many times." )
|
||||
return
|
||||
end
|
||||
|
||||
player.rolls = player.rolls - 1
|
||||
local t = ms_roll and mainspec_rolls or os_roll and offspec_rolls or tmog_rolls
|
||||
table.insert( t, make_roll( player, roll_type, roll ) )
|
||||
controller.roll_was_accepted( player.name, player.class, roll_type, roll )
|
||||
|
||||
if have_all_rolls_been_exhausted() then find_winner() end
|
||||
end
|
||||
|
||||
local function stop_accepting_rolls()
|
||||
find_winner()
|
||||
end
|
||||
|
||||
local function on_timer()
|
||||
seconds_left = seconds_left - 1
|
||||
|
||||
if seconds_left <= 0 then
|
||||
stop_accepting_rolls()
|
||||
return
|
||||
end
|
||||
|
||||
controller.tick( seconds_left )
|
||||
end
|
||||
|
||||
local function accept_rolls()
|
||||
rolling = true
|
||||
timer = ace_timer.ScheduleRepeatingTimer( M, on_timer, 1.7 )
|
||||
end
|
||||
|
||||
local function start_rolling()
|
||||
local count_str = item_count > 1 and string.format( "%sx", item_count ) or ""
|
||||
local tmog_info = tmog_rolling_enabled and string.format( " or /roll %s (TMOG)", config.tmog_roll_threshold() ) or ""
|
||||
local default_ms = config.ms_roll_threshold() ~= 100 and string.format( "%s ", config.ms_roll_threshold() ) or ""
|
||||
local roll_info = string.format( " /roll %s(MS) or /roll %s (OS)%s", default_ms, config.os_roll_threshold(), tmog_info )
|
||||
local info_str = info and info ~= "" and string.format( " %s", info ) or roll_info
|
||||
local x_rolls_win = item_count > 1 and string.format( ". %d top rolls win.", item_count ) or ""
|
||||
|
||||
if item.classes and config.auto_class_announce() then
|
||||
local class_str = table.concat( item.classes, "s, " )
|
||||
class_str = string.gsub( class_str, ", (%S+)$", " and %1" )
|
||||
info_str = string.format( " %ss", class_str )
|
||||
end
|
||||
|
||||
chat.announce( string.format( "Roll for %s%s:%s%s", count_str, item.link, info_str, x_rolls_win ), true )
|
||||
accept_rolls()
|
||||
end
|
||||
|
||||
local function show_sorted_rolls( limit )
|
||||
local function show( prefix, sorted_rolls )
|
||||
if getn( sorted_rolls ) == 0 then return end
|
||||
|
||||
chat.info( string.format( "%s rolls:", prefix ) )
|
||||
local i = 0
|
||||
|
||||
for _, v in ipairs( sorted_rolls ) do
|
||||
if limit and limit > 0 and i > limit then return end
|
||||
|
||||
chat.info( string.format( "[%s]: %s", hl( v.roll ), v.player.name ) )
|
||||
i = i + 1
|
||||
end
|
||||
end
|
||||
|
||||
local total_mainspec_rolls = count_elements( mainspec_rolls )
|
||||
local total_offspec_rolls = count_elements( offspec_rolls )
|
||||
|
||||
if total_mainspec_rolls + total_offspec_rolls == 0 then
|
||||
chat.info( "No rolls found." )
|
||||
return
|
||||
end
|
||||
|
||||
sort_rolls()
|
||||
show( "Mainspec", mainspec_rolls )
|
||||
show( "Offspec", offspec_rolls )
|
||||
show( "Transmog", tmog_rolls )
|
||||
end
|
||||
|
||||
local function print_rolling_complete( canceled )
|
||||
chat.info( string.format( "Rolling for %s %s.", item.link, canceled and "was canceled" or "finished" ) )
|
||||
end
|
||||
|
||||
local function cancel_rolling()
|
||||
stop_listening()
|
||||
print_rolling_complete( true )
|
||||
chat.announce( string.format( "Rolling for %s was canceled.", item.link ) )
|
||||
end
|
||||
|
||||
local function is_rolling()
|
||||
return rolling
|
||||
end
|
||||
|
||||
---@type RollingStrategy
|
||||
return {
|
||||
start_rolling = start_rolling,
|
||||
on_roll = on_roll,
|
||||
show_sorted_rolls = show_sorted_rolls,
|
||||
stop_accepting_rolls = stop_accepting_rolls,
|
||||
cancel_rolling = cancel_rolling,
|
||||
is_rolling = is_rolling,
|
||||
get_type = function() return m.Types.RollingStrategy.NormalRoll end
|
||||
}
|
||||
end
|
||||
|
||||
m.NonSoftResRollingLogic = M
|
||||
return M
|
||||
465
src/OgLootFrameSkin.lua
Normal file
@ -0,0 +1,465 @@
|
||||
RollFor = RollFor or {}
|
||||
local m = RollFor
|
||||
|
||||
if m.OgLootFrameSkin then return end
|
||||
|
||||
local M = {}
|
||||
|
||||
local gui = m.GuiElements
|
||||
|
||||
local texture_size = 512
|
||||
local right_side_width = 32
|
||||
local item_height = 41
|
||||
local header_height = 73
|
||||
local footer_height = 11
|
||||
local min_width = 183
|
||||
|
||||
local texture_dimensions = {
|
||||
total = { width = texture_size, height = texture_size },
|
||||
topleft = { width = texture_size - right_side_width, height = 73 },
|
||||
topright = { width = right_side_width, height = 73 },
|
||||
middleleft = { width = texture_size - right_side_width, height = item_height },
|
||||
middleright = { width = right_side_width, height = item_height },
|
||||
bottomleft = { width = texture_size - right_side_width, height = 11 },
|
||||
bottomright = { width = right_side_width, height = 11 }
|
||||
}
|
||||
|
||||
local td = texture_dimensions
|
||||
|
||||
---@param frame_builder FrameBuilderFactory
|
||||
function M.new( frame_builder )
|
||||
---@param og_set_width function
|
||||
---@param update function?
|
||||
local function set_width( og_set_width, update )
|
||||
return function( self, width )
|
||||
local w = width < min_width and min_width or min_width
|
||||
og_set_width( self, w )
|
||||
if update then update( w ) end
|
||||
end
|
||||
end
|
||||
|
||||
---@param parent Frame
|
||||
local function create_close_button( parent )
|
||||
local button = frame_builder.button():parent( parent ):width( 32 ):height( 32 ):build()
|
||||
|
||||
button:SetNormalTexture( "Interface\\Buttons\\UI-Panel-MinimizeButton-Up" )
|
||||
button:SetPushedTexture( "Interface\\Buttons\\UI-Panel-MinimizeButton-Down" )
|
||||
|
||||
button:Show()
|
||||
|
||||
local highlight_texture = button:CreateTexture( nil, "HIGHLIGHT" )
|
||||
highlight_texture:SetTexture( "Interface\\Buttons\\UI-Panel-MinimizeButton-Highlight" )
|
||||
highlight_texture:SetBlendMode( "ADD" )
|
||||
highlight_texture:SetAllPoints( button )
|
||||
|
||||
---@diagnostic disable-next-line: undefined-field
|
||||
button:SetScript( "OnClick", function()
|
||||
parent:Hide()
|
||||
m.api.CloseLoot()
|
||||
end )
|
||||
|
||||
return button
|
||||
end
|
||||
|
||||
---@param parent Frame
|
||||
local function texture( parent )
|
||||
local result = parent:CreateTexture( nil, "BACKGROUND" )
|
||||
result:SetTexture( "Interface\\AddOns\\RollFor\\assets\\og-loot-frame.tga" )
|
||||
return result
|
||||
end
|
||||
|
||||
---@param parent Frame
|
||||
local function create_portrait( parent )
|
||||
local overlay = parent:CreateTexture( nil, "OVERLAY" )
|
||||
overlay:SetTexture( "Interface\\TargetingFrame\\TargetDead" )
|
||||
overlay:SetWidth( 55 )
|
||||
overlay:SetHeight( 56 )
|
||||
overlay:SetPoint( "TOPLEFT", parent, "TOPLEFT", 9, -5 )
|
||||
end
|
||||
|
||||
---@param parent Frame
|
||||
local function create_title( parent )
|
||||
local font_string = parent:CreateFontString( nil, "ARTWORK", "GameFontNormal" )
|
||||
font_string:SetText( "Items" )
|
||||
font_string:SetJustifyH( "CENTER" )
|
||||
font_string:SetWidth( 90 )
|
||||
font_string:SetHeight( 30 )
|
||||
font_string:SetPoint( "TOP", parent, "TOP", 20, -6 )
|
||||
end
|
||||
|
||||
---@param parent Frame
|
||||
local function dropped_item( parent )
|
||||
local container = m.create_backdrop_frame( m.api, "Frame", nil, parent )
|
||||
|
||||
local w = 38
|
||||
local h = 38
|
||||
local spacing = 6
|
||||
local mouse_down = false
|
||||
local icon_zoom = 0
|
||||
|
||||
local item
|
||||
|
||||
container:SetHeight( item_height )
|
||||
container.name = gui.create_text_in_container( "Button", container, 20, "LEFT", nil, "text", "GameFontNormal" )
|
||||
container.name.text:SetJustifyH( "LEFT" )
|
||||
container.name.text:SetTextColor( 1, 1, 1 )
|
||||
container.name.text:SetWidth( 86 )
|
||||
container.name:SetHeight( h )
|
||||
container.name:SetWidth( 102 )
|
||||
local name_texture = container.name:CreateTexture( nil, "BACKGROUND" )
|
||||
name_texture:SetTexture( "Interface\\QuestFrame\\UI-QuestItemNameFrame" )
|
||||
name_texture:SetWidth( 133 )
|
||||
name_texture:SetHeight( 62 )
|
||||
name_texture:SetPoint( "LEFT", -15, 0 )
|
||||
container.icon = gui.create_icon_in_container( m.vanilla and "LootButton" or "Button", container, w, h, icon_zoom )
|
||||
container.icon:SetPoint( "LEFT", container, "LEFT", 20, 0 )
|
||||
local pushed_texture = container.icon:CreateTexture( nil, "OVERLAY" )
|
||||
pushed_texture:SetAllPoints( container.icon )
|
||||
pushed_texture:SetTexture( "Interface\\Buttons\\UI-Quickslot-Depress" )
|
||||
pushed_texture:Hide()
|
||||
container.icon.pushed_texture = pushed_texture
|
||||
local highlight_texture = container.icon:CreateTexture( nil, "OVERLAY" )
|
||||
highlight_texture:SetAllPoints( container.icon )
|
||||
highlight_texture:SetTexture( "Interface\\Buttons\\ButtonHilight-Square" )
|
||||
highlight_texture:SetBlendMode( "ADD" )
|
||||
highlight_texture:Hide()
|
||||
container.icon.highlight_texture = highlight_texture
|
||||
container.comment = m.create_backdrop_frame( m.api, "Frame", nil, container )
|
||||
container.comment:SetPoint( "CENTER", container.icon, "CENTER", 0, 0 )
|
||||
container.comment:SetWidth( 16 )
|
||||
container.comment:SetHeight( 15 )
|
||||
container.comment:SetFrameLevel( container.icon:GetFrameLevel() + 1 )
|
||||
container.comment.inner = gui.create_text_in_container( "Frame", container.comment, 12, "CENTER", nil, "text", "GameFontNormalSmall" )
|
||||
container.comment.inner:ClearAllPoints()
|
||||
container.comment.inner:SetAllPoints( container.comment )
|
||||
container.comment.inner.text:ClearAllPoints()
|
||||
|
||||
-- Small differences in rendering between OG and modern clients.
|
||||
if m.vanilla then
|
||||
container.comment.inner.text:SetPoint( "CENTER", container.comment.inner, "CENTER", 0, 1 )
|
||||
else
|
||||
container.comment.inner.text:SetPoint( "CENTER", container.comment.inner, "CENTER", 1, 0 )
|
||||
end
|
||||
|
||||
container.comment.inner:SetScale( 0.7 )
|
||||
container.comment.inner:SetFrameLevel( container.comment:GetFrameLevel() + 1 )
|
||||
container.comment:SetBackdrop( {
|
||||
bgFile = "Interface/Buttons/WHITE8x8",
|
||||
edgeFile = "Interface/Buttons/WHITE8x8",
|
||||
tile = false,
|
||||
tileSize = 0,
|
||||
edgeSize = 0.8
|
||||
} )
|
||||
container.comment:SetBackdropColor( 0, 0, 0, 0.7 )
|
||||
container.comment:SetBackdropBorderColor( 1, 0.561, 0.184, 1 )
|
||||
|
||||
container.quantity = gui.create_text_in_container( "Frame", container.icon, 20, "CENTER", nil, "text", "NumberFontNormal" )
|
||||
container.quantity:SetPoint( "BOTTOMRIGHT", -4, 1 )
|
||||
container.quantity:SetHeight( 16 )
|
||||
|
||||
local middleleft = texture( container )
|
||||
local middleright = texture( container )
|
||||
|
||||
local function resize()
|
||||
container.icon:Show()
|
||||
|
||||
local icon_width = container.icon:GetWidth() + spacing
|
||||
local text_width = container.name.text:GetStringWidth() + spacing + 1
|
||||
local total_width = icon_width + text_width
|
||||
|
||||
container:SetWidth( total_width )
|
||||
container:SetPoint( "LEFT", 0, 0 )
|
||||
container:SetPoint( "RIGHT", 0, 0 )
|
||||
end
|
||||
|
||||
local function not_hovered_color()
|
||||
container:SetBackdropColor( 0, 0, 0, 0 )
|
||||
end
|
||||
|
||||
local function update()
|
||||
if not item then return end
|
||||
|
||||
if not item.is_enabled then
|
||||
container.icon:SetAlpha( 0.35 )
|
||||
container.name:SetAlpha( 0.35 )
|
||||
return
|
||||
end
|
||||
|
||||
container.icon:SetAlpha( 1 )
|
||||
container.name:SetAlpha( 1 )
|
||||
end
|
||||
|
||||
---@param v LootFrameItem
|
||||
container.SetItem = function( _, v )
|
||||
item = v
|
||||
container.icon.texture:SetTexture( v.texture )
|
||||
container.name.text:SetText( m.colorize_item_by_quality( v.name, v.quality ) )
|
||||
container.name:SetPoint( "LEFT", container.icon, "RIGHT", spacing + 1, 0 )
|
||||
|
||||
if v.quantity and v.quantity > 1 then
|
||||
container.quantity:Show()
|
||||
container.quantity.text:SetText( v.quantity )
|
||||
container.quantity:SetWidth( container.quantity.text:GetStringWidth() )
|
||||
else
|
||||
container.quantity:Hide()
|
||||
end
|
||||
|
||||
if v.comment then
|
||||
container.comment.inner.text:SetText( v.comment )
|
||||
container.comment:Show()
|
||||
else
|
||||
container.comment:Hide()
|
||||
end
|
||||
|
||||
local function modifier_fn()
|
||||
if m.is_ctrl_key_down() then
|
||||
m.api.DressUpItemLink( v.link )
|
||||
return
|
||||
end
|
||||
|
||||
if m.is_shift_key_down() then
|
||||
m.link_item_in_chat( v.link )
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
container.icon:SetScript( "OnClick", v.is_enabled and not v.is_selected and v.click_fn or modifier_fn )
|
||||
|
||||
if m.vanilla then
|
||||
-- Fucking hell this took forever to figure out. Fuck you Blizzard.
|
||||
-- For looting to work in vanilla, the frame must be of a "LootButton" type and
|
||||
-- then it comes with the SetSlot function that we need to use to set the slot.
|
||||
-- This will probably be a pain in the ass when porting.
|
||||
container.icon:SetSlot( v.slot or 0 )
|
||||
end
|
||||
|
||||
update()
|
||||
resize()
|
||||
end
|
||||
|
||||
local function on_enter( self )
|
||||
if m.vanilla then self = this end
|
||||
|
||||
if not item or item.is_enabled then container.icon.highlight_texture:Show() end
|
||||
if not item then return end
|
||||
|
||||
if item.tooltip_link then
|
||||
m.api.GameTooltip:SetOwner( self, "ANCHOR_RIGHT" )
|
||||
m.api.GameTooltip:SetHyperlink( item.tooltip_link )
|
||||
m.api.GameTooltip:Show()
|
||||
end
|
||||
|
||||
if not item.is_enabled then return end
|
||||
end
|
||||
|
||||
container:SetBackdrop( {
|
||||
bgFile = "Interface/Buttons/WHITE8x8",
|
||||
tile = false,
|
||||
tileSize = 0,
|
||||
} )
|
||||
|
||||
not_hovered_color()
|
||||
|
||||
local function on_leave()
|
||||
container.icon.highlight_texture:Hide()
|
||||
m.api.GameTooltip:Hide()
|
||||
mouse_down = false
|
||||
end
|
||||
|
||||
container.name:SetScript( "OnEnter", function( self )
|
||||
if not item then return end
|
||||
if item.comment_tooltip then
|
||||
if m.vanilla then self = this end
|
||||
|
||||
self.tooltip_scale = m.api.GameTooltip:GetScale()
|
||||
m.api.GameTooltip:SetOwner( self, "ANCHOR_RIGHT" )
|
||||
|
||||
local result = ""
|
||||
|
||||
for _, line in ipairs( item.comment_tooltip ) do
|
||||
if result ~= "" then result = result .. "\n" end
|
||||
result = result .. line
|
||||
end
|
||||
|
||||
m.api.GameTooltip:AddLine( result, 1, 1, 1 )
|
||||
m.api.GameTooltip:SetScale( 0.9 )
|
||||
m.api.GameTooltip:Show()
|
||||
end
|
||||
end )
|
||||
|
||||
container.name:SetScript( "OnLeave", function( self )
|
||||
if m.vanilla then self = this end
|
||||
|
||||
m.api.GameTooltip:Hide()
|
||||
m.api.GameTooltip:SetScale( self.tooltip_scale or 1 )
|
||||
mouse_down = false
|
||||
|
||||
not_hovered_color()
|
||||
end )
|
||||
|
||||
container.icon:SetScript( "OnEnter", on_enter )
|
||||
container.icon:SetScript( "OnLeave", on_leave )
|
||||
|
||||
local function on_mouse_down()
|
||||
if not item or item.is_enabled then container.icon.pushed_texture:Show() end
|
||||
if not item then return end
|
||||
if not item.is_enabled or item.is_selected then return end
|
||||
|
||||
mouse_down = true
|
||||
end
|
||||
|
||||
local function on_mouse_up()
|
||||
container.icon.pushed_texture:Hide()
|
||||
|
||||
if not item then return end
|
||||
if not item.is_enabled or item.is_selected then return end
|
||||
|
||||
if not mouse_down then return end
|
||||
end
|
||||
|
||||
container.icon:SetScript( "OnMouseUp", on_mouse_up )
|
||||
container.icon:SetScript( "OnMouseDown", on_mouse_down )
|
||||
|
||||
container:SetScript( "OnShow", function()
|
||||
mouse_down = false
|
||||
end )
|
||||
|
||||
local function update_textures( width )
|
||||
local left_side_width = width - right_side_width
|
||||
local height_offset = 0.498 + ((td.middleleft.height + 1) / 512)
|
||||
|
||||
middleleft:SetTexCoord( 0, (left_side_width + 2) / td.total.width, 0.511, height_offset )
|
||||
middleleft:SetWidth( left_side_width + 2 )
|
||||
middleleft:SetHeight( td.middleleft.height )
|
||||
middleleft:SetPoint( "TOPLEFT", container, "TOPLEFT", 0, 0 )
|
||||
|
||||
middleright:SetTexCoord( (td.total.width - td.middleright.width) / td.total.width, 1, 0.511, height_offset )
|
||||
middleright:SetWidth( td.middleright.width )
|
||||
middleright:SetHeight( td.middleright.height )
|
||||
middleright:SetPoint( "TOPRIGHT", container, "TOPRIGHT", 0, 0 )
|
||||
end
|
||||
|
||||
local og_set_width = container.SetWidth
|
||||
container.SetWidth = set_width( og_set_width, update_textures )
|
||||
return container
|
||||
end
|
||||
|
||||
---@param on_drag_stop function
|
||||
---@param on_show function
|
||||
---@param on_hide function
|
||||
local function header( on_drag_stop, on_show, on_hide )
|
||||
local frame = frame_builder.new() ---@type Frame
|
||||
:name( "RollForLootFrameHeader" )
|
||||
:parent( m.api.UIParent )
|
||||
:width( min_width )
|
||||
:height( header_height )
|
||||
:sound()
|
||||
:gui_elements( {} )
|
||||
:movable()
|
||||
:on_show( on_show )
|
||||
:on_hide( on_hide )
|
||||
:on_drag_stop( on_drag_stop )
|
||||
:hidden()
|
||||
:build()
|
||||
|
||||
local topleft = texture( frame )
|
||||
local topright = texture( frame )
|
||||
create_portrait( frame )
|
||||
create_title( frame )
|
||||
local close_button = create_close_button( frame )
|
||||
close_button:ClearAllPoints()
|
||||
close_button:SetPoint( "TOPRIGHT", frame, "TOPRIGHT", 5, -6 )
|
||||
|
||||
local function update( width )
|
||||
local topoffset = td.topleft.height / td.total.height
|
||||
|
||||
local left_side_width = width - right_side_width
|
||||
topleft:SetTexCoord( 0, left_side_width / td.total.width, 0, topoffset )
|
||||
topleft:SetWidth( left_side_width )
|
||||
topleft:SetHeight( td.topleft.height )
|
||||
topleft:SetPoint( "TOPLEFT", frame, "TOPLEFT", 0, 0 )
|
||||
|
||||
topright:SetTexCoord( (td.total.width - td.topright.width) / td.total.width, 1, 0, topoffset )
|
||||
topright:SetWidth( td.topright.width )
|
||||
topright:SetHeight( td.topright.height )
|
||||
topright:SetPoint( "TOPLEFT", frame, "TOPLEFT", left_side_width, 0 )
|
||||
end
|
||||
|
||||
local og_set_width = frame.SetWidth
|
||||
frame.SetWidth = set_width( og_set_width, update )
|
||||
|
||||
return frame
|
||||
end
|
||||
|
||||
---@param parent Frame
|
||||
local function body( parent )
|
||||
local frame = frame_builder.new()
|
||||
:name( "RollForLootFrame" )
|
||||
:parent( parent )
|
||||
:width( 280 )
|
||||
:height( 100 )
|
||||
:gui_elements( { dropped_item = dropped_item } )
|
||||
:build()
|
||||
|
||||
frame:ClearAllPoints()
|
||||
frame:SetPoint( "TOP", parent, "BOTTOM", 0, 1 )
|
||||
|
||||
local og_set_width = frame.SetWidth
|
||||
frame.SetWidth = set_width( og_set_width )
|
||||
return frame
|
||||
end
|
||||
|
||||
---@param parent Frame
|
||||
local function footer( parent )
|
||||
local frame = frame_builder.new() ---@type Frame
|
||||
:name( "RollForLootFrameFooter" )
|
||||
:parent( parent )
|
||||
:width( min_width )
|
||||
:height( footer_height )
|
||||
:gui_elements( {} )
|
||||
:movable()
|
||||
:build()
|
||||
|
||||
local bottomleft = texture( frame )
|
||||
local bottomright = texture( frame )
|
||||
|
||||
local function update( width )
|
||||
local left_side_width = width - right_side_width
|
||||
bottomleft:SetTexCoord( 0, left_side_width / td.total.width, 1.001 - (td.bottomleft.height / td.total.height), 0.999 )
|
||||
bottomleft:SetWidth( left_side_width )
|
||||
bottomleft:SetHeight( td.bottomleft.height )
|
||||
bottomleft:SetPoint( "TOPLEFT", frame, "TOPLEFT", 0, 0 )
|
||||
|
||||
bottomright:SetTexCoord( 1 - (td.bottomright.width / td.total.width), 1, 1.001 - (td.bottomright.height / td.total.height), 0.999 )
|
||||
bottomright:SetWidth( td.bottomright.width )
|
||||
bottomright:SetHeight( td.bottomright.height )
|
||||
bottomright:SetPoint( "TOPRIGHT", frame, "TOPRIGHT", 0, 0 )
|
||||
end
|
||||
|
||||
local og_set_width = frame.SetWidth
|
||||
frame.SetWidth = set_width( og_set_width, update )
|
||||
|
||||
return frame
|
||||
end
|
||||
|
||||
local function get_item_height()
|
||||
return item_height
|
||||
end
|
||||
|
||||
local function get_footer_height()
|
||||
return footer_height
|
||||
end
|
||||
|
||||
---@type LootFrameSkin
|
||||
return {
|
||||
header = header,
|
||||
body = body,
|
||||
dropped_item = dropped_item,
|
||||
footer = footer,
|
||||
get_item_height = get_item_height,
|
||||
get_footer_height = get_footer_height
|
||||
}
|
||||
end
|
||||
|
||||
m.OgLootFrameSkin = M
|
||||
return M
|
||||
68
src/PlayerInfo.lua
Normal file
@ -0,0 +1,68 @@
|
||||
RollFor = RollFor or {}
|
||||
local m = RollFor
|
||||
|
||||
if m.PlayerInfo then return end
|
||||
|
||||
---@class PlayerInfo
|
||||
---@field get_name fun(): string
|
||||
---@field get_class fun(): string
|
||||
---@field is_master_looter fun(): boolean
|
||||
---@field is_leader fun(): boolean
|
||||
---@field is_assistant fun(): boolean
|
||||
|
||||
local M = {}
|
||||
|
||||
---@param api table
|
||||
function M.new( api )
|
||||
local function get_name()
|
||||
return api.UnitName( "player" )
|
||||
end
|
||||
|
||||
local function get_class()
|
||||
return api.UnitClass( "player" )
|
||||
end
|
||||
|
||||
local function is_master_looter()
|
||||
if not api.IsInGroup() then return false end
|
||||
|
||||
local loot_method, id = api.GetLootMethod()
|
||||
if loot_method ~= "master" or not id then return false end
|
||||
if id == 0 then return true end
|
||||
|
||||
if api.IsInRaid() then
|
||||
local name = api.GetRaidRosterInfo( id )
|
||||
return name == get_name()
|
||||
end
|
||||
|
||||
return api.UnitName( "party" .. id ) == get_name()
|
||||
end
|
||||
|
||||
local function is_leader()
|
||||
return api.UnitIsGroupLeader( "player" )
|
||||
end
|
||||
|
||||
local function is_assistant()
|
||||
if not api.IsInRaid() then return false end
|
||||
local my_name = get_name()
|
||||
|
||||
for i = 1, 40 do
|
||||
local name, rank = api.GetRaidRosterInfo( i )
|
||||
|
||||
if name and name == my_name then
|
||||
return rank and rank > 0 or false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@type PlayerInfo
|
||||
return {
|
||||
get_name = get_name,
|
||||
get_class = get_class,
|
||||
is_master_looter = is_master_looter,
|
||||
is_leader = is_leader,
|
||||
is_assistant = is_assistant
|
||||
}
|
||||
end
|
||||
|
||||
m.PlayerInfo = M
|
||||
return M
|
||||
170
src/PopupBuilder.lua
Normal file
@ -0,0 +1,170 @@
|
||||
RollFor = RollFor or {}
|
||||
local m = RollFor
|
||||
|
||||
if m.PopupBuilder then return end
|
||||
|
||||
local M = {}
|
||||
|
||||
local getn = m.getn
|
||||
|
||||
---@class Popup : Frame
|
||||
---@field resize fun( self: Popup, lines: table )
|
||||
|
||||
---@class PopupBuilder
|
||||
---@field name fun( self: PopupBuilder, name: string ): PopupBuilder
|
||||
---@field parent fun( self: PopupBuilder, parent: Frame ): PopupBuilder
|
||||
---@field height fun( self: PopupBuilder, height: number ): PopupBuilder
|
||||
---@field width fun( self: PopupBuilder, width: number ): PopupBuilder
|
||||
---@field point fun( self: PopupBuilder, p: table ): PopupBuilder
|
||||
---@field sound fun( self: PopupBuilder ): PopupBuilder
|
||||
---@field frame_level fun( self: PopupBuilder, frame_level: number ): PopupBuilder
|
||||
---@field backdrop_color fun( self: PopupBuilder, r: number, g: number, b: number, a: number ): PopupBuilder
|
||||
---@field bg_file fun( self: PopupBuilder, bg_file: string ): PopupBuilder
|
||||
---@field esc fun( self: PopupBuilder ): PopupBuilder
|
||||
---@field gui_elements fun( self: PopupBuilder, gui_elements: table ): PopupBuilder
|
||||
---@field frame_style fun( self: PopupBuilder, frame_style: FrameStyle ): PopupBuilder
|
||||
---@field on_drag_stop fun( self: PopupBuilder, callback: function ): PopupBuilder
|
||||
---@field movable fun( self: PopupBuilder ): PopupBuilder
|
||||
---@field resizable fun( self ): PopupBuilder
|
||||
---@field on_resize fun( self: PopupBuilder, callback: function ): PopupBuilder
|
||||
---@field border_size fun( self: PopupBuilder, border_size: number ): PopupBuilder
|
||||
---@field on_show fun( self: PopupBuilder, on_show: function ): PopupBuilder
|
||||
---@field on_hide fun( self: PopupBuilder, on_hide: function ): PopupBuilder
|
||||
---@field border_color fun( self: PopupBuilder, r: number, g: number, b: number, a: number ): PopupBuilder
|
||||
---@field self_centered_anchor fun( self: PopupBuilder ): PopupBuilder
|
||||
---@field scale fun( self: PopupBuilder, scale: number ): PopupBuilder
|
||||
---@field strata fun( self: PopupBuilder, strata: FrameStrata ): PopupBuilder
|
||||
---@field build fun( self: PopupBuilder ): Popup
|
||||
|
||||
---@param frame_builder FrameBuilderFactory
|
||||
---@param bottom_margin number?
|
||||
---@param bottom_button_margin number?
|
||||
---@param side_margin number?
|
||||
local function new( frame_builder, bottom_margin, bottom_button_margin, side_margin )
|
||||
local m_button_padding = 10
|
||||
local m_bottom_button_margin = bottom_button_margin or 8
|
||||
local m_bottom_margin = bottom_margin or (30 + m_bottom_button_margin)
|
||||
local m_side_margin = side_margin or 35
|
||||
|
||||
local function align_buttons( popup, lines )
|
||||
if not popup.buttons_frame then
|
||||
local frame = m.api.CreateFrame( "Frame", nil, popup )
|
||||
frame:SetPoint( "BOTTOM", 0, m_bottom_button_margin )
|
||||
popup.buttons_frame = frame
|
||||
end
|
||||
|
||||
local total_width = 0
|
||||
local max_height = 0
|
||||
local last_anchor = nil
|
||||
|
||||
local buttons = m.filter( lines, function( line ) return line.line_type == "button" end )
|
||||
|
||||
for _, button in ipairs( buttons ) do
|
||||
local frame = button.frame
|
||||
local height = frame:GetHeight()
|
||||
local width = frame:GetWidth()
|
||||
local scale = frame:GetScale()
|
||||
|
||||
if height > max_height then max_height = height end
|
||||
|
||||
if not last_anchor then
|
||||
frame:SetPoint( "LEFT", popup.buttons_frame, "LEFT", 0, 0 )
|
||||
else
|
||||
frame:SetPoint( "LEFT", last_anchor, "RIGHT", m_button_padding, 0 )
|
||||
total_width = total_width + (m_button_padding * scale)
|
||||
end
|
||||
|
||||
total_width = total_width + (width * scale)
|
||||
last_anchor = frame
|
||||
end
|
||||
|
||||
popup.buttons_frame:SetWidth( total_width )
|
||||
popup.buttons_frame:SetHeight( max_height )
|
||||
end
|
||||
|
||||
local function get_total_width( buttons )
|
||||
local result = 0
|
||||
|
||||
for _, button in ipairs( buttons ) do
|
||||
local frame = button.frame
|
||||
result = result + frame:GetWidth() * frame:GetScale()
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
local function resize( popup, lines )
|
||||
local max_width = 0
|
||||
local height = 0
|
||||
|
||||
for _, line in ipairs( lines ) do
|
||||
if line.line_type ~= "button" and line.line_type ~= "info" then
|
||||
local frame = line.frame
|
||||
local scale = frame.GetScale and frame:GetScale() or 1
|
||||
local width = frame:GetWidth() * scale
|
||||
|
||||
height = height + frame:GetHeight() * scale
|
||||
height = height + line.padding
|
||||
if width > max_width then max_width = width end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
local buttons = m.filter( lines, function( line ) return line.line_type == "button" end )
|
||||
local button_count = getn( buttons )
|
||||
local button_width = get_total_width( buttons ) + (button_count - 1) * m_button_padding
|
||||
|
||||
if button_width > max_width then max_width = button_width end
|
||||
|
||||
if button_count > 0 then
|
||||
height = height + 23
|
||||
end
|
||||
|
||||
popup:SetWidth( max_width + m_side_margin )
|
||||
|
||||
|
||||
popup:SetHeight( height + (button_count > 0 and m_bottom_margin or 23) )
|
||||
|
||||
align_buttons( popup, lines )
|
||||
end
|
||||
|
||||
local decoratee = frame_builder.new()
|
||||
local build = decoratee.build
|
||||
|
||||
decoratee.build = function()
|
||||
---@class Popup
|
||||
local result = build( decoratee )
|
||||
result.resize = resize
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
return decoratee
|
||||
end
|
||||
|
||||
---@param frame_builder FrameBuilderFactory
|
||||
---@param bottom_margin number?
|
||||
---@param bottom_button_margin number?
|
||||
---@param side_margin number?
|
||||
function M.modern( frame_builder, bottom_margin, bottom_button_margin, side_margin )
|
||||
local builder = new( frame_builder, bottom_margin, bottom_button_margin, side_margin )
|
||||
:frame_style( "Modern" )
|
||||
:backdrop_color( 0, 0, 0, 0.6 )
|
||||
|
||||
return builder
|
||||
end
|
||||
|
||||
---@param frame_builder FrameBuilderFactory
|
||||
---@param bottom_margin number?
|
||||
---@param bottom_button_margin number?
|
||||
---@param side_margin number?
|
||||
function M.classic( frame_builder, bottom_margin, bottom_button_margin, side_margin )
|
||||
local builder = new( frame_builder, bottom_margin, bottom_button_margin, side_margin )
|
||||
:frame_style( "Classic" )
|
||||
:border_size( 25 )
|
||||
|
||||
return builder
|
||||
end
|
||||
|
||||
m.PopupBuilder = M
|
||||
return M
|
||||
137
src/RaidRollRollingLogic.lua
Normal file
@ -0,0 +1,137 @@
|
||||
RollFor = RollFor or {}
|
||||
local m = RollFor
|
||||
|
||||
if m.RaidRollRollingLogic then return end
|
||||
|
||||
local M = {}
|
||||
|
||||
local getn = m.getn
|
||||
local hl = m.colors.hl
|
||||
local strategy = m.Types.RollingStrategy.RaidRoll
|
||||
local roll_type = m.Types.RollType.MainSpec
|
||||
local clear_table = m.clear_table
|
||||
|
||||
---@type MakeWinnerFn
|
||||
local make_winner = m.Types.make_winner
|
||||
|
||||
-- TODO: Lots of similarity with InstaRaidRollRollingLogic. Perhaps refactor.
|
||||
|
||||
---@param chat Chat
|
||||
---@param ace_timer AceTimer
|
||||
---@param item Item
|
||||
---@param item_count number
|
||||
---@param winner_tracker WinnerTracker
|
||||
---@param controller RollControllerFacade
|
||||
---@param candidates ItemCandidate[]|Player[]
|
||||
---@param roller PlayerInfo
|
||||
function M.new(
|
||||
chat,
|
||||
ace_timer,
|
||||
item,
|
||||
item_count,
|
||||
winner_tracker,
|
||||
controller,
|
||||
candidates,
|
||||
roller
|
||||
)
|
||||
local m_rolling = false
|
||||
local m_winners = {}
|
||||
|
||||
local function clear_winners()
|
||||
clear_table( m_winners )
|
||||
if m.vanilla then m_winners.n = 0 end
|
||||
end
|
||||
|
||||
local function print_players( players )
|
||||
local buffer = ""
|
||||
|
||||
for i, player in ipairs( players ) do
|
||||
local separator = ""
|
||||
if buffer ~= "" then separator = separator .. ", " end
|
||||
local next_player = string.format( "[%d]:%s", i, player.name )
|
||||
|
||||
if (string.len( buffer .. separator .. next_player ) > 255) then
|
||||
chat.announce( buffer )
|
||||
buffer = next_player
|
||||
else
|
||||
buffer = buffer .. separator .. next_player
|
||||
end
|
||||
end
|
||||
|
||||
if buffer ~= "" then chat.announce( buffer ) end
|
||||
end
|
||||
|
||||
local function raid_roll()
|
||||
m_rolling = true
|
||||
m.api.RandomRoll( 1, getn( candidates ) )
|
||||
end
|
||||
|
||||
local function start_rolling()
|
||||
m_rolling = true
|
||||
clear_winners()
|
||||
|
||||
chat.announce( string.format( "Raid rolling %s%s...", item_count and item_count > 1 and string.format( "%sx", item_count ) or "", item.link ) )
|
||||
|
||||
print_players( candidates )
|
||||
ace_timer.ScheduleTimer( M, function()
|
||||
for _ = 1, item_count do
|
||||
raid_roll()
|
||||
end
|
||||
end, 1 )
|
||||
end
|
||||
|
||||
---@param player Player
|
||||
---@param roll number
|
||||
---@param min number
|
||||
---@param max number
|
||||
local function on_roll( player, roll, min, max )
|
||||
if player.name ~= roller.get_name() then return end
|
||||
if min ~= 1 or max ~= getn( candidates ) then return end
|
||||
|
||||
table.insert( m_winners, candidates[ roll ] )
|
||||
if getn( m_winners ) < item_count then return end
|
||||
|
||||
local winners = m.map( m_winners,
|
||||
---@param p ItemCandidate|Player
|
||||
function( p )
|
||||
if type( p ) == "table" then -- Fucking lua50 and its n.
|
||||
local winner = make_winner( p.name, p.class, item, p.type == "ItemCandidate" or false, roll_type, nil )
|
||||
winner_tracker.track( winner.name, item.link, roll_type, nil, m.Types.RollingStrategy.RaidRoll ) -- TODO: Get the fuck outta here.
|
||||
return winner
|
||||
end
|
||||
end )
|
||||
|
||||
controller.winners_found( item, item_count, winners, strategy )
|
||||
controller.finish()
|
||||
m_rolling = false
|
||||
end
|
||||
|
||||
local function is_rolling()
|
||||
return m_rolling
|
||||
end
|
||||
|
||||
local function show_sorted_rolls()
|
||||
if getn( m_winners ) == 0 then
|
||||
chat.info( "There is no winner yet.", nil, "RaidRoll" )
|
||||
return
|
||||
end
|
||||
|
||||
for _, winner in ipairs( m_winners ) do
|
||||
chat.info( string.format( "%s won %s.", hl( winner.name ), item.link ), nil, "RaidRoll" )
|
||||
end
|
||||
end
|
||||
|
||||
---@type RollingStrategy
|
||||
return {
|
||||
start_rolling = start_rolling, -- This probably doesn't belong here either.
|
||||
on_roll = on_roll,
|
||||
is_rolling = is_rolling,
|
||||
show_sorted_rolls = show_sorted_rolls,
|
||||
get_type = function() return m.Types.RollingStrategy.RaidRoll end,
|
||||
cancel_rolling = m.noop(),
|
||||
stop_accepting_rolls = m.noop()
|
||||
}
|
||||
end
|
||||
|
||||
m.RaidRollRollingLogic = M
|
||||
return M
|
||||
1274
src/RollController.lua
Normal file
34
src/RollForAd.lua
Normal file
@ -0,0 +1,34 @@
|
||||
RollFor = RollFor or {}
|
||||
local m = RollFor
|
||||
|
||||
if m.RollForAd then return end
|
||||
|
||||
local M = {}
|
||||
|
||||
local url = "https://github.com/obszczymucha/roll-for-vanilla/releases/download/latest/RollFor.zip"
|
||||
|
||||
---@param player_info PlayerInfo
|
||||
function M.new( player_info )
|
||||
local function on_chat_msg( channel )
|
||||
return function( message, player_name )
|
||||
if message == "RollFor" and player_name == player_info.get_name() then
|
||||
m.api.SendChatMessage( url, channel )
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function on_chat_msg_whisper_inform( message, player_name )
|
||||
if message == "RollFor" then
|
||||
m.api.SendChatMessage( url, "WHISPER", nil, player_name )
|
||||
end
|
||||
end
|
||||
|
||||
return {
|
||||
on_chat_msg_party = on_chat_msg( "PARTY" ),
|
||||
on_chat_msg_raid = on_chat_msg( "RAID" ),
|
||||
on_chat_msg_whisper_inform = on_chat_msg_whisper_inform
|
||||
}
|
||||
end
|
||||
|
||||
m.RollForAd = M
|
||||
return M
|
||||
222
src/RollResultAnnouncer.lua
Normal file
@ -0,0 +1,222 @@
|
||||
RollFor = RollFor or {}
|
||||
local m = RollFor
|
||||
|
||||
if m.RollResultAnnouncer then return end
|
||||
|
||||
local M = {}
|
||||
local getn = m.getn
|
||||
|
||||
local RT = m.Types.RollType
|
||||
local RS = m.Types.RollingStrategy
|
||||
local hl = m.colors.hl
|
||||
local grey = m.colors.grey
|
||||
|
||||
---@param chat Chat
|
||||
---@param roll_controller RollController
|
||||
---@param softres GroupAwareSoftRes
|
||||
---@param config Config
|
||||
function M.new( chat, roll_controller, softres, config )
|
||||
---@param winners Winner[]
|
||||
---@param top_roll boolean
|
||||
local announce_winner = function( winners, top_roll )
|
||||
local roll_value = winners[ 1 ].winning_roll
|
||||
|
||||
if not roll_value then
|
||||
return
|
||||
end
|
||||
|
||||
local roll_type = winners[ 1 ].roll_type
|
||||
local roll_type_str = roll_type == RT.MainSpec and "" or string.format( " (%s)", m.roll_type_abbrev_chat( roll_type ) )
|
||||
local rerolling = winners[ 1 ].rerolling
|
||||
local item = winners[ 1 ].item
|
||||
|
||||
local function sr_plus( value )
|
||||
local sr_players = softres.get( item.id )
|
||||
local sr_player = m.find( winners[ 1 ].name, sr_players, 'name' )
|
||||
|
||||
if sr_player and sr_player.sr_plus then
|
||||
local plus_value = sr_player.sr_plus
|
||||
value = value - plus_value
|
||||
return string.format( "%s+%s=%s", value, plus_value, value + plus_value )
|
||||
end
|
||||
|
||||
return value
|
||||
end
|
||||
|
||||
local function message( rollers, f )
|
||||
return string.format(
|
||||
"%s %srolled the %shighest (%s) for %s%s.",
|
||||
rollers,
|
||||
rerolling and "re-" or "",
|
||||
top_roll and "" or "next ",
|
||||
f and f( sr_plus( roll_value ) ) or sr_plus( roll_value ),
|
||||
-- item_count and item_count > 1 and string.format( "%sx", item_count ) or "",
|
||||
item.link,
|
||||
roll_type_str
|
||||
)
|
||||
end
|
||||
|
||||
local rollers = m.prettify_table( winners, function( p ) return p.name end )
|
||||
chat.info( message( rollers, hl ) )
|
||||
chat.announce( message( rollers ) )
|
||||
end
|
||||
|
||||
---@param winners Winner[]
|
||||
---@return table<number, Winner[]>
|
||||
local function split_winners_by_roll( winners )
|
||||
if getn( winners ) == 0 then return {} end
|
||||
local result = {}
|
||||
|
||||
local i = 0
|
||||
local last_roll
|
||||
|
||||
for _, winner in ipairs( winners ) do
|
||||
if not last_roll or last_roll ~= winner.winning_roll then
|
||||
table.insert( result, { winner } )
|
||||
i = i + 1
|
||||
last_roll = winner.winning_roll
|
||||
else
|
||||
table.insert( result[ i ], winner )
|
||||
end
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
---@param data WinnersFoundData
|
||||
local function on_winners_found( data )
|
||||
if not data then return end
|
||||
|
||||
local item, item_count, winners, strategy = data.item, data.item_count, data.winners, data.rolling_strategy
|
||||
local winner_count = getn( winners )
|
||||
|
||||
if winner_count == 0 then
|
||||
return
|
||||
end
|
||||
|
||||
if strategy == RS.RaidRoll or strategy == RS.InstaRaidRoll then
|
||||
for _, winner in ipairs( winners ) do
|
||||
chat.announce( string.format( "%s wins %s (raid-roll).", winner.name, item.link ) )
|
||||
end
|
||||
|
||||
return
|
||||
end
|
||||
|
||||
if strategy == RS.SoftResRoll and winner_count == item_count and not winners[ 1 ].winning_roll then
|
||||
local ressed_by = m.prettify_table( m.map( winners, function( winner ) return winner.name end ) )
|
||||
chat.announce( string.format( "%s soft-ressed %s.", ressed_by, item.link ), true )
|
||||
|
||||
return
|
||||
end
|
||||
|
||||
for i, winners_by_roll in ipairs( split_winners_by_roll( winners ) ) do
|
||||
announce_winner( winners_by_roll, i == 1 )
|
||||
end
|
||||
end
|
||||
|
||||
---@param data { players: RollingPlayer[], item: Item, item_count: number, roll_type: RollType, roll: number, rerolling: boolean?, top_roll: boolean? }
|
||||
local function on_tie( data )
|
||||
local players = data.players
|
||||
local roll_type = data.roll_type
|
||||
local roll_value = data.roll
|
||||
local rerolling = data.rerolling
|
||||
local top_roll = data.top_roll
|
||||
local item = data.item
|
||||
|
||||
local player_names = m.map( players,
|
||||
function( p )
|
||||
if type( p ) == "table" then -- Fucking lua50 and its n.
|
||||
return p.name
|
||||
end
|
||||
end )
|
||||
|
||||
local top_rollers_str = m.prettify_table( player_names )
|
||||
local top_rollers_str_colored = m.prettify_table( player_names, hl )
|
||||
local roll_type_str = roll_type == RT.MainSpec and "" or string.format( " (%s)", m.roll_type_abbrev_chat( roll_type ) )
|
||||
|
||||
local function message( rollers, f )
|
||||
return string.format(
|
||||
"%s %srolled the %shighest (%s) for %s%s.",
|
||||
rollers,
|
||||
rerolling and "re-" or "",
|
||||
top_roll and "" or "next ",
|
||||
f and f( roll_value ) or roll_value,
|
||||
-- item_count and item_count > 1 and string.format( "%sx", item_count ) or "",
|
||||
item.link,
|
||||
roll_type_str
|
||||
)
|
||||
end
|
||||
|
||||
chat.info( message( top_rollers_str_colored ) )
|
||||
chat.announce( message( top_rollers_str ) )
|
||||
end
|
||||
|
||||
---@param event_data TieStartData
|
||||
local function on_tie_start( event_data )
|
||||
local data, iteration = event_data.tracker_data, event_data.iteration
|
||||
if not data or not iteration then return end
|
||||
|
||||
local player_count = getn( iteration.rolls )
|
||||
if player_count == 0 then return end
|
||||
|
||||
local roll_type = iteration.rolls[ 1 ].roll_type
|
||||
local item, item_count, winners = data.item, data.item_count, data.winners
|
||||
local winner_count = getn( winners )
|
||||
local count = item_count - winner_count
|
||||
local prefix = count > 1 and string.format( "%sx", count ) or ""
|
||||
local suffix = count > 1 and string.format( " %s top rolls win.", count ) or ""
|
||||
|
||||
local player_names = m.map( iteration.rolls,
|
||||
---@param roll_data RollData
|
||||
function( roll_data )
|
||||
return roll_data.player_name
|
||||
end )
|
||||
|
||||
local top_rollers_str = m.prettify_table( player_names )
|
||||
local roll_threshold_str = config.roll_threshold( roll_type ).str
|
||||
|
||||
chat.announce( string.format( "%s %s for %s%s now.%s", top_rollers_str, roll_threshold_str, prefix, item.link, suffix ) )
|
||||
end
|
||||
|
||||
local function on_tick( data )
|
||||
if not data or not data.seconds_left then return end
|
||||
|
||||
local seconds_left = data.seconds_left
|
||||
|
||||
if seconds_left == 3 then
|
||||
chat.announce( "Stopping rolls in 3" )
|
||||
elseif seconds_left < 3 then
|
||||
chat.announce( tostring( seconds_left ) )
|
||||
end
|
||||
end
|
||||
|
||||
---@param event_data RollingFinishedData
|
||||
local function on_finish( event_data )
|
||||
local data = event_data.roll_tracker_data
|
||||
if not data or not data.item then return end
|
||||
|
||||
local winner_count = getn( data.winners )
|
||||
|
||||
if winner_count == 0 then
|
||||
local message = string.format( "No one rolled for %s.", data.item.link )
|
||||
chat.info( message )
|
||||
chat.announce( message )
|
||||
end
|
||||
end
|
||||
|
||||
---@param data LootAwardedData
|
||||
local function on_loot_awarded( data )
|
||||
local player_name = data.player_class and m.colorize_player_by_class( data.player_name, data.player_class ) or grey( data.player_name )
|
||||
chat.info( string.format( "%s received %s.", player_name, data.item_link ) )
|
||||
end
|
||||
|
||||
roll_controller.subscribe( "finish", on_finish )
|
||||
roll_controller.subscribe( "winners_found", on_winners_found )
|
||||
roll_controller.subscribe( "there_was_a_tie", on_tie )
|
||||
roll_controller.subscribe( "tie_start", on_tie_start )
|
||||
roll_controller.subscribe( "tick", on_tick )
|
||||
roll_controller.subscribe( "loot_awarded", on_loot_awarded )
|
||||
end
|
||||
|
||||
m.RollResultAnnouncer = M
|
||||
return M
|
||||
358
src/RollTracker.lua
Normal file
@ -0,0 +1,358 @@
|
||||
RollFor = RollFor or {}
|
||||
local m = RollFor
|
||||
|
||||
if m.RollTracker then return end
|
||||
|
||||
-- I hold the entire journey of rolls.
|
||||
-- The first iteration starts with either a normal or soft-res rolling.
|
||||
-- Then there's either a winner or a tie.
|
||||
-- For each tie we have a new iteration, because a tie can result in another tie.
|
||||
local M = m.Module.new( "RollTracker" )
|
||||
local getn = m.getn
|
||||
|
||||
local clear_table = m.clear_table
|
||||
local RS = m.Types.RollingStrategy
|
||||
local RT = m.Types.RollType
|
||||
local S = m.Types.RollingStatus
|
||||
|
||||
---@class RollData
|
||||
---@field player_name string
|
||||
---@field player_class string
|
||||
---@field roll_type RollType
|
||||
---@field roll number?
|
||||
|
||||
---@class RollIteration
|
||||
---@field rolling_strategy RollingStrategyType
|
||||
---@field message string
|
||||
---@field rolls RollData[]
|
||||
---@field ignored_rolls RollData[]?
|
||||
---@field tied_roll number?
|
||||
|
||||
-- The status data is different for each type. TODO: split this.
|
||||
---@class RollStatus
|
||||
---@field type RollingStatus
|
||||
---@field seconds_left number?
|
||||
---@field winners RollingPlayer[]?
|
||||
---@field ml_candidates ItemCandidate[]?
|
||||
|
||||
---@alias RollTrackerData {
|
||||
--- item: Item|MasterLootDistributableItem,
|
||||
--- item_count: number,
|
||||
--- status: RollStatus,
|
||||
--- iterations: RollIteration[],
|
||||
--- winners: Winner[],
|
||||
--- ml_candidates: ItemCandidate[] }
|
||||
|
||||
---@class RollTracker
|
||||
---@field preview fun( count: number, ml_candidates: ItemCandidate[], soft_ressers: RollingPlayer[], hard_ressed: boolean )
|
||||
---@field start fun( rolling_strategy: RollingStrategyType, count: number, seconds: number?, message: string?, required_rolling_players: RollingPlayer[]? )
|
||||
---@field waiting_for_rolls fun()
|
||||
---@field add_winners fun( winners: Winner[] )
|
||||
---@field finish fun( ml_candidates: ItemCandidate[] )
|
||||
---@field rolling_canceled fun()
|
||||
---@field tie fun( required_rolling_players: RollingPlayer[], roll_type: RollType, roll: number )
|
||||
---@field tie_start fun()
|
||||
---@field add fun( player_name: string, player_class: string, roll_type: RollType, roll: number )
|
||||
---@field add_ignored fun( player_name: string, roll_type: RollType, roll: number, reason: string )
|
||||
---@field get fun(): RollTrackerData, RollIteration
|
||||
---@field tick fun( seconds_left: number )
|
||||
---@field clear fun()
|
||||
---@field loot_awarded fun( player_name: string, item_id: number )
|
||||
---@field create_roll_data fun( players: RollingPlayer[] ): RollData[]
|
||||
|
||||
---@param item_on_roll Item
|
||||
function M.new( item_on_roll )
|
||||
local status
|
||||
local item_on_roll_count = 0
|
||||
local iterations = {}
|
||||
local current_iteration = 0
|
||||
local master_loot_candidates = {}
|
||||
|
||||
---@type Winner[]
|
||||
local winners = {}
|
||||
|
||||
local function lua50_clear_table( t )
|
||||
clear_table( t )
|
||||
if m.vanilla then t.n = 0 end
|
||||
end
|
||||
|
||||
local function update_roll( rolls, data )
|
||||
M.debug.add( "update_roll" )
|
||||
|
||||
for _, line in ipairs( rolls ) do
|
||||
if line.player_name == data.player_name and not line.roll then
|
||||
line.roll = data.roll
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function sort( rolls )
|
||||
table.sort( rolls, function( a, b )
|
||||
if a.roll_type ~= b.roll_type then return a.roll_type < b.roll_type end
|
||||
|
||||
if a.roll and b.roll then
|
||||
if a.roll == b.roll then
|
||||
return a.player_name < b.player_name
|
||||
end
|
||||
|
||||
return a.roll > b.roll
|
||||
end
|
||||
|
||||
if a.roll then
|
||||
return true
|
||||
end
|
||||
|
||||
if b.roll then
|
||||
return false
|
||||
end
|
||||
|
||||
return a.player_name < b.player_name
|
||||
end )
|
||||
end
|
||||
|
||||
local function add( player_name, player_class, roll_type, roll )
|
||||
if current_iteration == 0 then return end
|
||||
M.debug.add( "add" )
|
||||
|
||||
---@type RollData
|
||||
local data = { player_name = player_name, player_class = player_class, roll_type = roll_type, roll = roll }
|
||||
local iteration = iterations[ current_iteration ]
|
||||
|
||||
if roll and (iteration.rolling_strategy == RS.SoftResRoll or iteration.rolling_strategy == RS.TieRoll) then
|
||||
update_roll( iteration.rolls, data )
|
||||
else
|
||||
table.insert( iteration.rolls, data )
|
||||
end
|
||||
|
||||
sort( iteration.rolls )
|
||||
end
|
||||
|
||||
---@param players RollingPlayer[]
|
||||
local function create_roll_data( players )
|
||||
local result = {}
|
||||
|
||||
for _, player in ipairs( players ) do
|
||||
for _ = 1, player.rolls do
|
||||
---@type RollData
|
||||
local data = { player_name = player.name, player_class = player.class, roll_type = RT.SoftRes }
|
||||
table.insert( result, data )
|
||||
end
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
---@param count number
|
||||
---@param ml_candidates ItemCandidate[]
|
||||
---@param soft_ressers RollingPlayer[]
|
||||
---@param hard_ressed boolean
|
||||
local function preview( count, ml_candidates, soft_ressers, hard_ressed )
|
||||
M.debug.add( "preview" )
|
||||
current_iteration = 1
|
||||
status = { type = S.Preview }
|
||||
item_on_roll_count = count
|
||||
|
||||
local soft_ressed = getn( soft_ressers ) > 0
|
||||
local ressed_item = soft_ressed or hard_ressed
|
||||
|
||||
table.insert( iterations, {
|
||||
rolling_strategy = ressed_item and RS.SoftResRoll or RS.NormalRoll,
|
||||
rolls = {}
|
||||
} )
|
||||
|
||||
if soft_ressed then
|
||||
status.winners = soft_ressers
|
||||
|
||||
for _, player in ipairs( soft_ressers or {} ) do
|
||||
for _ = 1, player.rolls or 1 do
|
||||
add( player.name, player.class, RT.SoftRes )
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if ressed_item then
|
||||
status.ml_candidates = ml_candidates
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
---@param rolling_strategy RollingStrategyType
|
||||
---@param count number
|
||||
---@param seconds number
|
||||
---@param message string
|
||||
---@param required_rolling_players RollingPlayer[]?
|
||||
local function start( rolling_strategy, count, seconds, message, required_rolling_players )
|
||||
M.debug.add( "start" )
|
||||
lua50_clear_table( iterations )
|
||||
lua50_clear_table( winners )
|
||||
lua50_clear_table( master_loot_candidates )
|
||||
current_iteration = 1
|
||||
status = { type = S.InProgress, seconds_left = seconds }
|
||||
|
||||
item_on_roll_count = count
|
||||
|
||||
table.insert( iterations, {
|
||||
rolling_strategy = rolling_strategy,
|
||||
message = message,
|
||||
rolls = {}
|
||||
} )
|
||||
|
||||
for _, player in ipairs( required_rolling_players or {} ) do
|
||||
for _ = 1, player.rolls or 1 do
|
||||
add( player.name, player.class, rolling_strategy == RS.SoftResRoll and RT.SoftRes or RS.TieRoll )
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@param new_winners Winner[]
|
||||
local function add_winners( new_winners )
|
||||
M.debug.add( "add_winners" )
|
||||
|
||||
for _, winner in ipairs( new_winners ) do
|
||||
table.insert( winners, winner )
|
||||
end
|
||||
end
|
||||
|
||||
---@param ml_candidates ItemCandidate[]
|
||||
local function update_ml_candidates( ml_candidates )
|
||||
lua50_clear_table( master_loot_candidates )
|
||||
|
||||
for _, ml_candidate in ipairs( ml_candidates ) do
|
||||
table.insert( master_loot_candidates, ml_candidate )
|
||||
end
|
||||
end
|
||||
|
||||
---@param ml_candidates ItemCandidate[]
|
||||
local function finish( ml_candidates )
|
||||
M.debug.add( "finish" )
|
||||
status = { type = S.Finished }
|
||||
update_ml_candidates( ml_candidates )
|
||||
end
|
||||
|
||||
--- @param players RollingPlayer[]
|
||||
--- @param roll_type RollType
|
||||
--- @param roll number
|
||||
local function tie( players, roll_type, roll )
|
||||
M.debug.add( "tie" )
|
||||
current_iteration = current_iteration + 1
|
||||
status = { type = S.TieFound }
|
||||
|
||||
table.insert( iterations, {
|
||||
rolling_strategy = RS.TieRoll,
|
||||
tied_roll = roll,
|
||||
rolls = {}
|
||||
} )
|
||||
|
||||
for _, player in ipairs( players or {} ) do
|
||||
add( player.name, player.class, roll_type )
|
||||
end
|
||||
end
|
||||
|
||||
local function tie_start()
|
||||
M.debug.add( "tie_start" )
|
||||
status = { type = S.Waiting }
|
||||
end
|
||||
|
||||
local function add_ignored( player_name, roll_type, roll, reason )
|
||||
M.debug.add( "add_ignored" )
|
||||
if current_iteration == 0 then return end
|
||||
iterations[ current_iteration ].ignored_rolls = iterations[ current_iteration ].ignored_rolls or {}
|
||||
local rolls = iterations[ current_iteration ].ignored_rolls
|
||||
local data = { player_name = player_name, roll_type = roll_type, roll = roll, reason = reason }
|
||||
table.insert( rolls, data )
|
||||
end
|
||||
|
||||
local function get()
|
||||
M.debug.add( "get" )
|
||||
|
||||
return {
|
||||
item = item_on_roll,
|
||||
item_count = item_on_roll_count,
|
||||
status = status,
|
||||
iterations = iterations,
|
||||
winners = winners,
|
||||
ml_candidates = master_loot_candidates
|
||||
}, current_iteration > 0 and iterations[ current_iteration ] or nil
|
||||
end
|
||||
|
||||
local function tick( seconds_left )
|
||||
M.debug.add( "tick" )
|
||||
|
||||
if status.type == S.InProgress then
|
||||
status.seconds_left = seconds_left
|
||||
end
|
||||
end
|
||||
|
||||
local function waiting_for_rolls()
|
||||
M.debug.add( "waiting_for_rolls" )
|
||||
status.type = S.Waiting
|
||||
end
|
||||
|
||||
local function rolling_canceled()
|
||||
M.debug.add( "rolling_canceled" )
|
||||
if not status then return end
|
||||
status.type = S.Canceled
|
||||
end
|
||||
|
||||
local function clear()
|
||||
error( "Nothing should be clearing this.", 2 )
|
||||
-- M.debug.add( "clear" )
|
||||
-- lua50_clear_table( iterations )
|
||||
-- lua50_clear_table( winners )
|
||||
-- lua50_clear_table( master_loot_candidates )
|
||||
-- current_iteration = 0
|
||||
-- status = nil
|
||||
-- item_on_roll = nil
|
||||
-- item_on_roll_count = 0
|
||||
-- M.debug.add( "cleared" )
|
||||
end
|
||||
|
||||
local function mark_as_awarded_if_no_more_items()
|
||||
if item_on_roll_count == 0 then
|
||||
status.type = S.Awarded
|
||||
end
|
||||
end
|
||||
|
||||
---@param player_name string
|
||||
---@param item_id number
|
||||
local function loot_awarded( player_name, item_id )
|
||||
if item_on_roll.id ~= item_id then return end -- TODO: this makes no sense now
|
||||
|
||||
item_on_roll_count = item_on_roll_count - 1
|
||||
local w = status.type == S.Preview and status.winners or winners
|
||||
|
||||
for i, winner in ipairs( w ) do
|
||||
if winner.name == player_name then
|
||||
table.remove( w, i )
|
||||
mark_as_awarded_if_no_more_items()
|
||||
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
mark_as_awarded_if_no_more_items()
|
||||
end
|
||||
|
||||
---@type RollTracker
|
||||
return {
|
||||
preview = preview,
|
||||
start = start,
|
||||
waiting_for_rolls = waiting_for_rolls,
|
||||
add_winners = add_winners,
|
||||
finish = finish,
|
||||
rolling_canceled = rolling_canceled,
|
||||
tie = tie,
|
||||
tie_start = tie_start,
|
||||
add = add,
|
||||
add_ignored = add_ignored,
|
||||
get = get,
|
||||
tick = tick,
|
||||
clear = clear,
|
||||
loot_awarded = loot_awarded,
|
||||
create_roll_data = create_roll_data
|
||||
}
|
||||
end
|
||||
|
||||
m.RollTracker = M
|
||||
return M
|
||||
304
src/RollingLogic.lua
Normal file
@ -0,0 +1,304 @@
|
||||
RollFor = RollFor or {}
|
||||
local m = RollFor
|
||||
|
||||
if m.RollingLogic then return end
|
||||
|
||||
local M = {}
|
||||
|
||||
local getn = m.getn
|
||||
local RS = m.Types.RollingStrategy
|
||||
|
||||
---@alias SoftresRollsAvailableCallback fun( rollers: RollingPlayer[] )
|
||||
|
||||
---@alias RollingFinishedCallback fun(
|
||||
--- item: Item,
|
||||
--- item_count: number,
|
||||
--- winning_rolls: Roll[],
|
||||
--- rerolling: boolean? )
|
||||
|
||||
---@class RollingLogic
|
||||
---@field on_softres_rolls_available SoftresRollsAvailableCallback
|
||||
---@field on_rolling_finished RollingFinishedCallback
|
||||
---@field is_rolling fun(): boolean
|
||||
---@field on_roll fun( player: Player, roll_value: number, min: number, max: number )
|
||||
---@field show_sorted_rolls fun( limit: number? )
|
||||
|
||||
---@param chat Chat
|
||||
---@param ace_timer AceTimer
|
||||
---@param roll_controller RollController
|
||||
---@param strategy_factory RollingStrategyFactory
|
||||
---@param master_loot_candidates MasterLootCandidates
|
||||
---@param winner_tracker WinnerTracker
|
||||
function M.new( chat, ace_timer, roll_controller, strategy_factory, master_loot_candidates, winner_tracker, config )
|
||||
---@type RollingStrategy | nil
|
||||
local m_rolling_strategy
|
||||
|
||||
---@param rollers RollingPlayer[]
|
||||
local function on_softres_rolls_available( rollers )
|
||||
local remaining_rollers = m.reindex_table( rollers )
|
||||
|
||||
local transform = function( player )
|
||||
local rolls = player.rolls == 1 and "1 roll" or string.format( "%s rolls", player.rolls )
|
||||
return string.format( "%s (%s)", player.name, rolls )
|
||||
end
|
||||
|
||||
roll_controller.waiting_for_rolls()
|
||||
local message = m.prettify_table( remaining_rollers, transform )
|
||||
chat.announce( string.format( "SR rolls remaining: %s", message ) )
|
||||
end
|
||||
|
||||
---@param strategy RollingStrategy
|
||||
---@param item Item?
|
||||
---@param item_count number?
|
||||
---@param seconds number?
|
||||
---@param message string?
|
||||
---@param rolling_players RollingPlayer[]?
|
||||
local function roll( strategy, item, item_count, seconds, message, rolling_players )
|
||||
if m_rolling_strategy and m_rolling_strategy.is_rolling() then
|
||||
m.err( "Rolling is already in progress." )
|
||||
return
|
||||
end
|
||||
|
||||
m_rolling_strategy = strategy
|
||||
|
||||
if item and item_count then
|
||||
roll_controller.rolling_started( strategy.get_type(), item, item_count, seconds, message, rolling_players )
|
||||
end
|
||||
|
||||
m_rolling_strategy.start_rolling()
|
||||
end
|
||||
|
||||
local function is_rolling()
|
||||
return m_rolling_strategy and m_rolling_strategy.is_rolling() or false
|
||||
end
|
||||
|
||||
---param winning_rolls Roll[]
|
||||
local function count_top_rolls( winning_rolls )
|
||||
local roll_count = winning_rolls and getn( winning_rolls ) or 0
|
||||
if roll_count == 0 then return 0 end
|
||||
|
||||
local top_roll = winning_rolls[ 1 ].roll
|
||||
local result = 1
|
||||
|
||||
for i = 2, roll_count do
|
||||
if winning_rolls[ i ].roll == top_roll then result = result + 1 end
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
---@param rolls Roll[]
|
||||
---@param item_count number
|
||||
---@return Roll[], Roll[]
|
||||
local function split_winners_and_tied_rollers( rolls, item_count )
|
||||
local top_roll_count = count_top_rolls( rolls )
|
||||
if top_roll_count >= item_count then return {}, rolls end
|
||||
|
||||
local winning_rolls, tied_rolls = {}, {}
|
||||
|
||||
for i, top_roll in ipairs( rolls ) do
|
||||
if i <= top_roll_count then
|
||||
table.insert( winning_rolls, top_roll )
|
||||
else
|
||||
table.insert( tied_rolls, top_roll )
|
||||
end
|
||||
end
|
||||
|
||||
return winning_rolls, tied_rolls
|
||||
end
|
||||
|
||||
---@type RollControllerFacade
|
||||
local facade = {
|
||||
roll_was_ignored = roll_controller.add_ignored,
|
||||
roll_was_accepted = roll_controller.add,
|
||||
tick = roll_controller.tick,
|
||||
winners_found = roll_controller.winners_found,
|
||||
finish = roll_controller.finish
|
||||
}
|
||||
|
||||
---@param item Item
|
||||
---@param item_count number
|
||||
---@param rolls Roll[]
|
||||
---@param rerolling boolean
|
||||
local function there_was_a_tie( item, item_count, rolls, rerolling, on_rolling_finished )
|
||||
local winning_rolls, tied_rolls = split_winners_and_tied_rollers( rolls, item_count )
|
||||
local count = item_count
|
||||
|
||||
local winners = m.map( winning_rolls,
|
||||
---@param winning_roll Roll
|
||||
function( winning_roll )
|
||||
return master_loot_candidates.transform_to_winner( winning_roll.player, item, winning_roll.roll_type, winning_roll.roll, rerolling )
|
||||
end )
|
||||
|
||||
local winner_count = getn( winners )
|
||||
count = count - winner_count
|
||||
|
||||
if winner_count > 0 then
|
||||
roll_controller.winners_found( item, item_count, winners, RS.TieRoll )
|
||||
end
|
||||
|
||||
local roll_type = tied_rolls[ 1 ].roll_type
|
||||
local roll_value = tied_rolls[ 1 ].roll
|
||||
|
||||
---@type RollingPlayer[]
|
||||
local players = m.map( tied_rolls,
|
||||
---@param tied_roll Roll
|
||||
function( tied_roll )
|
||||
return tied_roll.player
|
||||
end )
|
||||
|
||||
roll_controller.there_was_a_tie( players, item, count, roll_type, roll_value, rerolling, getn( winning_rolls ) == 0 or false )
|
||||
|
||||
local strategy = strategy_factory.tie_roll( players, item, count, on_rolling_finished, roll_type, facade )
|
||||
if not strategy then return end
|
||||
|
||||
ace_timer.ScheduleTimer( M,
|
||||
function()
|
||||
roll_controller.tie_start()
|
||||
m_rolling_strategy = nil
|
||||
roll( strategy )
|
||||
end, 2 )
|
||||
end
|
||||
|
||||
---@param item Item
|
||||
---@param item_count number
|
||||
---@param winning_rolls Roll[]
|
||||
---@param rerolling boolean?
|
||||
---@type RollingFinishedCallback
|
||||
local function on_rolling_finished( item, item_count, winning_rolls, rerolling )
|
||||
local winning_roll_count = getn( winning_rolls )
|
||||
|
||||
if winning_roll_count == 0 then
|
||||
roll_controller.finish()
|
||||
|
||||
if not rerolling and config.auto_raid_roll() and m_rolling_strategy and m_rolling_strategy.get_type() ~= RS.SoftResRoll then
|
||||
-- At some point item_count gets to 0.
|
||||
if item_count == 0 then
|
||||
m.trace( "Item count is 0." )
|
||||
end
|
||||
|
||||
m_rolling_strategy = nil
|
||||
roll_controller.start( "RaidRoll", item, item_count )
|
||||
elseif m_rolling_strategy and not m_rolling_strategy.is_rolling() then
|
||||
chat.info( string.format( "Rolling for %s finished.", item.link ) )
|
||||
end
|
||||
|
||||
return
|
||||
end
|
||||
|
||||
if winning_roll_count > item_count then
|
||||
there_was_a_tie( item, item_count, winning_rolls, rerolling or false, on_rolling_finished )
|
||||
return
|
||||
end
|
||||
|
||||
local function handle_winners()
|
||||
local strategy = m_rolling_strategy and m_rolling_strategy.get_type()
|
||||
|
||||
if not strategy then
|
||||
m.err( "Rolling strategy is missing." )
|
||||
return
|
||||
end
|
||||
|
||||
local winners = m.map( winning_rolls,
|
||||
---@param winning_roll Roll
|
||||
function( winning_roll )
|
||||
return master_loot_candidates.transform_to_winner( winning_roll.player, item, winning_roll.roll_type, winning_roll.roll, rerolling )
|
||||
end )
|
||||
|
||||
roll_controller.winners_found( item, item_count, winners, strategy )
|
||||
|
||||
m.map( winners, function( winner )
|
||||
winner_tracker.track( winner.name, item.link, winner.roll_type, winner.winning_roll, strategy ) -- TODO: remove from here and subscribe to the event.
|
||||
end )
|
||||
|
||||
roll_controller.finish()
|
||||
end
|
||||
|
||||
handle_winners()
|
||||
|
||||
if not is_rolling() then
|
||||
chat.info( string.format( "Rolling for %s finished.", item.link ) )
|
||||
end
|
||||
end
|
||||
|
||||
local function cancel_rolling()
|
||||
if not m_rolling_strategy then return end
|
||||
m_rolling_strategy.cancel_rolling()
|
||||
roll_controller.rolling_canceled()
|
||||
end
|
||||
|
||||
---@param player Player
|
||||
---@param roll_value number
|
||||
---@param min number
|
||||
---@param max number
|
||||
local function on_roll( player, roll_value, min, max )
|
||||
if m_rolling_strategy and m_rolling_strategy.is_rolling() then
|
||||
m_rolling_strategy.on_roll( player, roll_value, min, max )
|
||||
end
|
||||
end
|
||||
|
||||
local function finish_rolling_early()
|
||||
if m_rolling_strategy then m_rolling_strategy.stop_accepting_rolls( true ) end
|
||||
end
|
||||
|
||||
---@param limit number
|
||||
local function show_sorted_rolls( limit )
|
||||
if m_rolling_strategy then m_rolling_strategy.show_sorted_rolls( limit ) end
|
||||
end
|
||||
|
||||
---@param data RollControllerStartData
|
||||
local function start( data )
|
||||
---@return RollingStrategy?
|
||||
---@return RollingPlayer[]?
|
||||
local function make_strategy()
|
||||
local seconds = data.seconds or config.default_rolling_time_seconds()
|
||||
|
||||
if data.strategy_type == RS.SoftResRoll then
|
||||
return strategy_factory.softres_roll(
|
||||
data.item,
|
||||
data.item_count,
|
||||
data.message,
|
||||
seconds,
|
||||
on_rolling_finished,
|
||||
on_softres_rolls_available,
|
||||
facade
|
||||
)
|
||||
elseif data.strategy_type == RS.NormalRoll then
|
||||
return strategy_factory.normal_roll(
|
||||
data.item,
|
||||
data.item_count,
|
||||
data.message,
|
||||
seconds,
|
||||
on_rolling_finished,
|
||||
facade
|
||||
)
|
||||
elseif data.strategy_type == RS.RaidRoll then
|
||||
return strategy_factory.raid_roll( data.item, data.item_count, facade )
|
||||
elseif data.strategy_type == RS.InstaRaidRoll then
|
||||
return strategy_factory.insta_raid_roll( data.item, data.item_count, facade )
|
||||
end
|
||||
end
|
||||
|
||||
local strategy, rolling_players = make_strategy()
|
||||
if not strategy then return end
|
||||
|
||||
winner_tracker.start_rolling( data.item.link )
|
||||
roll( strategy, data.item, data.item_count, data.seconds, data.message, rolling_players )
|
||||
end
|
||||
|
||||
roll_controller.subscribe( "finish_rolling_early", finish_rolling_early )
|
||||
roll_controller.subscribe( "cancel_rolling", cancel_rolling )
|
||||
roll_controller.subscribe( "start", start )
|
||||
|
||||
---@type RollingLogic
|
||||
return {
|
||||
on_rolling_finished = on_rolling_finished,
|
||||
on_softres_rolls_available = on_softres_rolls_available,
|
||||
is_rolling = is_rolling,
|
||||
on_roll = on_roll,
|
||||
show_sorted_rolls = show_sorted_rolls
|
||||
}
|
||||
end
|
||||
|
||||
m.RollingLogic = M
|
||||
return M
|
||||