diff --git a/data/cards/eredivisie.json b/data/cards/eredivisie.json new file mode 100644 index 0000000..0437945 --- /dev/null +++ b/data/cards/eredivisie.json @@ -0,0 +1,10 @@ +[ + {"id":"card_er_001","name":"Daan van der Berg","overall":84,"position":"CAM","nation":"Netherlands","league":"Eredivisie","club":"Ajax Amsterdam","pace":80,"shooting":78,"passing":88,"dribbling":86,"defending":45,"physical":70,"rarity":"gold","image_path":null}, + {"id":"card_er_002","name":"Luca van Dijk","overall":83,"position":"CB","nation":"Netherlands","league":"Eredivisie","club":"PSV Eindhoven","pace":76,"shooting":44,"passing":70,"dribbling":66,"defending":87,"physical":84,"rarity":"gold","image_path":null}, + {"id":"card_er_003","name":"Joost Bakker","overall":82,"position":"ST","nation":"Netherlands","league":"Eredivisie","club":"Feyenoord Rotterdam","pace":84,"shooting":86,"passing":68,"dribbling":80,"defending":28,"physical":80,"rarity":"gold","image_path":null}, + {"id":"card_er_004","name":"Sven Bosman","overall":81,"position":"CM","nation":"Netherlands","league":"Eredivisie","club":"Ajax Amsterdam","pace":76,"shooting":68,"passing":84,"dribbling":78,"defending":66,"physical":74,"rarity":"gold","image_path":null}, + {"id":"card_er_005","name":"Tom Hendriks","overall":80,"position":"GK","nation":"Netherlands","league":"Eredivisie","club":"PSV Eindhoven","pace":55,"shooting":18,"passing":60,"dribbling":28,"defending":81,"physical":80,"rarity":"gold","image_path":null}, + {"id":"card_er_006","name":"Lars Kuipers","overall":79,"position":"RB","nation":"Netherlands","league":"Eredivisie","club":"Feyenoord Rotterdam","pace":84,"shooting":54,"passing":70,"dribbling":72,"defending":78,"physical":72,"rarity":"gold","image_path":null}, + {"id":"card_er_007","name":"Arjan Visser","overall":78,"position":"LW","nation":"Netherlands","league":"Eredivisie","club":"Ajax Amsterdam","pace":86,"shooting":72,"passing":70,"dribbling":82,"defending":32,"physical":62,"rarity":"gold","image_path":null}, + {"id":"card_er_008","name":"Pieter Smits","overall":77,"position":"CDM","nation":"Netherlands","league":"Eredivisie","club":"PSV Eindhoven","pace":72,"shooting":56,"passing":72,"dribbling":68,"defending":82,"physical":78,"rarity":"gold","image_path":null} +] diff --git a/data/cards/liga_nos.json b/data/cards/liga_nos.json new file mode 100644 index 0000000..bacccff --- /dev/null +++ b/data/cards/liga_nos.json @@ -0,0 +1,12 @@ +[ + {"id":"card_ln_001","name":"Diogo Ferreira","overall":85,"position":"ST","nation":"Portugal","league":"Primeira Liga","club":"Lisboa United","pace":82,"shooting":88,"passing":74,"dribbling":84,"defending":30,"physical":82,"rarity":"raregold","image_path":null}, + {"id":"card_ln_002","name":"Rúben Costa","overall":83,"position":"CAM","nation":"Portugal","league":"Primeira Liga","club":"Porto Sul","pace":78,"shooting":78,"passing":88,"dribbling":84,"defending":46,"physical":70,"rarity":"gold","image_path":null}, + {"id":"card_ln_003","name":"André Sousa","overall":82,"position":"CB","nation":"Portugal","league":"Primeira Liga","club":"Sporting Algarve","pace":72,"shooting":44,"passing":68,"dribbling":62,"defending":86,"physical":82,"rarity":"gold","image_path":null}, + {"id":"card_ln_004","name":"Tiago Fonseca","overall":81,"position":"CDM","nation":"Portugal","league":"Primeira Liga","club":"Braga Norte","pace":70,"shooting":56,"passing":74,"dribbling":70,"defending":84,"physical":80,"rarity":"gold","image_path":null}, + {"id":"card_ln_005","name":"João Mendes","overall":80,"position":"LB","nation":"Portugal","league":"Primeira Liga","club":"Lisboa United","pace":82,"shooting":54,"passing":72,"dribbling":74,"defending":80,"physical":72,"rarity":"gold","image_path":null}, + {"id":"card_ln_006","name":"Nuno Pinto","overall":79,"position":"GK","nation":"Portugal","league":"Primeira Liga","club":"Porto Sul","pace":52,"shooting":16,"passing":58,"dribbling":26,"defending":80,"physical":80,"rarity":"gold","image_path":null}, + {"id":"card_ln_007","name":"Gonçalo Lopes","overall":78,"position":"RW","nation":"Portugal","league":"Primeira Liga","club":"Sporting Algarve","pace":84,"shooting":72,"passing":68,"dribbling":80,"defending":32,"physical":64,"rarity":"gold","image_path":null}, + {"id":"card_ln_008","name":"Carlos Rodrigues","overall":77,"position":"CM","nation":"Portugal","league":"Primeira Liga","club":"Braga Norte","pace":74,"shooting":66,"passing":78,"dribbling":74,"defending":68,"physical":72,"rarity":"gold","image_path":null}, + {"id":"card_ln_009","name":"Mamadou Diallo","overall":80,"position":"ST","nation":"Senegal","league":"Primeira Liga","club":"Lisboa United","pace":90,"shooting":80,"passing":62,"dribbling":76,"defending":26,"physical":84,"rarity":"gold","image_path":null}, + {"id":"card_ln_010","name":"Bruno Fernandes Júnior","overall":76,"position":"LW","nation":"Portugal","league":"Primeira Liga","club":"Porto Sul","pace":82,"shooting":70,"passing":70,"dribbling":78,"defending":30,"physical":62,"rarity":"gold","image_path":null} +] diff --git a/data/cards/ligue1.json b/data/cards/ligue1.json new file mode 100644 index 0000000..c946f34 --- /dev/null +++ b/data/cards/ligue1.json @@ -0,0 +1,14 @@ +[ + {"id":"card_l1_001","name":"Antoine Mercier","overall":88,"position":"ST","nation":"France","league":"Ligue 1","club":"Paris Olympique","pace":85,"shooting":91,"passing":75,"dribbling":86,"defending":30,"physical":83,"rarity":"raregold","image_path":null}, + {"id":"card_l1_002","name":"Kylian Dupont","overall":86,"position":"CAM","nation":"France","league":"Ligue 1","club":"Marseille Olympia","pace":92,"shooting":80,"passing":84,"dribbling":90,"defending":38,"physical":70,"rarity":"raregold","image_path":null}, + {"id":"card_l1_003","name":"Théo Girard","overall":84,"position":"CM","nation":"France","league":"Ligue 1","club":"Lyon Premier","pace":76,"shooting":72,"passing":86,"dribbling":82,"defending":65,"physical":74,"rarity":"gold","image_path":null}, + {"id":"card_l1_004","name":"Hugo Blanc","overall":83,"position":"GK","nation":"France","league":"Ligue 1","club":"Paris Olympique","pace":54,"shooting":18,"passing":60,"dribbling":28,"defending":84,"physical":82,"rarity":"gold","image_path":null}, + {"id":"card_l1_005","name":"Alexis Bernard","overall":82,"position":"CB","nation":"France","league":"Ligue 1","club":"Marseille Olympia","pace":74,"shooting":42,"passing":66,"dribbling":62,"defending":86,"physical":84,"rarity":"gold","image_path":null}, + {"id":"card_l1_006","name":"Lucas Petit","overall":81,"position":"LB","nation":"France","league":"Ligue 1","club":"Lyon Premier","pace":84,"shooting":56,"passing":76,"dribbling":74,"defending":80,"physical":72,"rarity":"gold","image_path":null}, + {"id":"card_l1_007","name":"Mathieu Renard","overall":80,"position":"CDM","nation":"France","league":"Ligue 1","club":"Bordeaux Atlantique","pace":72,"shooting":58,"passing":74,"dribbling":70,"defending":83,"physical":80,"rarity":"gold","image_path":null}, + {"id":"card_l1_008","name":"Romain Favre","overall":79,"position":"RW","nation":"France","league":"Ligue 1","club":"Paris Olympique","pace":86,"shooting":74,"passing":70,"dribbling":84,"defending":36,"physical":64,"rarity":"gold","image_path":null}, + {"id":"card_l1_009","name":"Nicolas Aubert","overall":78,"position":"RB","nation":"France","league":"Ligue 1","club":"Marseille Olympia","pace":82,"shooting":52,"passing":70,"dribbling":72,"defending":80,"physical":72,"rarity":"gold","image_path":null}, + {"id":"card_l1_010","name":"Samuel Perrin","overall":77,"position":"LW","nation":"France","league":"Ligue 1","club":"Lyon Premier","pace":84,"shooting":70,"passing":68,"dribbling":80,"defending":34,"physical":62,"rarity":"gold","image_path":null}, + {"id":"card_l1_011","name":"Ibrahim Coulibaly","overall":81,"position":"ST","nation":"Ivory Coast","league":"Ligue 1","club":"Bordeaux Atlantique","pace":88,"shooting":82,"passing":65,"dribbling":80,"defending":28,"physical":82,"rarity":"gold","image_path":null}, + {"id":"card_l1_012","name":"Pedro Alves","overall":80,"position":"CAM","nation":"Portugal","league":"Ligue 1","club":"Paris Olympique","pace":80,"shooting":76,"passing":82,"dribbling":84,"defending":44,"physical":68,"rarity":"gold","image_path":null} +] diff --git a/data/cards/serie_a.json b/data/cards/serie_a.json new file mode 100644 index 0000000..780a5d6 --- /dev/null +++ b/data/cards/serie_a.json @@ -0,0 +1,17 @@ +[ + {"id":"card_sa_001","name":"Lorenzo Ricci","overall":87,"position":"ST","nation":"Italy","league":"Serie A","club":"Roma Capitale FC","pace":83,"shooting":90,"passing":72,"dribbling":84,"defending":32,"physical":85,"rarity":"raregold","image_path":null}, + {"id":"card_sa_002","name":"Marco Ferretti","overall":85,"position":"CAM","nation":"Italy","league":"Serie A","club":"Milano Azzurri","pace":80,"shooting":82,"passing":88,"dribbling":87,"defending":40,"physical":72,"rarity":"raregold","image_path":null}, + {"id":"card_sa_003","name":"Davide Conti","overall":83,"position":"CB","nation":"Italy","league":"Serie A","club":"Napoli Sud FC","pace":72,"shooting":45,"passing":68,"dribbling":65,"defending":87,"physical":84,"rarity":"gold","image_path":null}, + {"id":"card_sa_004","name":"Emanuele Vitali","overall":82,"position":"CM","nation":"Italy","league":"Serie A","club":"Roma Capitale FC","pace":74,"shooting":70,"passing":84,"dribbling":80,"defending":68,"physical":76,"rarity":"gold","image_path":null}, + {"id":"card_sa_005","name":"Filippo Marchetti","overall":84,"position":"GK","nation":"Italy","league":"Serie A","club":"Milano Azzurri","pace":55,"shooting":20,"passing":58,"dribbling":30,"defending":85,"physical":82,"rarity":"gold","image_path":null}, + {"id":"card_sa_006","name":"Gianluca Russo","overall":81,"position":"LB","nation":"Italy","league":"Serie A","club":"Torino Granata","pace":82,"shooting":52,"passing":74,"dribbling":76,"defending":80,"physical":74,"rarity":"gold","image_path":null}, + {"id":"card_sa_007","name":"Alessandro Greco","overall":80,"position":"RB","nation":"Italy","league":"Serie A","club":"Napoli Sud FC","pace":84,"shooting":55,"passing":72,"dribbling":74,"defending":78,"physical":72,"rarity":"gold","image_path":null}, + {"id":"card_sa_008","name":"Federico Palermo","overall":82,"position":"CDM","nation":"Italy","league":"Serie A","club":"Torino Granata","pace":70,"shooting":58,"passing":76,"dribbling":72,"defending":84,"physical":82,"rarity":"gold","image_path":null}, + {"id":"card_sa_009","name":"Simone Bianchi","overall":79,"position":"RW","nation":"Italy","league":"Serie A","club":"Roma Capitale FC","pace":88,"shooting":76,"passing":72,"dribbling":82,"defending":38,"physical":65,"rarity":"gold","image_path":null}, + {"id":"card_sa_010","name":"Roberto Neri","overall":78,"position":"LW","nation":"Italy","league":"Serie A","club":"Milano Azzurri","pace":86,"shooting":74,"passing":70,"dribbling":80,"defending":35,"physical":62,"rarity":"gold","image_path":null}, + {"id":"card_sa_011","name":"Matteo De Luca","overall":77,"position":"CB","nation":"Italy","league":"Serie A","club":"Napoli Sud FC","pace":68,"shooting":40,"passing":62,"dribbling":60,"defending":80,"physical":80,"rarity":"gold","image_path":null}, + {"id":"card_sa_012","name":"Claudio Esposito","overall":76,"position":"ST","nation":"Italy","league":"Serie A","club":"Torino Granata","pace":80,"shooting":78,"passing":62,"dribbling":74,"defending":28,"physical":76,"rarity":"gold","image_path":null}, + {"id":"card_sa_013","name":"Nicolás Peralta","overall":83,"position":"ST","nation":"Argentina","league":"Serie A","club":"Roma Capitale FC","pace":82,"shooting":86,"passing":70,"dribbling":82,"defending":30,"physical":80,"rarity":"gold","image_path":null}, + {"id":"card_sa_014","name":"Henrique Sousa","overall":80,"position":"CAM","nation":"Brazil","league":"Serie A","club":"Milano Azzurri","pace":82,"shooting":76,"passing":82,"dribbling":86,"defending":42,"physical":68,"rarity":"gold","image_path":null}, + {"id":"card_sa_015","name":"Sven Holm","overall":75,"position":"CM","nation":"Sweden","league":"Serie A","club":"Napoli Sud FC","pace":72,"shooting":65,"passing":78,"dribbling":74,"defending":70,"physical":72,"rarity":"gold","image_path":null} +] diff --git a/data/sbcs/league_sbcs.json b/data/sbcs/league_sbcs.json new file mode 100644 index 0000000..4e55c98 --- /dev/null +++ b/data/sbcs/league_sbcs.json @@ -0,0 +1,202 @@ +[ + { + "id": "sbc_serie_a_rising", + "name": "Serie A Rising Stars", + "description": "Build a squad using 7 or more Serie A players (average overall 74+).", + "requirements": { + "squad_size": 11, + "min_overall": 74, + "max_overall": null, + "min_chemistry": null, + "required_leagues": ["Serie A"], + "required_nations": [], + "required_clubs": [], + "min_players_from_same_league": 7, + "min_players_from_same_nation": null, + "min_players_from_same_club": null + }, + "reward": { "coins": 2000, "xp": 250, "pack_id": "gold_pack" }, + "expires_at": null, + "repeatable": false + }, + { + "id": "sbc_ligue1_galacticos", + "name": "Ligue 1 Galacticos", + "description": "Field a Ligue 1 XI with at least one player rated 84 or higher.", + "requirements": { + "squad_size": 11, + "min_overall": 72, + "max_overall": null, + "min_chemistry": null, + "required_leagues": ["Ligue 1"], + "required_nations": [], + "required_clubs": [], + "min_players_from_same_league": 11, + "min_players_from_same_nation": null, + "min_players_from_same_club": null + }, + "reward": { "coins": 3500, "xp": 400, "pack_id": "rare_gold_pack" }, + "expires_at": null, + "repeatable": false + }, + { + "id": "sbc_iberian_derby", + "name": "Iberian Derby", + "description": "An all-Iberian battle — include at least 3 Spanish and 3 Portuguese players.", + "requirements": { + "squad_size": 11, + "min_overall": 70, + "max_overall": null, + "min_chemistry": null, + "required_leagues": [], + "required_nations": ["Spain", "Portugal"], + "required_clubs": [], + "min_players_from_same_league": null, + "min_players_from_same_nation": 3, + "min_players_from_same_club": null + }, + "reward": { "coins": 2500, "xp": 300, "pack_id": "silver_pack" }, + "expires_at": null, + "repeatable": false + }, + { + "id": "sbc_dutch_masters", + "name": "Dutch Masters", + "description": "8 or more Eredivisie players in one squad (average overall 70+).", + "requirements": { + "squad_size": 11, + "min_overall": 70, + "max_overall": null, + "min_chemistry": null, + "required_leagues": ["Eredivisie"], + "required_nations": [], + "required_clubs": [], + "min_players_from_same_league": 8, + "min_players_from_same_nation": null, + "min_players_from_same_club": null + }, + "reward": { "coins": 2000, "xp": 250, "pack_id": "gold_pack" }, + "expires_at": null, + "repeatable": false + }, + { + "id": "sbc_elite_strikers", + "name": "Elite Strikers", + "description": "Submit any 3 players rated 82+ — the club receives a bonus pack.", + "requirements": { + "squad_size": 3, + "min_overall": 82, + "max_overall": null, + "min_chemistry": null, + "required_leagues": [], + "required_nations": [], + "required_clubs": [], + "min_players_from_same_league": null, + "min_players_from_same_nation": null, + "min_players_from_same_club": null + }, + "reward": { "coins": 1000, "xp": 200, "pack_id": "rare_gold_pack" }, + "expires_at": null, + "repeatable": true + }, + { + "id": "sbc_world_tour", + "name": "World Tour", + "description": "11 players from at least 8 different nations — celebrate global football.", + "requirements": { + "squad_size": 11, + "min_overall": 65, + "max_overall": null, + "min_chemistry": null, + "required_leagues": [], + "required_nations": [], + "required_clubs": [], + "min_players_from_same_league": null, + "min_players_from_same_nation": null, + "min_players_from_same_club": null + }, + "reward": { "coins": 1500, "xp": 200, "pack_id": "silver_pack" }, + "expires_at": null, + "repeatable": true + }, + { + "id": "sbc_bundesliga_clash", + "name": "Bundesliga Clash", + "description": "7+ Bundesliga players (average overall 72+) for a gold pack reward.", + "requirements": { + "squad_size": 11, + "min_overall": 72, + "max_overall": null, + "min_chemistry": null, + "required_leagues": ["Bundesliga"], + "required_nations": [], + "required_clubs": [], + "min_players_from_same_league": 7, + "min_players_from_same_nation": null, + "min_players_from_same_club": null + }, + "reward": { "coins": 2000, "xp": 250, "pack_id": "gold_pack" }, + "expires_at": null, + "repeatable": false + }, + { + "id": "sbc_premier_power", + "name": "Premier Power", + "description": "7+ Premier League players (average overall 75+) for a rare gold pack.", + "requirements": { + "squad_size": 11, + "min_overall": 75, + "max_overall": null, + "min_chemistry": null, + "required_leagues": ["Premier League"], + "required_nations": [], + "required_clubs": [], + "min_players_from_same_league": 7, + "min_players_from_same_nation": null, + "min_players_from_same_club": null + }, + "reward": { "coins": 3000, "xp": 350, "pack_id": "rare_gold_pack" }, + "expires_at": null, + "repeatable": false + }, + { + "id": "sbc_south_american_fire", + "name": "South American Fire", + "description": "At least 4 Brazilian and 4 Argentinian players in a squad.", + "requirements": { + "squad_size": 11, + "min_overall": 65, + "max_overall": null, + "min_chemistry": null, + "required_leagues": [], + "required_nations": ["Brazil", "Argentina"], + "required_clubs": [], + "min_players_from_same_league": null, + "min_players_from_same_nation": 4, + "min_players_from_same_club": null + }, + "reward": { "coins": 3000, "xp": 400, "pack_id": "silver_pack" }, + "expires_at": null, + "repeatable": false + }, + { + "id": "sbc_bronze_to_silver", + "name": "Bronze to Silver", + "description": "Submit 7 bronze players (overall 55–64) to get a silver pack.", + "requirements": { + "squad_size": 7, + "min_overall": 55, + "max_overall": 64, + "min_chemistry": null, + "required_leagues": [], + "required_nations": [], + "required_clubs": [], + "min_players_from_same_league": null, + "min_players_from_same_nation": null, + "min_players_from_same_club": null + }, + "reward": { "coins": 0, "xp": 150, "pack_id": "silver_pack" }, + "expires_at": null, + "repeatable": true + } +] diff --git a/src/services/market.rs b/src/services/market.rs index dfa7ac0..cb24e2d 100644 --- a/src/services/market.rs +++ b/src/services/market.rs @@ -8,7 +8,7 @@ use crate::{ }, services::{card_db::CardDb, club, event as event_svc}, }; -use rand::Rng; +use rand::{seq::SliceRandom, Rng}; use uuid::Uuid; pub async fn get_active_listings( @@ -61,7 +61,7 @@ pub async fn refresh_npc_listings( if all_cards.is_empty() { return Ok(0); } - let count = 20usize.min(all_cards.len()); + let count = 24usize.min(all_cards.len()); let npc_names = [ "FC Rovers NPC", "Market Bot", @@ -72,12 +72,18 @@ pub async fn refresh_npc_listings( ]; let mut rng = rand::thread_rng(); - let mut listings: Vec = all_cards - .iter() + let mut indices: Vec = (0..all_cards.len()).collect(); + indices.shuffle(&mut rng); + + let mut listings: Vec = indices + .into_iter() .take(count) - .map(|card| { + .map(|i| { + let card = all_cards[i]; let base = price_for_card(card.overall); - let price = rng.gen_range((base / 2)..=(base * 2)).max(100); + // ±25% variance for realistic NPC pricing + let pct = rng.gen_range(75i64..=125i64); + let price = (base * pct / 100).max(100); let seller = npc_names[rng.gen_range(0..npc_names.len())]; MarketListing::new(&card.id, seller, price) }) diff --git a/tests/integration_test.rs b/tests/integration_test.rs index 903f66b..ebfd20b 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -1506,7 +1506,7 @@ async fn test_match_result_includes_level_ups_on_first_win() { #[tokio::test] async fn test_level_up_grants_milestone_pack_at_level_5() { - use openfut_core::models::profile::{level_for_xp, pack_for_level, XP_THRESHOLDS}; + use openfut_core::models::profile::{level_for_xp, pack_for_level}; // Level 5 requires 3000 XP total assert_eq!(level_for_xp(3000), 5); assert_eq!(pack_for_level(5), Some("bronze_pack"));