Modelo de datos
RacingCard y CardStats
Sección titulada «RacingCard y CardStats»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: rifa + rareza gacha
Sección titulada «PerformanceRoller: rifa + rareza gacha»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:
1. Rifar valores dentro de rangos fijos
Sección titulada «1. Rifar valores dentro de rangos fijos»private val topSpeedRange = StatRange(min = 280, max = 380, higherIsBetter = true) // km/hprivate val zeroToHundredTenthsRange = StatRange(min = 20, max = 45, higherIsBetter = false) // 2.0s–4.5sprivate val powerRange = StatRange(min = 450, max = 800, higherIsBetter = true) // kWprivate 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.
