Module:NodeInfo

Revision as of 17:46, 8 April 2018 by がか (talk | contribs)

Documentation for this module may be created at Module:NodeInfo/doc

local U = require("Module:Utils")
local format = require('Module:StringInterpolation').format
local BaseTable = require("Module:BaseTable")
local EnemyShip = require("Module:EnemyShip")
local Formatting = require('Module:Formatting')
local ShipBattleCardKai = require("Module:ShipBattleCardKai")

local NodeInfo = BaseTable({
    _item_class = EnemyShip,
    _table_start = [[{| class="wikitable typography-xl-optout" style="width:680px"]],
    _header_template = [[!#
    !Formation
    !${node_type}
    !AD/AP<br>AS/AS+]],
    _header_template_simple = [[!#
    !colspan="3"|${node_type}]],
    _column_cell_templates = {
        node = [[| colspan="${colspan}" rowspan="${rowspan}" style="text-align: center; color: ${color}; background-color: ${bg_color};" |${values.node}]],
        formation = [[| style="text-align: center; background-color: ${bg_color}; color: ${color};" |${values.formation}]],
        fleet = [[| style="width: 490px; background-color: ${bg_color};" |${values.fleet}]],
        as = [[| style="text-align: center; background-color: ${bg_color}; color: ${color};" |${values.as}]],
    },
    _empty_node_template = [[| style="text-align: center;" |${values.node}
    | colspan="3" style="text-align: center;" |Must be my imagination (battle avoided)/No enemies sighted<br /><span lang="ja">気のせいだった(戦闘回避)/敵影を見ず。(戦闘なし)</span>]],
    _selection_node_template = [[|style="text-align: center;" |${values.node}
    | colspan="3" style="text-align: center;" |You may choose which direction your fleet will go. Admiral, which path will you choose?<br /><span lang="ja">艦隊針路選択可能!/艦隊の針路を選択できます。提督、どちらの針路をとられますか?</span>]],
    _resource_node_template = [[| style="text-align: center; background-color: ${values.bg_color}; color: ${values.color};" |${values.node}
    | colspan="3" style="text-align: center; background-color: ${values.bg_color}; color: ${values.color};" |${values.text}]],
    _collapser_template = [[<div class="mw-customtoggle-${toggle_id} wikia-menu-button">${button_display}</div>
    <div class="mw-collapsible mw-collapsed" id="mw-customcollapsible-${toggle_id}">]],
    _collapser_end = [[</div>]],
    _columns = {
        "node",
        "formation",
        "fleet",
        "as",
    },
    _day_battle_color = "gold",
    _night_battle_color = "blue",
    _night_battle_bg_color = "lightblue", -- #BBDEFB
    _aerial_battle_bg_color = "lightgreen", -- #C8E6C9
    _defense_battle_bg_color = "#81C784",
    _raid_battle_bg_color = "#81C784",
    _boss_battle_color = "red",
    --_resource_node_bg_color = "lightgreen",
    _resource_node_bg_color = "initial",
    --_maelstrom_node_bg_color = "pink",
    _maelstrom_node_bg_color = "initial",
})

function NodeInfo:node(row)
    local color, bg_color = "initial", "initial"
    if row.tags.boss then
        color = self._boss_battle_color
    end
    if row.tags.aerial then
        bg_color = self._aerial_battle_bg_color
    end
    if row.tags.defense then
        bg_color = self._defense_battle_bg_color
    end
    if row.tags.dogfight then
        bg_color = self._defense_battle_bg_color
    end
    if row.tags.raid then
        bg_color = self._raid_battle_bg_color
    end
    if row.tags.night then
        bg_color = self._night_battle_bg_color
    end
    if row.tags.nighttoday then
        bg_color = self._night_battle_bg_color
    end
    return { values = { node = Formatting:japanese_text(row.node) }, color = color, bg_color = bg_color }
end

function NodeInfo:formation(row)
    if row.tags.final then
        row.formation = row.formation .. "<br />(Final)"
    end
    local color = "initial"
    if row.tags.boss then
        color = self._boss_battle_color
    end
    return { values = { formation = row.formation }, color = color }
end

function NodeInfo:fleet(row)
    return { values = { fleet = row.fleet } }
end

function NodeInfo:as(row)
    color = "initial"
    if row.tags.boss then
        color = self._boss_battle_color
    end
    return { values = { as = row.as }, color = color }
end

function NodeInfo:upcase(str)
    str = str:gsub("(%s)(%l)", function(a, b) return a .. string.upper(b) end)
    str = str:gsub("^(%l)", function(a) return string.upper(a) end)
    return str
end

function NodeInfo:insert_item(node, formation, fleet, as, tags, as_complete)
--[=[
    -- Can give weird results when major contributors to air power are unknown
    -- [[Category:Todo]] : use tooltips with explanations instead
    local air_parity = (as_complete or as > 0) and string.format("%.1d", math.ceil((2./3.) * as)) or "??"
    local air_superiority = (as_complete or as > 0) and string.format("%.1d", math.ceil(as * (3 / 2))) or "??"
    local air_supremacy = (as_complete or as > 0) and tostring(as * 3) or "??"
    local air_string = not as_complete and as > 0 and (air_parity .. "+/" .. air_superiority .. "+/" .. air_supremacy .. "+")
        or (air_parity .. "/" .. air_superiority .. "/" .. air_supremacy)
--]=]
    local air_denial_string = "?"
    local air_parity_string = "?"
    local air_superiority_string = "?"
    local air_supremacy_string = "?"
    if as_complete then
        local air_denial = as > 0 and math.floor(as / 3 + 1) or 0
        local air_parity = as > 0 and math.floor(as * 2 / 3 + 1) or 0
        local air_superiority = math.ceil(as * 3 / 2)
        local air_supremacy = as * 3
        air_denial_string = string.format("%.1d", air_denial)
        air_parity_string = string.format("%.1d", air_parity)
        air_superiority_string = string.format("%.1d", air_superiority)
        air_supremacy_string = string.format("%.1d", air_supremacy)
    end
    local air_string = air_denial_string .. "/" .. air_parity_string .. "<br>" .. air_superiority_string .. "/" .. air_supremacy_string
    table.insert(self._items, {
        node = node,
        formation = formation,
        fleet = fleet,
        as = air_string,
        tags = tags,
    })
end

function NodeInfo:create_items() 
    --Modes are as follows:
    --1 = Node
    --2 = Tag processing
    --3 = Resource type
    --4 = Amount of resources
    --5 = Formation
    --6 = Fleet building
    local mode = 1
    
    local node, formation = nil, nil
    local fleet = {}
    local as_rating, as_complete = 0, true
    
    local tags = {}
    local resource
	for index, item_key in ipairs(self._args) do
		if item_key == "-" then
		    if mode == 6 then --We're at a break and have built a full row; time to insert it
		        self:insert_item(node, formation, table.concat(fleet, " "), as_rating, tags, as_complete)
		    end
		    
			table.insert(self._items, "break")
			
			fleet, as_rating, as_complete = {}, 0, true
			tags = {}
			mode = 1
		else
			if mode == 1 then
			    --First item should always be the node
			    node = item_key
			    mode = 2
			elseif mode == 2 then
			    self._node_type = string.lower(string.match(item_key, "(.-)/") or item_key)
			    if mw.ustring.find(string.lower(item_key), "resource") or string.lower(item_key) == "storm" then
			        local split = mw.ustring.find(item_key, '/')
			        if split then
			            tags[string.lower(mw.ustring.sub(item_key, 1, split - 1))] = true
			            item_key = mw.ustring.sub(item_key, split + 1)
			        end
			        tags[string.lower(item_key)] = true
			        mode = 3
			    elseif string.lower(item_key) == "empty" then
			        table.insert(self._items, node .. "/empty")
			        mode = 1
			    elseif string.lower(item_key) == "select" then
			        table.insert(self._items, node .. "/select")
			        mode = 1
			    else
			        while mw.ustring.find(item_key, '/') do
			            local split = mw.ustring.find(item_key, '/')
			            tags[string.lower(mw.ustring.sub(item_key, 1, split - 1))] = true
			            item_key = mw.ustring.sub(item_key, split + 1)
			        end
			        tags[string.lower(item_key)] = true
			        mode = 5
			    end
		    elseif mode == 3 then
		        resource = self:upcase(item_key)
		        mode = 4
		    elseif mode == 4 then
		        local amount = item_key
		        if tags["storm"] and mw.ustring.sub(amount, 1, 1) ~= "-" then
		            amount = "-" .. amount
		        end
		        local boss = tags["boss"] and "true" or "false"
		        local string = node .. "/" .. resource .. "/" .. amount .. "/" .. boss
		        table.insert(self._items, string)
		        mode = 1
		    elseif mode == 5 then
		        formation = self:upcase(item_key)
		        mode = 6
		    else
		        --Fleets are of variable size, so we append onto a string until we hit the next node declaration
		        local split = mw.ustring.find(item_key, '/')
		        local ship_name, ship_suffix
		        if split then
		            ship_name = mw.ustring.sub(item_key, 1, split - 1)
		            ship_suffix = mw.ustring.sub(item_key, split + 1)
		        else
		            ship_name = item_key
		            ship_suffix = ""
		        end

		        local ship = EnemyShip(ship_name, ship_suffix)
		        local ship_air_power = ship:air_power(tags.raid)

		        local ship_caption =
		            (ship:name() or "?")
		            .. " (" .. Formatting:format_enemy_stat(ship:api_id()) .. "): "
		            .. (ship:armor() or "?") .. " Armor, " .. (ship:hp() or "?") .. " HP"
		            .. (ship_air_power ~= 0 and ", " .. (ship_air_power or "?") .. " AP" or "")

		        table.insert(fleet, ShipBattleCardKai:get{
		            ship = ship,
		            caption = ship_caption,
		            link = ship:link(),
		            flagship = #fleet == 0
                })

		        if ship_air_power then
		            as_rating = as_rating + ship_air_power
		        else
		            as_complete = false
		        end
            end
		end
    end
    if mode == 6 then
        self:insert_item(node, formation, table.concat(fleet, " "), as_rating, tags, as_complete)
    end
end

function NodeInfo:create_data_rows()
    for index, item in ipairs(self._items) do
		local row_values
		if type(item) == "string" then
			row_values = item
		else
			row_values = {}
			for _, column in ipairs(self._columns) do
				row_values[column] = self[column](self, item)
			end
			if index > 1 then
			    for _, column in ipairs(self._columns) do
			    	for i = index - 1, 1, -1 do
			    	    if column == "node" then
						    local previous_cell = self._data_rows[i][column]
    						if previous_cell then
	    					    if row_values[column].values.node == previous_cell.values.node then
		    				    	previous_cell.rowspan = previous_cell.rowspan and previous_cell.rowspan + 1 or 2
			    			    	row_values[column] = nil
				    		    else
					    	    	row_values[column].rowspan = 1
    					    		row_values[column].colspan = 1
    	    					end
	    	    				break
	    	    			end
		    			end
		            end
		        end
		    else
				for _, column in ipairs(self._columns) do
			    	row_values[column].rowspan = 1
			    	row_values[column].colspan = 1
				end
			end
		end
		table.insert(self._data_rows, row_values)
	end
end

function NodeInfo:format_node_type()
    local node_types = {
        normal = 'Normal Battle Node',
        boss = 'Boss Battle Node',
        resource = 'Resource Node',
        storm = 'Maelstrom Node',
        empty = 'Empty Node',
        select = 'Selection Node',
        night = 'Night Battle Node',
        aerial = 'Aerial Battle Node',
        defense = 'Air Defense Node',
        nighttoday = 'Night to Day Battle Node',
        raid = 'Air Raids',
    }
    return self._args["comment"] or node_types[self._node_type] or "Fleet"
end

function NodeInfo:is_simple_node_type()
    return self._node_type == 'resource' or self._node_type == 'storm' or self._node_type == 'empty' or self._node_type == 'select'
end

function NodeInfo:create_header()
    local header_string = format{
        self:is_simple_node_type() and self._header_template_simple or self._header_template,
        node_type = self:format_node_type()
    }
    self._header = header_string
    self._header_bottom = header_string
end

function NodeInfo:start_rows()
    self._rows = {}
    
    if self._args["toggle_id"] then
        table.insert(self._rows, format{self._collapser_template,
            toggle_id = self._args["toggle_id"],
            button_display = self._args["button_display"] or "Show/Hide Formation Table",
        })
    end
    
    table.insert(self._rows, self._table_start)
    table.insert(self._rows, self._header)
end

function NodeInfo:process_resource_node(resource, amount)
    --Amount may or may not be just numbers
    local action, units, node_type, bg_color = "Gained", "", "Resource", self._resource_node_bg_color
    
    if mw.ustring.sub(amount, 1, 1) == "-" then
        action = "Lost"
        amount = mw.ustring.sub(amount, 2)
        node_type = "Storm"
        bg_color = self._maelstrom_node_bg_color
    end
    
    if mw.ustring.find(amount, " ") then
        local split = mw.ustring.find(amount, " ")
        units = mw.ustring.sub(amount, split + 1)
        amount = mw.ustring.sub(amount, 1, split - 1)
    end
    
    local text = action .. " " .. amount .. " " .. resource .. " " .. units
    return text, node_type, bg_color
end

function NodeInfo:build_rows()
    local bg_color
	for index, row_values in ipairs(self._data_rows) do
		if row_values ~= "break" then
			table.insert(self._rows, self._row_starter)
			if row_values == "header" then
				table.insert(self._rows, self._header)
			elseif type(row_values) == "table" then
			    if row_values["node"] then
			        bg_color = row_values["node"].bg_color
			    elseif bg_color == nil then
			        bg_color = "initial"
			    end
				for _, column in ipairs(self._columns) do
			        if row_values[column] then
				        row_values[column].bg_color = bg_color
				    end
					if row_values[column] then
						table.insert(self._rows, format(self._column_cell_templates[column] or self._cell, row_values[column]))
					end
			    end
    	elseif mw.ustring.find(row_values, '/') then
    	        --node/resource/amount/boss
    	        local values = {}
    	        while mw.ustring.find(row_values, '/') do
    	            local split = mw.ustring.find(row_values, '/')
    	            if split then
    	                table.insert(values, mw.ustring.sub(row_values, 1, split - 1))
    	                row_values = mw.ustring.sub(row_values, split + 1)
    	            end
	            end
	            table.insert(values, row_values)
	            if values[2] == "empty" then
	                table.insert(self._rows, format{self._empty_node_template, values = { node = Formatting:japanese_text(values[1]) } })
	            elseif values[2] == "select" then
	                table.insert(self._rows, format{self._selection_node_template, values = { node = Formatting:japanese_text(values[1]) } })
    	        else
	                local resource = Formatting:format_image{values[2] .. ".png", caption = self:upcase(values[2]), size = "22x22px"}
	                local text, node_type, bg_color = self:process_resource_node(resource, values[3])
	                local color = "initial"
	                if values[4] == "true" then
	                    color = self._boss_battle_color
	                end
    	            table.insert(self._rows, format{self._resource_node_template, values = {
    	                node = Formatting:japanese_text(values[1]),
    	                text = text,
    	                color = color,
    	                bg_color = bg_color,
    	            }})
	            end
		    end
		end
    end
end

function NodeInfo:finish_rows()
    table.insert(self._rows, self._row_starter)
	table.insert(self._rows, self._header_bottom or self._header)
	table.insert(self._rows, self._table_end)
	if self._args["toggle_id"] then
        table.insert(self._rows, self._collapser_end)
    end
end

U.registerTableTests(NodeInfo, {
    { "A", "Boss", "Line Ahead", "Northern Princess" }
})
-- p.run_table_tests()

return NodeInfo