feat: 1v9 convertite en leyenda LoL career simulator
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
# Arquitectura — 1v9: Convertite en Leyenda
|
||||
|
||||
## Visión general
|
||||
|
||||
Es un **juego de decisiones** (historia interactiva) en un **único archivo** `index.html`
|
||||
(HTML + CSS + JS inline, sin dependencias externas para jugar). Todo el estado vive en memoria
|
||||
durante la partida; no hay persistencia obligatoria (un `localStorage` opcional guarda el
|
||||
"Salón de la fama" con las mejores carreras).
|
||||
|
||||
```
|
||||
Landing → Creación → Oferta de academia → Loop de carrera (N splits) → Resumen final (compartible)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Flujo de pantallas
|
||||
|
||||
| Pantalla | Rol |
|
||||
|----------|-----|
|
||||
| **Landing** | Presentación + botón "Convertite en leyenda" + Salón de la fama |
|
||||
| **Creación** | Nick, nº de invocador, mano, región, rol |
|
||||
| **Oferta de academia** | 3 equipos aleatorios de tu liga para elegir |
|
||||
| **Carrera (Screen A)** | Panel de jugador + decisión + timeline, 2 columnas |
|
||||
| **Resumen (Screen B)** | 3 paneles + grid de orgs + botón "Jugar de nuevo" |
|
||||
|
||||
---
|
||||
|
||||
## Datos del juego
|
||||
|
||||
### REGIONES
|
||||
`LCK / LPL / LEC / LCS / LTA / VCS`, cada una con:
|
||||
- `code` (KR/CN/EU/NA/AM/VN), `flag`, `country`, `flavor`.
|
||||
- `threshold`: umbral de ELO para competir en su primera división (LCK=68…VCS=50).
|
||||
|
||||
### TEAMS (68 equipos reales)
|
||||
`{ id, name, div }` — `div: 1` = primera división, `div: 2` = segunda/academia.
|
||||
Los logos se resuelven con `LOGO_EXT[id]` → `logos/<id>.<ext>`.
|
||||
|
||||
### ROLES
|
||||
`{ em, fl, kill, ast, death, pentaW }` — coeficientes de farmeo/perf de cada rol.
|
||||
|
||||
### EVENTS (26 eventos de decisión)
|
||||
Cada evento: `{ cat, title, ctx, opts[] }`.
|
||||
Cada opción: `{ t, d, risk, pos, neg, x? }`
|
||||
- `d`: deltas a stats (`mecanica, vision, farmeo, sinergia, marca`).
|
||||
- `risk`: probabilidad de fracaso (0-1).
|
||||
- `pos`/`neg`: textos de resultado (éxito/fracaso).
|
||||
- `x`: efectos extra (`rolChange`, `transfer`).
|
||||
|
||||
### COMPETITIONS (5 internacionales)
|
||||
`{ split, ic, flavor }` — First Stand (2), MSI (4), EWC (6), Asian Games (8), Worlds (último).
|
||||
|
||||
---
|
||||
|
||||
## Estado de partida (`G`)
|
||||
|
||||
```js
|
||||
{
|
||||
nick, num, hand, region, role,
|
||||
teamId, div,
|
||||
stats: { mecanica, vision, farmeo, sinergia, marca },
|
||||
split, totalSplits,
|
||||
kills, assists, deaths, games, penta,
|
||||
value, maxValue, salary, ratingMax,
|
||||
teamTitles, indTitles, intl,
|
||||
teams, events, history,
|
||||
regSelect: { games, kills, ast, apps },
|
||||
retired, retiring, finalRating
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Mecánicas clave
|
||||
|
||||
### Rating / ELO
|
||||
Media ponderada de las 5 stats (mecánica 1.15, farmeo 1.05, sinergia 1.0, visión 0.95, marca 0.85),
|
||||
clamp `[40, 99]`.
|
||||
|
||||
### Market value
|
||||
`50000 * 10^((rating-40)*0.055)` × bonus por títulos, redondeado a `10k`.
|
||||
|
||||
### Crecimiento pasivo
|
||||
Cada split suma `+0.48 + rand*0.2` (+ bonus por winRate) a cada stat; rendimiento malo reduce.
|
||||
|
||||
### Decisiones
|
||||
`ok = Math.random() >= (risk + 0.03)`. Éxito: `+delta*2.6`. Fracaso: `-delta*2.6*0.9`.
|
||||
|
||||
### Fichajes
|
||||
Solo desde **split 4**, con ~22% de chance por split y `rating >= threshold-4`.
|
||||
Ofertas de otros equipos (misma región o extranjeros de primera si el rating alcanza).
|
||||
|
||||
### Competencias internacionales
|
||||
**Solo equipos de primera división** (un equipo de 2ª no va a MSI/Worlds). Bracket de 4 rondas
|
||||
con probabilidad por rating: grupos → cuartos → semis → final.
|
||||
|
||||
### Títulos
|
||||
- **Regional**: cada split par, si `rating >= threshold-X` (primera o segunda división).
|
||||
- **Individuales**: Guante de Oro (ELO≥62), Rey del Farmeo (farmeo≥60), Carry Definitivo (penta≥1), Community Darling (marca≥58), MVP del Split.
|
||||
|
||||
---
|
||||
|
||||
## UI (estilo Copero)
|
||||
|
||||
Design tokens en `:root`:
|
||||
```
|
||||
--bg-base:#0a0a0c --bg-card:#18181b --bg-card-alt:#1f1f23 --bg-row-active:#0e3a3f
|
||||
--accent-ovr:#d9730a --accent-role:#7c3aed
|
||||
--success:#22c55e --danger:#ef4444 --border-subtle:#27272a
|
||||
```
|
||||
|
||||
- **Screen A**: 2 columnas (3fr/2fr). Izquierda: badge ELO + panel jugador + stats + logros + decisión.
|
||||
Derecha: timeline de carrera (SPLIT/ORG/ELO/GAM/KIL/AST) con fila activa teal y filas futuras grises.
|
||||
- **Screen B**: 3 paneles (identidad, selección regional, premios) + grid de orgs + botón "Jugar de nuevo".
|
||||
- **Responsive**: <768px colapsa la timeline (default cerrada) y apila a 1 columna.
|
||||
|
||||
---
|
||||
|
||||
## Export de imagen
|
||||
|
||||
Un `<canvas>` 1080×1920 dibuja el resumen completo (dark bg, badges ELO, stats, títulos, orgs)
|
||||
y se descarga como PNG. Es el mecanismo de viralidad del juego.
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
# Deploy — lolero.cbcren.online
|
||||
|
||||
El sitio corre en un servidor estático (`python3 -m http.server`) en el puerto **8899**,
|
||||
protegido por un reverse proxy **Caddy** con TLS automático.
|
||||
|
||||
```
|
||||
Internet → Caddy (80/443, TLS automático)
|
||||
└─ lolero.cbcren.online → reverse_proxy 172.20.0.1:8899
|
||||
└─ python3 -m http.server (systemd: lolero.service)
|
||||
├─ index.html
|
||||
└─ logos/ (68 archivos)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 1. Servir los archivos (systemd)
|
||||
|
||||
```ini
|
||||
# /etc/systemd/system/lolero.service
|
||||
[Unit]
|
||||
Description=1v9 Convertite en Leyenda - static site
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
User=ren
|
||||
WorkingDirectory=/home/ren/lolero
|
||||
ExecStart=/usr/bin/python3 -m http.server 8899 --bind 0.0.0.0
|
||||
Restart=always
|
||||
RestartSec=3
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
```bash
|
||||
sudo cp lolero.service /etc/systemd/system/lolero.service
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable --now lolero
|
||||
```
|
||||
|
||||
> **Nota:** `http.server` lee los archivos del disco en cada request, así que **no hace falta
|
||||
> reiniciar el servicio** cuando se actualiza `index.html`. El cambio se sirve solo.
|
||||
|
||||
## 2. Caddy (reverse proxy)
|
||||
|
||||
El Caddyfile activo está en `/home/ren/immich/Caddyfile` (es un bind mount del contenedor
|
||||
`caddy-ingress`, no `/etc/caddy/Caddyfile`). Se agregó:
|
||||
|
||||
```
|
||||
lolero.cbcren.online {
|
||||
reverse_proxy 172.20.0.1:8899
|
||||
encode gzip
|
||||
header {
|
||||
X-Frame-Options SAMEORIGIN
|
||||
X-Content-Type-Options nosniff
|
||||
}
|
||||
log {
|
||||
output file /data/caddy/lolero.cbcren.online.log
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Validar y recargar:
|
||||
|
||||
```bash
|
||||
docker exec caddy-ingress caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile
|
||||
docker exec caddy-ingress caddy reload --config /etc/caddy/Caddyfile --adapter caddyfile
|
||||
```
|
||||
|
||||
> `172.20.0.1` es el gateway de la red docker `caddy` (donde corre `caddy-ingress`).
|
||||
> Cualquier proxy hacia el host usa esa IP.
|
||||
|
||||
## 3. DNS
|
||||
|
||||
Registro A: `lolero.cbcren.online → 194.163.191.200` (ya creado). Caddy emite el certificado
|
||||
Let's Encrypt automáticamente en el primer request.
|
||||
|
||||
## 4. Verificación
|
||||
|
||||
```bash
|
||||
curl -sk -o /dev/null -w "%{http_code}\n" https://lolero.cbcren.online/ # 200
|
||||
curl -sk -o /dev/null -w "%{http_code}\n" https://lolero.cbcren.online/logos/T1.png # 200
|
||||
```
|
||||
|
||||
## 5. Actualizar contenido
|
||||
|
||||
Reemplazar `/home/ren/lolero/index.html` y/o `/home/ren/lolero/logos/`. No requiere restart.
|
||||
|
||||
## 6. Rollback
|
||||
|
||||
```bash
|
||||
sudo systemctl stop lolero # baja el servicio
|
||||
# el 503 de Caddy se devuelve mientras tanto; subirlo de nuevo con:
|
||||
sudo systemctl start lolero
|
||||
```
|
||||
|
||||
## Notas de puertos
|
||||
|
||||
El puerto **8899** se eligió libre (verificado con `ss -tln`). No se tocan puertos en uso
|
||||
(8080, 8090, 3000, 8000, etc.).
|
||||
@@ -0,0 +1,49 @@
|
||||
# Próximos pasos — 1v9: Convertite en Leyenda
|
||||
|
||||
Estado actual y mejoras posibles, en orden de impacto.
|
||||
|
||||
## ✅ Hecho
|
||||
|
||||
- [x] Juego completo en un solo archivo (`index.html`), autocontenido, offline.
|
||||
- [x] 68 equipos reales con logos, 5 roles, 6 regiones.
|
||||
- [x] 26 eventos de decisión en 6 categorías.
|
||||
- [x] 5 competencias internacionales (First Stand, MSI, EWC, Asian Games, Worlds).
|
||||
- [x] Títulos regionales (1ª y 2ª) + premios individuales.
|
||||
- [x] Fichajes con cadencia realista (≥ split 4, ~2/carrera).
|
||||
- [x] Gating: equipos de 2ª división no van a internacionales.
|
||||
- [x] UI estilo Copero (dark dashboard, badges, pills, timeline).
|
||||
- [x] Responsive mobile/desktop, timeline colapsable.
|
||||
- [x] Export de imagen 1080×1920 compartible.
|
||||
- [x] Deploy en https://lolero.cbcren.online (Caddy + systemd + puerto 8899).
|
||||
- [x] Documentación (README, ARCHITECTURE, TESTING, DEPLOY).
|
||||
|
||||
## 🔜 Ideas de mejora
|
||||
|
||||
### Gameplay
|
||||
- [ ] Más eventos por categoría (replayability).
|
||||
- [ ] Más premios individuales (Mejor Novato, MVP del Split con más variantes).
|
||||
- [ ] Eventos de "momento" (ganar/revivir un BO5, finales emocionantes).
|
||||
- [ ] Sistema de lesiones/visa más ligado a la selección regional.
|
||||
|
||||
### UI / UX
|
||||
- [ ] Animaciones de transición entre pantallas.
|
||||
- [ ] Sonido (efectos mínimos, sin dependencias externas).
|
||||
- [ ] Soporte de idioma (EN) según el locale del navegador.
|
||||
- [ ] Tema claro opcional.
|
||||
|
||||
### Datos
|
||||
- [ ] Más equipos de segunda división por región.
|
||||
- [ ] Verificar logos nuevos si cambian (los `_std` de Liquidpedia son pequeños).
|
||||
|
||||
### Distribución / viralidad
|
||||
- [ ] PWA (manifest + service worker) para instalación y offline total.
|
||||
- [ ] Open Graph / tarjetas de compartir (el export ya es la imagen).
|
||||
- [ ] Analítica ligera (opcional, sin trackeo de datos personales).
|
||||
|
||||
### Infra
|
||||
- [ ] HTTPS ya resuelto por Caddy. Considerar caché de logos con `Cache-Control` largo.
|
||||
- [ ] Backup del repo en Gitea (este repo).
|
||||
|
||||
## Contribución
|
||||
|
||||
Ver `TESTING.md` para el workflow de verificación antes de mergear cambios.
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
# Testing — 1v9: Convertite en Leyenda
|
||||
|
||||
Este documento describe el workflow de testing que usamos para verificar el juego de punta a punta.
|
||||
Es reproducible en cualquier máquina con Node.js ≥18 y Python 3 + Playwright.
|
||||
|
||||
---
|
||||
|
||||
## 1. Validación de sintaxis JS
|
||||
|
||||
Extraemos el `<script>` inline y lo chequeamos con Node (sin ejecutarlo):
|
||||
|
||||
```bash
|
||||
python3 -c "import re; open('/tmp/game.js','w').write(re.search(r'<script>(.*?)</script>', open('index.html').read(), re.S).group(1))"
|
||||
node --check /tmp/game.js && echo "JS OK"
|
||||
```
|
||||
|
||||
Esto detecta errores de sintaxis antes de tocar el navegador.
|
||||
|
||||
---
|
||||
|
||||
## 2. Smoke test local (Playwright)
|
||||
|
||||
Levantamos un servidor estático y jugamos una carrera completa con un bot, detectando
|
||||
errores de consola y fallos de red:
|
||||
|
||||
```bash
|
||||
python3 -m http.server 8899 --bind 0.0.0.0 &
|
||||
```
|
||||
|
||||
Un script Playwright (chromium headless) que:
|
||||
1. Abre la página en viewport móvil (390×844).
|
||||
2. Completa la creación de player.
|
||||
3. Elige una oferta de academia.
|
||||
4. Juega hasta el resumen (clic en opciones/continuar).
|
||||
5. Captura **0 errores de consola**, **0 requests fallidos** y que el resumen se muestre.
|
||||
|
||||
```python
|
||||
from playwright.async_api import async_playwright
|
||||
async def main():
|
||||
async with async_playwright() as p:
|
||||
browser = await p.chromium.launch()
|
||||
page = await browser.new_page(viewport={"width":390,"height":844})
|
||||
errs = []
|
||||
page.on("pageerror", lambda e: errs.append(str(e)))
|
||||
page.on("console", lambda m: errs.append(m.text) if m.type=="error" else None)
|
||||
page.on("requestfailed", lambda r: errs.append(r.url))
|
||||
await page.goto("http://127.0.0.1:8899/")
|
||||
# ... click thru ...
|
||||
print("ERRORS:", errs)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Layout check (sin lectura de imágenes)
|
||||
|
||||
Como el modelo no puede ver screenshots, verificamos el layout programáticamente:
|
||||
|
||||
- **Overflow horizontal**: `document.documentElement.scrollWidth > window.innerWidth` debe ser `false`.
|
||||
- **Timeline colapsada en mobile**: el `.tl-body` debe tener la clase `closed` por default en viewport <768px.
|
||||
- **Design tokens**: `getComputedStyle(document.documentElement)` debe devolver `--accent-ovr:#d9730a`, etc.
|
||||
|
||||
---
|
||||
|
||||
## 4. Verificación de mecánicas (tests dirigidos)
|
||||
|
||||
### Gating de competencias (1ª división)
|
||||
Empezar en un equipo de **2ª división** y confirmar que NO aparecen eventos internacionales
|
||||
hasta ascender a 1ª. Se rastrea el `dc-cat` de cada decisión contra el `G.div`.
|
||||
|
||||
### Cadencia de fichajes
|
||||
Jugar y registrar en qué splits aparecen eventos "Fichaje". Debe ser **≥ split 4** y ~2 por carrera
|
||||
(no en cada split).
|
||||
|
||||
### Diversidad de competencias
|
||||
Jugar 10+ carreras y confirmar que aparecen las 5 (First Stand, MSI, EWC, Asian Games, Worlds).
|
||||
|
||||
### Stats / backend
|
||||
Verificar que `G.stats` no tenga claves fantasma (`valor`) ni `NaN` al final de la carrera.
|
||||
|
||||
---
|
||||
|
||||
## 5. Export de imagen
|
||||
|
||||
Se fuerza el fin de carrera con datos de prueba, se clickea "Descargar imagen" y se valida el PNG con PIL:
|
||||
|
||||
```python
|
||||
from PIL import Image
|
||||
img = Image.open(path)
|
||||
print(img.size, img.mode) # 1080x1920 RGBA
|
||||
print(img.convert('RGB').getpixel((100,100))) # ~(10,10,12) dark bg
|
||||
print(img.convert('L').getextrema()) # rango de luminancia (texto presente)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Test final contra producción (HTTPS)
|
||||
|
||||
Repetimos los smoke tests contra `https://lolero.cbcren.online` (no solo local), para validar
|
||||
el deploy real (Caddy + TLS + serve de logos).
|
||||
|
||||
---
|
||||
|
||||
## Resultado esperado
|
||||
|
||||
- 0 errores de consola, 0 requests fallidos.
|
||||
- Resumen alcanzado en 100% de las carreras.
|
||||
- Sin overflow horizontal en desktop ni mobile.
|
||||
- Export 1080×1920 PNG válido.
|
||||
- Las 5 competencias aparecen; fichajes ≥ split 4; 2ª división no va a internacionales.
|
||||
Reference in New Issue
Block a user