Star Signs

Banner Star Signs.webp


Introdução

Star Signs funcionam como árvores de talentos únicas, permitindo que os jogadores desbloqueiem e combinem diferentes constelações para criar estilos de progressão e habilidades personalizadas.
Os pontos de constelação são compartilhados entre todos os personagens da conta, porém o jogador tem total liberdade para criar a sua própria build, conforme preferir.

Pontos de Constelação

Atualmente, estão disponíveis 45 pontos de talento para serem utilizados, além de 1 ponto especial, que é desbloqueado ao investir 30 pontos de talento na mesma árvore.

Podem ser obtidos através de:

  • Completar as três primeiras Rocket Quests de Johto = 1 ponto de constelação.
  • Completar a quarta Rocket Quest de Johto, a Time Traveler Quest = 3 pontos de constelação.
  • Completar a quinta Rocket Quest de Johto = 3 pontos de constelação.
  • Completar a The Rainbow Hero Quest = 5 pontos de constelação.
  • Completar o Regional level de Johto (level 200) = 2 pontos de constelação.
  • Completar a Echoes of Mt. Silver Quest = 1 ponto de constelação.
  • Completar a Liga Pokémon de Johto = 3 pontos de constelação.
  • Completar todos os unown trials de Johto = 1 Ponto de constelação.
  • Gastar energia azul de Mystery Dungeon (até 7 etapas) = 1 Ponto de constelação em cada etapa (totalizando 7 ao final).
  • Coletar metade dos Rainbow Orbs = 1 ponto de constelação.
  • Coletar todos Rainbow Orbs = 1 ponto de constelação.
  • Derrotar todos os World Bosses de Johto = 1 ponto de constelação.
  • Alcançar Level 605 = 2 pontos de constelação.
  • Alcançar Level 610 = 3 pontos de constelação.
  • Alcançar Level 615 = 3 pontos de constelação.
  • Alcançar Level 620 = 3 pontos de constelação.
  • Alcançar Level 625 = 3 pontos de constelação.
  • Red, The Greatest Trainer = 2 ponto de constelação.


Observação: Lembra-se que as conquistas relacionadas aos pontos de constelação, ao serem concluídas pela primeira vez na conta, desbloqueiam automaticamente os pontos de constelação para todos os personagens dessa conta.

Star Paths

Todas as contas começam com um Star Path, e o jogador poderá adquirir páginas extras na Diamond Shop para ter ainda mais opções.
Será possível adquirir até 2 Star Paths adicionais na Diamond Shop, pelos valores de 20 e 35 Diamonds, respectivamente. Dessa forma, cada conta poderá possuir até 3 Star Paths no total.
O jogador poderá montar livremente sua build, especializando-se em diferentes estilos de jogo, como combate contra Bosses ou captura de Pokémon.
Caso possua um personagem secundário e um segundo Star Path na conta, poderá criar uma build totalmente distinta, por exemplo, focada em ganho de experiência, facilitando o gerenciamento de diferentes estilos de jogo simultaneamente.

Observação: Será possível resetar completamente a constelação pagando 20k por ponto distribuído. Não é possível resetar parcialmente uma constelação.

Star Signs Builder

A seguir, encontrará uma ferramenta que permite simular e planejar a distribuição de pontos no sistema de talentos de Star Signs.
Por meio de uma interface interativa, os jogadores podem explorar as árvores de talentos, atribuir pontos manualmente e visualizar em tempo real os bônus e estatísticas obtidos com cada combinação.
O objetivo desta ferramenta é oferecer uma visão completa do sistema, permitindo comparar rotas e descobrir qual caminho se adapta melhor ao estilo de jogo de cada jogador.

`; // Assign a predictable ID to the grid for CSS targeting from external config // The grid ID created above is random, but we can add a secondary ID or Class // Better approach: We set the ID of the grid element to be predictable based on container // But we already used random ID. Let's just update the grid creation. this.dom.filter = document.getElementById(this.ids.filter); this.dom.grid = document.getElementById(this.ids.grid); // Assign predictable ID for CSS targeting this.dom.grid.id = `${this.containerId}-grid`; this.dom.modal = document.getElementById(this.ids.modal); this.dom.modalContent = this.dom.modal.querySelector('.fw-modal-content'); this.dom.closeBtn = this.dom.modal.querySelector('.fw-close-btn'); this.renderFilters(); this.applyFilters(); this.setupEvents(); } renderFilters() { this.dom.filter.innerHTML = ''; this.filtersConfig.forEach(filter => { const group = document.createElement('div'); group.className = 'fw-filter-group'; const label = document.createElement('label'); label.className = 'fw-filter-label'; label.textContent = filter.label; group.appendChild(label); if (filter.type === 'text') { const input = document.createElement('input'); input.className = 'fw-input'; input.placeholder = filter.placeholder || '...'; input.addEventListener('input', (e) => this.handleFilter(filter.key, e.target.value, 'text', filter)); group.appendChild(input); } else if (filter.type === 'number-range') { const wrap = document.createElement('div'); wrap.style.display = 'flex'; wrap.style.gap = '0.5rem'; const createInput = (placeholder) => { const inp = document.createElement('input'); inp.className = 'fw-input'; inp.placeholder = placeholder; inp.type = 'number'; inp.min = 0; inp.max = 625; // Clamp value on blur inp.addEventListener('blur', () => { let val = parseInt(inp.value); if (isNaN(val)) return; if (val 625) inp.value = 625; update(); // Trigger filter update after clamp }); return inp; }; const min = createInput('Min'); const max = createInput('Max'); const update = () => this.handleFilter(filter.key, { min: min.value, max: max.value }, 'range', filter); min.addEventListener('input', update); max.addEventListener('input', update); wrap.append(min, max); group.appendChild(wrap); } else if (filter.type === 'boolean') { const wrap = document.createElement('label'); wrap.className = 'fw-checkbox-wrapper'; const check = document.createElement('input'); check.type = 'checkbox'; const span = document.createElement('span'); span.textContent = filter.checkboxLabel; if (filter.expectedValue !== undefined) { check.dataset.expected = filter.expectedValue; } check.addEventListener('change', (e) => this.handleFilter(filter.key, e.target.checked, 'boolean', filter)); wrap.append(check, span); group.appendChild(wrap); } else if (filter.type === 'toggle') { const wrap = document.createElement('label'); wrap.className = 'fw-custom-toggle-container'; const check = document.createElement('input'); check.type = 'checkbox'; check.className = 'fw-toggle-input'; if (filter.expectedValue !== undefined) { check.dataset.expected = filter.expectedValue; } if (filter.defaultValue === true) { check.checked = true; this.activeFilters[filter.key] = { value: true, type: 'toggle', filterDef: filter }; } const switchVisual = document.createElement('span'); switchVisual.className = 'fw-toggle-switch'; const text = document.createElement('span'); text.className = 'fw-toggle-text'; text.textContent = check.checked ? (filter.textOn || 'SIM') : (filter.textOff || 'NÃO'); check.addEventListener('change', (e) => { text.textContent = e.target.checked ? (filter.textOn || 'SIM') : (filter.textOff || 'NÃO'); this.handleFilter(filter.key, e.target.checked, 'toggle', filter); }); wrap.append(check, switchVisual, text); group.appendChild(wrap); } else if (filter.type === 'multi-select') { const wrapper = document.createElement('div'); wrapper.className = 'fw-custom-select'; // Extract options if not provided let options = filter.options; if (!options) { const unique = new Set(); this.data.forEach(d => { if (d[filter.key]) unique.add(d[filter.key]); }); options = Array.from(unique).sort(); } // Trigger const trigger = document.createElement('div'); trigger.className = 'fw-select-trigger'; trigger.textContent = filter.placeholder || 'Selecionar...'; // Options Container const optsContainer = document.createElement('div'); optsContainer.className = 'fw-select-options'; const selectedValues = new Set(); // Close dropdown on click outside document.addEventListener('click', (e) => { if (!wrapper.contains(e.target)) optsContainer.classList.remove('open'); }); trigger.addEventListener('click', () => { optsContainer.classList.toggle('open'); }); options.forEach(opt => { const label = document.createElement('label'); label.className = 'fw-select-option'; const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.className = 'fw-select-checkbox'; checkbox.value = opt; checkbox.addEventListener('change', () => { if (checkbox.checked) selectedValues.add(opt); else selectedValues.delete(opt); // Update Trigger Text if (selectedValues.size === 0) trigger.textContent = filter.placeholder || 'Selecionar...'; else if (selectedValues.size === 1) trigger.textContent = Array.from(selectedValues)[0]; else trigger.textContent = `${selectedValues.size} seleccionados`; this.handleFilter(filter.key, Array.from(selectedValues), 'multi-select', filter); }); const span = document.createElement('span'); span.textContent = opt; label.append(checkbox, span); optsContainer.appendChild(label); }); wrapper.append(trigger, optsContainer); group.appendChild(wrapper); } this.dom.filter.appendChild(group); }); // === SORTING UI === if (this.sortsConfig.length > 0) { const sortGroup = document.createElement('div'); sortGroup.className = 'fw-filter-group'; // Label const label = document.createElement('label'); label.className = 'fw-filter-label'; label.textContent = 'Ordenar Por'; sortGroup.appendChild(label); // Sort Wrapper const sortWrap = document.createElement('div'); sortWrap.style.display = 'flex'; sortWrap.style.gap = '0.5rem'; // Select const sortSelect = document.createElement('select'); sortSelect.className = 'fw-input'; sortSelect.style.cursor = 'pointer'; // Default Option const defOpt = document.createElement('option'); defOpt.value = ""; defOpt.textContent = "Padrão"; sortSelect.appendChild(defOpt); this.sortsConfig.forEach(s => { const opt = document.createElement('option'); opt.value = s.key; opt.textContent = s.label; sortSelect.appendChild(opt); }); // Direction Button const dirBtn = document.createElement('button'); dirBtn.className = 'fw-toggle-btn'; // Reuse style dirBtn.style.width = '48px'; // Ensure square dirBtn.innerHTML = 'arrow_downward'; dirBtn.title = 'Descendente'; dirBtn.style.cursor = 'pointer'; // Events sortSelect.addEventListener('change', (e) => { const key = e.target.value; // Find default direction for this key const constr = this.sortsConfig.find(sc => sc.key === key); const dir = constr ? (constr.defaultDirection || 'asc') : 'asc'; this.handleSort(key, dir); // Update visual icon for new default dirBtn.dataset.dir = dir; dirBtn.innerHTML = `${dir === 'asc' ? 'arrow_upward' : 'arrow_downward'}`; }); dirBtn.addEventListener('click', () => { const currentDir = dirBtn.dataset.dir || 'desc'; const newDir = currentDir === 'asc' ? 'desc' : 'asc'; dirBtn.dataset.dir = newDir; dirBtn.innerHTML = `${newDir === 'asc' ? 'arrow_upward' : 'arrow_downward'}`; dirBtn.title = newDir === 'asc' ? 'Ascendente' : 'Descendente'; // Apply with current key (if any) this.handleSort(this.activeSort.key, newDir); }); sortWrap.appendChild(sortSelect); sortWrap.appendChild(dirBtn); sortGroup.appendChild(sortWrap); this.dom.filter.appendChild(sortGroup); } // Add View Toggle ONLY if both templates exist if (this.templates.item && this.templates.itemList) { const toggleGroup = document.createElement('div'); toggleGroup.className = 'fw-view-toggle'; toggleGroup.innerHTML = ` `; const btns = toggleGroup.querySelectorAll('.fw-toggle-btn'); btns.forEach(btn => { btn.addEventListener('click', () => { btns.forEach(b => b.classList.remove('active')); btn.classList.add('active'); this.setViewMode(btn.dataset.mode); }); }); this.dom.filter.appendChild(toggleGroup); this.dom.filter.appendChild(toggleGroup); } // === HIDE COMPLETED BUTTON (Conditional) === if (this.selectable && this.showHideCompletedButton) { const hideGroup = document.createElement('div'); hideGroup.className = 'fw-filter-group'; const label = document.createElement('label'); label.className = 'fw-filter-label'; label.textContent = 'COMPLETADOS'; hideGroup.appendChild(label); const hideBtn = document.createElement('button'); // Match input styling but make it clickable hideBtn.className = 'fw-input'; hideBtn.style.cursor = 'pointer'; hideBtn.style.justifyContent = 'start'; hideBtn.style.color = '#64748b'; hideBtn.style.fontWeight = '600'; hideBtn.title = 'Ocultar Completados'; const updateBtnState = () => { if (this.hideCompleted) { hideBtn.style.background = '#f0fdf4'; // Light green for active/hidden state logic or just active // Actually user image shows check-like or eye. Let's keep it clean. // If "Hidden", maybe show "Show"? // Logic: Button says what it DOES or what IS? // User image says "Ocultar" (Hide) with an eye-slash. This implies clicking it will hide. // So currently they are SHOWING. // If they are HIDDEN, button should say "Mostrar" (Show). // Let's stick to previous logic just better style. hideBtn.style.background = '#9f4eea'; hideBtn.style.color = 'white'; hideBtn.style.borderColor = '#9f4eea'; hideBtn.innerHTML = ` visibility Mostrar `; } else { hideBtn.style.background = '#f8fafc'; hideBtn.style.color = '#64748b'; hideBtn.style.borderColor = '#e2e8f0'; hideBtn.innerHTML = ` visibility_off Ocultar `; } }; updateBtnState(); // Initial state hideBtn.addEventListener('click', () => { this.hideCompleted = !this.hideCompleted; updateBtnState(); this.applyFilters(); }); // Match hover effect to css hideBtn.addEventListener('mouseenter', () => { if (!this.hideCompleted) { hideBtn.style.background = '#ffffff'; hideBtn.style.borderColor = '#cbd5e1'; } }); hideBtn.addEventListener('mouseleave', () => { if (!this.hideCompleted) { hideBtn.style.background = '#f8fafc'; hideBtn.style.borderColor = '#e2e8f0'; } }); hideGroup.appendChild(hideBtn); // Insert before view toggles if possible. const viewToggleGroup = this.dom.filter.querySelector('.fw-view-toggle'); if (viewToggleGroup) { this.dom.filter.insertBefore(hideGroup, viewToggleGroup); } else { this.dom.filter.appendChild(hideGroup); } } } setViewMode(mode) { this.viewMode = mode; this.renderGrid(); } handleFilter(key, value, type, filterDef) { this.activeFilters[key] = { value, type, filterDef }; this.applyFilters(); } handleSort(key, direction) { this.activeSort = { key, direction }; this.sortData(); // Just re-sort and render, no need to re-filter this.renderGrid(); } applyFilters() { this.filteredData = this.data.filter(item => { // 0. Filter Completed if Hiding if (this.selectable && this.hideCompleted) { // We need the ID. Based on current logic, ID is item.name if (this.completedItems.has(item.name)) return false; } for (const key in this.activeFilters) { const f = this.activeFilters[key]; // Support multiple keys if defined in filterDef, fallback to loop key const targetKey = f.filterDef ? f.filterDef.key : key; if (f.type === 'text') { if (!f.value) continue; const q = f.value.toLowerCase(); if (key === 'generic_search') { if (!Object.values(item).some(v => v && String(v).toLowerCase().includes(q))) return false; } else { // Support array of keys or single key const keysToCheck = Array.isArray(targetKey) ? targetKey : [targetKey]; const match = keysToCheck.some(k => { const val = item[k]; if (!val) return false; if (Array.isArray(val)) { return val.some(v => (typeof v === 'object' && v !== null) ? JSON.stringify(v).toLowerCase().includes(q) : String(v).toLowerCase().includes(q) ); } return String(val).toLowerCase().includes(q); }); if (!match) return false; } } else { // For range/boolean, assume single key const val = item[targetKey]; if (f.type === 'range') { const min = parseFloat(f.value.min), max = parseFloat(f.value.max); if (!isNaN(min) && (val === undefined || val max)) return false; } else if (f.type === 'boolean') { if (f.value === true) { const expected = (f.filterDef && f.filterDef.expectedValue !== undefined) ? f.filterDef.expectedValue : true; if (val !== expected) return false; } } else if (f.type === 'toggle') { const expected = f.value ? (f.filterDef && f.filterDef.expectedValueOn !== undefined ? f.filterDef.expectedValueOn : true) : (f.filterDef && f.filterDef.expectedValueOff !== undefined ? f.filterDef.expectedValueOff : false); if (expected !== '*' && val !== expected) return false; } else if (f.type === 'multi-select') { if (f.value.length > 0) { if (val === undefined || val === null) return false; const itemValues = Array.isArray(val) ? val : [val]; const hasMatch = itemValues.some(v => f.value.includes(v)); if (!hasMatch) return false; } } } } return true; }); this.sortData(); // Apply sort after filtering this.renderGrid(); } sortData() { const { key, direction } = this.activeSort; if (!key) return; // No sort active this.filteredData.sort((a, b) => { let va = a[key]; let vb = b[key]; // Handle undefined/null (push to bottom usually) if (va === undefined || va === null) va = ''; if (vb === undefined || vb === null) vb = ''; // Try to compare as numbers const na = parseFloat(va); const nb = parseFloat(vb); // Check if they are effectively numbers (and not just starting with a number like "10th") // Actually, if both parse to valid numbers, treat as numbers for sorting is usually desired. // But for names like "2nd street" vs "10th street", numeric is better. // For "Alfredo", parseFloat is NaN. const aIsNum = !isNaN(na); const bIsNum = !isNaN(nb); if (aIsNum && bIsNum) { return direction === 'asc' ? na - nb : nb - na; } // Fallback to string comparison const sa = String(va).toLowerCase(); const sb = String(vb).toLowerCase(); if (sa sb) return direction === 'asc' ? 1 : -1; return 0; }); } renderGrid() { // 1. Update grid classes based on view mode if (this.viewMode === 'list') { this.dom.grid.classList.add('fw-list-view'); } else { this.dom.grid.classList.remove('fw-list-view'); } if (this.filteredData.length === 0) { this.dom.grid.innerHTML = '
No results found
'; return; } // 2. Choose template and cache based on view mode let templateName = (this.viewMode === 'list') ? 'itemList' : 'item'; // Safety check: if current view mode template is missing, try fallback if (this.viewMode === 'list' && !this.templates.itemList && this.templates.item) { this.viewMode = 'grid'; templateName = 'item'; this.dom.grid.classList.remove('fw-list-view'); } const template = this.templates[templateName]; const activeCache = this.elementCache[this.viewMode]; if (!template) { this.dom.grid.innerHTML = '
No template found for ' + this.viewMode + ' view
'; return; } // 3. Collect Nodes (Reuse from Cache or Create) const nodesToRender = []; this.filteredData.forEach(item => { // Use a unique key for the item. Assuming 'name' describes identity. // Fallback to index if name missing effectively disables valid caching for stability, so name is preferred. const itemId = item.name || JSON.stringify(item); let el = activeCache.get(itemId); if (!el) { // Create New Element const html = this.renderTemplate(template, item); const wrap = document.createElement('div'); wrap.innerHTML = html; el = wrap.firstElementChild; if (el) { // === Handle Selectable Logic === if (this.selectable) { if (this.completedItems.has(itemId)) { el.classList.add('is-completed'); } // Create Check Button const checkBtn = document.createElement('div'); checkBtn.className = 'fw-check-toggle'; checkBtn.innerHTML = 'check'; checkBtn.title = this.completedItems.has(itemId) ? "Marcar como pendente" : "Marcar como concluído"; checkBtn.addEventListener('click', (e) => { e.stopPropagation(); this.toggleItemCompletion(itemId, el, checkBtn); }); // Append button el.appendChild(checkBtn); } // === Handle Modal logic === if (this.templates.modal) { const btn = el.querySelector('.fw-details-btn'); if (btn) { btn.addEventListener('click', (e) => { e.stopPropagation(); this.openModal(item); }); el.style.cursor = 'default'; } else { el.addEventListener('click', () => this.openModal(item)); el.style.cursor = 'pointer'; } } else { el.style.cursor = 'default'; } // Update Cache activeCache.set(itemId, el); } } // else: Element exists, reuse it directly (preserves image load state) if (el) nodesToRender.push(el); }); // 5. Update DOM efficiently if (this.dom.grid.replaceChildren) { this.dom.grid.replaceChildren(...nodesToRender); } else { this.dom.grid.innerHTML = ''; nodesToRender.forEach(node => this.dom.grid.appendChild(node)); } } openModal(item) { if (!this.templates.modal) return; // Do not open if no template const itemId = item.name || JSON.stringify(item); let cachedModalWrap = this.modalCache.get(itemId); if (!cachedModalWrap) { const html = this.renderTemplate(this.templates.modal, item); cachedModalWrap = document.createElement('div'); cachedModalWrap.style.display = 'contents'; // Crucial so it doesn't affect flex properties cachedModalWrap.innerHTML = html; this.modalCache.set(itemId, cachedModalWrap); } // Use the cached node directly instead of triggering innerHTML parsing and reload if (this.dom.modalContent.replaceChildren) { this.dom.modalContent.replaceChildren(cachedModalWrap); } else { this.dom.modalContent.innerHTML = ''; this.dom.modalContent.appendChild(cachedModalWrap); } this.dom.modal.classList.add('active'); document.body.style.overflow = 'hidden'; } renderTemplate(templateString, data) { if (!templateString) return ''; let output = templateString; // 1. Handle #if key output = output.replace(/\{\{#if\s+(\w+)\}\}([\s\S]*?)\{\{\/if\}\}/g, (match, key, content) => { const val = data[key]; // Check for truthy, not 'None', not empty array const isTrue = val && val !== 'None' && (!Array.isArray(val) || val.length > 0); return isTrue ? content : ''; }); // 2. Handle #each key output = output.replace(/\{\{#each\s+(\w+)\}\}([\s\S]*?)\{\{\/each\}\}/g, (match, key, content) => { const list = data[key]; if (!Array.isArray(list) || list.length === 0) return ''; return list.map(item => { // Normalize item context: if primitive, map to { label: item, this: item } let ctx = item; if (typeof item !== 'object' || item === null) { ctx = { this: item, label: item }; } let itemHtml = content; // Internal #if support (simple 1-level nesting) itemHtml = itemHtml.replace(/\{\{#if\s+(\w+)\}\}([\s\S]*?)\{\{\/if\}\}/g, (m, k, c) => { const v = ctx[k]; return (v) ? c : ''; }); // Internal values itemHtml = itemHtml.replace(/\{\{\s*(\w+)\s*\?\s*'([^']*)'\s*:\s*'([^']*)'\s*\}\}/g, (m, k, t, f) => ctx[k] ? t : f); // Handle "isSub" logic specifically for styling if used in ternary itemHtml = itemHtml.replace(/\{\{\s*(\w+)\s*\}\}/g, (m, k) => ctx[k] !== undefined ? ctx[k] : ''); return itemHtml; }).join(''); }); // 3. Conditional Values (Top Level) output = output.replace(/\{\{\s*(\w+)\s*\?\s*'([^']*)'\s*:\s*'([^']*)'\s*\}\}/g, (match, key, tVal, fVal) => data[key] ? tVal : fVal); output = output.replace(/\{\{\s*(\w+)\s*\?\s*"([^"]*)"\s*:\s*"([^"]*)"\s*\}\}/g, (match, key, tVal, fVal) => data[key] ? tVal : fVal); // 4. Numeric Thresholds output = output.replace(/\{\{\s*(\w+)\s*(>=|| { const val = data[key]; const threshold = parseFloat(num); let result = false; if (val !== undefined) { if (op === '>=') result = val >= threshold; if (op === '') result = val > threshold; if (op === ' data[key] !== undefined ? data[key] : ''); return output; } setupEvents() { this.dom.closeBtn.addEventListener('click', () => { this.dom.modal.classList.remove('active'); document.body.style.overflow = ''; }); this.dom.modal.addEventListener('click', (e) => { if (e.target === this.dom.modal) this.dom.closeBtn.click(); }); } toggleItemCompletion(id, cardEl, btnEl) { if (this.completedItems.has(id)) { this.completedItems.delete(id); cardEl.classList.remove('is-completed'); if (btnEl) btnEl.title = "Marcar como concluído"; } else { this.completedItems.add(id); cardEl.classList.add('is-completed'); if (btnEl) btnEl.title = "Marcar como pendente"; } this.saveCompletedItems(); // If we are currently hiding completed items, we should re-apply filters to remove this item immediately if (this.hideCompleted) { this.applyFilters(); } } saveCompletedItems() { if (!this.storageKey) return; localStorage.setItem(this.storageKey, JSON.stringify(Array.from(this.completedItems))); } } if (window.initFlexibleWidget === undefined) { window.initFlexibleWidget = function (cid, cfg) { new FlexibleWidget(cid, cfg); }; } })();