diff options
author | chai <chaifix@163.com> | 2021-11-15 13:53:59 +0800 |
---|---|---|
committer | chai <chaifix@163.com> | 2021-11-15 13:53:59 +0800 |
commit | 942a030afd348ab2e02eac8054b43e3c3a72ea48 (patch) | |
tree | a13459f39a3d2f1b533fbd1b5ab523d7a621f673 /Data/BuiltIn/Libraries/lua-addons/addons/scoreboard/display.lua | |
parent | e307051a56a54c27f10438fd2025edf61d0dfeed (diff) |
*rename
Diffstat (limited to 'Data/BuiltIn/Libraries/lua-addons/addons/scoreboard/display.lua')
-rw-r--r-- | Data/BuiltIn/Libraries/lua-addons/addons/scoreboard/display.lua | 443 |
1 files changed, 443 insertions, 0 deletions
diff --git a/Data/BuiltIn/Libraries/lua-addons/addons/scoreboard/display.lua b/Data/BuiltIn/Libraries/lua-addons/addons/scoreboard/display.lua new file mode 100644 index 0000000..b2582d6 --- /dev/null +++ b/Data/BuiltIn/Libraries/lua-addons/addons/scoreboard/display.lua @@ -0,0 +1,443 @@ +-- Display object +local texts = require('texts') + +local Display = { + visible = true, + settings = nil, + tb_name = 'scoreboard' +} + +local valid_fonts = T{ + 'fixedsys', + 'lucida console', + 'courier', + 'courier new', + 'ms mincho', + 'consolas', + 'dejavu sans mono' +} + +local valid_fields = T{ + 'name', + 'dps', + 'percent', + 'total', + 'mavg', + 'mrange', + 'critavg', + 'critrange', + 'ravg', + 'rrange', + 'rcritavg', + 'rcritrange', + 'acc', + 'racc', + 'crit', + 'rcrit', + 'wsavg' +} + + +function Display:set_position(posx, posy) + self.text:pos(posx, posy) +end + +function Display:new(settings, db) + local repr = setmetatable({db = db}, self) + self.settings = settings + self.__index = self + self.visible = settings.visible + + self.text = texts.new(settings.display, settings) + + if not valid_fonts:contains(self.settings.display.text.font:lower()) then + error('Invalid font specified: ' .. self.settings.display.text.font) + self.text:font(self.settings.display.text.font) + self.text:size(self.settings.display.text.fontsize) + else + self.text:font(self.settings.display.text.font, 'consolas', 'courier new', 'monospace') + self.text:size(self.settings.display.text.size) + end + + self:visibility(self.visible) + + return repr +end + + +function Display:visibility(v) + if v then + self.text:show() + else + self.text:hide() + end + + self.visible = v + self.settings.visible = v + self.settings:save() +end + + +function Display:report_filters() + local mob_str + local filters = self.db:get_filters() + + if filters:empty() then + mob_str = "Scoreboard filters: None (Displaying damage for all mobs)" + else + mob_str = "Scoreboard filters: " .. filters:concat(', ') + end + windower.add_to_chat(55, mob_str) + +end + + +-- Returns the string for the scoreboard header with updated info +-- about current mob filtering and whether or not time is currently +-- contributing to the DPS value. +function Display:build_scoreboard_header() + local mob_filter_str + local filters = self.db:get_filters() + + if filters:empty() then + mob_filter_str = 'All' + else + mob_filter_str = table.concat(filters, ', ') + end + + local labels + if self.db:empty() then + labels = '\n' + else + labels = '%32s%7s%9s\n':format('Tot', 'Pct', 'DPS') + end + + local dps_status + if dps_clock:is_active() then + dps_status = 'Active' + else + dps_status = 'Paused' + end + + local dps_clock_str = '' + if dps_clock:is_active() or dps_clock.clock > 1 then + dps_clock_str = ' (%s)':format(dps_clock:to_string()) + end + + local dps_chunk = 'DPS: %s%s':format(dps_status, dps_clock_str) + + return '%s%s\nMobs: %-9s\n%s':format(dps_chunk, ' ':rep(29 - dps_chunk:len()) .. '//sb help', mob_filter_str, labels) +end + + +-- Returns following two element pair: +-- 1) table of sorted 2-tuples containing {player, damage} +-- 2) integer containing the total damage done +function Display:get_sorted_player_damage() + -- In order to sort by damage, we have to first add it all up into a table + -- and then build a table of sortable 2-tuples and then finally we can sort... + local mob, players + local player_total_dmg = T{} + + if self.db:empty() then + return {}, 0 + end + + for mob, players in self.db:iter() do + -- If the filter isn't active, include all mobs + for player_name, player in pairs(players) do + if player_total_dmg[player_name] then + player_total_dmg[player_name] = player_total_dmg[player_name] + player.damage + else + player_total_dmg[player_name] = player.damage + end + end + end + + local sortable = T{} + local total_damage = 0 + for player, damage in pairs(player_total_dmg) do + total_damage = total_damage + damage + sortable:append({player, damage}) + end + + table.sort(sortable, function(a, b) + return a[2] > b[2] + end) + + return sortable, total_damage +end + + +-- Updates the main display with current filter/damage/dps status +function Display:update() + if not self.visible then + -- no need build a display while it's hidden + return + end + + if self.db:empty() then + self:reset() + return + end + local damage_table, total_damage + damage_table, total_damage = self:get_sorted_player_damage() + + local display_table = T{} + local player_lines = 0 + local alli_damage = 0 + for k, v in pairs(damage_table) do + if player_lines < self.settings.numplayers then + local dps + if dps_clock.clock == 0 then + dps = "N/A" + else + dps = '%.2f':format(math.round(v[2] / dps_clock.clock, 2)) + end + + local percent + if total_damage > 0 then + percent = '(%.1f%%)':format(100 * v[2] / total_damage) + else + percent = '(0%)' + end + display_table:append('%-25s%7d%8s %7s':format(v[1], v[2], percent, dps)) + end + alli_damage = alli_damage + v[2] -- gather this even for players not displayed + player_lines = player_lines + 1 + end + + if self.settings.showallidps and dps_clock.clock > 0 then + display_table:append('-':rep(17)) + display_table:append('Alli DPS: ' .. '%7.1f':format(alli_damage / dps_clock.clock)) + end + + self.text:text(self:build_scoreboard_header() .. table.concat(display_table, '\n')) +end + + +local function build_input_command(chatmode, tell_target) + local input_cmd = 'input ' + if chatmode then + input_cmd = input_cmd .. '/' .. chatmode .. ' ' + if tell_target then + input_cmd = input_cmd .. tell_target .. ' ' + end + end + + return input_cmd +end + +-- Takes a table of elements to be wrapped across multiple lines and returns +-- a table of strings, each of which fits within one FFXI line. +local function wrap_elements(elements, header, sep) + local max_line_length = 120 -- game constant + if not sep then + sep = ', ' + end + + local lines = T{} + local current_line = nil + local line_length + + local i = 1 + while i <= #elements do + if not current_line then + current_line = T{} + line_length = header:len() + lines:append(current_line) + end + + local new_line_length = line_length + elements[i]:len() + sep:len() + if new_line_length > max_line_length then + current_line = T{} + lines:append(current_line) + new_line_length = elements[i]:len() + sep:len() + end + + current_line:append(elements[i]) + line_length = new_line_length + i = i + 1 + end + + local baked_lines = lines:map(function (ls) return ls:concat(sep) end) + if header:len() > 0 and #baked_lines > 0 then + baked_lines[1] = header .. baked_lines[1] + end + + return baked_lines +end + + +local function slow_output(chatprefix, lines, limit) + -- this is funky but if we don't wait like this, the lines will spew too fast and error + windower.send_command(lines:map(function (l) return chatprefix .. l end):concat('; wait 1.2 ; ')) +end + + +function Display:report_summary (...) + local chatmode, tell_target = table.unpack({...}) + + local damage_table, total_damage + damage_table, total_damage = self:get_sorted_player_damage() + + local elements = T{} + for k, v in pairs(damage_table) do + elements:append('%s %d(%.1f%%)':format(v[1], v[2], 100 * v[2]/total_damage)) + end + + -- Send the report to the specified chatmode + slow_output(build_input_command(chatmode, tell_target), + wrap_elements(elements:slice(1, self.settings.numplayers), 'Dmg: '), self.settings.numplayers) +end + +-- This is a table of the line aggregators and related utilities +Display.stat_summaries = {} + + +Display.stat_summaries._format_title = function (msg) + local line_length = 40 + local msg_length = msg:len() + local border_len = math.floor(line_length / 2 - msg_length / 2) + + return ' ':rep(border_len) .. msg .. ' ':rep(border_len) + end + + +Display.stat_summaries['range'] = function (stats, filters, options) + + local lines = T{} + for name, pair in pairs(stats) do + lines:append('%-20s %d min %d max':format(name, pair[1], pair[2])) + end + + if #lines > 0 and options and options.name then + sb_output(Display.stat_summaries._format_title('-= '..options.name..' (' .. filters .. ') =-')) + sb_output(lines) + end + end + + +Display.stat_summaries['average'] = function (stats, filters, options) + + local lines = T{} + for name, pair in pairs(stats) do + if options and options.percent then + lines:append('%-20s %.2f%% (%d sample%s)':format(name, 100 * pair[1], pair[2], + pair[2] == 1 and '' or 's')) + else + lines:append('%-20s %d (%ds)':format(name, pair[1], pair[2])) + end + end + + if #lines > 0 and options and options.name then + sb_output(Display.stat_summaries._format_title('-= '..options.name..' (' .. filters .. ') =-')) + sb_output(lines) + end + end + + +-- This is a closure around a hash-based dispatcher. Some conveniences are +-- defined for the actual stat display functions. +Display.show_stat = function() + return function (self, stat, player_filter) + local stats = self.db:query_stat(stat, player_filter) + local filters = self.db:get_filters() + local filter_str + + if filters:empty() then + filter_str = 'All mobs' + else + filter_str = filters:concat(', ') + end + + Display.stat_summaries[Display.stat_summaries._all_stats[stat].category](stats, filter_str, Display.stat_summaries._all_stats[stat]) + end +end() + + +-- TODO: This needs to be factored somehow to take better advantage of similar +-- code already written for reporting and stat queries. +Display.stat_summaries._all_stats = T{ + ['acc'] = {percent=true, category="average", name='Accuracy'}, + ['racc'] = {percent=true, category="average", name='Ranged Accuracy'}, + ['crit'] = {percent=true, category="average", name='Melee Crit. Rate'}, + ['rcrit'] = {percent=true, category="average", name='Ranged Crit. Rate'}, + ['wsavg'] = {percent=false, category="average", name='WS Average'}, + ['wsacc'] = {percent=true, category="average", name='WS Accuracy'}, + ['mavg'] = {percent=false, category="average", name='Melee Non-Crit. Avg. Damage'}, + ['mrange'] = {percent=false, category="range", name='Melee Non-Crit. Range'}, + ['critavg'] = {percent=false, category="average", name='Melee Crit. Avg. Damage'}, + ['critrange'] = {percent=false, category="range", name='Melee Crit. Range'}, + ['ravg'] = {percent=false, category="average", name='Ranged Non-Crit. Avg. Damage'}, + ['rrange'] = {percent=false, category="range", name='Ranged Non-Crit. Range'}, + ['rcritavg'] = {percent=false, category="average", name='Ranged Crit. Avg. Damage'}, + ['rcritrange'] = {percent=false, category="range", name='Ranged Crit. Range'},} +function Display:report_stat(stat, args) + if Display.stat_summaries._all_stats:containskey(stat) then + local stats = self.db:query_stat(stat, args.player) + + local elements = T{} + local header = Display.stat_summaries._all_stats[stat].name .. ': ' + for name, stat_pair in pairs(stats) do + if stat_pair[2] > 0 then + if Display.stat_summaries._all_stats[stat].category == 'range' then + elements:append({stat_pair[1], ('%s %d~%d'):format(name, stat_pair[1], stat_pair[2])}) + elseif Display.stat_summaries._all_stats[stat].percent then + elements:append({stat_pair[1], ('%s %.2f%% (%ds)'):format(name, stat_pair[1] * 100, stat_pair[2])}) + else + elements:append({stat_pair[1], ('%s %d (%ds)'):format(name, stat_pair[1], stat_pair[2])}) + end + end + end + table.sort(elements, function(a, b) + return a[1] > b[1] + end) + + -- Send the report to the specified chatmode + local wrapped = wrap_elements(elements:slice(1, self.settings.numplayers):map(function (p) return p[2] end), header) + slow_output(build_input_command(args.chatmode, args.telltarget), wrapped, self.settings.numplayers) + end +end + + +function Display:reset() + -- the number of spaces here was counted to keep the table width + -- consistent even when there's no data being displayed + self.text:text(self:build_scoreboard_header() .. + 'Waiting for results...' .. + ' ':rep(17)) +end + + +return Display + +--[[ +Copyright © 2013-2014, Jerry Hebert +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of Scoreboard nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL JERRY HEBERT BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +]] + + |