fifa17 store: real 6-pack economy + full-DB pool; drop extPrice
- store_catalog: replace the invented catalogue with the real always-available FUT17 regular packs (Bronze/Prem Bronze/Silver/Prem Silver/Gold/Prem Gold) at real prices + tier composition; PackDef now carries per-tier quantities. - pack_body: drop extPrice (its mtx side-effect switched on the broken "or %1s" FIFA-Points tile line; plan-2026-08-05-store-subsystem.md section 3.4). - pack_content: tier-aware generator draws each pack bronze/silver/gold composition with special_chance bias + empty-tier fallback. - host: CoreAccess::all_definitions (GET /cards); build_content_pool draws the FULL card universe via non-minting catalog lookup, owned-inventory fallback. - economy_differential: store ops reclassified DIFFERENT-BY-DESIGN (Rust is the authoritative store; Python oracle stays the untouched rollback baseline). - fixtures/tests updated to the real catalogue. Odds are DESIGNED placeholders (FUT17 pack probabilities were never published); club items remain excluded (cardtype-9 mapping unknown). Full regression green; real prices + tier-correct draws verified server-side on staging.
This commit is contained in:
@@ -663,6 +663,17 @@ pub trait CoreAccess: Send + Sync {
|
||||
Ok(self.query_owned(&[])?.items)
|
||||
}
|
||||
|
||||
/// Every card DEFINITION in Core content (`GET /cards`) — the full card
|
||||
/// universe a pack can draw from, independent of ownership. Same shape as
|
||||
/// owned items (rating/position/nation/league/club/attributes) so
|
||||
/// [`build_content_pool`] treats definitions and owned items uniformly.
|
||||
/// Default: unimplemented (callers fall back to owned inventory).
|
||||
fn all_definitions(&self) -> Result<Vec<CoreOwnedItem>, CoreError> {
|
||||
Err(CoreError::Parse(
|
||||
"Core content enumeration is not implemented".into(),
|
||||
))
|
||||
}
|
||||
|
||||
/// Read the active squad + its opaque extension for `namespace`.
|
||||
fn read_squad_ext(&self, namespace: &str) -> Result<CoreSquadRead, CoreError>;
|
||||
|
||||
@@ -746,6 +757,22 @@ impl CoreAccess for HttpCoreClient {
|
||||
let v: Value = resp.json().map_err(|e| CoreError::Parse(e.to_string()))?;
|
||||
parse_core_page(&v)
|
||||
}
|
||||
|
||||
fn all_definitions(&self) -> Result<Vec<CoreOwnedItem>, CoreError> {
|
||||
let url = format!("{}/cards", self.base_url);
|
||||
let resp = self
|
||||
.client
|
||||
.get(&url)
|
||||
.header("X-OpenFUT-Game", &self.game)
|
||||
.send()
|
||||
.map_err(|e| CoreError::Http(e.to_string()))?;
|
||||
let status = resp.status().as_u16();
|
||||
if !(200..300).contains(&status) {
|
||||
return Err(CoreError::Status(status));
|
||||
}
|
||||
let v: Value = resp.json().map_err(|e| CoreError::Parse(e.to_string()))?;
|
||||
parse_core_definitions(&v)
|
||||
}
|
||||
fn read_squad_ext(&self, namespace: &str) -> Result<CoreSquadRead, CoreError> {
|
||||
let url = format!("{}/squad/ext", self.base_url);
|
||||
let resp = self
|
||||
@@ -1361,6 +1388,38 @@ fn core_item_from_json(e: &Value) -> Option<CoreOwnedItem> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Parse Core's `/cards` response `{ "cards": [CardDefinition...], ... }` into the
|
||||
/// same [`CoreOwnedItem`] shape as owned items (`owned_card_id` empty — a
|
||||
/// definition is not an instance), for full-universe pool building.
|
||||
pub fn parse_core_definitions(v: &Value) -> Result<Vec<CoreOwnedItem>, CoreError> {
|
||||
let arr = v
|
||||
.get("cards")
|
||||
.and_then(|c| c.as_array())
|
||||
.ok_or_else(|| CoreError::Parse("missing `cards` array".into()))?;
|
||||
Ok(arr.iter().filter_map(core_item_from_definition).collect())
|
||||
}
|
||||
|
||||
fn core_item_from_definition(card: &Value) -> Option<CoreOwnedItem> {
|
||||
let attr = |k: &str| card.get(k).and_then(|v| v.as_i64()).unwrap_or(0) as u8;
|
||||
Some(CoreOwnedItem {
|
||||
owned_card_id: String::new(),
|
||||
card_id: card.get("id")?.as_str()?.to_string(),
|
||||
rating: card.get("overall").and_then(|v| v.as_i64()).unwrap_or(0) as u8,
|
||||
position: card.get("position")?.as_str()?.to_string(),
|
||||
nation: card.get("nation")?.as_str()?.to_string(),
|
||||
league: card.get("league")?.as_str()?.to_string(),
|
||||
club: card.get("club")?.as_str()?.to_string(),
|
||||
attributes: [
|
||||
attr("pace"),
|
||||
attr("shooting"),
|
||||
attr("passing"),
|
||||
attr("dribbling"),
|
||||
attr("defending"),
|
||||
attr("physical"),
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
// ───────────────────────────── Item identity resolver ───────────────────────
|
||||
|
||||
/// The single production [`ItemIdentityResolver`]: it composes the two distinct
|
||||
@@ -1419,6 +1478,15 @@ impl Fifa17IdentityResolver {
|
||||
.map(|c| c.rareflag)
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
/// Non-minting definition identity from the catalog: `Some((rareflag, kind))`
|
||||
/// if the card resolves, else `None`. Used to build the pack pool over ALL
|
||||
/// content definitions WITHOUT allocating a wire id per definition (that would
|
||||
/// pollute the identity store); a real wire id is minted only when a pack draw
|
||||
/// actually mints the card.
|
||||
pub fn definition_identity(&self, card_id: &str) -> Option<(i64, ContentKind)> {
|
||||
self.catalog.lookup(card_id).map(|c| (c.rareflag, c.kind))
|
||||
}
|
||||
}
|
||||
|
||||
impl ItemIdentityResolver for Fifa17IdentityResolver {
|
||||
@@ -2372,30 +2440,40 @@ pub struct EconomyServices {
|
||||
pub sold_experiment: crate::sold_experiment::SoldExperiment,
|
||||
}
|
||||
|
||||
/// Build the pack-content candidate pool from Core's current content, evidenced
|
||||
/// by the owned inventory: every distinct owned card definition that resolves to
|
||||
/// a real FIFA asset id is a candidate (`gold` = rating ≥ 75; `special` from the
|
||||
/// catalog `rareflag > 1`). This is the resolvable FIFA∩Core card universe — the
|
||||
/// cards a pack can award and the shared shaper can render. An empty pool (no
|
||||
/// content, or Core unreachable) is fail-closed by construction: the generator
|
||||
/// returns no cards, so the Store neither mints nor debits.
|
||||
/// Build the pack-content candidate pool. Prefers the FULL card universe (Core
|
||||
/// content, `GET /cards` via [`CoreAccess::all_definitions`]) so a pack can award
|
||||
/// any card in the game, not only cards the profile already owns; falls back to
|
||||
/// owned inventory when content enumeration is unavailable (older Core / tests).
|
||||
/// Each candidate resolves in the FIFA catalog (unmapped or non-player cards are
|
||||
/// dropped, never faked); `gold` = rating ≥ 75, `special` = catalog `rareflag > 1`.
|
||||
/// An empty pool (no content, or Core unreachable) is fail-closed by construction:
|
||||
/// the generator returns no cards, so the Store neither mints nor debits.
|
||||
pub fn build_content_pool(
|
||||
core: &dyn CoreAccess,
|
||||
resolver: &Fifa17IdentityResolver,
|
||||
) -> Vec<GeneratedCandidate> {
|
||||
let owned = match core.all_owned() {
|
||||
Ok(v) => v,
|
||||
Err(_) => return Vec::new(),
|
||||
// Full universe first; owned inventory only as a degrade path.
|
||||
let items = match core.all_definitions() {
|
||||
Ok(v) if !v.is_empty() => v,
|
||||
_ => match core.all_owned() {
|
||||
Ok(v) => v,
|
||||
Err(_) => return Vec::new(),
|
||||
},
|
||||
};
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
let mut pool = Vec::new();
|
||||
for item in &owned {
|
||||
for item in &items {
|
||||
if !seen.insert(item.card_id.clone()) {
|
||||
continue;
|
||||
}
|
||||
let Some(id) = resolver.resolve(item) else {
|
||||
// Non-minting catalog lookup: drop unmapped cards and non-player content
|
||||
// (consumables/staff never enter the player-card pack pool).
|
||||
let Some((rareflag, kind)) = resolver.definition_identity(&item.card_id) else {
|
||||
continue;
|
||||
};
|
||||
if kind != ContentKind::Player {
|
||||
continue;
|
||||
}
|
||||
pool.push(GeneratedCandidate {
|
||||
card_id: item.card_id.clone(),
|
||||
rating: item.rating,
|
||||
@@ -2405,7 +2483,7 @@ pub fn build_content_pool(
|
||||
club: item.club.clone(),
|
||||
attributes: item.attributes,
|
||||
gold: item.rating >= 75,
|
||||
special: id.rareflag > 1,
|
||||
special: rareflag > 1,
|
||||
});
|
||||
}
|
||||
pool
|
||||
|
||||
Reference in New Issue
Block a user