054a912357
Staging served `kits=2 kitsHome=0 kitsAway=0`: the split compared the catalog `asset_id` against `fcc_kitcards.assetid` (14/15), but a kit's catalog `asset_id` IS its carddbid (6300006), not that column, so neither family ever matched. The carddbid range is the same fact in the form we actually carry: across all 1482 kit rows, assetid 14 covers precisely the 828 `63xxxxx` ids and assetid 15 precisely the 654 `64xxxxx` ids, with no exceptions either way. Keying on the id we already have avoids carrying `assetid` as a second source of truth for the same split. Tests now use the real team-21 pair (6300006 home / 6400003 away). Verified against the staging stack: `kits=2 kitsHome=1 kitsAway=1`, team-21 bucket 2, `?type=kit` still activeHomeKit/activeAwayKit, `?type=player` 12.
571 lines
20 KiB
Rust
571 lines
20 KiB
Rust
//! FIFA17 MY CLUB stat set (`GET …/club/stats/{year,consumables}`), computed
|
|
//! from OpenFUT Core's authoritative owned inventory.
|
|
//!
|
|
//! Faithful port of the Python oracle's `fut_club_stats.py` (`global_counts` +
|
|
//! `context_rows` + `stats_body`), which is itself censused from CardsDLL
|
|
//! (`FUN_18012fd40` atom table). The body is `{"stat":[{contextId,contextValue,
|
|
//! type,typeValue}, …]}`:
|
|
//! * a GLOBAL bucket (contextId 1, contextValue 0) with player tier counts,
|
|
//! staff/consumable families, owned-kit count, and honest zeros for other
|
|
//! club-item families;
|
|
//! * per-NATION buckets (contextId 3, contextValue = nation id) with the tier
|
|
//! counts the MY CLUB summary panel sums into PLAYERS_EMPLOYED.
|
|
//!
|
|
//! Unlike the oracle (which counts its own stale profile + a synthetic consumable
|
|
//! shelf), this counts Core — so staff and consumable families reflect the real
|
|
//! imported content. Unrecognized atoms are inert in the client, so this is a
|
|
//! low-risk cosmetic surface; the tier/staff/consumable atoms are the ones the
|
|
//! screen reads and they are Core-accurate here.
|
|
|
|
use std::collections::BTreeMap;
|
|
|
|
use serde_json::{json, Value};
|
|
|
|
use crate::fut::content_taxonomy::{consumable_family, ContentKind};
|
|
|
|
/// One owned item, already classified from the catalog + entity tables by the
|
|
/// host. `subtype`/`rare`/`asset_id` come from the FIFA catalog; `nation_id`/
|
|
/// `league_id`/`team_id` from the reverse entity resolver for players and from
|
|
/// the kit table for kits (None = unresolved, bucket skipped).
|
|
#[derive(Debug, Clone)]
|
|
pub struct ClubStatInput {
|
|
pub kind: ContentKind,
|
|
pub subtype: i64,
|
|
pub rating: i64,
|
|
pub rare: bool,
|
|
/// Base FIFA asset id. For a kit this is the `fcc_kitcards.assetid` family
|
|
/// discriminator, which is what splits the home/away kit counters.
|
|
pub asset_id: i64,
|
|
pub nation_id: Option<i64>,
|
|
pub league_id: Option<i64>,
|
|
pub team_id: Option<i64>,
|
|
}
|
|
|
|
/// Which entity the per-context (`contextId 3`) buckets are keyed by — the FIFA
|
|
/// `MY CLUB` sub-screen selector (`fut_club_stats.py::context_rows`):
|
|
/// * `Nation` — the default screen (year/consumables/club/newcards): nation buckets.
|
|
/// * `League` — URL `club/stats/country/<id>`: league (leagueId) buckets, tier stats.
|
|
/// * `Team` — URL `club/stats/league/<id>`: team (teamid) buckets, players/kits/badge.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum ContextField {
|
|
Nation,
|
|
League,
|
|
Team,
|
|
}
|
|
|
|
// Stat ids (CardsDLL atom table, fut_club_stats.py VOCAB).
|
|
const S_PLAYERS: i64 = 0x01;
|
|
const S_BRONZE: i64 = 0x02;
|
|
const S_SILVER: i64 = 0x03;
|
|
const S_GOLD: i64 = 0x04;
|
|
const S_RARE: i64 = 0x05;
|
|
const S_STAFF: i64 = 0x0A;
|
|
const S_CONSUMABLES: i64 = 0x3C;
|
|
const S_KITS: i64 = 0x28;
|
|
const S_KITS_HOME: i64 = 0x29;
|
|
const S_KITS_AWAY: i64 = 0x2A;
|
|
const S_BADGES: i64 = 0x2D;
|
|
|
|
/// First `carddbid` of the AWAY kit family. `fcc_kitcards` is split into a
|
|
/// `63xxxxx` home family and a `64xxxxx` away family, and the table's own
|
|
/// `assetid` column agrees exactly: across all 1482 rows, assetid 14 covers
|
|
/// precisely the 828 `63xxxxx` ids and assetid 15 precisely the 654 `64xxxxx`
|
|
/// ids, with no exceptions either way. A kit's catalog `asset_id` IS its
|
|
/// carddbid, so the id itself is the family key -- the `assetid` column is not
|
|
/// carried on the wire and would be a second source of truth for the same fact.
|
|
const KIT_AWAY_FLOOR: i64 = 6_400_000;
|
|
|
|
/// Which kit family an owned kit belongs to. Only meaningful for
|
|
/// [`ContentKind::Kit`]; the caller filters first.
|
|
fn is_home_kit(kit: &ClubStatInput) -> bool {
|
|
kit.asset_id < KIT_AWAY_FLOOR
|
|
}
|
|
|
|
/// cardsubtypeid (staff family) -> stat id (STAFF_SUBTYPE_STAT).
|
|
fn staff_stat(subtype: i64) -> Option<i64> {
|
|
match subtype {
|
|
4 => Some(0x0B), // manager
|
|
5 => Some(0x0C), // head coach
|
|
6 => Some(0x0D), // GK coach
|
|
7 => Some(0x0E), // physio
|
|
8 => Some(0x0F), // fitness coach
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
/// consumable family `kind` -> stat id (CONSUMABLE_KIND_STAT).
|
|
fn consumable_stat(kind: &str) -> Option<i64> {
|
|
Some(match kind {
|
|
"player_contract" => 0x42,
|
|
"manager_contract" => 0x47,
|
|
"healing" => 0x41,
|
|
"player_fitness" => 0x44,
|
|
"squad_fitness" => 0x4A,
|
|
"gk_training" => 0x46,
|
|
"player_training" => 0x43,
|
|
"position_mod" => 0x45,
|
|
"player_playstyle" => 0x4B,
|
|
"gk_playstyle" => 0x4C,
|
|
"manager_league" => 0x4D,
|
|
"manager_formation_mod" | "formation_mod" => 0x48,
|
|
_ => return None,
|
|
})
|
|
}
|
|
|
|
/// The JSON `type` atom name for a stat id (VOCAB). Only the ids this module
|
|
/// emits are mapped; an unmapped id would panic (guards a transcription slip).
|
|
fn vocab(stat_id: i64) -> &'static str {
|
|
match stat_id {
|
|
0x01 => "players",
|
|
0x02 => "playersBronze",
|
|
0x03 => "playersSilver",
|
|
0x04 => "playersGold",
|
|
0x05 => "rarePlayers",
|
|
0x0A => "staff",
|
|
0x0B => "staffManager",
|
|
0x0C => "staffHeadCoach",
|
|
0x0D => "staffGKCoach",
|
|
0x0E => "staffPhysio",
|
|
0x0F => "staffFitnessCoach",
|
|
0x14 => "stadia",
|
|
0x1E => "balls",
|
|
0x28 => "kits",
|
|
0x29 => "kitsHome",
|
|
0x2A => "kitsAway",
|
|
0x2D => "badges",
|
|
0x2E => "badgeDBid",
|
|
0x2F => "leagueLogos",
|
|
0x32 => "trophies",
|
|
0x33 => "trophiesOffline",
|
|
0x34 => "trophiesOnline",
|
|
0x35 => "trophiesFeaturedOffline",
|
|
0x36 => "trophiesFeaturedOnline",
|
|
0x37 => "trophiesSeasonOffline",
|
|
0x38 => "trophiesSeasonOnline",
|
|
0x3C => "consumables",
|
|
0x41 => "consumablesHealing",
|
|
0x42 => "consumablesContractPlayer",
|
|
0x43 => "consumablesTrainingPlayer",
|
|
0x44 => "consumablesFitnessPlayer",
|
|
0x45 => "consumablesPosition",
|
|
0x46 => "consumablesTrainingGk",
|
|
0x47 => "consumablesContractManager",
|
|
0x48 => "consumablesFormationManager",
|
|
0x49 => "consumablesTrainingManager",
|
|
0x4A => "consumablesFitnessTeam",
|
|
0x4B => "consumablesTrainingPlayerPlayStyle",
|
|
0x4C => "consumablesTrainingGkPlayStyle",
|
|
0x4D => "consumablesTrainingManagerLeagueModifier",
|
|
other => panic!("club_stats: unmapped stat id {other:#x}"),
|
|
}
|
|
}
|
|
|
|
fn row(context_id: i64, context_value: i64, stat_id: i64, value: i64) -> Value {
|
|
json!({
|
|
"contextId": context_id,
|
|
"contextValue": context_value,
|
|
"type": vocab(stat_id),
|
|
"typeValue": value,
|
|
})
|
|
}
|
|
|
|
fn is_player(i: &ClubStatInput) -> bool {
|
|
matches!(i.kind, ContentKind::Player)
|
|
}
|
|
|
|
/// Build the full `{"stat":[…]}` body for a club/stats screen — the global bucket
|
|
/// (identical for every mode) plus per-context buckets keyed by `ctx`
|
|
/// (nation / league / team), mirroring `fut_club_stats.py::stats_body`.
|
|
pub fn club_stats_body(items: &[ClubStatInput], ctx: ContextField) -> Value {
|
|
// ---- global bucket (contextId 1, contextValue 0), sorted by stat id ----
|
|
let mut g: BTreeMap<i64, i64> = BTreeMap::new();
|
|
let players: Vec<&ClubStatInput> = items.iter().filter(|i| is_player(i)).collect();
|
|
g.insert(S_PLAYERS, players.len() as i64);
|
|
g.insert(
|
|
S_GOLD,
|
|
players.iter().filter(|i| i.rating >= 75).count() as i64,
|
|
);
|
|
g.insert(
|
|
S_SILVER,
|
|
players
|
|
.iter()
|
|
.filter(|i| (65..75).contains(&i.rating))
|
|
.count() as i64,
|
|
);
|
|
g.insert(
|
|
S_BRONZE,
|
|
players
|
|
.iter()
|
|
.filter(|i| i.rating > 0 && i.rating < 65)
|
|
.count() as i64,
|
|
);
|
|
g.insert(S_RARE, players.iter().filter(|i| i.rare).count() as i64);
|
|
|
|
// staff per family + total
|
|
for sid in [0x0B, 0x0C, 0x0D, 0x0E, 0x0F] {
|
|
g.insert(sid, 0);
|
|
}
|
|
let mut staff_total = 0i64;
|
|
for it in items
|
|
.iter()
|
|
.filter(|i| matches!(i.kind, ContentKind::Staff))
|
|
{
|
|
if let Some(sid) = staff_stat(it.subtype) {
|
|
*g.get_mut(&sid).unwrap() += 1;
|
|
staff_total += 1;
|
|
}
|
|
}
|
|
g.insert(S_STAFF, staff_total);
|
|
|
|
// consumables per family + total
|
|
for sid in [
|
|
0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, 0x4B, 0x4C, 0x4D,
|
|
] {
|
|
g.insert(sid, 0);
|
|
}
|
|
let mut cons_total = 0i64;
|
|
for it in items
|
|
.iter()
|
|
.filter(|i| matches!(i.kind, ContentKind::Consumable))
|
|
{
|
|
cons_total += 1;
|
|
if let Some((kind, _label)) = consumable_family(it.subtype) {
|
|
if let Some(sid) = consumable_stat(kind) {
|
|
*g.entry(sid).or_insert(0) += 1;
|
|
}
|
|
}
|
|
}
|
|
g.insert(S_CONSUMABLES, cons_total);
|
|
|
|
// Club items: kits are Core-owned and counted (total plus the home/away
|
|
// family split); unimplemented families stay honest zeros.
|
|
for sid in [
|
|
0x14, 0x1E, 0x2D, 0x2E, 0x2F, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38,
|
|
] {
|
|
g.entry(sid).or_insert(0);
|
|
}
|
|
let kits: Vec<&ClubStatInput> = items
|
|
.iter()
|
|
.filter(|item| matches!(item.kind, ContentKind::Kit))
|
|
.collect();
|
|
let home = kits.iter().filter(|kit| is_home_kit(kit)).count() as i64;
|
|
g.insert(S_KITS, kits.len() as i64);
|
|
g.insert(S_KITS_HOME, home);
|
|
g.insert(S_KITS_AWAY, kits.len() as i64 - home);
|
|
|
|
let mut stat: Vec<Value> = g.iter().map(|(sid, v)| row(1, 0, *sid, *v)).collect();
|
|
|
|
// ---- per-context buckets (contextId 3, contextValue = entity id) ----
|
|
// Nation/League read the tier set (gold/silver/bronze/rare/kits/badges);
|
|
// Team (the league screen) reads players/kits/badgeDBid. Mirrors context_rows.
|
|
let mut by_ctx: BTreeMap<i64, Vec<&ClubStatInput>> = BTreeMap::new();
|
|
for p in &players {
|
|
let id = match ctx {
|
|
ContextField::Nation => p.nation_id,
|
|
ContextField::League => p.league_id,
|
|
ContextField::Team => p.team_id,
|
|
};
|
|
if let Some(id) = id {
|
|
by_ctx.entry(id).or_default().push(p);
|
|
}
|
|
}
|
|
|
|
// A kit belongs to the team that wears it and has no nation/league of its
|
|
// own, so it only buckets on the team screen -- and it buckets there even if
|
|
// the club owns no player from that team, which is the normal case for a kit
|
|
// won from a pack.
|
|
let mut kits_by_team: BTreeMap<i64, i64> = BTreeMap::new();
|
|
if ctx == ContextField::Team {
|
|
for kit in &kits {
|
|
if let Some(id) = kit.team_id {
|
|
*kits_by_team.entry(id).or_insert(0) += 1;
|
|
by_ctx.entry(id).or_default();
|
|
}
|
|
}
|
|
}
|
|
|
|
for (cid, sel) in &by_ctx {
|
|
if ctx == ContextField::Team {
|
|
stat.push(row(3, *cid, S_PLAYERS, sel.len() as i64));
|
|
stat.push(row(
|
|
3,
|
|
*cid,
|
|
S_KITS,
|
|
kits_by_team.get(cid).copied().unwrap_or(0),
|
|
));
|
|
stat.push(row(3, *cid, 0x2E, 0)); // badgeDBid
|
|
} else {
|
|
let gold = sel.iter().filter(|i| i.rating >= 75).count() as i64;
|
|
let silver = sel.iter().filter(|i| (65..75).contains(&i.rating)).count() as i64;
|
|
let bronze = sel.iter().filter(|i| i.rating > 0 && i.rating < 65).count() as i64;
|
|
let rare = sel.iter().filter(|i| i.rare).count() as i64;
|
|
stat.push(row(3, *cid, S_GOLD, gold));
|
|
stat.push(row(3, *cid, S_SILVER, silver));
|
|
stat.push(row(3, *cid, S_BRONZE, bronze));
|
|
stat.push(row(3, *cid, S_RARE, rare));
|
|
stat.push(row(3, *cid, S_KITS, 0));
|
|
stat.push(row(3, *cid, S_BADGES, 0));
|
|
}
|
|
}
|
|
|
|
json!({ "stat": stat })
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
fn player(rating: i64, rare: bool, nation: Option<i64>) -> ClubStatInput {
|
|
ClubStatInput {
|
|
kind: ContentKind::Player,
|
|
subtype: 0,
|
|
rating,
|
|
rare,
|
|
asset_id: 158023,
|
|
nation_id: nation,
|
|
league_id: None,
|
|
team_id: None,
|
|
}
|
|
}
|
|
fn staff(subtype: i64) -> ClubStatInput {
|
|
ClubStatInput {
|
|
kind: ContentKind::Staff,
|
|
subtype,
|
|
rating: 0,
|
|
rare: false,
|
|
asset_id: 0,
|
|
nation_id: None,
|
|
league_id: None,
|
|
team_id: None,
|
|
}
|
|
}
|
|
fn consumable(subtype: i64) -> ClubStatInput {
|
|
ClubStatInput {
|
|
kind: ContentKind::Consumable,
|
|
subtype,
|
|
rating: 0,
|
|
rare: false,
|
|
asset_id: 0,
|
|
nation_id: None,
|
|
league_id: None,
|
|
team_id: None,
|
|
}
|
|
}
|
|
/// A kit worn by `team`. `carddbid` is the real `fcc_kitcards` id, which is
|
|
/// also the catalog `asset_id` and therefore the home/away family key.
|
|
fn kit_of(carddbid: i64, team: i64) -> ClubStatInput {
|
|
ClubStatInput {
|
|
kind: ContentKind::Kit,
|
|
subtype: 9,
|
|
rating: 0,
|
|
rare: false,
|
|
asset_id: carddbid,
|
|
nation_id: None,
|
|
league_id: None,
|
|
team_id: Some(team),
|
|
}
|
|
}
|
|
/// Real team-21 kits from `fcc_kitcards`: 6300006 is its home kit and
|
|
/// 6400003 its away kit.
|
|
const HOME_KIT: i64 = 6_300_006;
|
|
const AWAY_KIT: i64 = 6_400_003;
|
|
|
|
fn kit() -> ClubStatInput {
|
|
kit_of(HOME_KIT, 21)
|
|
}
|
|
|
|
fn global(body: &Value) -> std::collections::HashMap<String, i64> {
|
|
body["stat"]
|
|
.as_array()
|
|
.unwrap()
|
|
.iter()
|
|
.filter(|r| r["contextId"] == 1)
|
|
.map(|r| {
|
|
(
|
|
r["type"].as_str().unwrap().to_string(),
|
|
r["typeValue"].as_i64().unwrap(),
|
|
)
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
#[test]
|
|
fn tiers_and_rare_counted() {
|
|
let items = vec![
|
|
player(90, true, Some(52)),
|
|
player(70, true, Some(52)),
|
|
player(60, false, Some(21)),
|
|
];
|
|
let g = global(&club_stats_body(&items, ContextField::Nation));
|
|
assert_eq!(g["players"], 3);
|
|
assert_eq!(g["playersGold"], 1);
|
|
assert_eq!(g["playersSilver"], 1);
|
|
assert_eq!(g["playersBronze"], 1);
|
|
assert_eq!(g["rarePlayers"], 2);
|
|
}
|
|
|
|
#[test]
|
|
fn staff_by_family() {
|
|
let items = vec![staff(6), staff(8), staff(8)]; // 1 gk coach, 2 fitness
|
|
let g = global(&club_stats_body(&items, ContextField::Nation));
|
|
assert_eq!(g["staffGKCoach"], 1);
|
|
assert_eq!(g["staffFitnessCoach"], 2);
|
|
assert_eq!(g["staff"], 3);
|
|
assert_eq!(g["staffManager"], 0);
|
|
}
|
|
|
|
#[test]
|
|
fn consumables_by_family() {
|
|
// 54 gk_training, 201 player_contract, 217 healing, 258 player_playstyle
|
|
let items = vec![
|
|
consumable(54),
|
|
consumable(201),
|
|
consumable(217),
|
|
consumable(258),
|
|
];
|
|
let g = global(&club_stats_body(&items, ContextField::Nation));
|
|
assert_eq!(g["consumables"], 4);
|
|
assert_eq!(g["consumablesTrainingGk"], 1);
|
|
assert_eq!(g["consumablesContractPlayer"], 1);
|
|
assert_eq!(g["consumablesHealing"], 1);
|
|
assert_eq!(g["consumablesTrainingPlayerPlayStyle"], 1);
|
|
}
|
|
|
|
#[test]
|
|
fn owned_kits_increment_global_kit_count() {
|
|
let items = vec![player(90, false, None), kit(), kit()];
|
|
let g = global(&club_stats_body(&items, ContextField::Nation));
|
|
assert_eq!(g["players"], 1);
|
|
assert_eq!(g["kits"], 2);
|
|
}
|
|
|
|
/// `kits` is the total and `kitsHome`/`kitsAway` are its family split, the
|
|
/// same total/subset shape as players/playersGold and staff/staffManager.
|
|
#[test]
|
|
fn kit_counts_split_by_home_and_away_family() {
|
|
let items = vec![
|
|
kit_of(HOME_KIT, 21),
|
|
kit_of(6_300_010, 38),
|
|
kit_of(AWAY_KIT, 21),
|
|
];
|
|
let g = global(&club_stats_body(&items, ContextField::Nation));
|
|
assert_eq!(g["kits"], 3);
|
|
assert_eq!(g["kitsHome"], 2);
|
|
assert_eq!(g["kitsAway"], 1);
|
|
}
|
|
|
|
/// A kit buckets onto the team that wears it -- including a team the club
|
|
/// owns no player from, which is the normal case for a kit won from a pack.
|
|
#[test]
|
|
fn kits_bucket_onto_their_own_team_on_the_team_screen() {
|
|
let mut with_team = player(90, false, None);
|
|
with_team.team_id = Some(21);
|
|
let items = vec![
|
|
with_team,
|
|
kit_of(HOME_KIT, 21),
|
|
kit_of(AWAY_KIT, 21),
|
|
kit_of(6_300_010, 38),
|
|
];
|
|
let body = club_stats_body(&items, ContextField::Team);
|
|
let kits_for = |team: i64| {
|
|
body["stat"]
|
|
.as_array()
|
|
.unwrap()
|
|
.iter()
|
|
.find(|r| r["contextId"] == 3 && r["contextValue"] == team && r["type"] == "kits")
|
|
.map(|r| r["typeValue"].as_i64().unwrap())
|
|
};
|
|
assert_eq!(kits_for(21), Some(2));
|
|
// Team 38 has no players, so only the kit creates its bucket.
|
|
assert_eq!(kits_for(38), Some(1));
|
|
|
|
// A nation/league screen has no team context, so kits stay out of it.
|
|
let nation = club_stats_body(&items, ContextField::Nation);
|
|
assert!(nation["stat"]
|
|
.as_array()
|
|
.unwrap()
|
|
.iter()
|
|
.filter(|r| r["contextId"] == 3 && r["type"] == "kits")
|
|
.all(|r| r["typeValue"] == 0));
|
|
}
|
|
|
|
#[test]
|
|
fn nation_buckets_emitted_and_players_excludes_nonplayers() {
|
|
let items = vec![
|
|
player(90, true, Some(52)),
|
|
player(80, false, Some(52)),
|
|
staff(8),
|
|
];
|
|
let body = club_stats_body(&items, ContextField::Nation);
|
|
let g = global(&body);
|
|
assert_eq!(g["players"], 2, "staff not counted as player");
|
|
let buckets: Vec<&Value> = body["stat"]
|
|
.as_array()
|
|
.unwrap()
|
|
.iter()
|
|
.filter(|r| r["contextId"] == 3 && r["contextValue"] == 52)
|
|
.collect();
|
|
// gold, silver, bronze, rare, kits, badges
|
|
assert_eq!(buckets.len(), 6);
|
|
let gold = buckets.iter().find(|r| r["type"] == "playersGold").unwrap();
|
|
assert_eq!(gold["typeValue"], 2);
|
|
}
|
|
|
|
#[test]
|
|
fn honest_zero_club_items_present() {
|
|
let g = global(&club_stats_body(
|
|
&[player(90, false, None)],
|
|
ContextField::Nation,
|
|
));
|
|
for atom in [
|
|
"stadia",
|
|
"balls",
|
|
"kits",
|
|
"badges",
|
|
"trophies",
|
|
"leagueLogos",
|
|
] {
|
|
assert_eq!(g[atom], 0, "{atom} present as honest zero");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn league_and_team_context_modes() {
|
|
let mut a = player(90, true, Some(52));
|
|
a.league_id = Some(13);
|
|
a.team_id = Some(240);
|
|
let mut b = player(60, false, Some(52));
|
|
b.league_id = Some(13);
|
|
b.team_id = Some(9);
|
|
let items = vec![a, b];
|
|
|
|
// country screen -> league (leagueId) buckets, tier set (6 rows).
|
|
let body = club_stats_body(&items, ContextField::League);
|
|
let league_rows: Vec<&Value> = body["stat"]
|
|
.as_array()
|
|
.unwrap()
|
|
.iter()
|
|
.filter(|r| r["contextId"] == 3 && r["contextValue"] == 13)
|
|
.collect();
|
|
assert_eq!(league_rows.len(), 6);
|
|
let gold = league_rows
|
|
.iter()
|
|
.find(|r| r["type"] == "playersGold")
|
|
.unwrap();
|
|
assert_eq!(gold["typeValue"], 1);
|
|
|
|
// league screen -> team (teamid) buckets: players/kits/badgeDBid (3 rows).
|
|
let body = club_stats_body(&items, ContextField::Team);
|
|
let team_rows: Vec<&Value> = body["stat"]
|
|
.as_array()
|
|
.unwrap()
|
|
.iter()
|
|
.filter(|r| r["contextId"] == 3 && r["contextValue"] == 240)
|
|
.collect();
|
|
assert_eq!(team_rows.len(), 3);
|
|
let players = team_rows.iter().find(|r| r["type"] == "players").unwrap();
|
|
assert_eq!(players["typeValue"], 1);
|
|
assert!(team_rows.iter().any(|r| r["type"] == "badgeDBid"));
|
|
}
|
|
}
|