Skip to content

Analytics

Developer-facing API reference for the analysis package.

Package

analysis

Pure analysis package for theTowerStats.

This package contains deterministic, testable computations that operate on in-memory inputs and return DTOs. It must not import Django or perform any database I/O.

Modules

analysis.aggregations

Aggregation helpers for the Analysis Engine.

This module provides deterministic, reusable aggregation functions used by the UI (charts, comparisons) without introducing Django dependencies.

RunAnalysis dataclass

Per-run analysis result.

Attributes:

Name Type Description
run_id int | None

Optional identifier for the underlying persisted record.

battle_date datetime

The battle date used as a time-series x-axis.

tier int | None

Optional tier value when available on the input.

preset_name str | None

Optional preset label when available on the input.

coins_per_hour float

Derived rate metric for Phase 1 charts.

WindowSummary dataclass

A summarized view of runs within a date window.

Attributes:

Name Type Description
start_date date

Window start date (inclusive).

end_date date

Window end date (inclusive).

run_count int

Number of runs included in the window.

average_coins_per_hour float | None

Average coins/hour across runs, if any.

average_coins_per_hour(runs)

Compute average coins/hour across runs.

Parameters:

Name Type Description Default
runs Iterable[RunAnalysis]

Per-run analysis results.

required

Returns:

Type Description
float | None

The arithmetic mean of coins_per_hour, or None when no runs exist.

average_metric(runs, *, value_getter)

Compute an average across runs for a selected metric.

Parameters:

Name Type Description Default
runs Iterable[RunAnalysis]

Per-run analysis results.

required
value_getter Callable[[RunAnalysis], float | None]

Callable that extracts a float value from a run.

required

Returns:

Type Description
float | None

Arithmetic mean across extracted values, or None when no values exist.

daily_average_series(runs, *, value_getter=None)

Aggregate runs into a daily average series keyed by ISO date.

Parameters:

Name Type Description Default
runs Iterable[RunAnalysis]

Per-run analysis results.

required
value_getter Callable[[RunAnalysis], float | None] | None

Optional callable extracting the metric value from a run. Defaults to run.coins_per_hour.

None

Returns:

Type Description
dict[str, float]

Mapping of YYYY-MM-DD -> average metric value for that day.

filter_runs_by_date(runs, *, start_date, end_date)

Filter analyzed runs by an inclusive date range.

Parameters:

Name Type Description Default
runs Iterable[RunAnalysis]

Per-run analysis results.

required
start_date date | None

Optional start date (inclusive).

required
end_date date | None

Optional end date (inclusive).

required

Returns:

Type Description
tuple[RunAnalysis, ...]

A tuple of runs whose battle_date.date() falls within the given range.

simple_moving_average(values, *, window)

Compute a simple moving average over a numeric series.

Parameters:

Name Type Description Default
values Sequence[float | None]

A sequence of values aligned to chart labels (None for missing).

required
window int

Window size (>= 2).

required

Returns:

Type Description
list[float | None]

A list the same length as values, with None for indices that cannot be

list[float | None]

computed due to insufficient history or missing inputs.

summarize_window(runs, *, start_date, end_date)

Summarize coins/hour metrics for a date window.

Parameters:

Name Type Description Default
runs Iterable[RunAnalysis]

Per-run analysis results.

required
start_date date

Window start date (inclusive).

required
end_date date

Window end date (inclusive).

required

Returns:

Type Description
WindowSummary

WindowSummary including run count and average coins/hour.

analysis.battle_report_extract

Battle Report value extraction for canonical Phase 6 metrics.

This module extracts additional observed values from raw Battle Report text. It intentionally stays within the analysis layer: - pure (no Django imports), - deterministic and testable, - defensive on unknown labels (missing labels return None unless a caller chooses a default).

_LABEL_SEPARATOR = '(?:[ \\t]*:[ \\t]*|\\t+[ \\t]*|[ \\t]{2,})' module-attribute

_LABEL_VALUE_RE = re.compile(f'(?im)^[ \t]*(?P<label>.+?){_LABEL_SEPARATOR}(?P<value>.*?)[ \t]*$') module-attribute

ExtractedNumber dataclass

Extracted numeric value from a Battle Report line.

Parameters:

Name Type Description Default
raw_value str

Raw value string from the report.

required
value float

Parsed numeric value as a float (unit-normalized).

required

UnitContract dataclass

Contract describing the expected unit type for a parsed value.

Parameters:

Name Type Description Default
unit_type UnitType

Expected UnitType for the value.

required
allow_zero bool

Whether a numeric zero is considered valid.

True

UnitType

Bases: Enum

Supported unit categories for Phase 1.5.

UnitValidationError

Bases: ValueError

Raised when a quantity does not satisfy a unit contract.

__init__(*, raw_value, expected, actual)

Initialize the error.

Parameters:

Name Type Description Default
raw_value str

Original raw input.

required
expected UnitType

Expected UnitType contract.

required
actual UnitType

Parsed UnitType from the raw value.

required

_normalize_label(label)

Normalize labels for dictionary lookup.

Parameters:

Name Type Description Default
label str

Raw label text.

required

Returns:

Type Description
str

Normalized label key suitable for dictionary matching.

extract_label_values(raw_text) cached

Extract normalized label/value pairs from raw Battle Report text.

Parameters:

Name Type Description Default
raw_text str

Raw Battle Report text.

required

Returns:

Type Description
dict[str, str]

Mapping of normalized label -> raw value string.

extract_numeric_value(raw_text, *, label, unit_type)

Extract and parse a numeric value for a specific Battle Report label.

Parameters:

Name Type Description Default
raw_text str

Raw Battle Report text.

required
label str

Exact label as shown in Battle Reports.

required
unit_type UnitType

Expected unit type for strict validation.

required

Returns:

Type Description
ExtractedNumber | None

ExtractedNumber when the label is present and parseable; otherwise None.

Notes

The parsing rules come from analysis.quantity.parse_quantity. This wrapper additionally enforces that the raw string cannot represent a different unit type (e.g. 15% for a coins metric).

parse_validated_quantity(raw_value, *, contract)

Parse and validate a quantity string against a strict unit contract.

Parameters:

Name Type Description Default
raw_value str

Raw Battle Report value (e.g. 7.67M, $55.90M, x1.15, 15%).

required
contract UnitContract

UnitContract describing the expected unit type.

required

Returns:

Type Description
ValidatedQuantity

ValidatedQuantity with a non-None Decimal value.

Raises:

Type Description
UnitValidationError

When the parsed unit type does not match the contract.

ValueError

When the value cannot be parsed into a numeric Decimal.

analysis.categories

Shared metric category definitions.

MetricCategory is a semantic classification (not purely visual) used to ensure new metrics are registered consistently and scoped predictably.

MetricCategory

Bases: StrEnum

Semantic category for a metric.

Values are stable identifiers used across the analysis registry and UI.

analysis.chart_config_dto

DTO schema for Phase 7 Chart Builder configurations.

The Chart Builder emits a constrained configuration that is: - schema-driven (no free-form expressions), - serializable for snapshots, - validated before execution, - consumed by the analysis layer to produce chart-ready DTO outputs.

AggregationMode = Literal['sum', 'avg'] module-attribute

ChartType = Literal['line', 'bar', 'area', 'scatter', 'donut'] module-attribute

ComparisonMode = Literal['none', 'before_after', 'run_vs_run'] module-attribute

GroupBy = Literal['time', 'tier', 'preset'] module-attribute

SmoothingMode = Literal['none', 'rolling_avg'] module-attribute

XAxisMode = Literal['time', 'metric'] module-attribute

ChartConfigDTO dataclass

Constrained chart configuration produced by the Chart Builder.

Parameters:

Name Type Description Default
metrics tuple[str, ...]

One or more MetricSeries keys.

required
chart_type ChartType

Visualization type.

required
group_by GroupBy

Grouping selection for splitting datasets.

required
comparison ComparisonMode

Optional two-scope comparison mode.

required
smoothing SmoothingMode

Optional smoothing mode (rolling average).

required
aggregation AggregationMode | None

Optional aggregation override ("sum" or "avg").

None
context ChartContextDTO

Context filters used when producing the chart.

required
scopes tuple[ChartScopeDTO, ChartScopeDTO] | None

Exactly two scopes when comparison != "none".

None
x_axis XAxisMode

X-axis mode ("time" or metric-vs-metric).

'time'
version str

DTO version for forwards-compatible snapshot decoding.

'phase7_chart_config_v1'

ChartContextDTO dataclass

Context filters attached to a chart configuration.

Parameters:

Name Type Description Default
start_date date | None

Optional inclusive lower bound date.

required
end_date date | None

Optional inclusive upper bound date.

required
tier int | None

Optional tier filter.

None
tournament_filter str | None

Optional tournament filter ("all" or specific rank key).

None
preset_id int | None

Optional preset id filter.

None
excluded_preset_ids tuple[int, ...]

Preset ids to exclude from the scope.

()
include_tournaments bool

Whether tournament runs are included in the scope.

False
include_hidden bool

Whether hidden Battle Reports are included in the scope.

False
patch_boundaries tuple[date, ...]

Patch boundary dates used to define included windows.

()

ChartScopeDTO dataclass

A scope used by two-scope chart comparisons.

Parameters:

Name Type Description Default
label str

Display label for the scope.

required
run_id int | None

Optional BattleReport id used for run-vs-run comparisons.

None
start_date date | None

Optional inclusive start date used for before/after comparisons.

None
end_date date | None

Optional inclusive end date used for before/after comparisons.

None

analysis.chart_config_engine

Chart execution for Phase 7 ChartConfigDTO.

This module consumes ChartConfigDTO and produces deterministic DTO outputs that the UI can render without performing calculations inline.

ChartConfigDTO dataclass

Constrained chart configuration produced by the Chart Builder.

Parameters:

Name Type Description Default
metrics tuple[str, ...]

One or more MetricSeries keys.

required
chart_type ChartType

Visualization type.

required
group_by GroupBy

Grouping selection for splitting datasets.

required
comparison ComparisonMode

Optional two-scope comparison mode.

required
smoothing SmoothingMode

Optional smoothing mode (rolling average).

required
aggregation AggregationMode | None

Optional aggregation override ("sum" or "avg").

None
context ChartContextDTO

Context filters used when producing the chart.

required
scopes tuple[ChartScopeDTO, ChartScopeDTO] | None

Exactly two scopes when comparison != "none".

None
x_axis XAxisMode

X-axis mode ("time" or metric-vs-metric).

'time'
version str

DTO version for forwards-compatible snapshot decoding.

'phase7_chart_config_v1'

ChartDataDTO dataclass

Chart output DTO produced from a ChartConfigDTO.

Parameters:

Name Type Description Default
labels list[str]

ISO date labels for time-based charts.

required
datasets list[ChartDatasetDTO]

Datasets aligned to labels or metric-vs-metric points.

required
chart_type str

Config chart type (line/bar/area/scatter/donut).

required
x_axis str

X-axis mode ("time" or "metric").

'time'
x_label str | None

X-axis label for metric-vs-metric charts.

None
x_unit str | None

X-axis unit for metric-vs-metric charts.

None
y_label str | None

Y-axis label for metric-vs-metric charts.

None
y_unit str | None

Y-axis unit for metric-vs-metric charts.

None
run_ids list[int | None] | None

Run id list aligned to metric-vs-metric points.

None

ChartDatasetDTO dataclass

A chart dataset produced from a ChartConfigDTO.

Parameters:

Name Type Description Default
label str

Dataset label shown in legends.

required
metric_key str

MetricSeries key used for the dataset.

required
unit str

Display unit string.

required
values list[float | None] | list[dict[str, float | int | None]]

Values aligned to ChartDataDTO.labels or metric-vs-metric points.

required
run_counts list[int]

Count of non-null contributing points aligned to labels.

required
scope_label str | None

Optional scope label when produced from a two-scope comparison.

None

MetricPoint dataclass

A per-run metric point for charting.

Attributes:

Name Type Description
run_id int | None

Optional identifier for the underlying persisted record.

battle_date datetime

Timestamp used as the x-axis.

tier int | None

Optional tier value when available.

preset_name str | None

Optional preset label when available.

value float | None

Metric value, or None when inputs are missing.

MetricSeriesRegistry

Lookup and validation helpers for metric series definitions.

__init__(specs)

Initialize a registry from a collection of specs.

formula_metric_keys(formula)

Return metric keys referenced by a derived formula.

Prefer inspect_formula when validation needs unknown identifier detection and safety guarantees.

get(key)

Return a spec for a metric key, or None when missing.

inspect_formula(formula)

Inspect a derived-metric formula for identifiers and safety.

The formula language matches analysis.derived_formula.evaluate_formula: constants, metric-key identifiers, unary +/- and binary + - * / only.

Parameters:

Name Type Description Default
formula str

An expression containing metric keys as variable names.

required

Returns:

Type Description
FormulaInspection

FormulaInspection describing referenced/unknown identifiers and whether

FormulaInspection

the expression is syntactically valid and safe.

list()

Return all specs in a stable order.

_aggregate_points(points, labels, *, aggregation)

Aggregate points into daily series aligned to label dates.

_analyze_donut(records, *, config, registry)

Analyze a donut chart by aggregating selected metrics across records.

_analyze_metric_axis(records, *, config, registry)

Analyze a metric-vs-metric chart configuration into point datasets.

Parameters:

Name Type Description Default
records Iterable[object]

Iterable/QuerySet of battle report records already filtered by context.

required
config ChartConfigDTO

ChartConfigDTO to execute (metric-vs-metric mode).

required
registry MetricSeriesRegistry

MetricSeriesRegistry for labels/units.

required

Returns:

Type Description
ChartDataDTO

ChartDataDTO with x/y labels and point datasets.

_group_points(points, *, config)

Group points according to config grouping/comparison settings.

_iterable_has_any(records)

Return True when an iterable contains at least one record.

analyze_chart_config_dto(records, *, config, registry, moving_average_window, entity_selections)

Analyze a ChartConfigDTO into chart-ready datasets.

Parameters:

Name Type Description Default
records Iterable[object]

Iterable/QuerySet of battle report records already filtered by context.

required
config ChartConfigDTO

ChartConfigDTO to execute.

required
registry MetricSeriesRegistry

MetricSeriesRegistry for labels/units and aggregation semantics.

required
moving_average_window int | None

Optional rolling window for smoothing.

required
entity_selections dict[str, str | None]

Mapping for entity selections (unused for Phase 7 builder config).

required

Returns:

Type Description
ChartDataDTO

ChartDataDTO suitable for conversion into Chart.js data.

analyze_metric_series(records, *, metric_key, transform='none', context=None, entity_type=None, entity_name=None, monte_carlo_trials=None, monte_carlo_seed=None)

Analyze runs for a specific metric, returning a chart-friendly series.

Parameters:

Name Type Description Default
records Iterable[object]

An iterable of RunProgress-like objects, or GameData objects with a run_progress attribute.

required
metric_key str

Metric key to compute (observed or derived).

required
transform str

Optional transform to apply (e.g. "rate_per_hour").

'none'
context PlayerContextInput | None

Optional player context + selected parameter tables.

None
entity_type str | None

Optional entity category for entity-scoped derived metrics (e.g. "ultimate_weapon", "guardian_chip", "bot").

None
entity_name str | None

Optional entity name for entity-scoped derived metrics.

None
monte_carlo_trials int | None

Optional override for Monte Carlo trial count used by simulated EV metrics.

None
monte_carlo_seed int | None

Optional override for the Monte Carlo RNG seed.

None

Returns:

Type Description
MetricSeriesResult

MetricSeriesResult with per-run points and transparent metadata about

MetricSeriesResult

used parameters/assumptions.

Notes

Records missing a battle_date are skipped. Other missing fields do not raise; values become None instead.

simple_moving_average(values, *, window)

Compute a simple moving average over a numeric series.

Parameters:

Name Type Description Default
values Sequence[float | None]

A sequence of values aligned to chart labels (None for missing).

required
window int

Window size (>= 2).

required

Returns:

Type Description
list[float | None]

A list the same length as values, with None for indices that cannot be

list[float | None]

computed due to insufficient history or missing inputs.

analysis.chart_config_validator

Validation for Phase 7 ChartConfigDTO values.

ChartConfigDTO dataclass

Constrained chart configuration produced by the Chart Builder.

Parameters:

Name Type Description Default
metrics tuple[str, ...]

One or more MetricSeries keys.

required
chart_type ChartType

Visualization type.

required
group_by GroupBy

Grouping selection for splitting datasets.

required
comparison ComparisonMode

Optional two-scope comparison mode.

required
smoothing SmoothingMode

Optional smoothing mode (rolling average).

required
aggregation AggregationMode | None

Optional aggregation override ("sum" or "avg").

None
context ChartContextDTO

Context filters used when producing the chart.

required
scopes tuple[ChartScopeDTO, ChartScopeDTO] | None

Exactly two scopes when comparison != "none".

None
x_axis XAxisMode

X-axis mode ("time" or metric-vs-metric).

'time'
version str

DTO version for forwards-compatible snapshot decoding.

'phase7_chart_config_v1'

ChartConfigValidationResult dataclass

Validation result for ChartConfigDTO.

Parameters:

Name Type Description Default
is_valid bool

True when no errors exist.

required
errors tuple[str, ...]

Fatal validation errors.

()
warnings tuple[str, ...]

Non-fatal warnings intended for UI display.

()

MetricSeriesRegistry

Lookup and validation helpers for metric series definitions.

__init__(specs)

Initialize a registry from a collection of specs.

formula_metric_keys(formula)

Return metric keys referenced by a derived formula.

Prefer inspect_formula when validation needs unknown identifier detection and safety guarantees.

get(key)

Return a spec for a metric key, or None when missing.

inspect_formula(formula)

Inspect a derived-metric formula for identifiers and safety.

The formula language matches analysis.derived_formula.evaluate_formula: constants, metric-key identifiers, unary +/- and binary + - * / only.

Parameters:

Name Type Description Default
formula str

An expression containing metric keys as variable names.

required

Returns:

Type Description
FormulaInspection

FormulaInspection describing referenced/unknown identifiers and whether

FormulaInspection

the expression is syntactically valid and safe.

list()

Return all specs in a stable order.

allowed_chart_builder_aggregations(spec)

Return the allowed aggregations for Chart Builder selections.

Parameters:

Name Type Description Default
spec MetricSeriesSpec

MetricSeriesSpec to evaluate.

required

Returns:

Type Description
tuple[Aggregation, ...]

Tuple of allowed aggregation keys for Chart Builder use.

validate_chart_config_dto(config, *, registry)

Validate a ChartConfigDTO against the MetricSeries registry.

Parameters:

Name Type Description Default
config ChartConfigDTO

ChartConfigDTO from the Chart Builder.

required
registry MetricSeriesRegistry

MetricSeriesRegistry for unit/category/transform capability checks.

required

Returns:

Type Description
ChartConfigValidationResult

ChartConfigValidationResult containing errors and warnings.

analysis.context

DTOs for passing player context and parameter tables into analysis.

The Analysis Engine must remain pure: no Django imports, no database access, and no side effects. The core app is responsible for building these DTOs from ORM models.

ParameterInput dataclass

A single parameter value selected for analysis.

Parameters:

Name Type Description Default
key str

Stable parameter key (caller-defined).

required
raw_value str

Raw string as captured from wiki or user input.

required
parsed Quantity

Best-effort parsed quantity containing normalized value.

required
wiki_revision_id int | None

Optional core.WikiData primary key representing the immutable revision of the source table row used for this parameter.

required

PlayerBotInput dataclass

Player bot context for analysis computations.

PlayerCardInput dataclass

Player card context for analysis computations.

PlayerContextInput dataclass

Container for all player context provided to analysis.

The Analysis Engine must handle missing context gracefully. Callers may pass None instead of a PlayerContextInput when no player state is available.

PlayerGuardianChipInput dataclass

Player guardian chip context for analysis computations.

PlayerUltimateWeaponInput dataclass

Player ultimate weapon context for analysis computations.

Quantity dataclass

A parsed quantity with both raw and normalized representations.

Attributes:

Name Type Description
raw_value str

The original raw string value (trimmed).

normalized_value Decimal | None

The parsed numeric value as a Decimal, or None if the value could not be parsed.

magnitude str | None

The compact magnitude suffix (e.g. k, m, b, t, q), or None when not applicable.

unit_type UnitType

The category of unit this value represents.

analysis.deltas

Delta calculations for the Analysis Engine.

This module computes deterministic differences between two values. Deltas are computed on-demand and are never persisted.

MetricDelta dataclass

A deterministic delta between two numeric metric values.

Attributes:

Name Type Description
baseline float

Baseline value (A).

comparison float

Comparison value (B).

absolute float

comparison - baseline.

percent float | None

(comparison - baseline) / baseline, or None when baseline is 0.

delta(baseline, comparison)

Compute absolute and percentage delta between two values.

Parameters:

Name Type Description Default
baseline float

Baseline value (A).

required
comparison float

Comparison value (B).

required

Returns:

Type Description
MetricDelta

MetricDelta with absolute and percentage changes. Percentage delta is

MetricDelta

None when the baseline is 0.

analysis.derived

Derived metric helpers based on parameterized game mechanics.

This module contains deterministic, testable computations that combine observations (run data) with parameterized effects (wiki-derived tables and player context). It must remain pure and defensive: callers should receive partial results instead of exceptions when inputs are missing.

MonteCarloConfig dataclass

Configuration for deterministic Monte Carlo computations.

Parameters:

Name Type Description Default
trials int

Number of simulated trials (must be > 0).

required
seed int

RNG seed used to ensure deterministic results in tests and UI.

required

apply_multiplier(value, *, multiplier)

Apply a multiplier to a numeric value.

Parameters:

Name Type Description Default
value float | None

Baseline value.

required
multiplier float | None

Multiplier factor.

required

Returns:

Type Description
float | None

value * multiplier, or None if inputs are missing/invalid.

effective_cooldown_seconds(*, base_seconds, reduction_fractions)

Compute an effective cooldown in seconds after reductions.

Formula (assumption): effective = base_seconds * (1 - sum(reduction_fractions))

Reductions are treated as additive fractions (e.g. 10% -> 0.10). The result is clamped to >= 0.

Parameters:

Name Type Description Default
base_seconds float | None

Base cooldown in seconds.

required
reduction_fractions tuple[float, ...]

Sequence of fractional reductions (0..1).

required

Returns:

Type Description
float | None

Effective cooldown in seconds, or None if base is missing/invalid.

expected_multiplier_bernoulli(*, proc_chance, proc_multiplier)

Compute the expected multiplier for a Bernoulli proc.

Parameters:

Name Type Description Default
proc_chance float | None

Probability of proc in [0, 1].

required
proc_multiplier float | None

Multiplier applied on proc (>= 0).

required

Returns:

Type Description
float | None

Expected multiplier E[M] = (1-p)1 + pm, or None if inputs are invalid.

monte_carlo_expected_multiplier_bernoulli(*, proc_chance, proc_multiplier, config)

Estimate expected multiplier via deterministic Monte Carlo simulation.

Parameters:

Name Type Description Default
proc_chance float | None

Probability of proc in [0, 1].

required
proc_multiplier float | None

Multiplier applied on proc (>= 0).

required
config MonteCarloConfig

MonteCarloConfig controlling trial count and RNG seed.

required

Returns:

Type Description
float | None

Estimated expected multiplier, or None if inputs are invalid.

analysis.derived_formula

Safe evaluation for simple derived-metric formulas.

Derived chart configs reference base metric keys in a small expression language. This module evaluates those expressions safely (no attribute access, no calls, no comprehensions) and returns None when inputs are missing.

_eval_node(node, variables)

Recursively evaluate an AST node with strict safety rules.

evaluate_formula(formula, variables)

Evaluate a derived-metric formula using provided variables.

Parameters:

Name Type Description Default
formula str

Expression referencing metric keys as identifiers (e.g. "a / b").

required
variables Mapping[str, float | None]

Mapping from identifier name to numeric value (or None).

required

Returns:

Type Description
float | None

The computed float value, or None when inputs are missing/invalid.

analysis.dto

DTO types returned by the Analysis Engine.

DTOs are plain data containers used to transport analysis results to the UI. They intentionally avoid any Django/ORM dependencies.

AnalysisResult dataclass

Container for analysis results.

Attributes:

Name Type Description
runs tuple['RunAnalysis', ...]

Per-run analysis results.

MetricCategory

Bases: StrEnum

Semantic category for a metric.

Values are stable identifiers used across the analysis registry and UI.

MetricDefinition dataclass

Definition for an observed or derived metric.

Attributes:

Name Type Description
key str

Stable metric key used by UI selection and charting.

label str

Human-friendly label.

unit str

Display unit string (e.g. "coins/hour", "seconds").

category MetricCategory

Semantic category used for filtering and validation.

kind str

Either "observed" or "derived".

MetricDelta dataclass

A deterministic delta between two numeric metric values.

Attributes:

Name Type Description
baseline float

Baseline value (A).

comparison float

Comparison value (B).

absolute float

comparison - baseline.

percent float | None

(comparison - baseline) / baseline, or None when baseline is 0.

MetricPoint dataclass

A per-run metric point for charting.

Attributes:

Name Type Description
run_id int | None

Optional identifier for the underlying persisted record.

battle_date datetime

Timestamp used as the x-axis.

tier int | None

Optional tier value when available.

preset_name str | None

Optional preset label when available.

value float | None

Metric value, or None when inputs are missing.

MetricSeriesResult dataclass

A computed time series for a selected metric.

Attributes:

Name Type Description
metric MetricDefinition

MetricDefinition describing the series.

points tuple[MetricPoint, ...]

Per-run metric points.

used_parameters tuple[UsedParameter, ...]

Parameters referenced (for derived metrics and UI transparency).

assumptions tuple[str, ...]

Human-readable, non-prescriptive notes about formulas/policies.

RunAnalysis dataclass

Per-run analysis result.

Attributes:

Name Type Description
run_id int | None

Optional identifier for the underlying persisted record.

battle_date datetime

The battle date used as a time-series x-axis.

tier int | None

Optional tier value when available on the input.

preset_name str | None

Optional preset label when available on the input.

coins_per_hour float

Derived rate metric for Phase 1 charts.

RunProgressInput dataclass

Minimal run-progress input used by Phase 1 analysis.

Attributes:

Name Type Description
battle_date datetime

The battle date to use as a time-series x-axis.

coins int | None

Total coins earned for the run.

wave int

Final wave reached.

real_time_seconds int

Run duration (seconds).

UsedParameter dataclass

A parameter value referenced during derived metric computation.

Attributes:

Name Type Description
entity_type str

High-level entity type (card, ultimate_weapon, guardian_chip, bot).

entity_name str

Human-friendly entity name.

key str

Parameter key used by the computation.

raw_value str

Raw string as stored.

normalized_value float | None

Best-effort normalized float value, if parseable.

wiki_revision_id int | None

Optional wiki revision id (core.WikiData pk) used.

WindowSummary dataclass

A summarized view of runs within a date window.

Attributes:

Name Type Description
start_date date

Window start date (inclusive).

end_date date

Window end date (inclusive).

run_count int

Number of runs included in the window.

average_coins_per_hour float | None

Average coins/hour across runs, if any.

analysis.effects

Parameterized effects for wiki-derived entities.

Effects are deterministic computations that transform a selected entity's raw wiki parameters into derived numeric metrics. Effects must remain pure and defensive: missing inputs should yield partial outputs (None values) rather than exceptions.

EffectResult dataclass

Result container for an effect computation.

Parameters:

Name Type Description Default
value float | None

Derived numeric value for the effect, or None when inputs are missing/invalid.

required
used_parameters tuple[UsedParameter, ...]

Parameters referenced during evaluation (for UI transparency).

required

ParameterInput dataclass

A single parameter value selected for analysis.

Parameters:

Name Type Description Default
key str

Stable parameter key (caller-defined).

required
raw_value str

Raw string as captured from wiki or user input.

required
parsed Quantity

Best-effort parsed quantity containing normalized value.

required
wiki_revision_id int | None

Optional core.WikiData primary key representing the immutable revision of the source table row used for this parameter.

required

UsedParameter dataclass

A parameter value referenced during derived metric computation.

Attributes:

Name Type Description
entity_type str

High-level entity type (card, ultimate_weapon, guardian_chip, bot).

entity_name str

Human-friendly entity name.

key str

Parameter key used by the computation.

raw_value str

Raw string as stored.

normalized_value float | None

Best-effort normalized float value, if parseable.

wiki_revision_id int | None

Optional wiki revision id (core.WikiData pk) used.

_float_or_none(value)

Convert a Decimal to float, returning None when missing.

_uptime_percent(*, duration_seconds, cooldown_seconds)

Return uptime percent for (duration, cooldown) inputs.

_used(*, entity_type, entity_name, param)

Build a UsedParameter record for a parameter input.

activations_per_minute_from_parameters(*, entity_type, entity_name, parameters, cooldown_key='cooldown')

Compute activations/minute from a cooldown parameter.

Formula

activations_per_minute = 60 / cooldown_seconds

Parameters:

Name Type Description Default
entity_type str

Entity category label (e.g. "guardian_chip").

required
entity_name str

Human-readable entity name for trace output.

required
parameters tuple[ParameterInput, ...]

ParameterInput entries (raw + parsed values).

required
cooldown_key str

Parameter key used for cooldown seconds.

'cooldown'

Returns:

Type Description
EffectResult

EffectResult with activations/minute and referenced parameters.

effective_cooldown_seconds_from_parameters(*, entity_type, entity_name, parameters, cooldown_key='cooldown')

Compute an effective cooldown in seconds from a cooldown parameter.

This is a validation-focused effect used to prove that: - wiki-derived parameter revisions are referenced by analysis at request time, and - derived metrics can be charted alongside observed metrics with explicit units.

Formula

effective_cooldown_seconds = cooldown_seconds

Parameters:

Name Type Description Default
entity_type str

Entity category label (e.g. "ultimate_weapon").

required
entity_name str

Human-readable entity name for trace output.

required
parameters tuple[ParameterInput, ...]

ParameterInput entries (raw + parsed values).

required
cooldown_key str

Parameter key used for cooldown seconds.

'cooldown'

Returns:

Type Description
EffectResult

EffectResult with effective cooldown seconds and referenced parameters.

uptime_percent_from_parameters(*, entity_type, entity_name, parameters, duration_key='duration', cooldown_key='cooldown')

Compute uptime percent from duration and cooldown parameters.

Formula

uptime_percent = 100 * clamp(duration_seconds / cooldown_seconds, 0..1)

Parameters:

Name Type Description Default
entity_type str

Entity category label (e.g. "ultimate_weapon", "bot").

required
entity_name str

Human-readable entity name for trace output.

required
parameters tuple[ParameterInput, ...]

ParameterInput entries (raw + parsed values).

required
duration_key str

Parameter key used for duration seconds.

'duration'
cooldown_key str

Parameter key used for cooldown seconds.

'cooldown'

Returns:

Type Description
EffectResult

EffectResult with uptime percent and referenced parameters.

analysis.engine

Orchestration entry points for the Analysis Engine.

The Analysis Engine is a pure, non-Django module that accepts in-memory inputs and returns DTOs. It must not import Django or perform database writes.

_COINS_LINE_RE = re.compile(f'(?im)^[ \t]*(?:Coins|Coins Earned){_LABEL_SEPARATOR}([0-9][0-9,]*(?:\.[0-9]+)?[kmbtq]?)\b[ \t]*.*$') module-attribute

_LABEL_SEPARATOR = '(?:[ \\t]*:[ \\t]*|\\t+[ \\t]*|[ \\t]{2,})' module-attribute

AnalysisResult dataclass

Container for analysis results.

Attributes:

Name Type Description
runs tuple['RunAnalysis', ...]

Per-run analysis results.

MetricComputeConfig dataclass

Configuration for metric computations.

Parameters:

Name Type Description Default
monte_carlo MonteCarloConfig | None

Optional MonteCarloConfig for metrics that use simulation.

None

MetricPoint dataclass

A per-run metric point for charting.

Attributes:

Name Type Description
run_id int | None

Optional identifier for the underlying persisted record.

battle_date datetime

Timestamp used as the x-axis.

tier int | None

Optional tier value when available.

preset_name str | None

Optional preset label when available.

value float | None

Metric value, or None when inputs are missing.

MetricSeriesResult dataclass

A computed time series for a selected metric.

Attributes:

Name Type Description
metric MetricDefinition

MetricDefinition describing the series.

points tuple[MetricPoint, ...]

Per-run metric points.

used_parameters tuple[UsedParameter, ...]

Parameters referenced (for derived metrics and UI transparency).

assumptions tuple[str, ...]

Human-readable, non-prescriptive notes about formulas/policies.

MonteCarloConfig dataclass

Configuration for deterministic Monte Carlo computations.

Parameters:

Name Type Description Default
trials int

Number of simulated trials (must be > 0).

required
seed int

RNG seed used to ensure deterministic results in tests and UI.

required

PlayerContextInput dataclass

Container for all player context provided to analysis.

The Analysis Engine must handle missing context gracefully. Callers may pass None instead of a PlayerContextInput when no player state is available.

RunAnalysis dataclass

Per-run analysis result.

Attributes:

Name Type Description
run_id int | None

Optional identifier for the underlying persisted record.

battle_date datetime

The battle date used as a time-series x-axis.

tier int | None

Optional tier value when available on the input.

preset_name str | None

Optional preset label when available on the input.

coins_per_hour float

Derived rate metric for Phase 1 charts.

UnitContract dataclass

Contract describing the expected unit type for a parsed value.

Parameters:

Name Type Description Default
unit_type UnitType

Expected UnitType for the value.

required
allow_zero bool

Whether a numeric zero is considered valid.

True

UnitType

Bases: Enum

Supported unit categories for Phase 1.5.

UnitValidationError

Bases: ValueError

Raised when a quantity does not satisfy a unit contract.

__init__(*, raw_value, expected, actual)

Initialize the error.

Parameters:

Name Type Description Default
raw_value str

Original raw input.

required
expected UnitType

Expected UnitType contract.

required
actual UnitType

Parsed UnitType from the raw value.

required

UsedParameter dataclass

A parameter value referenced during derived metric computation.

Attributes:

Name Type Description
entity_type str

High-level entity type (card, ultimate_weapon, guardian_chip, bot).

entity_name str

Human-friendly entity name.

key str

Parameter key used by the computation.

raw_value str

Raw string as stored.

normalized_value float | None

Best-effort normalized float value, if parseable.

wiki_revision_id int | None

Optional wiki revision id (core.WikiData pk) used.

_RunProgressLike

Bases: Protocol

Protocol for Phase 1 run-progress inputs (duck-typed).

_coerce_datetime(value)

Coerce an object into a datetime when safe.

_coerce_int(value)

Coerce an object into an int when safe.

_coins_from_raw_text(raw_text)

Extract total coins from raw Battle Report text (best-effort).

_looks_like_run_progress(obj)

Return True if an object exposes the Phase 1 RunProgress interface.

_preset_name_from_progress(progress)

Extract an optional preset name from a run-progress-like object.

analyze_metric_series(records, *, metric_key, transform='none', context=None, entity_type=None, entity_name=None, monte_carlo_trials=None, monte_carlo_seed=None)

Analyze runs for a specific metric, returning a chart-friendly series.

Parameters:

Name Type Description Default
records Iterable[object]

An iterable of RunProgress-like objects, or GameData objects with a run_progress attribute.

required
metric_key str

Metric key to compute (observed or derived).

required
transform str

Optional transform to apply (e.g. "rate_per_hour").

'none'
context PlayerContextInput | None

Optional player context + selected parameter tables.

None
entity_type str | None

Optional entity category for entity-scoped derived metrics (e.g. "ultimate_weapon", "guardian_chip", "bot").

None
entity_name str | None

Optional entity name for entity-scoped derived metrics.

None
monte_carlo_trials int | None

Optional override for Monte Carlo trial count used by simulated EV metrics.

None
monte_carlo_seed int | None

Optional override for the Monte Carlo RNG seed.

None

Returns:

Type Description
MetricSeriesResult

MetricSeriesResult with per-run points and transparent metadata about

MetricSeriesResult

used parameters/assumptions.

Notes

Records missing a battle_date are skipped. Other missing fields do not raise; values become None instead.

analyze_runs(records)

Analyze runs and return rate metrics (Phase 1).

Parameters:

Name Type Description Default
records Iterable[object]

An iterable of RunProgress-like objects, or GameData objects with a run_progress attribute.

required

Returns:

Type Description
AnalysisResult

AnalysisResult containing a per-run coins-per-hour series.

Notes

Any record missing required fields is skipped. If no records contain the required data, an empty result is returned.

coins_per_hour(coins, real_time_seconds)

Compute coins per hour for a single run.

Parameters:

Name Type Description Default
coins int

Total coins earned.

required
real_time_seconds int

Run duration in seconds.

required

Returns:

Type Description
float | None

Coins per hour, or None when inputs are invalid.

compute_metric_value(metric_key, *, record, coins, cash, interest_earned, cells, reroll_shards, wave, real_time_seconds, context, entity_type, entity_name, config)

Compute a metric value and return used parameters + assumptions.

Parameters:

Name Type Description Default
metric_key str

Metric key to compute.

required
record object

Run-like object used for relationship-based metrics (e.g. usage presence).

required
coins int | None

Observed coins for the run.

required
cash int | None

Observed cash for the run.

required
interest_earned int | None

Observed interest earned for the run.

required
cells int | None

Observed cells for the run.

required
reroll_shards int | None

Observed reroll shards for the run.

required
wave int | None

Observed wave reached for the run.

required
real_time_seconds int | None

Observed duration seconds for the run.

required
context PlayerContextInput | None

Optional player context + selected parameters.

required
entity_type str | None

Optional entity category for entity-scoped derived metrics.

required
entity_name str | None

Optional entity name for entity-scoped derived metrics.

required
config MetricComputeConfig

MetricComputeConfig.

required

Returns:

Type Description
tuple[float | None, tuple[UsedParameter, ...], tuple[str, ...]]

Tuple of (value, used_parameters, assumptions).

get_metric_definition(metric_key)

Return a MetricDefinition for a key, defaulting to observed coins/hour.

parse_validated_quantity(raw_value, *, contract)

Parse and validate a quantity string against a strict unit contract.

Parameters:

Name Type Description Default
raw_value str

Raw Battle Report value (e.g. 7.67M, $55.90M, x1.15, 15%).

required
contract UnitContract

UnitContract describing the expected unit type.

required

Returns:

Type Description
ValidatedQuantity

ValidatedQuantity with a non-None Decimal value.

Raises:

Type Description
UnitValidationError

When the parsed unit type does not match the contract.

ValueError

When the value cannot be parsed into a numeric Decimal.

analysis.event_windows

Event-window helpers for charts and dashboards.

The Tower in-game Events run in fixed 14-day windows. This module provides pure helpers (no Django imports) to compute and shift those windows deterministically.

DEFAULT_EVENT_WINDOW_ANCHOR = date(2025, 12, 9) module-attribute

EVENT_WINDOW_DAYS = 14 module-attribute

EventWindow dataclass

A 14-day inclusive date window used by in-game Events.

Attributes:

Name Type Description
start date

Inclusive window start date.

end date

Inclusive window end date.

coerce_window_bounds(*, start, end, window_days=EVENT_WINDOW_DAYS)

Coerce partial start/end inputs into a full inclusive window.

This helper is used by event navigation controls so that shifting always has both a start and an end.

Parameters:

Name Type Description Default
start date | None

Optional inclusive start date.

required
end date | None

Optional inclusive end date.

required
window_days int

Window size in days (defaults to 14).

EVENT_WINDOW_DAYS

Returns:

Type Description
EventWindow

EventWindow whose bounds match provided inputs when possible.

current_event_window(*, target=None, anchor=DEFAULT_EVENT_WINDOW_ANCHOR, window_days=EVENT_WINDOW_DAYS)

Return the current Event window using the shared app anchor date.

Parameters:

Name Type Description Default
target date | None

Date to evaluate. Defaults to today's date when omitted.

None
anchor date

Known Event start date used as the shared stepping origin.

DEFAULT_EVENT_WINDOW_ANCHOR
window_days int

Window size in days (defaults to 14).

EVENT_WINDOW_DAYS

Returns:

Type Description
EventWindow

EventWindow containing the target date.

event_window_for_date(*, target, anchor, window_days=EVENT_WINDOW_DAYS)

Return the Event window containing a target date.

Parameters:

Name Type Description Default
target date

The date to place into an Event window.

required
anchor date

A known Event start date (inclusive) used as the stepping origin.

required
window_days int

Window size in days (defaults to 14).

EVENT_WINDOW_DAYS

Returns:

Type Description
EventWindow

EventWindow containing target, with an inclusive end date.

shift_event_window(window, *, shift, window_days=EVENT_WINDOW_DAYS)

Shift an Event window forward/backward by N windows.

Parameters:

Name Type Description Default
window EventWindow

The base EventWindow to shift.

required
shift int

Number of windows to shift; negative means previous.

required
window_days int

Window size in days (defaults to 14).

EVENT_WINDOW_DAYS

Returns:

Type Description
EventWindow

Shifted EventWindow.

analysis.goals

Goal-oriented cost computations for upgradeable parameters.

This module is intentionally pure (no Django imports) so it can be unit-tested and reused across views without database coupling.

GoalCostBreakdown dataclass

Computed remaining cost and optional per-level breakdown for a goal.

PerLevelCost dataclass

A single upgrade step cost from from_level to to_level.

compute_goal_cost_breakdown(*, costs_by_level, currency, current_level_display, current_level_for_calc, current_is_assumed, target_level)

Compute remaining upgrade cost and per-level breakdown.

Cost rows are expected to be keyed by the resulting level: the cost at level N is the price to upgrade from N-1 -> N (matching typical wiki tables).

Parameters:

Name Type Description Default
costs_by_level dict[int, str]

Mapping of resulting level -> cost_raw.

required
currency str

Currency label (e.g. "stones", "medals", "bits").

required
current_level_display int

Level to display in UI (may be a fallback).

required
current_level_for_calc int

Level used for remaining-cost calculation.

required
current_is_assumed bool

Whether current level is a fallback value.

required
target_level int

Desired target level.

required

Returns:

Type Description
GoalCostBreakdown

GoalCostBreakdown with per-level costs and total remaining amount.

parse_cost_amount(*, cost_raw)

Parse an integer amount from a raw cost string.

Parameters:

Name Type Description Default
cost_raw str | None

Raw cost string (e.g. "50", "1,250", or "50 Medals").

required

Returns:

Type Description
int | None

Parsed integer amount when a number token is present, otherwise None.

analysis.metrics

Metric registry and computation helpers for the Analysis Engine.

This module centralizes the set of chartable metrics (observed and derived) and their labels/units so the UI can offer consistent selections.

DEFAULT_REGISTRY = MetricSeriesRegistry(specs=(MetricSeriesSpec(key='coins_earned', label='Coins earned', description=None, unit='coins', category=(MetricCategory.economy), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='coins_earned', allowed_transforms=(frozenset({'none', 'rate_per_hour', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='cash_earned', label='Cash earned', description=None, unit='cash', category=(MetricCategory.economy), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='cash_earned', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='interest_earned', label='Interest earned', description='Observed interest earned from Battle Reports.', unit='cash', category=(MetricCategory.economy), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='interest_earned', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='cells_earned', label='Cells earned', description=None, unit='cells', category=(MetricCategory.economy), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='cells_earned', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='reroll_shards_earned', label='Reroll shards earned', description=None, unit='shards', category=(MetricCategory.economy), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='reroll_shards_earned', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='reroll_dice_earned', label='Reroll dice earned', description='Alias for reroll shards earned (legacy naming).', unit='shards', category=(MetricCategory.economy), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='reroll_shards_earned', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='waves_reached', label='Waves reached', description=None, unit='waves', category=(MetricCategory.utility), kind='observed', source_model='BattleReport', aggregation='avg', time_index='timestamp', value_field='wave', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average', 'rate_per_hour'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='coins_per_wave', label='Coins per wave', description='Computed as coins earned divided by waves reached.', unit='coins/wave', category=(MetricCategory.economy), kind='derived', source_model='BattleReport', aggregation='avg', time_index='timestamp', value_field='coins_earned / wave', allowed_transforms=(frozenset({'none', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='coins_per_hour', label='Coins/hour', description='Observed coins earned divided by real time (hours).', unit='coins/hour', category=(MetricCategory.efficiency), kind='observed', source_model='BattleReport', aggregation='avg', time_index='timestamp', value_field='coins_per_hour', allowed_transforms=(frozenset({'none', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='cells_per_hour', label='Cells/hour', description='Observed cells earned divided by real time (hours).', unit='cells/hour', category=(MetricCategory.efficiency), kind='derived', source_model='BattleReport', aggregation='avg', time_index='timestamp', value_field='cells_earned / hours', allowed_transforms=(frozenset({'none', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='reroll_shards_per_hour', label='Reroll shards/hour', description='Observed reroll shards earned divided by real time (hours).', unit='shards/hour', category=(MetricCategory.efficiency), kind='derived', source_model='BattleReport', aggregation='avg', time_index='timestamp', value_field='reroll_shards_earned / hours', allowed_transforms=(frozenset({'none', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='real_time_hours', label='Run duration (hours)', description='Observed real-time duration converted to hours.', unit='hours', category=(MetricCategory.efficiency), kind='derived', source_model='BattleReport', aggregation='avg', time_index='timestamp', value_field='real_time_seconds / 3600', allowed_transforms=(frozenset({'none', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='waves_per_hour', label='Waves/hour', description='Observed waves reached divided by real time (hours).', unit='waves/hour', category=(MetricCategory.efficiency), kind='derived', source_model='BattleReport', aggregation='avg', time_index='timestamp', value_field='waves_reached / hours', allowed_transforms=(frozenset({'none', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='enemies_destroyed_total', label='Enemies destroyed (derived total)', description='Derived from per-type counts; ignores game-reported totals.', unit='enemies', category=(MetricCategory.enemy_destruction), kind='derived', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='sum(enemy_type_counts)', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average', 'rate_per_hour'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='enemies_destroyed_common', label='Enemies destroyed (common)', description='Derived from Basic, Fast, Ranged, Tank, and Protector counts.', unit='enemies', category=(MetricCategory.enemy_destruction), kind='derived', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='sum(common_enemy_counts)', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average', 'rate_per_hour'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='enemies_destroyed_elite', label='Enemies destroyed (elite)', description='Derived from Vampire, Ray, and Scatter counts.', unit='enemies', category=(MetricCategory.enemy_destruction), kind='derived', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='sum(elite_enemy_counts)', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average', 'rate_per_hour'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='enemies_destroyed_fleet', label='Enemies destroyed (fleet)', description='Derived from Saboteur, Commander, and Overcharge counts.', unit='enemies', category=(MetricCategory.enemy_destruction), kind='derived', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='sum(fleet_enemy_counts)', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average', 'rate_per_hour'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='enemies_destroyed_per_hour', label='Enemies destroyed/hour', description='Enemies destroyed (derived total) divided by real time (hours).', unit='enemies/hour', category=(MetricCategory.efficiency), kind='derived', source_model='BattleReport', aggregation='avg', time_index='timestamp', value_field='enemies_destroyed_total / hours', allowed_transforms=(frozenset({'none', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='damage_dealt', label='Damage dealt', description='Total damage dealt from Battle Reports.', unit='damage', category=(MetricCategory.damage), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='damage_dealt', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='projectiles_damage', label='Projectiles Damage', description=None, unit='damage', category=(MetricCategory.damage), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='projectiles_damage', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='thorn_damage', label='Thorn Damage', description=None, unit='damage', category=(MetricCategory.damage), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='thorn_damage', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='orb_damage', label='Orb Damage', description=None, unit='damage', category=(MetricCategory.damage), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='orb_damage', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='land_mine_damage', label='Land Mine Damage', description=None, unit='damage', category=(MetricCategory.damage), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='land_mine_damage', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='inner_land_mine_damage', label='Inner Land Mine Damage', description=None, unit='damage', category=(MetricCategory.damage), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='inner_land_mine_damage', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='chain_lightning_damage', label='Chain Lightning Damage', description=None, unit='damage', category=(MetricCategory.damage), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='chain_lightning_damage', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='death_wave_damage', label='Death Wave Damage', description=None, unit='damage', category=(MetricCategory.damage), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='death_wave_damage', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='death_ray_damage', label='Death Ray Damage', description=None, unit='damage', category=(MetricCategory.damage), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='death_ray_damage', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='smart_missile_damage', label='Smart Missile Damage', description=None, unit='damage', category=(MetricCategory.damage), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='smart_missile_damage', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='black_hole_damage', label='Black Hole Damage', description=None, unit='damage', category=(MetricCategory.damage), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='black_hole_damage', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='swamp_damage', label='Swamp Damage', description=None, unit='damage', category=(MetricCategory.damage), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='swamp_damage', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='electrons_damage', label='Electrons Damage', description=None, unit='damage', category=(MetricCategory.damage), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='electrons_damage', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='rend_armor_damage', label='Rend Armor Damage', description=None, unit='damage', category=(MetricCategory.damage), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='rend_armor_damage', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='enemies_hit_by_orbs', label='Enemies Hit by Orbs', description=None, unit='enemies', category=(MetricCategory.damage), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='enemies_hit_by_orbs', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='enemies_destroyed_basic', label='Basic', description=None, unit='enemies', category=(MetricCategory.enemy_destruction), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='enemies_destroyed_basic', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='enemies_destroyed_fast', label='Fast', description=None, unit='enemies', category=(MetricCategory.enemy_destruction), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='enemies_destroyed_fast', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='enemies_destroyed_tank', label='Tank', description=None, unit='enemies', category=(MetricCategory.enemy_destruction), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='enemies_destroyed_tank', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='enemies_destroyed_ranged', label='Ranged', description=None, unit='enemies', category=(MetricCategory.enemy_destruction), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='enemies_destroyed_ranged', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='enemies_destroyed_boss', label='Boss', description=None, unit='enemies', category=(MetricCategory.enemy_destruction), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='enemies_destroyed_boss', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='enemies_destroyed_protector', label='Protector', description=None, unit='enemies', category=(MetricCategory.enemy_destruction), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='enemies_destroyed_protector', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='enemies_destroyed_vampires', label='Vampires', description=None, unit='enemies', category=(MetricCategory.enemy_destruction), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='enemies_destroyed_vampires', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='enemies_destroyed_rays', label='Rays', description=None, unit='enemies', category=(MetricCategory.enemy_destruction), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='enemies_destroyed_rays', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='enemies_destroyed_scatters', label='Scatters', description=None, unit='enemies', category=(MetricCategory.enemy_destruction), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='enemies_destroyed_scatters', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='enemies_destroyed_saboteur', label='Saboteur', description=None, unit='enemies', category=(MetricCategory.enemy_destruction), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='enemies_destroyed_saboteur', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='enemies_destroyed_commander', label='Commander', description=None, unit='enemies', category=(MetricCategory.enemy_destruction), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='enemies_destroyed_commander', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='enemies_destroyed_overcharge', label='Overcharge', description=None, unit='enemies', category=(MetricCategory.enemy_destruction), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='enemies_destroyed_overcharge', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='enemies_destroyed_by_orbs', label='Destroyed By Orbs', description=None, unit='enemies', category=(MetricCategory.enemy_destruction), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='enemies_destroyed_by_orbs', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='enemies_destroyed_by_thorns', label='Destroyed by Thorns', description=None, unit='enemies', category=(MetricCategory.enemy_destruction), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='enemies_destroyed_by_thorns', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='enemies_destroyed_by_death_ray', label='Destroyed by Death Ray', description=None, unit='enemies', category=(MetricCategory.enemy_destruction), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='enemies_destroyed_by_death_ray', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='enemies_destroyed_by_land_mine', label='Destroyed by Land Mine', description=None, unit='enemies', category=(MetricCategory.enemy_destruction), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='enemies_destroyed_by_land_mine', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='enemies_destroyed_in_spotlight', label='Destroyed in Spotlight', description=None, unit='enemies', category=(MetricCategory.enemy_destruction), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='enemies_destroyed_in_spotlight', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='enemies_destroyed_in_golden_bot', label='Destroyed in Golden Bot', description=None, unit='enemies', category=(MetricCategory.enemy_destruction), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='enemies_destroyed_in_golden_bot', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='uw_runs_count', label='Runs using selected ultimate weapon', description=None, unit='runs', category=(MetricCategory.utility), kind='observed', source_model='RunCombat', aggregation='sum', time_index='timestamp', value_field='ultimate_weapon_present', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=(frozenset({'date_range', 'tier', 'preset', 'uw'}))), MetricSeriesSpec(key='guardian_runs_count', label='Runs using selected guardian chip', description=None, unit='runs', category=(MetricCategory.utility), kind='observed', source_model='RunGuardian', aggregation='sum', time_index='timestamp', value_field='guardian_chip_present', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=(frozenset({'date_range', 'tier', 'preset', 'guardian'}))), MetricSeriesSpec(key='bot_runs_count', label='Runs using selected bot', description=None, unit='runs', category=(MetricCategory.utility), kind='observed', source_model='RunBots', aggregation='sum', time_index='timestamp', value_field='bot_present', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=(frozenset({'date_range', 'tier', 'preset', 'bot'}))), MetricSeriesSpec(key='uw_uptime_percent', label='Ultimate Weapon uptime', description='Derived uptime percent for the selected Ultimate Weapon.', unit='percent', category=(MetricCategory.utility), kind='derived', source_model='RunCombat', aggregation='avg', time_index='timestamp', value_field='uw_uptime_percent', allowed_transforms=(frozenset({'none', 'moving_average'})), supported_filters=(frozenset({'date_range', 'tier', 'preset', 'uw'}))), MetricSeriesSpec(key='guardian_activations_per_minute', label='Guardian activations/minute', description='Derived activations/minute for the selected Guardian Chip.', unit='activations/min', category=(MetricCategory.utility), kind='derived', source_model='RunGuardian', aggregation='avg', time_index='timestamp', value_field='guardian_activations_per_minute', allowed_transforms=(frozenset({'none', 'moving_average'})), supported_filters=(frozenset({'date_range', 'tier', 'preset', 'guardian'}))), MetricSeriesSpec(key='uw_effective_cooldown_seconds', label='Ultimate Weapon effective cooldown', description='Derived cooldown seconds for the selected Ultimate Weapon.', unit='seconds', category=(MetricCategory.utility), kind='derived', source_model='RunCombat', aggregation='avg', time_index='timestamp', value_field='uw_effective_cooldown_seconds', allowed_transforms=(frozenset({'none', 'moving_average'})), supported_filters=(frozenset({'date_range', 'tier', 'preset', 'uw'}))), MetricSeriesSpec(key='bot_uptime_percent', label='Bot uptime', description='Derived uptime percent for the selected Bot.', unit='percent', category=(MetricCategory.utility), kind='derived', source_model='RunBots', aggregation='avg', time_index='timestamp', value_field='bot_uptime_percent', allowed_transforms=(frozenset({'none', 'moving_average'})), supported_filters=(frozenset({'date_range', 'tier', 'preset', 'bot'}))), MetricSeriesSpec(key='cooldown_reduction_effective', label='Effective cooldown', description='Entity-scoped effective cooldown in seconds (placeholder for future reduction modeling).', unit='seconds', category=(MetricCategory.utility), kind='derived', source_model='RunCombat', aggregation='avg', time_index='timestamp', value_field='effective_cooldown_seconds', allowed_transforms=(frozenset({'none', 'moving_average'})), supported_filters=(frozenset({'date_range', 'tier', 'preset', 'uw', 'guardian', 'bot'}))), MetricSeriesSpec(key='coins_from_death_wave', label='Coins From Death Wave', description='Battle Report utility breakdown: coins earned from Death Wave.', unit='coins', category=(MetricCategory.economy), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='coins_from_death_wave', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='coins_from_golden_tower', label='Coins From Golden Tower', description='Battle Report utility breakdown: coins earned from Golden Tower.', unit='coins', category=(MetricCategory.economy), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='coins_from_golden_tower', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='cash_from_golden_tower', label='Cash From Golden Tower', description='Battle Report utility breakdown: cash earned from Golden Tower.', unit='cash', category=(MetricCategory.economy), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='cash_from_golden_tower', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='cash_from_other_sources', label='Other cash', description='Residual cash not covered by named sources (derived).', unit='cash', category=(MetricCategory.economy), kind='derived', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='cash_earned - cash_from_golden_tower - interest_earned', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='coins_from_black_hole', label='Coins From Black Hole', description='Battle Report utility breakdown: coins earned from Black Hole.', unit='coins', category=(MetricCategory.economy), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='coins_from_black_hole', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='coins_from_spotlight', label='Coins From Spotlight', description='Battle Report utility breakdown: coins earned from Spotlight.', unit='coins', category=(MetricCategory.economy), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='coins_from_spotlight', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='coins_from_orb', label='Coins From Orb', description='Battle Report utility breakdown: coins earned from Orbs.', unit='coins', category=(MetricCategory.economy), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='coins_from_orb', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='coins_from_coin_upgrade', label='Coins from Coin Upgrade', description='Battle Report utility breakdown: coins earned from coin upgrades.', unit='coins', category=(MetricCategory.economy), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='coins_from_coin_upgrade', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='coins_from_coin_bonuses', label='Coins from Coin Bonuses', description='Battle Report utility breakdown: coins earned from coin bonuses.', unit='coins', category=(MetricCategory.economy), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='coins_from_coin_bonuses', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='free_attack_upgrades', label='Free Attack Upgrade', description='Battle Report utility breakdown: free attack upgrades.', unit='upgrades', category=(MetricCategory.economy), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='free_attack_upgrades', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='free_defense_upgrades', label='Free Defense Upgrade', description='Battle Report utility breakdown: free defense upgrades.', unit='upgrades', category=(MetricCategory.economy), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='free_defense_upgrades', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='free_utility_upgrades', label='Free Utility Upgrade', description='Battle Report utility breakdown: free utility upgrades.', unit='upgrades', category=(MetricCategory.economy), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='free_utility_upgrades', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='free_upgrades_total', label='Free Upgrades (Total)', description='Derived total of free attack, defense, and utility upgrades.', unit='upgrades', category=(MetricCategory.economy), kind='derived', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='free_attack_upgrades + free_defense_upgrades + free_utility_upgrades', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='recovery_packages', label='Recovery Packages', description='Battle Report derived metrics: recovery packages.', unit='count', category=(MetricCategory.utility), kind='derived', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='recovery_packages', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='coins_from_other_sources', label='Other coins', description='Residual coins not covered by named sources; ensures sources sum to total coins earned.', unit='coins', category=(MetricCategory.economy), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='coins_from_other_sources', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='guardian_damage', label='Guardian Damage', description='Battle Report Guardian section: damage dealt by the Guardian.', unit='damage', category=(MetricCategory.combat), kind='observed', source_model='BattleReport', aggregation='avg', time_index='timestamp', value_field='guardian_damage', allowed_transforms=(frozenset({'none', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='guardian_summoned_enemies', label='Guardian Summoned Enemies', description='Battle Report Guardian section: summoned enemies count.', unit='count', category=(MetricCategory.combat), kind='observed', source_model='BattleReport', aggregation='avg', time_index='timestamp', value_field='guardian_summoned_enemies', allowed_transforms=(frozenset({'none', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='guardian_coins_stolen', label='Guardian coins stolen', description='Battle Report Guardian section: coins stolen (rolls up into Coins Earned by Source).', unit='coins', category=(MetricCategory.economy), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='guardian_coins_stolen', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='guardian_coins_fetched', label='Coins Fetched', description='Battle Report Guardian section: coins fetched (rolls up into Coins Earned by Source).', unit='coins', category=(MetricCategory.economy), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='guardian_coins_fetched', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='guardian_gems_fetched', label='Gems', description='Battle Report Guardian section: gems fetched.', unit='count', category=(MetricCategory.fetch), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='guardian_gems_fetched', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='guardian_medals_fetched', label='Medals', description='Battle Report Guardian section: medals fetched.', unit='count', category=(MetricCategory.fetch), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='guardian_medals_fetched', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='guardian_reroll_shards_fetched', label='Reroll Shards', description='Battle Report Guardian section: reroll shards fetched.', unit='count', category=(MetricCategory.fetch), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='guardian_reroll_shards_fetched', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='guardian_cannon_shards_fetched', label='Cannon Shards', description='Battle Report Guardian section: cannon shards fetched.', unit='count', category=(MetricCategory.fetch), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='guardian_cannon_shards_fetched', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='guardian_armor_shards_fetched', label='Armor Shards', description='Battle Report Guardian section: armor shards fetched.', unit='count', category=(MetricCategory.fetch), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='guardian_armor_shards_fetched', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='guardian_generator_shards_fetched', label='Generator Shards', description='Battle Report Guardian section: generator shards fetched.', unit='count', category=(MetricCategory.fetch), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='guardian_generator_shards_fetched', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='guardian_core_shards_fetched', label='Core Shards', description='Battle Report Guardian section: core shards fetched.', unit='count', category=(MetricCategory.fetch), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='guardian_core_shards_fetched', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='guardian_common_modules_fetched', label='Common Modules', description='Battle Report Guardian section: common modules fetched.', unit='count', category=(MetricCategory.fetch), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='guardian_common_modules_fetched', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='guardian_rare_modules_fetched', label='Rare Modules', description='Battle Report Guardian section: rare modules fetched.', unit='count', category=(MetricCategory.fetch), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='guardian_rare_modules_fetched', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='flame_bot_damage', label='Flame Bot Damage', description='Battle Report bot section: damage dealt by Flame Bot.', unit='damage', category=(MetricCategory.damage), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='flame_bot_damage', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='thunder_bot_stuns', label='Thunder Bot Stuns', description='Battle Report bot section: stuns delivered by Thunder Bot.', unit='count', category=(MetricCategory.combat), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='thunder_bot_stuns', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='golden_bot_coins_earned', label='Golden Bot Coins Earned', description='Battle Report bot section: coins earned by Golden Bot.', unit='coins', category=(MetricCategory.economy), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='golden_bot_coins_earned', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS))) module-attribute

METRICS = {'coins_earned': MetricDefinition(key='coins_earned', label='Coins earned', unit='coins', category=(MetricCategory.economy), kind='observed'), 'cash_earned': MetricDefinition(key='cash_earned', label='Cash earned', unit='cash', category=(MetricCategory.economy), kind='observed'), 'interest_earned': MetricDefinition(key='interest_earned', label='Interest earned', unit='cash', category=(MetricCategory.economy), kind='observed'), 'cells_earned': MetricDefinition(key='cells_earned', label='Cells earned', unit='cells', category=(MetricCategory.economy), kind='observed'), 'reroll_shards_earned': MetricDefinition(key='reroll_shards_earned', label='Reroll shards earned', unit='shards', category=(MetricCategory.economy), kind='observed'), 'reroll_dice_earned': MetricDefinition(key='reroll_dice_earned', label='Reroll dice earned', unit='shards', category=(MetricCategory.economy), kind='observed'), 'cells_per_hour': MetricDefinition(key='cells_per_hour', label='Cells/hour', unit='cells/hour', category=(MetricCategory.efficiency), kind='derived'), 'reroll_shards_per_hour': MetricDefinition(key='reroll_shards_per_hour', label='Reroll shards/hour', unit='shards/hour', category=(MetricCategory.efficiency), kind='derived'), 'waves_reached': MetricDefinition(key='waves_reached', label='Waves reached', unit='waves', category=(MetricCategory.utility), kind='observed'), 'coins_per_wave': MetricDefinition(key='coins_per_wave', label='Coins per wave', unit='coins/wave', category=(MetricCategory.economy), kind='derived'), 'uw_runs_count': MetricDefinition(key='uw_runs_count', label='Runs using selected ultimate weapon', unit='runs', category=(MetricCategory.utility), kind='observed'), 'guardian_runs_count': MetricDefinition(key='guardian_runs_count', label='Runs using selected guardian chip', unit='runs', category=(MetricCategory.utility), kind='observed'), 'bot_runs_count': MetricDefinition(key='bot_runs_count', label='Runs using selected bot', unit='runs', category=(MetricCategory.utility), kind='observed'), 'coins_per_hour': MetricDefinition(key='coins_per_hour', label='Coins/hour', unit='coins/hour', category=(MetricCategory.efficiency), kind='observed'), 'real_time_hours': MetricDefinition(key='real_time_hours', label='Run duration (hours)', unit='hours', category=(MetricCategory.efficiency), kind='derived'), 'waves_per_hour': MetricDefinition(key='waves_per_hour', label='Waves/hour', unit='waves/hour', category=(MetricCategory.efficiency), kind='derived'), 'enemies_destroyed_per_hour': MetricDefinition(key='enemies_destroyed_per_hour', label='Enemies destroyed/hour', unit='enemies/hour', category=(MetricCategory.efficiency), kind='derived'), 'coins_from_death_wave': MetricDefinition(key='coins_from_death_wave', label='Coins From Death Wave', unit='coins', category=(MetricCategory.economy), kind='observed'), 'coins_from_golden_tower': MetricDefinition(key='coins_from_golden_tower', label='Coins From Golden Tower', unit='coins', category=(MetricCategory.economy), kind='observed'), 'cash_from_golden_tower': MetricDefinition(key='cash_from_golden_tower', label='Cash From Golden Tower', unit='cash', category=(MetricCategory.economy), kind='observed'), 'cash_from_other_sources': MetricDefinition(key='cash_from_other_sources', label='Other cash', unit='cash', category=(MetricCategory.economy), kind='derived'), 'coins_from_black_hole': MetricDefinition(key='coins_from_black_hole', label='Coins From Black Hole', unit='coins', category=(MetricCategory.economy), kind='observed'), 'coins_from_spotlight': MetricDefinition(key='coins_from_spotlight', label='Coins From Spotlight', unit='coins', category=(MetricCategory.economy), kind='observed'), 'coins_from_orb': MetricDefinition(key='coins_from_orb', label='Coins From Orb', unit='coins', category=(MetricCategory.economy), kind='observed'), 'coins_from_coin_upgrade': MetricDefinition(key='coins_from_coin_upgrade', label='Coins from Coin Upgrade', unit='coins', category=(MetricCategory.economy), kind='observed'), 'coins_from_coin_bonuses': MetricDefinition(key='coins_from_coin_bonuses', label='Coins from Coin Bonuses', unit='coins', category=(MetricCategory.economy), kind='observed'), 'free_attack_upgrades': MetricDefinition(key='free_attack_upgrades', label='Free Attack Upgrade', unit='upgrades', category=(MetricCategory.economy), kind='observed'), 'free_defense_upgrades': MetricDefinition(key='free_defense_upgrades', label='Free Defense Upgrade', unit='upgrades', category=(MetricCategory.economy), kind='observed'), 'free_utility_upgrades': MetricDefinition(key='free_utility_upgrades', label='Free Utility Upgrade', unit='upgrades', category=(MetricCategory.economy), kind='observed'), 'free_upgrades_total': MetricDefinition(key='free_upgrades_total', label='Free Upgrades (Total)', unit='upgrades', category=(MetricCategory.economy), kind='derived'), 'recovery_packages': MetricDefinition(key='recovery_packages', label='Recovery Packages', unit='count', category=(MetricCategory.utility), kind='derived'), 'coins_from_other_sources': MetricDefinition(key='coins_from_other_sources', label='Other coins', unit='coins', category=(MetricCategory.economy), kind='observed'), 'guardian_damage': MetricDefinition(key='guardian_damage', label='Guardian Damage', unit='damage', category=(MetricCategory.combat), kind='observed'), 'damage_dealt': MetricDefinition(key='damage_dealt', label='Damage dealt', unit='damage', category=(MetricCategory.damage), kind='observed'), 'projectiles_damage': MetricDefinition(key='projectiles_damage', label='Projectiles Damage', unit='damage', category=(MetricCategory.damage), kind='observed'), 'thorn_damage': MetricDefinition(key='thorn_damage', label='Thorn Damage', unit='damage', category=(MetricCategory.damage), kind='observed'), 'orb_damage': MetricDefinition(key='orb_damage', label='Orb Damage', unit='damage', category=(MetricCategory.damage), kind='observed'), 'land_mine_damage': MetricDefinition(key='land_mine_damage', label='Land Mine Damage', unit='damage', category=(MetricCategory.damage), kind='observed'), 'inner_land_mine_damage': MetricDefinition(key='inner_land_mine_damage', label='Inner Land Mine Damage', unit='damage', category=(MetricCategory.damage), kind='observed'), 'chain_lightning_damage': MetricDefinition(key='chain_lightning_damage', label='Chain Lightning Damage', unit='damage', category=(MetricCategory.damage), kind='observed'), 'death_wave_damage': MetricDefinition(key='death_wave_damage', label='Death Wave Damage', unit='damage', category=(MetricCategory.damage), kind='observed'), 'death_ray_damage': MetricDefinition(key='death_ray_damage', label='Death Ray Damage', unit='damage', category=(MetricCategory.damage), kind='observed'), 'smart_missile_damage': MetricDefinition(key='smart_missile_damage', label='Smart Missile Damage', unit='damage', category=(MetricCategory.damage), kind='observed'), 'black_hole_damage': MetricDefinition(key='black_hole_damage', label='Black Hole Damage', unit='damage', category=(MetricCategory.damage), kind='observed'), 'swamp_damage': MetricDefinition(key='swamp_damage', label='Swamp Damage', unit='damage', category=(MetricCategory.damage), kind='observed'), 'electrons_damage': MetricDefinition(key='electrons_damage', label='Electrons Damage', unit='damage', category=(MetricCategory.damage), kind='observed'), 'rend_armor_damage': MetricDefinition(key='rend_armor_damage', label='Rend Armor Damage', unit='damage', category=(MetricCategory.damage), kind='observed'), 'enemies_hit_by_orbs': MetricDefinition(key='enemies_hit_by_orbs', label='Enemies Hit by Orbs', unit='count', category=(MetricCategory.damage), kind='observed'), 'enemies_destroyed_total': MetricDefinition(key='enemies_destroyed_total', label='Enemies destroyed (derived total)', unit='count', category=(MetricCategory.enemy_destruction), kind='derived'), 'enemies_destroyed_common': MetricDefinition(key='enemies_destroyed_common', label='Enemies destroyed (common)', unit='count', category=(MetricCategory.enemy_destruction), kind='derived'), 'enemies_destroyed_elite': MetricDefinition(key='enemies_destroyed_elite', label='Enemies destroyed (elite)', unit='count', category=(MetricCategory.enemy_destruction), kind='derived'), 'enemies_destroyed_fleet': MetricDefinition(key='enemies_destroyed_fleet', label='Enemies destroyed (fleet)', unit='count', category=(MetricCategory.enemy_destruction), kind='derived'), 'enemies_destroyed_basic': MetricDefinition(key='enemies_destroyed_basic', label='Basic', unit='count', category=(MetricCategory.enemy_destruction), kind='observed'), 'enemies_destroyed_fast': MetricDefinition(key='enemies_destroyed_fast', label='Fast', unit='count', category=(MetricCategory.enemy_destruction), kind='observed'), 'enemies_destroyed_tank': MetricDefinition(key='enemies_destroyed_tank', label='Tank', unit='count', category=(MetricCategory.enemy_destruction), kind='observed'), 'enemies_destroyed_ranged': MetricDefinition(key='enemies_destroyed_ranged', label='Ranged', unit='count', category=(MetricCategory.enemy_destruction), kind='observed'), 'enemies_destroyed_boss': MetricDefinition(key='enemies_destroyed_boss', label='Boss', unit='count', category=(MetricCategory.enemy_destruction), kind='observed'), 'enemies_destroyed_protector': MetricDefinition(key='enemies_destroyed_protector', label='Protector', unit='count', category=(MetricCategory.enemy_destruction), kind='observed'), 'enemies_destroyed_vampires': MetricDefinition(key='enemies_destroyed_vampires', label='Vampires', unit='count', category=(MetricCategory.enemy_destruction), kind='observed'), 'enemies_destroyed_rays': MetricDefinition(key='enemies_destroyed_rays', label='Rays', unit='count', category=(MetricCategory.enemy_destruction), kind='observed'), 'enemies_destroyed_scatters': MetricDefinition(key='enemies_destroyed_scatters', label='Scatters', unit='count', category=(MetricCategory.enemy_destruction), kind='observed'), 'enemies_destroyed_saboteur': MetricDefinition(key='enemies_destroyed_saboteur', label='Saboteur', unit='count', category=(MetricCategory.enemy_destruction), kind='observed'), 'enemies_destroyed_commander': MetricDefinition(key='enemies_destroyed_commander', label='Commander', unit='count', category=(MetricCategory.enemy_destruction), kind='observed'), 'enemies_destroyed_overcharge': MetricDefinition(key='enemies_destroyed_overcharge', label='Overcharge', unit='count', category=(MetricCategory.enemy_destruction), kind='observed'), 'enemies_destroyed_by_orbs': MetricDefinition(key='enemies_destroyed_by_orbs', label='Destroyed By Orbs', unit='count', category=(MetricCategory.enemy_destruction), kind='observed'), 'enemies_destroyed_by_thorns': MetricDefinition(key='enemies_destroyed_by_thorns', label='Destroyed by Thorns', unit='count', category=(MetricCategory.enemy_destruction), kind='observed'), 'enemies_destroyed_by_death_ray': MetricDefinition(key='enemies_destroyed_by_death_ray', label='Destroyed by Death Ray', unit='count', category=(MetricCategory.enemy_destruction), kind='observed'), 'enemies_destroyed_by_land_mine': MetricDefinition(key='enemies_destroyed_by_land_mine', label='Destroyed by Land Mine', unit='count', category=(MetricCategory.enemy_destruction), kind='observed'), 'enemies_destroyed_in_spotlight': MetricDefinition(key='enemies_destroyed_in_spotlight', label='Destroyed in Spotlight', unit='count', category=(MetricCategory.enemy_destruction), kind='observed'), 'enemies_destroyed_in_golden_bot': MetricDefinition(key='enemies_destroyed_in_golden_bot', label='Destroyed in Golden Bot', unit='count', category=(MetricCategory.enemy_destruction), kind='observed'), 'guardian_summoned_enemies': MetricDefinition(key='guardian_summoned_enemies', label='Guardian Summoned Enemies', unit='count', category=(MetricCategory.combat), kind='observed'), 'guardian_coins_stolen': MetricDefinition(key='guardian_coins_stolen', label='Guardian coins stolen', unit='coins', category=(MetricCategory.economy), kind='observed'), 'guardian_coins_fetched': MetricDefinition(key='guardian_coins_fetched', label='Coins Fetched', unit='coins', category=(MetricCategory.economy), kind='observed'), 'guardian_gems_fetched': MetricDefinition(key='guardian_gems_fetched', label='Guardian gems fetched', unit='count', category=(MetricCategory.fetch), kind='observed'), 'guardian_medals_fetched': MetricDefinition(key='guardian_medals_fetched', label='Guardian medals fetched', unit='count', category=(MetricCategory.fetch), kind='observed'), 'guardian_reroll_shards_fetched': MetricDefinition(key='guardian_reroll_shards_fetched', label='Guardian reroll shards fetched', unit='count', category=(MetricCategory.fetch), kind='observed'), 'guardian_cannon_shards_fetched': MetricDefinition(key='guardian_cannon_shards_fetched', label='Guardian cannon shards fetched', unit='count', category=(MetricCategory.fetch), kind='observed'), 'guardian_armor_shards_fetched': MetricDefinition(key='guardian_armor_shards_fetched', label='Guardian armor shards fetched', unit='count', category=(MetricCategory.fetch), kind='observed'), 'guardian_generator_shards_fetched': MetricDefinition(key='guardian_generator_shards_fetched', label='Guardian generator shards fetched', unit='count', category=(MetricCategory.fetch), kind='observed'), 'guardian_core_shards_fetched': MetricDefinition(key='guardian_core_shards_fetched', label='Guardian core shards fetched', unit='count', category=(MetricCategory.fetch), kind='observed'), 'guardian_common_modules_fetched': MetricDefinition(key='guardian_common_modules_fetched', label='Guardian common modules fetched', unit='count', category=(MetricCategory.fetch), kind='observed'), 'guardian_rare_modules_fetched': MetricDefinition(key='guardian_rare_modules_fetched', label='Guardian rare modules fetched', unit='count', category=(MetricCategory.fetch), kind='observed'), 'flame_bot_damage': MetricDefinition(key='flame_bot_damage', label='Flame Bot Damage', unit='damage', category=(MetricCategory.damage), kind='observed'), 'thunder_bot_stuns': MetricDefinition(key='thunder_bot_stuns', label='Thunder Bot Stuns', unit='count', category=(MetricCategory.combat), kind='observed'), 'golden_bot_coins_earned': MetricDefinition(key='golden_bot_coins_earned', label='Golden Bot Coins Earned', unit='coins', category=(MetricCategory.economy), kind='observed'), 'uw_uptime_percent': MetricDefinition(key='uw_uptime_percent', label='Ultimate Weapon uptime', unit='percent', category=(MetricCategory.utility), kind='derived'), 'guardian_activations_per_minute': MetricDefinition(key='guardian_activations_per_minute', label='Guardian activations/minute', unit='activations/min', category=(MetricCategory.utility), kind='derived'), 'uw_effective_cooldown_seconds': MetricDefinition(key='uw_effective_cooldown_seconds', label='Ultimate Weapon effective cooldown', unit='seconds', category=(MetricCategory.utility), kind='derived'), 'cooldown_reduction_effective': MetricDefinition(key='cooldown_reduction_effective', label='Effective cooldown', unit='seconds', category=(MetricCategory.utility), kind='derived'), 'bot_uptime_percent': MetricDefinition(key='bot_uptime_percent', label='Bot uptime', unit='percent', category=(MetricCategory.utility), kind='derived')} module-attribute

RAW_TEXT_METRIC_SPECS = {'coins_from_death_wave': ('Coins From Death Wave', UnitType.coins), 'interest_earned': ('Interest earned', UnitType.cash), 'cash_from_golden_tower': ('Cash From Golden Tower', UnitType.cash), 'coins_from_golden_tower': ('Coins From Golden Tower', UnitType.coins), 'coins_from_black_hole': ('Coins From Black Hole', UnitType.coins), 'coins_from_spotlight': ('Coins From Spotlight', UnitType.coins), 'coins_from_orb': ('Coins From Orb', UnitType.coins), 'coins_from_coin_upgrade': ('Coins from Coin Upgrade', UnitType.coins), 'coins_from_coin_bonuses': ('Coins from Coin Bonuses', UnitType.coins), 'free_attack_upgrades': ('Free Attack Upgrade', UnitType.count), 'free_defense_upgrades': ('Free Defense Upgrade', UnitType.count), 'free_utility_upgrades': ('Free Utility Upgrade', UnitType.count), 'recovery_packages': ('Recovery Packages', UnitType.count), 'damage_dealt': ('Damage dealt', UnitType.damage), 'projectiles_damage': ('Projectiles Damage', UnitType.damage), 'thorn_damage': ('Thorn Damage', UnitType.damage), 'orb_damage': ('Orb Damage', UnitType.damage), 'land_mine_damage': ('Land Mine Damage', UnitType.damage), 'inner_land_mine_damage': ('Inner Land Mine Damage', UnitType.damage), 'chain_lightning_damage': ('Chain Lightning Damage', UnitType.damage), 'death_wave_damage': ('Death Wave Damage', UnitType.damage), 'death_ray_damage': ('Death Ray Damage', UnitType.damage), 'smart_missile_damage': ('Smart Missile Damage', UnitType.damage), 'black_hole_damage': ('Black Hole Damage', UnitType.damage), 'swamp_damage': ('Swamp Damage', UnitType.damage), 'electrons_damage': ('Electrons Damage', UnitType.damage), 'rend_armor_damage': ('Rend Armor Damage', UnitType.damage), 'enemies_hit_by_orbs': ('Enemies Hit by Orbs', UnitType.count), 'enemies_destroyed_basic': ('Basic', UnitType.count), 'enemies_destroyed_fast': ('Fast', UnitType.count), 'enemies_destroyed_tank': ('Tank', UnitType.count), 'enemies_destroyed_ranged': ('Ranged', UnitType.count), 'enemies_destroyed_boss': ('Boss', UnitType.count), 'enemies_destroyed_protector': ('Protector', UnitType.count), 'enemies_destroyed_vampires': ('Vampires', UnitType.count), 'enemies_destroyed_rays': ('Rays', UnitType.count), 'enemies_destroyed_scatters': ('Scatters', UnitType.count), 'enemies_destroyed_saboteur': ('Saboteur', UnitType.count), 'enemies_destroyed_commander': ('Commander', UnitType.count), 'enemies_destroyed_overcharge': ('Overcharge', UnitType.count), 'enemies_destroyed_by_orbs': ('Destroyed By Orbs', UnitType.count), 'enemies_destroyed_by_thorns': ('Destroyed by Thorns', UnitType.count), 'enemies_destroyed_by_death_ray': ('Destroyed by Death Ray', UnitType.count), 'enemies_destroyed_by_land_mine': ('Destroyed by Land Mine', UnitType.count), 'enemies_destroyed_in_spotlight': ('Destroyed in Spotlight', UnitType.count), 'enemies_destroyed_in_golden_bot': ('Destroyed in Golden Bot', UnitType.count), 'guardian_damage': ('Damage', UnitType.damage), 'guardian_summoned_enemies': ('Summoned enemies', UnitType.count), 'guardian_coins_stolen': ('Guardian coins stolen', UnitType.coins), 'guardian_coins_fetched': ('Coins Fetched', UnitType.coins), 'guardian_gems_fetched': ('Gems', UnitType.count), 'guardian_medals_fetched': ('Medals', UnitType.count), 'guardian_reroll_shards_fetched': ('Reroll Shards', UnitType.count), 'guardian_cannon_shards_fetched': ('Cannon Shards', UnitType.count), 'guardian_armor_shards_fetched': ('Armor Shards', UnitType.count), 'guardian_generator_shards_fetched': ('Generator Shards', UnitType.count), 'guardian_core_shards_fetched': ('Core Shards', UnitType.count), 'guardian_common_modules_fetched': ('Common Modules', UnitType.count), 'guardian_rare_modules_fetched': ('Rare Modules', UnitType.count), 'flame_bot_damage': ('Flame Bot Damage', UnitType.damage), 'thunder_bot_stuns': ('Thunder Bot Stuns', UnitType.count), 'golden_bot_coins_earned': ('Golden Bot Coins Earned', UnitType.coins)} module-attribute

MetricCategory

Bases: StrEnum

Semantic category for a metric.

Values are stable identifiers used across the analysis registry and UI.

MetricComputeConfig dataclass

Configuration for metric computations.

Parameters:

Name Type Description Default
monte_carlo MonteCarloConfig | None

Optional MonteCarloConfig for metrics that use simulation.

None

MetricDefinition dataclass

Definition for an observed or derived metric.

Attributes:

Name Type Description
key str

Stable metric key used by UI selection and charting.

label str

Human-friendly label.

unit str

Display unit string (e.g. "coins/hour", "seconds").

category MetricCategory

Semantic category used for filtering and validation.

kind str

Either "observed" or "derived".

MonteCarloConfig dataclass

Configuration for deterministic Monte Carlo computations.

Parameters:

Name Type Description Default
trials int

Number of simulated trials (must be > 0).

required
seed int

RNG seed used to ensure deterministic results in tests and UI.

required

ParameterInput dataclass

A single parameter value selected for analysis.

Parameters:

Name Type Description Default
key str

Stable parameter key (caller-defined).

required
raw_value str

Raw string as captured from wiki or user input.

required
parsed Quantity

Best-effort parsed quantity containing normalized value.

required
wiki_revision_id int | None

Optional core.WikiData primary key representing the immutable revision of the source table row used for this parameter.

required

PlayerContextInput dataclass

Container for all player context provided to analysis.

The Analysis Engine must handle missing context gracefully. Callers may pass None instead of a PlayerContextInput when no player state is available.

UsedParameter dataclass

A parameter value referenced during derived metric computation.

Attributes:

Name Type Description
entity_type str

High-level entity type (card, ultimate_weapon, guardian_chip, bot).

entity_name str

Human-friendly entity name.

key str

Parameter key used by the computation.

raw_value str

Raw string as stored.

normalized_value float | None

Best-effort normalized float value, if parseable.

wiki_revision_id int | None

Optional wiki revision id (core.WikiData pk) used.

_compute_enemies_destroyed_group_from_values(derived_values, *, keys)

Compute enemies destroyed totals for a group of per-type metrics.

_compute_enemies_destroyed_total_from_values(derived_values)

Compute enemies destroyed total from persisted derived metrics.

_compute_other_coins_from_sources(*, coins, derived_values)

Return residual coins not covered by named sources from persisted values.

_entity_parameters(context, *, entity_type, entity_name)

Return parameters for a specific entity selection, or None when missing.

Parameters:

Name Type Description Default
context PlayerContextInput

PlayerContextInput containing unlocked/owned entities.

required
entity_type str

One of "ultimate_weapon", "guardian_chip", "bot".

required
entity_name str | None

Display name to match against context entries.

required

Returns:

Type Description
tuple[ParameterInput, ...] | None

Tuple of ParameterInput entries, or None when selection/context is missing.

_record_derived_values(record)

Return persisted derived metric values from a record when available.

activations_per_minute_from_parameters(*, entity_type, entity_name, parameters, cooldown_key='cooldown')

Compute activations/minute from a cooldown parameter.

Formula

activations_per_minute = 60 / cooldown_seconds

Parameters:

Name Type Description Default
entity_type str

Entity category label (e.g. "guardian_chip").

required
entity_name str

Human-readable entity name for trace output.

required
parameters tuple[ParameterInput, ...]

ParameterInput entries (raw + parsed values).

required
cooldown_key str

Parameter key used for cooldown seconds.

'cooldown'

Returns:

Type Description
EffectResult

EffectResult with activations/minute and referenced parameters.

category_for_metric(metric_key)

Return the MetricCategory for a metric key, when registered.

coins_per_hour(coins, real_time_seconds)

Compute coins per hour for a single run.

Parameters:

Name Type Description Default
coins int

Total coins earned.

required
real_time_seconds int

Run duration in seconds.

required

Returns:

Type Description
float | None

Coins per hour, or None when inputs are invalid.

compute_metric_value(metric_key, *, record, coins, cash, interest_earned, cells, reroll_shards, wave, real_time_seconds, context, entity_type, entity_name, config)

Compute a metric value and return used parameters + assumptions.

Parameters:

Name Type Description Default
metric_key str

Metric key to compute.

required
record object

Run-like object used for relationship-based metrics (e.g. usage presence).

required
coins int | None

Observed coins for the run.

required
cash int | None

Observed cash for the run.

required
interest_earned int | None

Observed interest earned for the run.

required
cells int | None

Observed cells for the run.

required
reroll_shards int | None

Observed reroll shards for the run.

required
wave int | None

Observed wave reached for the run.

required
real_time_seconds int | None

Observed duration seconds for the run.

required
context PlayerContextInput | None

Optional player context + selected parameters.

required
entity_type str | None

Optional entity category for entity-scoped derived metrics.

required
entity_name str | None

Optional entity name for entity-scoped derived metrics.

required
config MetricComputeConfig

MetricComputeConfig.

required

Returns:

Type Description
tuple[float | None, tuple[UsedParameter, ...], tuple[str, ...]]

Tuple of (value, used_parameters, assumptions).

compute_observed_coins_per_hour(*, coins, real_time_seconds)

Compute observed coins/hour from raw run fields.

effective_cooldown_seconds_from_parameters(*, entity_type, entity_name, parameters, cooldown_key='cooldown')

Compute an effective cooldown in seconds from a cooldown parameter.

This is a validation-focused effect used to prove that: - wiki-derived parameter revisions are referenced by analysis at request time, and - derived metrics can be charted alongside observed metrics with explicit units.

Formula

effective_cooldown_seconds = cooldown_seconds

Parameters:

Name Type Description Default
entity_type str

Entity category label (e.g. "ultimate_weapon").

required
entity_name str

Human-readable entity name for trace output.

required
parameters tuple[ParameterInput, ...]

ParameterInput entries (raw + parsed values).

required
cooldown_key str

Parameter key used for cooldown seconds.

'cooldown'

Returns:

Type Description
EffectResult

EffectResult with effective cooldown seconds and referenced parameters.

get_metric_definition(metric_key)

Return a MetricDefinition for a key, defaulting to observed coins/hour.

is_ultimate_weapon_observed_active(raw_text, *, ultimate_weapon_name)

Return True when a Battle Report shows evidence of an Ultimate Weapon being active.

Parameters:

Name Type Description Default
raw_text str

Raw Battle Report text as imported/stored.

required
ultimate_weapon_name str

Display name for the Ultimate Weapon (e.g. "Black Hole").

required

Returns:

Type Description
bool

True when the mapped Battle Report metric parses and is > 0; otherwise False.

list_metric_definitions()

Return the available metric definitions in a stable order.

uptime_percent_from_parameters(*, entity_type, entity_name, parameters, duration_key='duration', cooldown_key='cooldown')

Compute uptime percent from duration and cooldown parameters.

Formula

uptime_percent = 100 * clamp(duration_seconds / cooldown_seconds, 0..1)

Parameters:

Name Type Description Default
entity_type str

Entity category label (e.g. "ultimate_weapon", "bot").

required
entity_name str

Human-readable entity name for trace output.

required
parameters tuple[ParameterInput, ...]

ParameterInput entries (raw + parsed values).

required
duration_key str

Parameter key used for duration seconds.

'duration'
cooldown_key str

Parameter key used for cooldown seconds.

'cooldown'

Returns:

Type Description
EffectResult

EffectResult with uptime percent and referenced parameters.

validate_metric_registry()

Validate that MetricDefinition keys and series registry keys match.

Raises:

Type Description
ValueError

When registered metric keys drift between METRICS and the DEFAULT_REGISTRY.

analysis.quantity

Best-effort unit/quantity parsing utilities.

Phase 1.5 introduces a small normalization layer for compact numeric strings commonly found in Battle Reports (e.g. 7.67M, x1.15, 15%).

This module is intentionally: - pure (no Django imports, no database writes), - defensive (never raises on unknown formats), - minimal (only supports formats needed by Phase 1 inputs so far).

_MAGNITUDE_MULTIPLIERS = {'': Decimal(1), 'k': Decimal(1000), 'm': Decimal(1000000), 'b': Decimal(1000000000), 't': Decimal(1000000000000), 'q': Decimal(1000000000000000), 'Q': Decimal(1000000000000000000)} module-attribute

Quantity dataclass

A parsed quantity with both raw and normalized representations.

Attributes:

Name Type Description
raw_value str

The original raw string value (trimmed).

normalized_value Decimal | None

The parsed numeric value as a Decimal, or None if the value could not be parsed.

magnitude str | None

The compact magnitude suffix (e.g. k, m, b, t, q), or None when not applicable.

unit_type UnitType

The category of unit this value represents.

UnitType

Bases: Enum

Supported unit categories for Phase 1.5.

_parse_compact_number(value, *, unit_type)

Parse 7.67M-style compact numbers.

_parse_decimal(number_text)

Parse a Decimal from a Battle Report-style numeric string.

_parse_multiplier(value)

Parse x1.15 multiplier strings.

_parse_percent(value)

Parse percent strings like 15% into fractional multipliers.

is_known_magnitude_suffix(suffix)

Return True when a compact magnitude suffix is recognized.

Parameters:

Name Type Description Default
suffix str

Raw magnitude suffix (e.g. k, m, q, Q).

required

Returns:

Type Description
bool

True when the suffix is supported by the compact number parser.

parse_quantity(raw_value, *, unit_type=UnitType.count)

Parse a compact quantity string into a normalized Decimal.

Parameters:

Name Type Description Default
raw_value str

Raw value string (e.g. 7.67M, x1.15, 15%).

required
unit_type UnitType

Unit category to assign for non-annotated values.

count

Returns:

Type Description
Quantity

Quantity where normalized_value is None when parsing fails.

Notes
  • A leading x forces unit_type=multiplier and parses the remainder.
  • A trailing % forces unit_type=multiplier and normalizes as a fraction (e.g. 15% -> 0.15).
  • Magnitude suffixes are case-insensitive except for Q (quintillion).
  • Supported suffixes include lowercase k..q and uppercase Q.

analysis.rates

Rate calculations for the Analysis Engine.

Phase 1 intentionally ships a single derived rate metric: coins per hour.

coins_per_hour(coins, real_time_seconds)

Compute coins per hour for a single run.

Parameters:

Name Type Description Default
coins int

Total coins earned.

required
real_time_seconds int

Run duration in seconds.

required

Returns:

Type Description
float | None

Coins per hour, or None when inputs are invalid.

analysis.series_registry

MetricSeries registry used by declarative chart configs.

This registry is a thin, analysis-layer description of which metric keys exist, which filter dimensions they support, and which transforms are allowed. It is used by the ChartConfig validator and the dashboard renderer.

The registry remains Django-free and describes capabilities only. Query scoping (date range, tier, preset, entity selection) happens at the view layer before records are passed into the Analysis Engine.

Aggregation = Literal['sum', 'avg'] module-attribute

DEFAULT_REGISTRY = MetricSeriesRegistry(specs=(MetricSeriesSpec(key='coins_earned', label='Coins earned', description=None, unit='coins', category=(MetricCategory.economy), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='coins_earned', allowed_transforms=(frozenset({'none', 'rate_per_hour', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='cash_earned', label='Cash earned', description=None, unit='cash', category=(MetricCategory.economy), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='cash_earned', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='interest_earned', label='Interest earned', description='Observed interest earned from Battle Reports.', unit='cash', category=(MetricCategory.economy), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='interest_earned', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='cells_earned', label='Cells earned', description=None, unit='cells', category=(MetricCategory.economy), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='cells_earned', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='reroll_shards_earned', label='Reroll shards earned', description=None, unit='shards', category=(MetricCategory.economy), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='reroll_shards_earned', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='reroll_dice_earned', label='Reroll dice earned', description='Alias for reroll shards earned (legacy naming).', unit='shards', category=(MetricCategory.economy), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='reroll_shards_earned', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='waves_reached', label='Waves reached', description=None, unit='waves', category=(MetricCategory.utility), kind='observed', source_model='BattleReport', aggregation='avg', time_index='timestamp', value_field='wave', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average', 'rate_per_hour'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='coins_per_wave', label='Coins per wave', description='Computed as coins earned divided by waves reached.', unit='coins/wave', category=(MetricCategory.economy), kind='derived', source_model='BattleReport', aggregation='avg', time_index='timestamp', value_field='coins_earned / wave', allowed_transforms=(frozenset({'none', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='coins_per_hour', label='Coins/hour', description='Observed coins earned divided by real time (hours).', unit='coins/hour', category=(MetricCategory.efficiency), kind='observed', source_model='BattleReport', aggregation='avg', time_index='timestamp', value_field='coins_per_hour', allowed_transforms=(frozenset({'none', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='cells_per_hour', label='Cells/hour', description='Observed cells earned divided by real time (hours).', unit='cells/hour', category=(MetricCategory.efficiency), kind='derived', source_model='BattleReport', aggregation='avg', time_index='timestamp', value_field='cells_earned / hours', allowed_transforms=(frozenset({'none', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='reroll_shards_per_hour', label='Reroll shards/hour', description='Observed reroll shards earned divided by real time (hours).', unit='shards/hour', category=(MetricCategory.efficiency), kind='derived', source_model='BattleReport', aggregation='avg', time_index='timestamp', value_field='reroll_shards_earned / hours', allowed_transforms=(frozenset({'none', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='real_time_hours', label='Run duration (hours)', description='Observed real-time duration converted to hours.', unit='hours', category=(MetricCategory.efficiency), kind='derived', source_model='BattleReport', aggregation='avg', time_index='timestamp', value_field='real_time_seconds / 3600', allowed_transforms=(frozenset({'none', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='waves_per_hour', label='Waves/hour', description='Observed waves reached divided by real time (hours).', unit='waves/hour', category=(MetricCategory.efficiency), kind='derived', source_model='BattleReport', aggregation='avg', time_index='timestamp', value_field='waves_reached / hours', allowed_transforms=(frozenset({'none', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='enemies_destroyed_total', label='Enemies destroyed (derived total)', description='Derived from per-type counts; ignores game-reported totals.', unit='enemies', category=(MetricCategory.enemy_destruction), kind='derived', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='sum(enemy_type_counts)', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average', 'rate_per_hour'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='enemies_destroyed_common', label='Enemies destroyed (common)', description='Derived from Basic, Fast, Ranged, Tank, and Protector counts.', unit='enemies', category=(MetricCategory.enemy_destruction), kind='derived', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='sum(common_enemy_counts)', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average', 'rate_per_hour'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='enemies_destroyed_elite', label='Enemies destroyed (elite)', description='Derived from Vampire, Ray, and Scatter counts.', unit='enemies', category=(MetricCategory.enemy_destruction), kind='derived', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='sum(elite_enemy_counts)', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average', 'rate_per_hour'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='enemies_destroyed_fleet', label='Enemies destroyed (fleet)', description='Derived from Saboteur, Commander, and Overcharge counts.', unit='enemies', category=(MetricCategory.enemy_destruction), kind='derived', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='sum(fleet_enemy_counts)', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average', 'rate_per_hour'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='enemies_destroyed_per_hour', label='Enemies destroyed/hour', description='Enemies destroyed (derived total) divided by real time (hours).', unit='enemies/hour', category=(MetricCategory.efficiency), kind='derived', source_model='BattleReport', aggregation='avg', time_index='timestamp', value_field='enemies_destroyed_total / hours', allowed_transforms=(frozenset({'none', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='damage_dealt', label='Damage dealt', description='Total damage dealt from Battle Reports.', unit='damage', category=(MetricCategory.damage), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='damage_dealt', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='projectiles_damage', label='Projectiles Damage', description=None, unit='damage', category=(MetricCategory.damage), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='projectiles_damage', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='thorn_damage', label='Thorn Damage', description=None, unit='damage', category=(MetricCategory.damage), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='thorn_damage', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='orb_damage', label='Orb Damage', description=None, unit='damage', category=(MetricCategory.damage), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='orb_damage', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='land_mine_damage', label='Land Mine Damage', description=None, unit='damage', category=(MetricCategory.damage), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='land_mine_damage', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='inner_land_mine_damage', label='Inner Land Mine Damage', description=None, unit='damage', category=(MetricCategory.damage), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='inner_land_mine_damage', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='chain_lightning_damage', label='Chain Lightning Damage', description=None, unit='damage', category=(MetricCategory.damage), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='chain_lightning_damage', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='death_wave_damage', label='Death Wave Damage', description=None, unit='damage', category=(MetricCategory.damage), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='death_wave_damage', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='death_ray_damage', label='Death Ray Damage', description=None, unit='damage', category=(MetricCategory.damage), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='death_ray_damage', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='smart_missile_damage', label='Smart Missile Damage', description=None, unit='damage', category=(MetricCategory.damage), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='smart_missile_damage', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='black_hole_damage', label='Black Hole Damage', description=None, unit='damage', category=(MetricCategory.damage), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='black_hole_damage', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='swamp_damage', label='Swamp Damage', description=None, unit='damage', category=(MetricCategory.damage), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='swamp_damage', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='electrons_damage', label='Electrons Damage', description=None, unit='damage', category=(MetricCategory.damage), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='electrons_damage', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='rend_armor_damage', label='Rend Armor Damage', description=None, unit='damage', category=(MetricCategory.damage), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='rend_armor_damage', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='enemies_hit_by_orbs', label='Enemies Hit by Orbs', description=None, unit='enemies', category=(MetricCategory.damage), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='enemies_hit_by_orbs', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='enemies_destroyed_basic', label='Basic', description=None, unit='enemies', category=(MetricCategory.enemy_destruction), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='enemies_destroyed_basic', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='enemies_destroyed_fast', label='Fast', description=None, unit='enemies', category=(MetricCategory.enemy_destruction), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='enemies_destroyed_fast', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='enemies_destroyed_tank', label='Tank', description=None, unit='enemies', category=(MetricCategory.enemy_destruction), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='enemies_destroyed_tank', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='enemies_destroyed_ranged', label='Ranged', description=None, unit='enemies', category=(MetricCategory.enemy_destruction), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='enemies_destroyed_ranged', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='enemies_destroyed_boss', label='Boss', description=None, unit='enemies', category=(MetricCategory.enemy_destruction), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='enemies_destroyed_boss', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='enemies_destroyed_protector', label='Protector', description=None, unit='enemies', category=(MetricCategory.enemy_destruction), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='enemies_destroyed_protector', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='enemies_destroyed_vampires', label='Vampires', description=None, unit='enemies', category=(MetricCategory.enemy_destruction), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='enemies_destroyed_vampires', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='enemies_destroyed_rays', label='Rays', description=None, unit='enemies', category=(MetricCategory.enemy_destruction), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='enemies_destroyed_rays', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='enemies_destroyed_scatters', label='Scatters', description=None, unit='enemies', category=(MetricCategory.enemy_destruction), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='enemies_destroyed_scatters', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='enemies_destroyed_saboteur', label='Saboteur', description=None, unit='enemies', category=(MetricCategory.enemy_destruction), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='enemies_destroyed_saboteur', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='enemies_destroyed_commander', label='Commander', description=None, unit='enemies', category=(MetricCategory.enemy_destruction), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='enemies_destroyed_commander', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='enemies_destroyed_overcharge', label='Overcharge', description=None, unit='enemies', category=(MetricCategory.enemy_destruction), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='enemies_destroyed_overcharge', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='enemies_destroyed_by_orbs', label='Destroyed By Orbs', description=None, unit='enemies', category=(MetricCategory.enemy_destruction), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='enemies_destroyed_by_orbs', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='enemies_destroyed_by_thorns', label='Destroyed by Thorns', description=None, unit='enemies', category=(MetricCategory.enemy_destruction), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='enemies_destroyed_by_thorns', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='enemies_destroyed_by_death_ray', label='Destroyed by Death Ray', description=None, unit='enemies', category=(MetricCategory.enemy_destruction), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='enemies_destroyed_by_death_ray', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='enemies_destroyed_by_land_mine', label='Destroyed by Land Mine', description=None, unit='enemies', category=(MetricCategory.enemy_destruction), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='enemies_destroyed_by_land_mine', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='enemies_destroyed_in_spotlight', label='Destroyed in Spotlight', description=None, unit='enemies', category=(MetricCategory.enemy_destruction), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='enemies_destroyed_in_spotlight', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='enemies_destroyed_in_golden_bot', label='Destroyed in Golden Bot', description=None, unit='enemies', category=(MetricCategory.enemy_destruction), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='enemies_destroyed_in_golden_bot', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='uw_runs_count', label='Runs using selected ultimate weapon', description=None, unit='runs', category=(MetricCategory.utility), kind='observed', source_model='RunCombat', aggregation='sum', time_index='timestamp', value_field='ultimate_weapon_present', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=(frozenset({'date_range', 'tier', 'preset', 'uw'}))), MetricSeriesSpec(key='guardian_runs_count', label='Runs using selected guardian chip', description=None, unit='runs', category=(MetricCategory.utility), kind='observed', source_model='RunGuardian', aggregation='sum', time_index='timestamp', value_field='guardian_chip_present', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=(frozenset({'date_range', 'tier', 'preset', 'guardian'}))), MetricSeriesSpec(key='bot_runs_count', label='Runs using selected bot', description=None, unit='runs', category=(MetricCategory.utility), kind='observed', source_model='RunBots', aggregation='sum', time_index='timestamp', value_field='bot_present', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=(frozenset({'date_range', 'tier', 'preset', 'bot'}))), MetricSeriesSpec(key='uw_uptime_percent', label='Ultimate Weapon uptime', description='Derived uptime percent for the selected Ultimate Weapon.', unit='percent', category=(MetricCategory.utility), kind='derived', source_model='RunCombat', aggregation='avg', time_index='timestamp', value_field='uw_uptime_percent', allowed_transforms=(frozenset({'none', 'moving_average'})), supported_filters=(frozenset({'date_range', 'tier', 'preset', 'uw'}))), MetricSeriesSpec(key='guardian_activations_per_minute', label='Guardian activations/minute', description='Derived activations/minute for the selected Guardian Chip.', unit='activations/min', category=(MetricCategory.utility), kind='derived', source_model='RunGuardian', aggregation='avg', time_index='timestamp', value_field='guardian_activations_per_minute', allowed_transforms=(frozenset({'none', 'moving_average'})), supported_filters=(frozenset({'date_range', 'tier', 'preset', 'guardian'}))), MetricSeriesSpec(key='uw_effective_cooldown_seconds', label='Ultimate Weapon effective cooldown', description='Derived cooldown seconds for the selected Ultimate Weapon.', unit='seconds', category=(MetricCategory.utility), kind='derived', source_model='RunCombat', aggregation='avg', time_index='timestamp', value_field='uw_effective_cooldown_seconds', allowed_transforms=(frozenset({'none', 'moving_average'})), supported_filters=(frozenset({'date_range', 'tier', 'preset', 'uw'}))), MetricSeriesSpec(key='bot_uptime_percent', label='Bot uptime', description='Derived uptime percent for the selected Bot.', unit='percent', category=(MetricCategory.utility), kind='derived', source_model='RunBots', aggregation='avg', time_index='timestamp', value_field='bot_uptime_percent', allowed_transforms=(frozenset({'none', 'moving_average'})), supported_filters=(frozenset({'date_range', 'tier', 'preset', 'bot'}))), MetricSeriesSpec(key='cooldown_reduction_effective', label='Effective cooldown', description='Entity-scoped effective cooldown in seconds (placeholder for future reduction modeling).', unit='seconds', category=(MetricCategory.utility), kind='derived', source_model='RunCombat', aggregation='avg', time_index='timestamp', value_field='effective_cooldown_seconds', allowed_transforms=(frozenset({'none', 'moving_average'})), supported_filters=(frozenset({'date_range', 'tier', 'preset', 'uw', 'guardian', 'bot'}))), MetricSeriesSpec(key='coins_from_death_wave', label='Coins From Death Wave', description='Battle Report utility breakdown: coins earned from Death Wave.', unit='coins', category=(MetricCategory.economy), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='coins_from_death_wave', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='coins_from_golden_tower', label='Coins From Golden Tower', description='Battle Report utility breakdown: coins earned from Golden Tower.', unit='coins', category=(MetricCategory.economy), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='coins_from_golden_tower', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='cash_from_golden_tower', label='Cash From Golden Tower', description='Battle Report utility breakdown: cash earned from Golden Tower.', unit='cash', category=(MetricCategory.economy), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='cash_from_golden_tower', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='cash_from_other_sources', label='Other cash', description='Residual cash not covered by named sources (derived).', unit='cash', category=(MetricCategory.economy), kind='derived', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='cash_earned - cash_from_golden_tower - interest_earned', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='coins_from_black_hole', label='Coins From Black Hole', description='Battle Report utility breakdown: coins earned from Black Hole.', unit='coins', category=(MetricCategory.economy), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='coins_from_black_hole', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='coins_from_spotlight', label='Coins From Spotlight', description='Battle Report utility breakdown: coins earned from Spotlight.', unit='coins', category=(MetricCategory.economy), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='coins_from_spotlight', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='coins_from_orb', label='Coins From Orb', description='Battle Report utility breakdown: coins earned from Orbs.', unit='coins', category=(MetricCategory.economy), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='coins_from_orb', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='coins_from_coin_upgrade', label='Coins from Coin Upgrade', description='Battle Report utility breakdown: coins earned from coin upgrades.', unit='coins', category=(MetricCategory.economy), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='coins_from_coin_upgrade', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='coins_from_coin_bonuses', label='Coins from Coin Bonuses', description='Battle Report utility breakdown: coins earned from coin bonuses.', unit='coins', category=(MetricCategory.economy), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='coins_from_coin_bonuses', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='free_attack_upgrades', label='Free Attack Upgrade', description='Battle Report utility breakdown: free attack upgrades.', unit='upgrades', category=(MetricCategory.economy), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='free_attack_upgrades', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='free_defense_upgrades', label='Free Defense Upgrade', description='Battle Report utility breakdown: free defense upgrades.', unit='upgrades', category=(MetricCategory.economy), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='free_defense_upgrades', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='free_utility_upgrades', label='Free Utility Upgrade', description='Battle Report utility breakdown: free utility upgrades.', unit='upgrades', category=(MetricCategory.economy), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='free_utility_upgrades', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='free_upgrades_total', label='Free Upgrades (Total)', description='Derived total of free attack, defense, and utility upgrades.', unit='upgrades', category=(MetricCategory.economy), kind='derived', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='free_attack_upgrades + free_defense_upgrades + free_utility_upgrades', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='recovery_packages', label='Recovery Packages', description='Battle Report derived metrics: recovery packages.', unit='count', category=(MetricCategory.utility), kind='derived', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='recovery_packages', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='coins_from_other_sources', label='Other coins', description='Residual coins not covered by named sources; ensures sources sum to total coins earned.', unit='coins', category=(MetricCategory.economy), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='coins_from_other_sources', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='guardian_damage', label='Guardian Damage', description='Battle Report Guardian section: damage dealt by the Guardian.', unit='damage', category=(MetricCategory.combat), kind='observed', source_model='BattleReport', aggregation='avg', time_index='timestamp', value_field='guardian_damage', allowed_transforms=(frozenset({'none', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='guardian_summoned_enemies', label='Guardian Summoned Enemies', description='Battle Report Guardian section: summoned enemies count.', unit='count', category=(MetricCategory.combat), kind='observed', source_model='BattleReport', aggregation='avg', time_index='timestamp', value_field='guardian_summoned_enemies', allowed_transforms=(frozenset({'none', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='guardian_coins_stolen', label='Guardian coins stolen', description='Battle Report Guardian section: coins stolen (rolls up into Coins Earned by Source).', unit='coins', category=(MetricCategory.economy), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='guardian_coins_stolen', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='guardian_coins_fetched', label='Coins Fetched', description='Battle Report Guardian section: coins fetched (rolls up into Coins Earned by Source).', unit='coins', category=(MetricCategory.economy), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='guardian_coins_fetched', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='guardian_gems_fetched', label='Gems', description='Battle Report Guardian section: gems fetched.', unit='count', category=(MetricCategory.fetch), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='guardian_gems_fetched', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='guardian_medals_fetched', label='Medals', description='Battle Report Guardian section: medals fetched.', unit='count', category=(MetricCategory.fetch), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='guardian_medals_fetched', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='guardian_reroll_shards_fetched', label='Reroll Shards', description='Battle Report Guardian section: reroll shards fetched.', unit='count', category=(MetricCategory.fetch), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='guardian_reroll_shards_fetched', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='guardian_cannon_shards_fetched', label='Cannon Shards', description='Battle Report Guardian section: cannon shards fetched.', unit='count', category=(MetricCategory.fetch), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='guardian_cannon_shards_fetched', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='guardian_armor_shards_fetched', label='Armor Shards', description='Battle Report Guardian section: armor shards fetched.', unit='count', category=(MetricCategory.fetch), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='guardian_armor_shards_fetched', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='guardian_generator_shards_fetched', label='Generator Shards', description='Battle Report Guardian section: generator shards fetched.', unit='count', category=(MetricCategory.fetch), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='guardian_generator_shards_fetched', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='guardian_core_shards_fetched', label='Core Shards', description='Battle Report Guardian section: core shards fetched.', unit='count', category=(MetricCategory.fetch), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='guardian_core_shards_fetched', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='guardian_common_modules_fetched', label='Common Modules', description='Battle Report Guardian section: common modules fetched.', unit='count', category=(MetricCategory.fetch), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='guardian_common_modules_fetched', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='guardian_rare_modules_fetched', label='Rare Modules', description='Battle Report Guardian section: rare modules fetched.', unit='count', category=(MetricCategory.fetch), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='guardian_rare_modules_fetched', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='flame_bot_damage', label='Flame Bot Damage', description='Battle Report bot section: damage dealt by Flame Bot.', unit='damage', category=(MetricCategory.damage), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='flame_bot_damage', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='thunder_bot_stuns', label='Thunder Bot Stuns', description='Battle Report bot section: stuns delivered by Thunder Bot.', unit='count', category=(MetricCategory.combat), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='thunder_bot_stuns', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS), MetricSeriesSpec(key='golden_bot_coins_earned', label='Golden Bot Coins Earned', description='Battle Report bot section: coins earned by Golden Bot.', unit='coins', category=(MetricCategory.economy), kind='observed', source_model='BattleReport', aggregation='sum', time_index='timestamp', value_field='golden_bot_coins_earned', allowed_transforms=(frozenset({'none', 'cumulative', 'moving_average'})), supported_filters=_COMMON_FILTERS))) module-attribute

FilterKey = Literal['date_range', 'tier', 'preset', 'uw', 'guardian', 'bot'] module-attribute

METRIC_AGGREGATION_OVERRIDES = {'guardian_damage': ('sum', 'avg'), 'guardian_summoned_enemies': ('sum', 'avg'), 'flame_bot_damage': ('sum', 'avg'), 'thunder_bot_stuns': ('sum', 'avg'), 'golden_bot_coins_earned': ('sum', 'avg')} module-attribute

MetricTransform = Literal['none', 'moving_average', 'cumulative', 'rate_per_hour'] module-attribute

SourceModel = Literal['BattleReport', 'RunCombat', 'RunGuardian', 'RunBots'] module-attribute

TimeIndex = Literal['timestamp', 'wave_number'] module-attribute

_COMMON_FILTERS = frozenset({'date_range', 'tier', 'preset'}) module-attribute

FormulaInspection dataclass

Result of inspecting a derived-metric formula.

Parameters:

Name Type Description Default
referenced_metric_keys frozenset[str]

Metric keys referenced by name in the formula.

required
unknown_identifiers frozenset[str]

Identifiers present in the formula that are not registered metric keys.

required
is_valid_syntax bool

Whether the formula parses as a Python expression.

required
is_safe bool

Whether the formula uses only the supported safe expression subset.

required

MetricCategory

Bases: StrEnum

Semantic category for a metric.

Values are stable identifiers used across the analysis registry and UI.

MetricSeriesRegistry

Lookup and validation helpers for metric series definitions.

__init__(specs)

Initialize a registry from a collection of specs.

formula_metric_keys(formula)

Return metric keys referenced by a derived formula.

Prefer inspect_formula when validation needs unknown identifier detection and safety guarantees.

get(key)

Return a spec for a metric key, or None when missing.

inspect_formula(formula)

Inspect a derived-metric formula for identifiers and safety.

The formula language matches analysis.derived_formula.evaluate_formula: constants, metric-key identifiers, unary +/- and binary + - * / only.

Parameters:

Name Type Description Default
formula str

An expression containing metric keys as variable names.

required

Returns:

Type Description
FormulaInspection

FormulaInspection describing referenced/unknown identifiers and whether

FormulaInspection

the expression is syntactically valid and safe.

list()

Return all specs in a stable order.

MetricSeriesSpec dataclass

Describe a chartable metric series.

Parameters:

Name Type Description Default
key str

Stable metric key referenced by ChartConfig.metric_series.metric_key.

required
label str

Human-friendly label.

required
description str | None

Optional description for Chart Builder / UI tooltips.

required
unit str

Display unit string.

required
category MetricCategory

Semantic category used for registry validation.

required
kind str

Either "observed" or "derived".

required
source_model SourceModel

Source model family backing the series.

required
aggregation Aggregation

Default aggregation used by dashboard charts.

required
time_index TimeIndex

The x-axis index used when charting the series.

required
value_field str

Field name or computed accessor label.

required
allowed_transforms frozenset[MetricTransform]

Allowed transforms for this metric.

required
supported_filters frozenset[FilterKey]

Filter dimensions supported by this metric.

required

allowed_chart_builder_aggregations(spec)

Return the allowed aggregations for Chart Builder selections.

Parameters:

Name Type Description Default
spec MetricSeriesSpec

MetricSeriesSpec to evaluate.

required

Returns:

Type Description
tuple[Aggregation, ...]

Tuple of allowed aggregation keys for Chart Builder use.

analysis.units

Unit validation and formatting helpers.

Phase 6 introduces a strict unit contract for any metric that is displayed in dashboards. Unlike analysis.quantity.parse_quantity, this module is allowed to fail fast when a value violates the expected unit contract.

Quantity dataclass

A parsed quantity with both raw and normalized representations.

Attributes:

Name Type Description
raw_value str

The original raw string value (trimmed).

normalized_value Decimal | None

The parsed numeric value as a Decimal, or None if the value could not be parsed.

magnitude str | None

The compact magnitude suffix (e.g. k, m, b, t, q), or None when not applicable.

unit_type UnitType

The category of unit this value represents.

UnitContract dataclass

Contract describing the expected unit type for a parsed value.

Parameters:

Name Type Description Default
unit_type UnitType

Expected UnitType for the value.

required
allow_zero bool

Whether a numeric zero is considered valid.

True

UnitType

Bases: Enum

Supported unit categories for Phase 1.5.

UnitValidationError

Bases: ValueError

Raised when a quantity does not satisfy a unit contract.

__init__(*, raw_value, expected, actual)

Initialize the error.

Parameters:

Name Type Description Default
raw_value str

Original raw input.

required
expected UnitType

Expected UnitType contract.

required
actual UnitType

Parsed UnitType from the raw value.

required

ValidatedQuantity dataclass

A validated quantity value that satisfies a UnitContract.

Parameters:

Name Type Description Default
raw_value str

Raw value string (trimmed).

required
normalized_value Decimal

Parsed Decimal value.

required
unit_type UnitType

UnitType that was validated against the contract.

required
magnitude str | None

Magnitude suffix (k/m/b/t/q) when present.

required

coerce_non_negative_int(quantity)

Coerce a parsed Quantity to a non-negative integer if possible.

Parameters:

Name Type Description Default
quantity Quantity

Quantity returned from parsing routines.

required

Returns:

Type Description
int | None

An int when normalized_value is present and non-negative; otherwise None.

parse_quantity(raw_value, *, unit_type=UnitType.count)

Parse a compact quantity string into a normalized Decimal.

Parameters:

Name Type Description Default
raw_value str

Raw value string (e.g. 7.67M, x1.15, 15%).

required
unit_type UnitType

Unit category to assign for non-annotated values.

count

Returns:

Type Description
Quantity

Quantity where normalized_value is None when parsing fails.

Notes
  • A leading x forces unit_type=multiplier and parses the remainder.
  • A trailing % forces unit_type=multiplier and normalizes as a fraction (e.g. 15% -> 0.15).
  • Magnitude suffixes are case-insensitive except for Q (quintillion).
  • Supported suffixes include lowercase k..q and uppercase Q.

parse_validated_quantity(raw_value, *, contract)

Parse and validate a quantity string against a strict unit contract.

Parameters:

Name Type Description Default
raw_value str

Raw Battle Report value (e.g. 7.67M, $55.90M, x1.15, 15%).

required
contract UnitContract

UnitContract describing the expected unit type.

required

Returns:

Type Description
ValidatedQuantity

ValidatedQuantity with a non-None Decimal value.

Raises:

Type Description
UnitValidationError

When the parsed unit type does not match the contract.

ValueError

When the value cannot be parsed into a numeric Decimal.

analysis.uw_sync

Deterministic Ultimate Weapon sync timeline helpers.

This module provides descriptive calculations for cooldown/duration alignment. It does not recommend timing changes and does not require database access.

UWSyncTimeline dataclass

Computed sync timeline suitable for charting.

Parameters:

Name Type Description Default
labels list[str]

Timeline labels ("t=0s", ...).

required
active_by_uw dict[str, list[int]]

Mapping of UW name to 0/1 activity list aligned to labels.

required
overlap_all list[int]

0/1 list aligned to labels where all entries are active.

required
overlap_percent_cumulative list[float]

Cumulative overlap percent (0–100) aligned to labels.

required
horizon_seconds int

Total modeled horizon in seconds.

required

UWTiming dataclass

Timing inputs for a single Ultimate Weapon.

Parameters:

Name Type Description Default
name str

Display name.

required
cooldown_seconds int

Cooldown in seconds (must be > 0).

required
duration_seconds int

Active duration in seconds (must be >= 0).

required

_lcm(a, b)

Return least common multiple for positive integers.

compute_uw_sync_timeline(timings, *, overlap_excluded_names=frozenset(), max_horizon_seconds=1800, step_seconds=1)

Compute a descriptive sync timeline for a set of UWs.

Parameters:

Name Type Description Default
timings Iterable[UWTiming]

Iterable of UWTiming entries (typically 3).

required
overlap_excluded_names frozenset[str]

UW names excluded from overlap calculations (e.g. Death Wave).

frozenset()
max_horizon_seconds int

Upper bound for the modeled horizon.

1800
step_seconds int

Step size in seconds for timeline sampling.

1

Returns:

Type Description
UWSyncTimeline

UWSyncTimeline suitable for rendering with a line/step chart.

Raises:

Type Description
ValueError

When timings are invalid (non-positive cooldowns, negative durations).

analysis.uw_usage

Observed Ultimate Weapon usage detection from Battle Report text.

This module provides a deterministic, best-effort mapping from Ultimate Weapon names to Battle Report metrics that can be used as evidence that a weapon was active during a run.

It intentionally stays in the analysis layer: - pure (no Django imports, no DB writes), - defensive on missing labels (missing -> inactive), - testable (stable inputs/outputs).

_UW_RULES_BY_NAME = {'black hole': UWUsageRule(label='Black Hole Damage', unit_type=(UnitType.damage)), 'chain lightning': UWUsageRule(label='Chain Lightning Damage', unit_type=(UnitType.damage)), 'death wave': UWUsageRule(label='Death Wave Damage', unit_type=(UnitType.damage)), 'golden tower': UWUsageRule(label='Coins From Golden Tower', unit_type=(UnitType.coins)), 'inner land mines': UWUsageRule(label='Inner Land Mine Damage', unit_type=(UnitType.damage)), 'poison swamp': UWUsageRule(label='Swamp Damage', unit_type=(UnitType.damage)), 'smart missiles': UWUsageRule(label='Smart Missile Damage', unit_type=(UnitType.damage)), 'spotlight': UWUsageRule(label='Destroyed in Spotlight', unit_type=(UnitType.count))} module-attribute

UWUsageRule dataclass

Rule describing how to infer UW activity from a Battle Report.

Parameters:

Name Type Description Default
label str

Exact Battle Report label to read.

required
unit_type UnitType

Unit category to validate when parsing the value.

required

UnitType

Bases: Enum

Supported unit categories for Phase 1.5.

_title_case_uw_name(name_casefold)

Return the canonical display casing for known Ultimate Weapon names.

Parameters:

Name Type Description Default
name_casefold str

Casefolded Ultimate Weapon name.

required

Returns:

Type Description
str

Display name matching the known title case stored in definitions.

extract_numeric_value(raw_text, *, label, unit_type)

Extract and parse a numeric value for a specific Battle Report label.

Parameters:

Name Type Description Default
raw_text str

Raw Battle Report text.

required
label str

Exact label as shown in Battle Reports.

required
unit_type UnitType

Expected unit type for strict validation.

required

Returns:

Type Description
ExtractedNumber | None

ExtractedNumber when the label is present and parseable; otherwise None.

Notes

The parsing rules come from analysis.quantity.parse_quantity. This wrapper additionally enforces that the raw string cannot represent a different unit type (e.g. 15% for a coins metric).

is_ultimate_weapon_observed_active(raw_text, *, ultimate_weapon_name)

Return True when a Battle Report shows evidence of an Ultimate Weapon being active.

Parameters:

Name Type Description Default
raw_text str

Raw Battle Report text as imported/stored.

required
ultimate_weapon_name str

Display name for the Ultimate Weapon (e.g. "Black Hole").

required

Returns:

Type Description
bool

True when the mapped Battle Report metric parses and is > 0; otherwise False.

observed_active_ultimate_weapons(raw_text)

Return a set of Ultimate Weapon names observed as active in a Battle Report.

Parameters:

Name Type Description Default
raw_text str

Raw Battle Report text as imported/stored.

required

Returns:

Type Description
frozenset[str]

Frozen set of Ultimate Weapon display names that appear active in the run.