Ir al contenido

Modelo de datos

data class RacingCard(
val name: String,
val cls: String,
val brand: String,
val carNumber: String,
val backgroundIndex: Int = 0,
val badgeIndex: Int = 0,
val carImageIndex: Int = 0,
val tier: String = "D",
val stats: CardStats,
)
data class CardStats(
val topSpeed: String,
val zeroToHundred: String,
val power: String,
val weight: String,
)

Todos los índices (backgroundIndex, badgeIndex, carImageIndex) son punteros a listas fijas definidas en ui/components/CardComponents.kt y ui/theme/Color.kt — no hay un catálogo de “autos” o “escuderías” como entidades propias, son simplemente arreglos de recursos:

private val carImages = listOf(R.drawable.car_1, R.drawable.car_2, R.drawable.car_3, R.drawable.car_4)
private val badgeImages = listOf(R.drawable.esc_1, R.drawable.esc_2, R.drawable.esc_3, R.drawable.esc_4)
private val badgeBrandNames = listOf("Raven", "Alpine", "Astra", "Vortex")

CardOptions (en model/RacingCard.kt) completa el modelo con las listas usadas por los pickers del formulario y la plantilla de carta vacía:

object CardOptions {
val classes = listOf("LMH", "LMDh", "GTP", "GT3", "GTE", "LMP2")
val filters = listOf("All cars", "LMH", "LMDh", "GT3", "GTE")
val blank: RacingCard = RacingCard(
name = "", cls = classes.first(), brand = "", carNumber = "",
stats = CardStats(topSpeed = "", zeroToHundred = "", power = "", weight = ""),
)
}

Diagrama de clases

classDiagram
  class RacingCard {
      +String name
      +String cls
      +String brand
      +String carNumber
      +Int backgroundIndex
      +Int badgeIndex
      +Int carImageIndex
      +String tier
      +CardStats stats
  }
  class CardStats {
      +String topSpeed
      +String zeroToHundred
      +String power
      +String weight
  }
  class CardOptions {
      <<object>>
      +List~String~ classes
      +List~String~ filters
      +RacingCard blank
  }
  class PerformanceRoller {
      <<object>>
      +rollStats() CardStats
      +tierFor(CardStats) String
      +formatTenths(Int) String
  }
  RacingCard "1" --> "1" CardStats : stats
  CardOptions ..> RacingCard : crea (blank)
  PerformanceRoller ..> CardStats : genera / evalúa

PerformanceRoller es el único punto del código con lógica de negocio real (todo lo demás es UI). Vive en model/PerformanceRoller.kt y hace dos cosas:

private val topSpeedRange = StatRange(min = 280, max = 380, higherIsBetter = true) // km/h
private val zeroToHundredTenthsRange = StatRange(min = 20, max = 45, higherIsBetter = false) // 2.0s–4.5s
private val powerRange = StatRange(min = 450, max = 800, higherIsBetter = true) // kW
private val weightRange = StatRange(min = 900, max = 1400, higherIsBetter = false) // kg
fun rollStats(): CardStats = CardStats(
topSpeed = topSpeedPool.random().toString(),
zeroToHundred = formatTenths(zeroToHundredTenthsPool.random()),
power = powerPool.random().toString(),
weight = weightPool.random().toString(),
)

2. Calificar esas estadísticas en una rareza S–D

Sección titulada «2. Calificar esas estadísticas en una rareza S–D»

Cada estadística se normaliza a un valor 0–1 dentro de su rango (invertido cuando “menor es mejor”, como el peso o el tiempo de 0–100), se promedian las 4, y ese score 0–100 se bucketiza:

private fun normalized(value: Float, range: StatRange): Float {
val fraction = ((value - range.min) / (range.max - range.min).toFloat()).coerceIn(0f, 1f)
return if (range.higherIsBetter) fraction else 1f - fraction
}
fun tierFor(stats: CardStats): String {
val score = listOf(
normalized(topSpeed, topSpeedRange),
normalized(zeroToHundredTenths, zeroToHundredTenthsRange),
normalized(power, powerRange),
normalized(weight, weightRange),
).average().toFloat() * 100f
return when {
score >= 85f -> "S"
score >= 68f -> "A"
score >= 48f -> "B"
score >= 28f -> "C"
else -> "D"
}
}

Algoritmo de rareza

flowchart TD
  A["rollStats: valores aleatorios uniformes dentro de cada rango"] --> B["normalized(valor, rango) por cada una de las 4 stats"]
  B --> C{"¿higherIsBetter?"}
  C -->|"sí (velocidad, potencia)"| D["fraction = (valor-min)/(max-min)"]
  C -->|"no (0-100, peso)"| E["fraction = 1 - (valor-min)/(max-min)"]
  D --> F[promedio de las 4 fracciones]
  E --> F
  F --> G["score = promedio * 100"]
  G --> H{score}
  H -->|">= 85"| S["Rareza S"]
  H -->|">= 68"| Sa["Rareza A"]
  H -->|">= 48"| Sb["Rareza B"]
  H -->|">= 28"| Sc["Rareza C"]
  H -->|"< 28"| Sd["Rareza D"]

Cada rareza tiene además un color de marca fijo, definido en ui/theme/Color.kt (tierColor(tier)), reutilizado tanto en el chip de la carta como en el sello grande que aparece al final del roll:

Rareza Color
S Amarillo de marca (BrandYellow)
A Púrpura hyper (HyperPurple)
B Azul de marca (BrandBlue)
C Verde de éxito (SuccessGreen)
D Gris neutro (NeutralGray)

Es el mismo mapeo de intención (S = mejor, con el color más “premium”) que usan la mayoría de juegos gacha, adaptado a la paleta de marca en vez de la escala arcoíris típica del género.