Mudanças entre as edições de "Widget:Teste"

De Wiki Gla
Ir para navegação Ir para pesquisar
m
Etiqueta: Revertido
m
Etiqueta: Revertido
Linha 1: Linha 1:
<script>
local p = {}
    (function () {
        const $ = (s) => document.querySelector(s);
        const $$ = (s) => Array.from(document.querySelectorAll(s));


        // ---- Tabs
function p.skin(frame)
        const tabBtns = $$('.tab-btn');
    local a = frame.args
        const panels = $$('.tab-content');
    local obj = {
 
         sprite = a.sprite or '',
        tabBtns.forEach(btn => {
         background = a.background or '',
            btn.addEventListener('click', () => {
         tooltip = a.tooltip or ''
                const id = btn.dataset.tab;
                tabBtns.forEach(b => b.classList.remove('is-active'));
                panels.forEach(p => p.classList.remove('is-active'));
                btn.classList.add('is-active');
                const panel = document.getElementById(id);
                if (panel) panel.classList.add('is-active');
            });
        });
 
        // ---- Skills
        const iconsContainer = document.querySelector('.cuadros-container');
        const iconItems = iconsContainer ? Array.from(iconsContainer.querySelectorAll('.cuadro')) : [];
        const descBox = document.querySelector('.descripcion-container');
        const videoBox = document.querySelector('.video-container');
 
        const videosCache = {};
         let totalVideos = 0, loadedVideos = 0, autoplay = false;
 
        // placeholder do vídeo
        let placeholder = null;
        if (videoBox) {
            placeholder = document.createElement('div');
            placeholder.className = 'video-placeholder';
            placeholder.innerHTML = '<img src="/images/d/d5/Icon_gla.png" alt="Carregando...">';
            videoBox.appendChild(placeholder);
        }
        const removePlaceholder = () => {
            if (!placeholder) return;
            placeholder.classList.add('fade-out');
            placeholder.addEventListener('transitionend', () => {
                placeholder?.remove();
                placeholder = null;
            }, { once: true });
         };
 
        // pré-carrega vídeos
        iconItems.forEach(el => {
            const src = (el.dataset.video || '').trim();
            const idx = el.dataset.index || '';
            if (!src || !videoBox || videosCache[idx]) return;
 
            totalVideos++;
            const v = document.createElement('video');
            v.controls = true;
            v.preload = 'auto';
            v.playsInline = true;
            v.style.display = 'none';
            v.dataset.index = idx;
 
            const source = document.createElement('source');
            source.src = src;
            source.type = 'video/webm';
            v.appendChild(source);
 
            v.addEventListener('canplay', () => {
                loadedVideos++;
                if (loadedVideos === 1) { v.pause(); v.currentTime = 0; }
                const active = document.querySelector('.cuadro.activo');
                if (active && active.dataset.index === idx) setTimeout(removePlaceholder, 180);
                if (loadedVideos === totalVideos) autoplay = true;
            });
 
            v.addEventListener('error', () => {
                loadedVideos++;
                removePlaceholder();
                if (loadedVideos === totalVideos) autoplay = true;
            });
 
            videoBox.appendChild(v);
            videosCache[idx] = v;
        });
 
        if (totalVideos === 0) removePlaceholder();
 
        // clique nas skills
        iconItems.forEach(el => {
            const name = el.dataset.name || '';
            const desc = (el.dataset.desc || '').replace(/'''(.*?)'''/g, '<b>$1</b>');
            const attrs = el.dataset.atr || '';
            const idx = el.dataset.index || '';
            const hasVideo = !!(el.dataset.video && el.dataset.video.trim() !== '');
 
            el.title = name;
 
            el.addEventListener('click', () => {
                if (!autoplay && loadedVideos > 0) autoplay = true;
 
                // título + atributos + texto
                if (descBox) {
                    descBox.innerHTML = `
          <div class="skill__title"><h3>${name}</h3></div>
          ${renderAttributes(attrs)}
          <div class="skill__text">${desc}</div>
         `;
                }
 
                // vídeo
                Object.values(videosCache).forEach(v => { v.pause(); v.style.display = 'none'; });
                if (videoBox) {
                    if (hasVideo && videosCache[idx]) {
                        const v = videosCache[idx];
                        videoBox.style.display = 'block';
                        v.style.display = 'block';
                        v.currentTime = 0;
                        if (autoplay) v.play().catch(() => { });
                    } else {
                        videoBox.style.display = 'none';
                    }
                }
 
                // estado ativo
                iconItems.forEach(i => i.classList.remove('activo'));
                el.classList.add('activo');
            });
        });
 
        // seleciona a primeira por padrão
        if (iconItems.length) iconItems[0].click();
 
        // scroll horizontal com roda do mouse
        if (iconsContainer) {
            iconsContainer.addEventListener('wheel', (e) => {
                if (e.deltaY) {
                    e.preventDefault();
                    iconsContainer.scrollLeft += e.deltaY;
                }
            });
        }
 
        // ---- Skins: setas
        initSkinsArrows();
        function initSkinsArrows() {
            const carousel = document.querySelector('.skins-carousel');
            const left = document.querySelector('.skins-arrow.left');
            const right = document.querySelector('.skins-arrow.right');
            if (!carousel || !left || !right) return;
 
            const scrollAmt = () => Math.round(carousel.clientWidth * 0.6);
 
            function update() {
                const max = carousel.scrollWidth - carousel.clientWidth;
                const x = carousel.scrollLeft;
                left.disabled = x <= 0;
                right.disabled = x >= max - 1;
            }
            function go(dir) {
                const max = carousel.scrollWidth - carousel.clientWidth;
                const next = dir < 0
                    ? Math.max(0, carousel.scrollLeft - scrollAmt())
                    : Math.min(max, carousel.scrollLeft + scrollAmt());
                carousel.scrollTo({ left: next, behavior: 'smooth' });
            }
 
            left.addEventListener('click', () => go(-1));
            right.addEventListener('click', () => go(1));
            carousel.addEventListener('scroll', update);
            new ResizeObserver(update).observe(carousel);
            update();
        }
 
        // ---- atributos (ordem fixa + espaços reservados)
        function renderAttributes(str) {
            const vals = (str || '').split(',').map(v => v.trim());
            const pve = vals[0]; const pvp = vals[1]; const ene = vals[2]; const cd = vals[3];
 
            const energia = (!ene || ene === '-' || isNaN(parseInt(ene, 10))) ? '-' :
                ((parseInt(ene, 10) > 0 ? '+' : '') + parseInt(ene, 10));
            const recarga = (!cd || cd === '-' || isNaN(parseInt(cd, 10))) ? '-' :
                (parseInt(cd, 10) + ' seg');
 
            const rows = [
                ['Recarga', recarga],
                ['Energia', energia],
                ['Poder', (isNaN(parseInt(pve, 10)) ? '-' : parseInt(pve, 10))],
                ['Poder PvP', (isNaN(parseInt(pvp, 10)) ? '-' : parseInt(pvp, 10))]
            ];
 
            return `
  <div class="attr-list">
    ${rows.map(([label, value]) => `
      <div class="attr-row${value === '-' ? ' is-empty' : ''}">
        <span class="attr-label">${label}:</span>
        <span class="attr-value">${value}</span>
      </div>
    `).join('')}
  </div>
`;
        }
    })();
</script>
<style>
    /* -------------------- Base / resets -------------------- */
    img {
        pointer-events: none;
        user-select: none;
    }
 
    video {
        max-height: 33.25em;
        object-fit: fill;
    }
 
    .mw-body {
        padding: unset !important;
    }
 
    .mw-body-content {
        line-height: 1.5 !important;
    }
 
    /* Se sua wiki injeta parágrafos soltos acima, mantenha: */
    .mw-body-content p {
        display: none;
    }
 
    /* -------------------- Character container -------------------- */
    .character {
        position: relative;
        width: 100%;
        margin: 0 auto;
        color: #000;
        font-family: 'Noto Sans', sans-serif !important;
        user-select: none;
    }
 
    .character p {
        display: unset;
    }
 
    /* Header + banner + art */
    .character__header {
        position: relative;
        overflow: hidden;
        display: flex;
        flex-direction: column;
        gap: 10px;
    }
 
    .character__banner {
        position: absolute;
        inset: 0;
        z-index: -9;
        background-size: cover;
        background-position: center;
    }
 
    .character__banner::before {
        content: "";
        position: absolute;
        inset: 0;
        background: linear-gradient(to right, rgba(0, 0, 0, .6), rgba(0, 0, 0, .2));
    }
 
    .character__art {
        position: absolute;
        right: 3.5rem;
        top: -3.1rem;
        width: 34.3vw;
        height: auto;
        z-index: 1;
        pointer-events: none;
    }
 
    /* Topbar: avatar, nome e tags */
    .character__topbar {
        display: flex;
        flex-direction: column;
        align-items: flex-start;
        padding: 8px 20px;
        padding-top: 4px;
    }
 
    .character__namebox {
        display: flex;
        align-items: center;
        gap: 14px;
    }
 
    .character__avatar {
        margin-top: 8px;
        width: 100px;
        height: 100px;
        object-fit: none;
    }
 
    .character__name {
        font-family: 'Orbitron', sans-serif;
        font-weight: 900;
        font-size: 56px;
        color: #fff;
        text-shadow: 0 0 6px #000, 0 0 9px #000;
    }
 
    .character__tags {
        display: flex;
        gap: 9px;
        flex-wrap: wrap;
        margin-left: .28rem;
    }
 
    .character__tag {
        background: #353420;
        color: #fff;
        border-radius: 4px;
        padding: 1px 6px;
        font-size: .9em;
        font-weight: bold;
        outline: 2px solid #000;
        box-shadow: 0 0 2px rgb(0 0 0 / 70%);
    }
 
    .character.tier-bronze .character__tag--tier {
        outline-color: #7b4e2f !important;
    }
 
    .character.tier-silver .character__tag--tier {
        outline-color: #d6d2d2 !important;
    }
 
    .character.tier-gold .character__tag--tier {
        outline-color: #fcd300 !important;
    }
 
    .character.tier-diamond .character__tag--tier {
        outline-color: #60dae2 !important;
    }
 
    /* -------------------- Tabs -------------------- */
    .tabs {
        margin: 4px 0 4px 8px;
        display: flex;
        gap: 12px;
        justify-content: flex-start;
    }
 
    .tabs__btn {
        padding: 5px 20px;
        background: #333;
        color: #fff;
        border: 2px solid transparent;
        border-radius: 8px;
        font-size: 20px;
        font-weight: 600;
        line-height: 1;
        cursor: pointer;
        transition: background .15s ease, border-color .15s ease;
    }
 
    .tabs__btn.is-active {
        background: #156bc7;
        border-color: #156bc7;
    }
 
    .tabs__panel {
        display: none;
        background: #26211cd6;
        padding: 0 8px 8px;
        position: relative;
        z-index: 3;
    }
 
    .tabs__panel.is-active {
        display: block;
     }
     }
    return mw.text.jsonEncode(obj)
end


     /* -------------------- Skills -------------------- */
function p.skill(frame)
     .skills {
     local a = frame.args
         display: flex;
     local obj = {
         gap: 20px;
        name = a.name or a.nome or '',
        icon = a.icon or '',
        level = tonumber(a.level) or nil,
        desc = a.desc or '',
        energy = a.energy or nil,
        powerpve = a.powerpve or nil,
        powerpvp = a.powerpvp or nil,
         cooldown = a.cooldown or nil,
         video = a.video or ''
     }
     }
    return mw.text.jsonEncode(obj)
end


     .skills__icons {
function p.generate(frame)
         display: flex;
    local args = frame:getParent().args
        gap: 10px;
     local html = mw.html.create('div')
        flex-wrap: nowrap;
    local function getVideoURL(filename)
        width: 100%;
         return tostring(mw.uri.fullUrl('Special:FilePath/' .. filename))
         overflow-x: auto;
    end
         overflow-y: hidden;
    local tier = (args.tier or ""):lower()
        padding: 10px 0 3px 1px;
    local tierMap = {
         margin-bottom: 6px;
         bronze = "tier-bronze",
         position: relative;
         bronce = "tier-bronze",
        z-index: 4;
         silver = "tier-silver",
         justify-content: flex-start;
         prata = "tier-silver",
         scrollbar-width: thin;
         gold = "tier-gold",
         scrollbar-color: #ababab transparent;
         ouro = "tier-gold",
         scroll-behavior: smooth;
         diamond = "tier-diamond",
         diamante = "tier-diamond"
     }
     }
    local tierClass = tierMap[tier]
    local box = html:tag('div'):addClass('personaje-box')
    if tierClass then
        box:addClass(tierClass)
    end
    local header = box:tag('div'):addClass('personaje-header')
    local topbar = header:tag('div'):addClass('personaje-topbar')
    local nomeBox = topbar:tag('div'):addClass('personaje-nome-box')


     .skills__icons::-webkit-scrollbar {
     local avatarImg = args.avatar or 'Franky_ts_medal.png'
        height: 6px;
    nomeBox:wikitext(string.format('[[Arquivo:%s|class=topbar-icon|link=|alt=Medal]]', avatarImg))
    }


     .skills__icons::-webkit-scrollbar-track {
     local nomeCat = nomeBox:tag('div'):addClass('personaje-nome-category')
         background: transparent;
    nomeCat:tag('div'):addClass('nome'):wikitext(args.nome or 'Franky (TS)')
     }
    local classesDiv = nomeCat:tag('div'):addClass('classes')
    -- Dividir clases por "/"
    local classeString = args.classe or ''
    local tierUpper = tier:gsub("^%l", string.upper)
    classesDiv:tag('div'):addClass('classe tier'):wikitext(tierUpper)
    for classe in mw.text.gsplit(classeString, '/', true) do
         classesDiv:tag('div'):addClass('classe'):wikitext(classe)
     end


     .skills__icons::-webkit-scrollbar-thumb {
     header:tag('div'):addClass('topbar-description'):wikitext(args.desc)
        background: #151515;
        border-radius: 3px;
    }


     .skills__icon {
     local banner = args.banner or ''
        flex: 0 0 auto;
    local bannerFile = mw.title.new('Arquivo:' .. banner)
         width: 50px;
    if bannerFile and bannerFile.exists then
        height: 50px;
         header:tag('div'):addClass('banner'):wikitext(string.format(
        border-radius: 5px;
            '[[Arquivo:%s|class=banner-personaje|link=|alt=Artwork do personagem]]', banner))
         cursor: pointer;
    else
        transition: transform .2s, box-shadow .2s;
         header:tag('div'):addClass('banner')
     }
     end


     .skills__icon.is-active {
     local tabs = header:tag('div'):addClass('personaje-tabs')
        box-shadow: 0 0 0 1.5px #FFD700;
    tabs:tag('div'):addClass('tab-btn active'):attr('data-tab', 'habilidades'):wikitext('Habilidades')
     }
     tabs:tag('div'):addClass('tab-btn'):attr('data-tab', 'skins'):wikitext('Skins')


     .skills__icon img {
     local artImg = args.artwork or 'Franky_ts_splash.png'
        width: 100%;
    header:tag('div'):css('text-align', 'center'):wikitext(string.format(
         height: 100%;
         '[[Arquivo:%s|class=art-personaje|link=|alt=Arte do personagem]]', artImg))
        object-fit: cover;
    }


     /* Descrição da skill */
     -- HABILIDADE
     .skills__desc {
     local habilidadesTab = box:tag('div'):addClass('tab-content active'):attr('id', 'habilidades')
        flex: 1;
    local cuadros = habilidadesTab:tag('div'):addClass('cuadros-container')
        width: 50%;
    local habilidadesContainer = habilidadesTab:tag('div'):addClass('habilidades-container')
        display: flex;
        flex-direction: column;
        gap: 10px;
        justify-content: center;
        min-height: 27.5rem;
        height: 100%;
        padding: 4px 16px !important;
        padding-top: 0 !important;
        background: #26211C;
        color: #fff;
        border-radius: 8px;
        position: relative;
        z-index: 99;
        box-shadow: 0 6px 18px rgba(0, 0, 0, .28);
        backdrop-filter: blur(2px);
        text-shadow: -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, 1px 1px 0 #000;
    }


     .skill__title h3 {
     local details = habilidadesContainer:tag('div'):addClass('habilidades-details')
         font-size: 2.7em;
    local descripcionContainer = details:tag('div'):addClass('descripcion-container')
         margin: 0;
    -- ====== NOVO: bloco de skills via subtemplate ======
        padding-top: 0;
    local skillsPacked = args.skills
        text-align: center;
    local usedSkills = false
        color: #fff;
    if skillsPacked and skillsPacked:match("{") then
    }
         -- Concat de objetos JSON: ...}{...}{...  -> ...},{...},{...
         local arr = "[" .. skillsPacked:gsub("}%s*{", "},{") .. "]"
        local ok, parsed = pcall(mw.text.jsonDecode, arr)
        if ok and type(parsed) == "table" and #parsed > 0 then
            usedSkills = true
            for i, sk in ipairs(parsed) do
                local nome = sk.name or sk.nome
                if nome and nome ~= "" then
                    local icon = sk.icon or ""
                    local level = sk.level or ""
                    local desc = sk.desc or ""
                    -- monta a string de atributos no formato esperado pelo JS:
                    -- PVE, PVP, Energia, Recarga
                    local atr = table.concat({sk.powerpve or "-", sk.powerpvp or "-", sk.energy or "-",
                                              sk.cooldown or "-"}, ", ")


    .skill__text {
                    local rawVideo = sk.video or ""
        font-size: 1.2em;
                    local videoURL = rawVideo ~= "" and getVideoURL(rawVideo) or ""
        margin: 0;
    }


    .skill__text * {
                    cuadros:tag('div'):addClass('cuadro'):attr('data-index', i):attr('data-nome', nome):attr(
        font-size: inherit !important;
                        'data-desc', desc):attr('data-atr', atr):attr('data-video', videoURL):attr('data-video-preload',
        line-height: inherit;
                        'auto'):wikitext(string.format("[[Arquivo:%s|class=habilidad-icon|link=]]", icon))
    }


    /* Atributos (ordem fixa, linhas reservadas) */
                    descripcionContainer:tag('div'):addClass('habilidad-descripcion'):attr('data-index', i)
    .attrs {
                end
        display: flex;
            end
        flex-direction: column;
         end
        gap: 4px;
     end
         margin: 4px 0 10px;
     }


     .skills__desc .attrs,
     -- ====== Fallback: mantém suporte aos hab1..hab21 se não usar 'skills' ======
    .skills__desc .attrs * {
    if not usedSkills then
        text-shadow: none;
        for i = 1, 21 do
        font-family: 'Noto Sans', sans-serif;
            local nome = args['hab' .. i .. '-nome']
    }
            if nome then
                local icon = args['hab' .. i .. '-icon'] or ''
                local level = args['hab' .. i .. '-level'] or ''
                local desc = args['hab' .. i .. '-desc'] or ''
                local atr = args['hab' .. i .. '-atr'] or ''
                local rawVideo = args['hab' .. i .. '-video'] or ''
                local videoURL = rawVideo ~= '' and getVideoURL(rawVideo) or ''


    .attrs__row {
                cuadros:tag('div'):addClass('cuadro'):attr('data-index', i):attr('data-nome', nome):attr('data-desc',
        min-height: 22px;
                    desc):attr('data-atr', atr):attr('data-video', videoURL):attr('data-video-preload', 'auto')
        display: flex;
                    :wikitext(string.format("[[Arquivo:%s|class=habilidad-icon|link=]]", icon))
        align-items: center;
        gap: 6px;
    }


    .attrs__row--empty {
                descripcionContainer:tag('div'):addClass('habilidad-descripcion'):attr('data-index', i)
         visibility: hidden;
            end
     }
         end
     end


     .attrs__label {
     for i = 1, 21 do
         font-weight: 700;
        local nome = args['hab' .. i .. '-nome']
        color: #f0c87b;
         if nome then
        font-size: .95rem;
            local icon = args['hab' .. i .. '-icon'] or ''
    }
            local level = args['hab' .. i .. '-level'] or ''
            local desc = args['hab' .. i .. '-desc'] or ''
            local atr = args['hab' .. i .. '-atr'] or ''
            local rawVideo = args['hab' .. i .. '-video'] or ''
            local videoURL = rawVideo ~= '' and getVideoURL(rawVideo) or ''


    .attrs__value {
            cuadros:tag('div'):addClass('cuadro'):attr('data-index', i):attr('data-nome', nome):attr('data-desc', desc)
        color: #fff;
                :attr('data-atr', atr):attr('data-video', videoURL):attr('data-video-preload', 'auto'):wikitext(
        font-weight: 800;
                    string.format("[[Arquivo:%s|class=habilidad-icon|link=]]", icon))
        font-size: 1.05rem;
        letter-spacing: .01em;
    }


    /* Vídeo da skill */
            descripcionContainer:tag('div'):addClass('habilidad-descripcion'):attr('data-index', i)
    .skills__video {
         end
        position: relative;
     end
        width: 43%;
        background: #000;
        display: flex;
        align-items: center;
        justify-content: center;
        border-radius: 2%;
        overflow: hidden;
        z-index: 999;
         box-shadow: 0 8px 24px rgba(0, 0, 0, .35);
     }


     .video-placeholder {
     details:done()
        position: absolute;
    habilidadesContainer:tag('div'):addClass('video-container'):done()
        inset: 0;
    habilidadesTab:done()
        background: #000;
        display: flex;
        align-items: center;
        justify-content: center;
        z-index: 2;
        opacity: 1;
        transition: opacity .9s ease;
    }


     .video-placeholder img {
     -- SKINS
        width: 120px;
    local skinsTab = box:tag('div'):addClass('tab-content'):attr('id', 'skins')
        height: auto;
     local cardSkins = skinsTab:tag('div'):addClass('card-skins')
        animation: breathe 2.5s ease-in-out infinite;
        filter: drop-shadow(0 0 6px rgba(255, 255, 255, .3));
     }


     .video-placeholder.fade-out {
     cardSkins:tag('span'):addClass('card-skins-title'):wikitext('SKINS & SPOTLIGHTS')
        opacity: 0;
    }


     @keyframes breathe {
     local wrapper = cardSkins:tag('div'):addClass('skins-carousel-wrapper')
    wrapper:tag('div'):addClass('skins-arrow left'):wikitext('«')


         0%,
    local carousel = wrapper:tag('div'):addClass('skins-carousel')
         100% {
    -- ====== NOVO: bloco de skins via subtemplate (robusto) ======
             transform: scale(1);
    local skinsPacked = args.skins
             opacity: 1;
    local usedSkins = false
        }
    if skinsPacked and skinsPacked:find("{", 1, true) then
         local count = 0
         for obj in skinsPacked:gmatch("%b{}") do
             local ok, sk = pcall(mw.text.jsonDecode, obj)
             if ok and type(sk) == "table" then
                count = count + 1
                local banner = sk.background or ''
                local image = sk.sprite or ''
                local tooltipRaw = sk.tooltip or ''
                local tooltipHtml = tooltipRaw:gsub("'''([^']+)'''", "<b>%1</b>"):gsub("\n", "<br>")


        50% {
                local skinCard = carousel:tag('div'):addClass(
            transform: scale(1.07);
                    'skin-card simple-tooltip simple-tooltip-inline tooltipstered'):attr('data-simple-tooltip',
            opacity: .85;
                    tooltipHtml)
        }
    }


    video::-webkit-media-controls {
                skinCard:tag('div'):addClass('skins--imageBanner'):wikitext(banner ~= '' and
        opacity: 0;
                                                                                string.format("[[Arquivo:%s|link=]]",
        transition: opacity .3s;
                        banner) or ''):attr('alt', 'banner')
    }


    video:hover::-webkit-media-controls {
                skinCard:tag('div'):addClass('skins--imageSkin'):wikitext(image ~= '' and
         opacity: 1;
                                                                              string.format("[[Arquivo:%s|link=]]",
     }
                        image) or ''):attr('alt', 'skin')
            end
        end
        if count > 0 then
            usedSkins = true
         end
     end


     /* -------------------- Skins -------------------- */
     -- ====== Fallback antigo (só roda se não vierem skins novas) ======
     .skins__title {
     if not usedSkins then
         display: block;
         for j = 1, 11 do
        width: 47%;
            local image = args['skin' .. j .. '-image']
        margin-bottom: 10px;
            if image then
        padding-bottom: 0;
                local banner = args['skin' .. j .. '-banner'] or ''
        font-family: 'Noto Sans', sans-serif !important;
                local tooltipRaw = args['skin' .. j .. '-tooltip'] or ''
        font-weight: 700;
                local tooltipHtml = tooltipRaw:gsub("'''([^']+)'''", "<b>%1</b>"):gsub("\n", "<br>")
        font-size: 40px;
        color: #fff;
        text-align: center;
        border-bottom: none;
    }


    .skins__wrapper {
                local skinCard = carousel:tag('div'):addClass(
        min-height: 21.1rem;
                    'skin-card simple-tooltip simple-tooltip-inline tooltipstered'):attr('data-simple-tooltip',
        max-height: 60%;
                    tooltipHtml)
        padding: 0 16px 1px !important;
        background: #26211C;
        color: #fff;
        border-radius: 8px;
        position: relative;
        z-index: 99;
        box-shadow: 0 8px 24px rgba(0, 0, 0, .35);
        backdrop-filter: blur(2px);
        display: flex;
        gap: 10px;
        justify-content: center;
        align-items: center;
        text-shadow: -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, 1px 1px 0 #000;
        overflow: visible;
        transition: all .3s ease;
    }


    /* Gradientes laterais quando houver rolagem */
                skinCard:tag('div'):addClass('skins--imageBanner'):wikitext(banner ~= '' and
    .skins__wrapper::before,
                                                                                string.format("[[Arquivo:%s|link=]]",
    .skins__wrapper::after {
                        banner) or ''):attr('alt', 'banner')
        content: "";
        position: absolute;
        top: 0;
        width: 60px;
        height: 100%;
        pointer-events: none;
        opacity: 0;
        transition: opacity .4s ease;
        z-index: 3;
    }


    .skins__wrapper::before {
                skinCard:tag('div'):addClass('skins--imageSkin'):wikitext(string.format("[[Arquivo:%s|link=]]", image))
        left: 0;
                    :attr('alt', 'skin')
        background: linear-gradient(to right, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 1) 100%);
            end
     }
        end
     end


     .skins__wrapper::after {
     for j = 1, 11 do
         right: 0;
        local image = args['skin' .. j .. '-image']
        background: linear-gradient(to left, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 1) 100%);
         if image then
    }
            local banner = args['skin' .. j .. '-banner'] or ''
            local tooltip = args['skin' .. j .. '-tooltip'] or ''
            local tooltipRaw = args['skin' .. j .. '-tooltip'] or ''
            local tooltipHtml = tooltipRaw:gsub("'''([^']+)'''", "<b>%1</b>")
            tooltipHtml = tooltipHtml:gsub("\n", "<br>")


    .skins__wrapper.has-left::before,
            local skinCard = carousel:tag('div')
    .skins__wrapper.has-right::after {
                :addClass('skin-card simple-tooltip simple-tooltip-inline tooltipstered'):attr('data-simple-tooltip',
        opacity: 1;
                    tooltipHtml)
    }


    .skins__carousel {
            skinCard:tag('div'):addClass('skins--imageBanner'):wikitext(string.format("[[Arquivo:%s|link=]]", banner))
        display: flex;
                :attr('alt', 'banner')
        gap: 1vw;
        flex-grow: 1;
        overflow-x: auto;
        padding: 10px 0;
        scroll-behavior: smooth;
    }


    .skins__carousel.both-mask {
            skinCard:tag('div'):addClass('skins--imageSkin'):wikitext(string.format("[[Arquivo:%s|link=]]", image))
        mask-image: linear-gradient(to right, transparent 0px, black 40px, black calc(100% - 40px), transparent 100%);
                :attr('alt', 'skin')
     }
        end
     end


     .skins__carousel.left-mask {
     wrapper:tag('div'):addClass('skins-arrow right'):wikitext('»')
        mask-image: linear-gradient(to right, transparent 0px, black 40px, black 100%);
    }


     .skins__carousel.right-mask {
     return tostring(html)
        mask-image: linear-gradient(to right, black 0px, black calc(100% - 40px), transparent 100%);
end
    }


    .skins__carousel.no-mask {
return p
        mask-image: none;
    }
 
    .skins__carousel::-webkit-scrollbar {
        display: none;
    }
 
    .skins__arrow {
        background: none;
        border: none;
        color: #fff;
        font-size: 36px;
        cursor: pointer;
        padding: 8px;
        z-index: 5;
        transition: opacity .3s ease, transform .3s ease;
    }
 
    .skins__arrow--left {
        margin-right: 8px;
    }
 
    .skins__arrow--right {
        margin-left: 8px;
    }
 
    .skins__arrow.hidden {
        opacity: 0;
        transform: scale(.8);
        pointer-events: none;
        visibility: hidden;
    }
 
    .skin-card {
        position: relative;
        width: 12vw;
        height: 39vh;
        flex: 0 0 auto;
        border: 2px solid #697EC9 !important;
        border-radius: 8px;
        overflow: hidden;
        background: #111;
        box-shadow: 0 2px 10px rgba(0, 0, 0, .25);
    }
 
    .skin-card::before {
        content: "";
        position: absolute;
        inset: 0;
        border-radius: inherit;
        pointer-events: none;
        z-index: 2;
        box-shadow: inset 0 0 8px rgba(180, 180, 180, .18);
    }
 
    .skins--imageBanner {
        width: 100%;
        height: 109%;
    }
 
    .skins--imageBanner img {
        width: 100%;
        height: 100%;
        object-fit: cover;
        filter: brightness(.5);
        scale: 1.1;
    }
 
    .skins--imageSkin img {
        position: absolute;
        bottom: 10px;
        left: 50%;
        transform: translateX(-50%);
        height: 140px;
        width: auto;
        z-index: 2;
        transition: transform .2s;
    }
 
    /* -------------------- Responsive (tela alta / mobile) -------------------- */
    @media (max-aspect-ratio: 3/4) {
        .skills {
            flex-direction: column-reverse;
            gap: 20px;
        }
 
        .skills__desc {
            padding: 22px !important;
        }
 
        .skills__icons {
            width: 98%;
            place-self: center;
            padding: 10px 0 16px 1px;
        }
 
        .skills__icon {
            width: 80px;
            height: 80px;
        }
 
        .skill__title h3 {
            font-size: 3.6em;
            margin-top: -14px;
        }
 
        .skill__text {
            font-size: 2.3em;
            margin-bottom: 5px;
        }
 
        .skills__video {
            width: 80%;
            border-radius: 3%;
            margin-top: 2%;
            align-self: center;
        }
 
        .character__art {
            display: none;
            width: 370px;
            height: 290px;
            right: .5rem;
            top: 1.1rem;
            z-index: 1;
        }
 
        .tabs__btn {
            padding: 10px 20px;
            font-size: 26px;
        }
 
        .tabs__panel {
            position: relative;
            z-index: 1;
            padding: 0 8px 20px;
        }
 
        .character__tag {
            padding: 0 5px;
            font-size: 1.4em;
        }
 
        .attrs__row {
            min-height: 26px;
        }
 
        .attrs__label {
            font-size: 1.2rem;
        }
 
        .attrs__value {
            font-size: 1.25rem;
        }
 
        .skins__carousel {
            gap: 20px;
        }
 
        .skin-card {
            width: 236px;
            height: 400px;
        }
 
        .skins--imageSkin img {
            height: 170px;
        }
 
        .skins__title {
            width: 100% !important;
        }
 
        .skins__arrow {
            display: none !important;
        }
 
        .skins__wrapper::before,
        .skins__wrapper::after {
            background: unset;
        }
 
        video::-webkit-media-controls {
            opacity: unset;
            transition: unset;
        }
 
        video:hover::-webkit-media-controls {
            opacity: unset;
        }
    }
</style>

Edição das 22h40min de 18 de agosto de 2025

local p = {}

function p.skin(frame)

   local a = frame.args
   local obj = {
       sprite = a.sprite or ,
       background = a.background or ,
       tooltip = a.tooltip or 
   }
   return mw.text.jsonEncode(obj)

end

function p.skill(frame)

   local a = frame.args
   local obj = {
       name = a.name or a.nome or ,
       icon = a.icon or ,
       level = tonumber(a.level) or nil,
       desc = a.desc or ,
       energy = a.energy or nil,
       powerpve = a.powerpve or nil,
       powerpvp = a.powerpvp or nil,
       cooldown = a.cooldown or nil,
       video = a.video or 
   }
   return mw.text.jsonEncode(obj)

end

function p.generate(frame)

   local args = frame:getParent().args
   local html = mw.html.create('div')
   local function getVideoURL(filename)
       return tostring(mw.uri.fullUrl('Special:FilePath/' .. filename))
   end
   local tier = (args.tier or ""):lower()
   local tierMap = {
       bronze = "tier-bronze",
       bronce = "tier-bronze",
       silver = "tier-silver",
       prata = "tier-silver",
       gold = "tier-gold",
       ouro = "tier-gold",
       diamond = "tier-diamond",
       diamante = "tier-diamond"
   }
   local tierClass = tierMap[tier]
   local box = html:tag('div'):addClass('personaje-box')
   if tierClass then
       box:addClass(tierClass)
   end
   local header = box:tag('div'):addClass('personaje-header')
   local topbar = header:tag('div'):addClass('personaje-topbar')
   local nomeBox = topbar:tag('div'):addClass('personaje-nome-box')
   local avatarImg = args.avatar or 'Franky_ts_medal.png'
   nomeBox:wikitext(string.format('Arquivo:%s', avatarImg))
   local nomeCat = nomeBox:tag('div'):addClass('personaje-nome-category')
   nomeCat:tag('div'):addClass('nome'):wikitext(args.nome or 'Franky (TS)')
   local classesDiv = nomeCat:tag('div'):addClass('classes')
   -- Dividir clases por "/"
   local classeString = args.classe or 
   local tierUpper = tier:gsub("^%l", string.upper)
   classesDiv:tag('div'):addClass('classe tier'):wikitext(tierUpper)
   for classe in mw.text.gsplit(classeString, '/', true) do
       classesDiv:tag('div'):addClass('classe'):wikitext(classe)
   end
   header:tag('div'):addClass('topbar-description'):wikitext(args.desc)
   local banner = args.banner or 
   local bannerFile = mw.title.new('Arquivo:' .. banner)
   if bannerFile and bannerFile.exists then
       header:tag('div'):addClass('banner'):wikitext(string.format(
           'Arquivo:%s', banner))
   else
       header:tag('div'):addClass('banner')
   end
   local tabs = header:tag('div'):addClass('personaje-tabs')
   tabs:tag('div'):addClass('tab-btn active'):attr('data-tab', 'habilidades'):wikitext('Habilidades')
   tabs:tag('div'):addClass('tab-btn'):attr('data-tab', 'skins'):wikitext('Skins')
   local artImg = args.artwork or 'Franky_ts_splash.png'
   header:tag('div'):css('text-align', 'center'):wikitext(string.format(
       'Arquivo:%s', artImg))
   -- HABILIDADE
   local habilidadesTab = box:tag('div'):addClass('tab-content active'):attr('id', 'habilidades')
   local cuadros = habilidadesTab:tag('div'):addClass('cuadros-container')
   local habilidadesContainer = habilidadesTab:tag('div'):addClass('habilidades-container')
   local details = habilidadesContainer:tag('div'):addClass('habilidades-details')
   local descripcionContainer = details:tag('div'):addClass('descripcion-container')
   -- ====== NOVO: bloco de skills via subtemplate ======
   local skillsPacked = args.skills
   local usedSkills = false
   if skillsPacked and skillsPacked:match("{") then
       -- Concat de objetos JSON: ...}{...}{...  -> ...},{...},{...
       local arr = "[" .. skillsPacked:gsub("}%s*{", "},{") .. "]"
       local ok, parsed = pcall(mw.text.jsonDecode, arr)
       if ok and type(parsed) == "table" and #parsed > 0 then
           usedSkills = true
           for i, sk in ipairs(parsed) do
               local nome = sk.name or sk.nome
               if nome and nome ~= "" then
                   local icon = sk.icon or ""
                   local level = sk.level or ""
                   local desc = sk.desc or ""
                   -- monta a string de atributos no formato esperado pelo JS:
                   -- PVE, PVP, Energia, Recarga
                   local atr = table.concat({sk.powerpve or "-", sk.powerpvp or "-", sk.energy or "-",
                                             sk.cooldown or "-"}, ", ")
                   local rawVideo = sk.video or ""
                   local videoURL = rawVideo ~= "" and getVideoURL(rawVideo) or ""
                   cuadros:tag('div'):addClass('cuadro'):attr('data-index', i):attr('data-nome', nome):attr(
                       'data-desc', desc):attr('data-atr', atr):attr('data-video', videoURL):attr('data-video-preload',
                       'auto'):wikitext(string.format("Arquivo:%s", icon))
                   descripcionContainer:tag('div'):addClass('habilidad-descripcion'):attr('data-index', i)
               end
           end
       end
   end
   -- ====== Fallback: mantém suporte aos hab1..hab21 se não usar 'skills' ======
   if not usedSkills then
       for i = 1, 21 do
           local nome = args['hab' .. i .. '-nome']
           if nome then
               local icon = args['hab' .. i .. '-icon'] or 
               local level = args['hab' .. i .. '-level'] or 
               local desc = args['hab' .. i .. '-desc'] or 
               local atr = args['hab' .. i .. '-atr'] or 
               local rawVideo = args['hab' .. i .. '-video'] or 
               local videoURL = rawVideo ~=  and getVideoURL(rawVideo) or 
               cuadros:tag('div'):addClass('cuadro'):attr('data-index', i):attr('data-nome', nome):attr('data-desc',
                   desc):attr('data-atr', atr):attr('data-video', videoURL):attr('data-video-preload', 'auto')
                   :wikitext(string.format("Arquivo:%s", icon))
               descripcionContainer:tag('div'):addClass('habilidad-descripcion'):attr('data-index', i)
           end
       end
   end
   for i = 1, 21 do
       local nome = args['hab' .. i .. '-nome']
       if nome then
           local icon = args['hab' .. i .. '-icon'] or 
           local level = args['hab' .. i .. '-level'] or 
           local desc = args['hab' .. i .. '-desc'] or 
           local atr = args['hab' .. i .. '-atr'] or 
           local rawVideo = args['hab' .. i .. '-video'] or 
           local videoURL = rawVideo ~=  and getVideoURL(rawVideo) or 
           cuadros:tag('div'):addClass('cuadro'):attr('data-index', i):attr('data-nome', nome):attr('data-desc', desc)
               :attr('data-atr', atr):attr('data-video', videoURL):attr('data-video-preload', 'auto'):wikitext(
                   string.format("Arquivo:%s", icon))
           descripcionContainer:tag('div'):addClass('habilidad-descripcion'):attr('data-index', i)
       end
   end
   details:done()
   habilidadesContainer:tag('div'):addClass('video-container'):done()
   habilidadesTab:done()
   -- SKINS
   local skinsTab = box:tag('div'):addClass('tab-content'):attr('id', 'skins')
   local cardSkins = skinsTab:tag('div'):addClass('card-skins')
   cardSkins:tag('span'):addClass('card-skins-title'):wikitext('SKINS & SPOTLIGHTS')
   local wrapper = cardSkins:tag('div'):addClass('skins-carousel-wrapper')
   wrapper:tag('div'):addClass('skins-arrow left'):wikitext('«')
   local carousel = wrapper:tag('div'):addClass('skins-carousel')
   -- ====== NOVO: bloco de skins via subtemplate (robusto) ======
   local skinsPacked = args.skins
   local usedSkins = false
   if skinsPacked and skinsPacked:find("{", 1, true) then
       local count = 0
       for obj in skinsPacked:gmatch("%b{}") do
           local ok, sk = pcall(mw.text.jsonDecode, obj)
           if ok and type(sk) == "table" then
               count = count + 1
               local banner = sk.background or 
               local image = sk.sprite or 
               local tooltipRaw = sk.tooltip or 
               local tooltipHtml = tooltipRaw:gsub("([^']+)", "%1"):gsub("\n", "
")
               local skinCard = carousel:tag('div'):addClass(
                   'skin-card simple-tooltip simple-tooltip-inline tooltipstered'):attr('data-simple-tooltip',
                   tooltipHtml)
               skinCard:tag('div'):addClass('skins--imageBanner'):wikitext(banner ~=  and
                                                                               string.format("Arquivo:%s",
                       banner) or ):attr('alt', 'banner')
               skinCard:tag('div'):addClass('skins--imageSkin'):wikitext(image ~=  and
                                                                             string.format("Arquivo:%s",
                       image) or ):attr('alt', 'skin')
           end
       end
       if count > 0 then
           usedSkins = true
       end
   end
   -- ====== Fallback antigo (só roda se não vierem skins novas) ======
   if not usedSkins then
       for j = 1, 11 do
           local image = args['skin' .. j .. '-image']
           if image then
               local banner = args['skin' .. j .. '-banner'] or 
               local tooltipRaw = args['skin' .. j .. '-tooltip'] or 
               local tooltipHtml = tooltipRaw:gsub("([^']+)", "%1"):gsub("\n", "
")
               local skinCard = carousel:tag('div'):addClass(
                   'skin-card simple-tooltip simple-tooltip-inline tooltipstered'):attr('data-simple-tooltip',
                   tooltipHtml)
               skinCard:tag('div'):addClass('skins--imageBanner'):wikitext(banner ~=  and
                                                                               string.format("Arquivo:%s",
                       banner) or ):attr('alt', 'banner')
               skinCard:tag('div'):addClass('skins--imageSkin'):wikitext(string.format("Arquivo:%s", image))
                   :attr('alt', 'skin')
           end
       end
   end
   for j = 1, 11 do
       local image = args['skin' .. j .. '-image']
       if image then
           local banner = args['skin' .. j .. '-banner'] or 
           local tooltip = args['skin' .. j .. '-tooltip'] or 
           local tooltipRaw = args['skin' .. j .. '-tooltip'] or 
           local tooltipHtml = tooltipRaw:gsub("([^']+)", "%1")
           tooltipHtml = tooltipHtml:gsub("\n", "
")
           local skinCard = carousel:tag('div')
               :addClass('skin-card simple-tooltip simple-tooltip-inline tooltipstered'):attr('data-simple-tooltip',
                   tooltipHtml)
           skinCard:tag('div'):addClass('skins--imageBanner'):wikitext(string.format("Arquivo:%s", banner))
               :attr('alt', 'banner')
           skinCard:tag('div'):addClass('skins--imageSkin'):wikitext(string.format("Arquivo:%s", image))
               :attr('alt', 'skin')
       end
   end
   wrapper:tag('div'):addClass('skins-arrow right'):wikitext('»')
   return tostring(html)

end

return p