buildFleetsContactChanceText = (
            viewFleet = PlayerManager.fleets[0],
            escortFleet = PlayerManager.fleets[1],
            isCombined = false,
            planeSelectionTopN = 5) => {
        var mainFleet = viewFleet;
        if(isCombined) { // force to 1st fleet if combined
            mainFleet = viewFleet = PlayerManager.fleets[0];
        }
        const iconStyles = {
            "width":"13px", "height":"13px",
            "margin-top":"-3px", "margin-right":"2px"
        };
        const battleConds = collectBattleConditions();
        // AS+ by default for unknown value (night start node) or non-battle,
        // but contact triggered only for abyssal side for air raid defensive battle.
        // note: AP is falsy value 0, so cannot use `||` operator, use `== undefined` in case of `null` value
        const isAirRaidBattle = battleConds.eventIdKind[1] === 7;
        const airBattleId = isAirRaidBattle ? 0 : battleConds.airBattleId == undefined ? 1 : battleConds.airBattleId;
        let contact = viewFleet.contactChanceInfo(airBattleId);
        if(isCombined && ConfigManager.air_combined) {
            // combine contact info from two fleets
            const main = contact;
            const escort = escortFleet.contactChanceInfo(airBattleId);
            // only trigger rate can be summed
            const combined = {
                trigger: main.trigger + escort.trigger,
            };
            // ship position + 6 for escort fleet
            escort.planes.forEach(p => {p.shipOrder += 6;});
            combined.planes = main.planes.concat(escort.planes);
            // resort based on both fleets
            combined.planes.sort((a, b) => b.accurcy - a.accurcy || a.shipOrder - b.shipOrder);
            // recompute failure rate and success rate
            combined.cancelled = combined.planes.map(p => p.rate).reduce((acc, v) => acc * (1 - v), 1);
            combined.success = Math.min(combined.trigger, 1.0) * (1.0 - combined.cancelled);
            contact = combined;
        }
        let planeListHtml = "";
        if(contact.planes.length) {
            const topN = Math.min(contact.planes.length, planeSelectionTopN);
            for(let idx = 0; idx < topN; idx++) {
                const p = contact.planes[idx];
                planeListHtml += "#{0}\u2003"
                    .format(1 + idx);
                planeListHtml += $("<img />")
                    .attr("src", KC3Meta.shipIcon(p.shipMasterId))
                    .addClass("shipiconimg")
                    .css(iconStyles)
                    .prop("outerHTML");
                planeListHtml += $("<img />").attr("src", KC3Meta.itemIcon(p.icon))
                    .attr("alt", p.icon)
                    .css(iconStyles).prop("outerHTML");
                planeListHtml += '<span style="color:#45a9a5">\u2605{0}</span>\u2003'
                    .format(p.stars);
                planeListHtml += "{0}%"
                    .format(Math.qckInt("floor", p.rate * 100, 1));
                if(idx < topN - 1) planeListHtml += "\n";
            }
            if(contact.planes.length > planeSelectionTopN) planeListHtml += "\n...";
            planeListHtml = KC3Meta.term("PanelAirContactPlanes")
                .format(planeSelectionTopN, planeListHtml);
        }
        let text = KC3Meta.term("PanelAirContactTip").format(
            isAirRaidBattle ? KC3Meta.term("BattleKindAirDefendOnly") : KC3Meta.airbattle(airBattleId)[2] || "",
            Math.qckInt("floor", contact.success * 100, 1),
            Math.qckInt("floor", contact.trigger * 100, 1),
            Math.qckInt("ceil", contact.cancelled * 100, 1),
            planeListHtml
        );
        // add night contact rate if night recon found
        const nightContactSuccess = (isCombined ? escortFleet : viewFleet).nightContactSuccessChance(airBattleId);
        if(nightContactSuccess >= 0) text += "\n" + KC3Meta.term("PanelNightContactTip")
            .format(Math.qckInt("floor", nightContactSuccess * 100, 1));
        return $("<p></p>")
            .css("font-size", "11px")
            .html(text)
            .prop("outerHTML");
    }
Build contact chance rates tooltip text from 1 or more fleet(s).
@param {Object} viewFleet - Fleet object currently being viewed, default 1st fleet.
@param {Object} escortFleet - Fleet object of escort for Combined Fleet, default 2nd fleet.
@param {boolean} isCombined - if current view is really Combined Fleet view, default false.
@param {number} planeSelectionTopN - how many planes to be listed for selection phase.
@return built html string.
@see Fleet.contactChanceInfo