From 9e481ec0725a377880d43784e87cbb8da769c7f4 Mon Sep 17 00:00:00 2001 From: funman300 Date: Thu, 25 Jun 2026 17:49:38 -0700 Subject: [PATCH] Phase 18: data expansion and market pricing fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four new card files (Serie A, Ligue 1, Primeira Liga, Eredivisie) add 45 cards across Italian, French, Portuguese, and Dutch football, bringing the total card pool from 115 to 160. Each file uses fictional club/player names and null image_path, keeping the project free of copyrighted assets. Ten new SBC challenges (league_sbcs.json) targeting specific leagues and nations increase the total from 7 to 17 challenges. Includes league purity SBCs (Premier League, Bundesliga, Serie A, Ligue 1, Eredivisie), nation combo SBCs (Iberian Derby, South American Fire), and utility SBCs (elite strikers mini-submission, bronze-to-silver recycler, world tour). NPC market refresh now shuffles the card pool before picking 24 listings (was: take first 20 unshuffled), and price variance tightened from ±100% to ±25% of the OVR-tier base price for more realistic NPC competition. Co-Authored-By: Claude Sonnet 4.6 --- data/cards/eredivisie.json | 10 ++ data/cards/liga_nos.json | 12 +++ data/cards/ligue1.json | 14 +++ data/cards/serie_a.json | 17 ++++ data/sbcs/league_sbcs.json | 202 +++++++++++++++++++++++++++++++++++++ src/services/market.rs | 18 ++-- tests/integration_test.rs | 2 +- 7 files changed, 268 insertions(+), 7 deletions(-) create mode 100644 data/cards/eredivisie.json create mode 100644 data/cards/liga_nos.json create mode 100644 data/cards/ligue1.json create mode 100644 data/cards/serie_a.json create mode 100644 data/sbcs/league_sbcs.json 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"));