Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions backend/analytics/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ def aggregate_card_stats(
type_filter = filters.get("upgrade_type") # For upgrades
text_filter = filters.get("search_text", "").lower()
ship_filter = filters.get("ship")
include_epic = filters.get("include_epic", False)
initiative_filter = filters.get("initiative")

# New Context Filters
Expand Down Expand Up @@ -154,9 +155,6 @@ def _int_or(val, fallback):
if data_source == DataSource.XWA:
if p_loadout < loadout_min or p_loadout > loadout_max:
continue


include_epic_content = filters.get("include_epic", False)

# Strict Format Visibility Filter
is_legal = p_info.get("valid_in_standard", False)
Expand Down Expand Up @@ -193,6 +191,10 @@ def _int_or(val, fallback):
show_card = True
elif data_source == DataSource.LEGACY:
show_card = True

# Epic Content Toggle: if true, show all epic pilots
if include_epic and is_epic:
show_card = True

if not show_card:
continue
Expand Down Expand Up @@ -410,6 +412,10 @@ def _int_or(val, fallback):
show_card = True
elif data_source == DataSource.LEGACY:
show_card = True

# Epic Content Toggle
if include_epic and is_epic:
show_card = True

if not show_card:
continue
Expand Down
3 changes: 2 additions & 1 deletion backend/analytics/factions.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ def aggregate_faction_stats(
results.sort(key=lambda x: x["popularity"], reverse=True)
return results

def get_meta_snapshot(data_source: DataSource = DataSource.XWA, allowed_formats: list[str] | None = None) -> dict:
def get_meta_snapshot(data_source: DataSource = DataSource.XWA, allowed_formats: list[str] | None = None, include_epic: bool = False) -> dict:
"""
Get a high-level summary of the current meta (last 90 days).
"""
Expand All @@ -123,6 +123,7 @@ def get_meta_snapshot(data_source: DataSource = DataSource.XWA, allowed_formats:

filters = {
"date_start": date_str,
"include_epic": include_epic
}
if allowed_formats:
filters["allowed_formats"] = get_active_formats(allowed_formats)
Expand Down
25 changes: 24 additions & 1 deletion backend/analytics/lists.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from ..data_structures.factions import Faction, get_faction_char
from ..data_structures.data_source import DataSource
from .filters import filter_query, get_active_formats, apply_tournament_filters
from ..utils.xwing_data.pilots import load_all_pilots
import json

def aggregate_list_stats(
Expand All @@ -30,7 +31,11 @@ def aggregate_list_stats(
# A more robust solution would use a canonical list hash.

list_stats = {}


# Epic filter: exclude lists containing any epic-only pilot
include_epic = filters.get("include_epic", False)
all_pilots = load_all_pilots(data_source)

for result, tournament in rows:
# Format filter optimization
t_fmt_raw = tournament.format
Expand All @@ -50,6 +55,24 @@ def aggregate_list_stats(

pilots = xws.get("pilots", [])
if not pilots: continue

# Epic-only list exclusion: skip entire list if ANY pilot is epic-only
if not include_epic:
has_epic_only = False
for p in pilots:
pid = p.get("id") or p.get("name")
if not pid:
continue
p_info = all_pilots.get(pid, {})
is_epic = p_info.get("epic", False)
is_legal = p_info.get("valid_in_standard", False)
is_wild = p_info.get("wildspace", False)
if is_epic and not is_legal and not is_wild:
has_epic_only = True
break
if has_epic_only:
continue


req_ships = filters.get("ships")
if req_ships:
Expand Down
18 changes: 18 additions & 0 deletions backend/analytics/ships.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ def aggregate_ship_stats(


allowed_formats = filters.get("allowed_formats") or None
include_epic = filters.get("include_epic", False)
allowed_date_start = filters.get("date_start") or None
allowed_date_end = filters.get("date_end") or None

Expand All @@ -73,6 +74,15 @@ def aggregate_ship_stats(

if not ship_xws or not faction:
continue

# Epic-only exclusion: skip pilots playable ONLY in Epic
is_epic = p_info.get("epic", False)
is_legal = p_info.get("valid_in_standard", False)
is_wild = p_info.get("wildspace", False)

if not include_epic:
if is_epic and not is_legal and not is_wild:
continue

# Normalize faction to xws format
try:
Expand Down Expand Up @@ -174,8 +184,16 @@ def aggregate_ship_stats(
# Fallback visibility if no formats selected
show_pilot = is_legal or is_wild

# Epic Content Toggle
if include_epic and is_epic:
show_pilot = True

if not show_pilot:
continue

# Epic-only exclusion: skip pilots playable ONLY in Epic
if not include_epic and is_epic and not is_legal and not is_wild:
continue

key = (ship_xws, faction_xws)
if key in ship_stats:
Expand Down
11 changes: 8 additions & 3 deletions backend/api/cards.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ def _build_filters(
date_end: Optional[str] = None,
player_count_min: Optional[int] = None,
player_count_max: Optional[int] = None,
include_epic: bool = False,
) -> dict:

# Base sizes mapping
Expand Down Expand Up @@ -81,7 +82,7 @@ def _build_filters(
"date_end": date_end,
"player_count_min": player_count_min,
"player_count_max": player_count_max,
"include_epic": False
"include_epic": include_epic
}


Expand Down Expand Up @@ -124,6 +125,7 @@ def get_pilots(
date_end: Optional[str] = Query(None),
player_count_min: Optional[int] = Query(None),
player_count_max: Optional[int] = Query(None),
include_epic: bool = Query(False),
):
try:
ds_enum = DataSource(data_source)
Expand Down Expand Up @@ -151,7 +153,8 @@ def get_pilots(
is_unique=is_unique, is_limited=is_limited, is_not_limited=is_not_limited,
base_sizes=base_sizes, platforms=platforms, continent=continent, country=country, city=city,
date_start=date_start, date_end=date_end,
player_count_min=player_count_min, player_count_max=player_count_max
player_count_min=player_count_min, player_count_max=player_count_max,
include_epic=include_epic
)

data = aggregate_card_stats(filters, criteria, s_dir, "pilots", ds_enum)
Expand Down Expand Up @@ -183,6 +186,7 @@ def get_upgrades(
date_end: Optional[str] = Query(None),
player_count_min: Optional[int] = Query(None),
player_count_max: Optional[int] = Query(None),
include_epic: bool = Query(False),
):
try:
ds_enum = DataSource(data_source)
Expand All @@ -204,7 +208,8 @@ def get_upgrades(
search_text=search_text, points_min=points_min, points_max=points_max,
platforms=platforms, continent=continent, country=country, city=city,
date_start=date_start, date_end=date_end,
player_count_min=player_count_min, player_count_max=player_count_max
player_count_min=player_count_min, player_count_max=player_count_max,
include_epic=include_epic
)

data = aggregate_card_stats(filters, criteria, s_dir, "upgrades", ds_enum)
Expand Down
2 changes: 2 additions & 0 deletions backend/api/lists.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ def get_lists(
date_end: Optional[str] = Query(None),
player_count_min: Optional[int] = Query(None),
player_count_max: Optional[int] = Query(None),
include_epic: bool = Query(False),
):
try:
ds_enum = DataSource(data_source)
Expand All @@ -44,6 +45,7 @@ def get_lists(
"date_end": date_end,
"player_count_min": player_count_min,
"player_count_max": player_count_max,
"include_epic": include_epic,
"ships": ships,
}
if formats:
Expand Down
6 changes: 4 additions & 2 deletions backend/api/pilot_detail.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ def get_pilot_upgrades(
formats: list[str] | None = Query(None),
search_text: str = Query(""),
upgrade_types: list[str] | None = Query(None),
include_epic: bool = Query(False),
):
"""Return upgrade stats filtered to this pilot's lists."""
ds = DataSource(data_source) if data_source in ("xwa", "legacy") else DataSource.XWA
Expand All @@ -54,7 +55,7 @@ def get_pilot_upgrades(
"search_text": search_text,
"upgrade_type": upgrade_types or [],
"pilot_id": pilot_xws,
"include_epic": False,
"include_epic": include_epic,
}
data = aggregate_card_stats(filters, criteria, direction, "upgrades", ds)
total = len(data)
Expand All @@ -69,11 +70,12 @@ def get_pilot_chart(
data_source: str = Query("xwa"),
formats: list[str] | None = Query(None),
comparison: list[str] | None = Query(None),
include_epic: bool = Query(False),
):
"""Return monthly usage history for the pilot and optional comparisons."""
filters = {
"allowed_formats": formats,
"include_epic": False,
"include_epic": include_epic,
}
chart_data = get_card_usage_history(
filters,
Expand Down
3 changes: 2 additions & 1 deletion backend/api/ship_detail.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ def get_ship_pilots(
data_source: str = Query("xwa"),
sort_metric: str = Query("Popularity"),
sort_direction: str = Query("desc"),
include_epic: bool = Query(False),
):
"""Return pilot stats filtered to this ship."""
ds = DataSource(data_source) if data_source in ("xwa", "legacy") else DataSource.XWA
Expand All @@ -58,7 +59,7 @@ def get_ship_pilots(

filters = {
"ship": [ship_xws],
"include_epic": False,
"include_epic": include_epic,
}
data = aggregate_card_stats(filters, criteria, direction, "pilots", ds)
return {"pilots": data}
Expand Down
2 changes: 2 additions & 0 deletions backend/api/ships.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ def get_ships(
date_end: Optional[str] = Query(None),
player_count_min: Optional[int] = Query(None),
player_count_max: Optional[int] = Query(None),
include_epic: bool = Query(False),
):
try:
ds_enum = DataSource(data_source)
Expand Down Expand Up @@ -77,6 +78,7 @@ def get_ships(
"date_end": date_end,
"player_count_min": player_count_min,
"player_count_max": player_count_max,
"include_epic": include_epic,
}

data = aggregate_ship_stats(filters, criteria, s_dir, ds_enum)
Expand Down
7 changes: 5 additions & 2 deletions backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,14 @@ def read_root():


@app.get("/api/meta-snapshot", response_model=MetaSnapshotResponse)
def get_snapshot(data_source: str = Query("xwa", description="Data source: xwa or legacy")):
def get_snapshot(
data_source: str = Query("xwa", description="Data source: xwa or legacy"),
include_epic: bool = Query(False, description="Include epic content")
):
ds_enum = DataSource.XWA if data_source == "xwa" else DataSource.LEGACY

# We parse what HomeState loaded
snapshot = get_meta_snapshot(ds_enum, allowed_formats=None)
snapshot = get_meta_snapshot(ds_enum, allowed_formats=None, include_epic=include_epic)

raw_lists = snapshot.get("lists", [])
enriched_lists = [enrich_list_data(l) for l in raw_lists]
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/lib/api/ships.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ export interface ShipChassis {
factions: string[];
}

export async function fetchAllShips(dataSource: string): Promise<ShipChassis[]> {
export async function fetchAllShips(dataSource: string, includeEpic: boolean = false): Promise<ShipChassis[]> {
try {
const response = await fetch(`${API_BASE}/ships/all?data_source=${dataSource}`);
const response = await fetch(`${API_BASE}/ships/all?data_source=${dataSource}&include_epic=${includeEpic}`);
if (!response.ok) throw new Error('Failed to fetch ships');
return await response.json();
} catch (e) {
Expand Down
27 changes: 24 additions & 3 deletions frontend/src/lib/components/AdvancedFilters.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@
>
<input
type="checkbox"
class="rounded border-border-dark bg-black w-3 h-3"
class="rounded border-border-dark bg-black w-3 h-3 text-red-500 focus:ring-red-500"
bind:checked={filters.isUnique}
/>
<span class="font-mono">Unique</span>
Expand All @@ -91,7 +91,7 @@
>
<input
type="checkbox"
class="rounded border-border-dark bg-black w-3 h-3"
class="rounded border-border-dark bg-black w-3 h-3 text-red-500 focus:ring-red-500"
bind:checked={filters.isLimited}
/>
<span class="font-mono">Limited</span>
Expand All @@ -101,14 +101,35 @@
>
<input
type="checkbox"
class="rounded border-border-dark bg-black w-3 h-3"
class="rounded border-border-dark bg-black w-3 h-3 text-red-500 focus:ring-red-500"
bind:checked={filters.isGeneric}
/>
<span class="font-mono">Generic</span>
</label>
</div>
</div>

<!-- Game Content -->
<div class="space-y-1">
<span
class="text-[10px] font-bold text-primary font-mono tracking-wider opacity-70 uppercase"
>Game Content</span
>
<div class="flex items-center gap-4 flex-wrap">
<label
class="flex items-center gap-2 cursor-pointer text-xs text-secondary hover:text-red-500 transition-colors"
title="Include Epic-only ships, pilots, and upgrades"
>
<input
type="checkbox"
class="rounded border-border-dark bg-black w-3 h-3 text-red-500 focus:ring-red-500"
bind:checked={filters.includeEpic}
/>
<span class="font-mono">Include Epic Content</span>
</label>
</div>
</div>

<!-- Base Size (Pilots Only) -->
{#if isPilotsTab}
<div class="space-y-1">
Expand Down
10 changes: 6 additions & 4 deletions frontend/src/lib/components/ShipChassisFilter.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -39,16 +39,18 @@
// Initial load
onMount(async () => {
isLoading = true;
ships = await fetchAllShips(filters.dataSource);
ships = await fetchAllShips(filters.dataSource, filters.includeEpic);
isLoading = false;
});

// Re-fetch when data source changes
// Re-fetch when data source or includeEpic changes
let currentDataSource = $state(filters.dataSource);
let currentIncludeEpic = $state(filters.includeEpic);
$effect(() => {
if (currentDataSource !== filters.dataSource) {
if (currentDataSource !== filters.dataSource || currentIncludeEpic !== filters.includeEpic) {
currentDataSource = filters.dataSource;
fetchAllShips(filters.dataSource).then((data) => (ships = data));
currentIncludeEpic = filters.includeEpic;
fetchAllShips(filters.dataSource, filters.includeEpic).then((data) => (ships = data));
}
});

Expand Down
2 changes: 1 addition & 1 deletion frontend/src/routes/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
error = false;
errorMsg = "";

const targetUrl = `/api/meta-snapshot?data_source=${source}`;
const targetUrl = `/api/meta-snapshot?data_source=${source}&include_epic=${filters.includeEpic}`;
fetch(targetUrl)
.then(async (res) => {
if (!res.ok) {
Expand Down
Loading