@ing.jorgeu/wargaming-overlay 2.2.0 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +170 -14
- package/announcements.js +130 -0
- package/battle_reader.py +9 -0
- package/data/faction-facts.json +6118 -0
- package/factions.js +20 -0
- package/game-store.js +4 -1
- package/listhammer.js +106 -0
- package/package.json +10 -3
- package/public/announcements-control.js +57 -0
- package/public/control.html +10 -1
- package/public/control.js +40 -20
- package/public/factions.json +39 -0
- package/public/live-score.js +2 -4
- package/public/match-view.js +26 -0
- package/public/overlay-art.html +1 -1
- package/public/style.css +9 -0
- package/public/toast.css +14 -0
- package/public/toast.js +21 -0
- package/scripts/fact-coverage.js +9 -0
- package/server.js +49 -4
- package/turn-tracker.js +37 -0
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ Overlay local para OBS con marcador, ronda, CP, primarias y secundarias leídos
|
|
|
4
4
|
|
|
5
5
|
**Paquete:** `@ing.jorgeu/wargaming-overlay` · **Comando:** `wargaming-overlay` · **Licencia del código:** 0BSD.
|
|
6
6
|
|
|
7
|
-
El paquete está
|
|
7
|
+
El paquete está publicado en [npm como @ing.jorgeu/wargaming-overlay](https://www.npmjs.com/package/@ing.jorgeu/wargaming-overlay). Para usarlo no necesitas clonar el repositorio. En macOS/Linux, si npm intenta escribir en una carpeta del sistema, usa la instalación en tu usuario que se explica abajo.
|
|
8
8
|
|
|
9
9
|
## Contenido
|
|
10
10
|
|
|
@@ -198,25 +198,95 @@ Ejemplo Windows:
|
|
|
198
198
|
npm.cmd install -g "$HOME\Downloads\ing.jorgeu-wargaming-overlay-2.2.0.tgz"
|
|
199
199
|
```
|
|
200
200
|
|
|
201
|
-
### C. Desde npm
|
|
201
|
+
### C. Desde npm: versión publicada
|
|
202
|
+
|
|
203
|
+
Puedes ejecutar esto desde cualquier carpeta:
|
|
202
204
|
|
|
203
205
|
```sh
|
|
204
206
|
npm install -g @ing.jorgeu/wargaming-overlay
|
|
207
|
+
wargaming-overlay --version
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
En Windows usa `npm.cmd` y `wargaming-overlay.cmd` si PowerShell bloquea los scripts `.ps1`.
|
|
211
|
+
|
|
212
|
+
Si macOS/Linux responde con `EACCES: permission denied, mkdir '/usr/local/lib/node_modules/...'`, el paquete sí fue encontrado: falta permiso de escritura en la carpeta global de npm. Usa los siguientes pasos, sin sudo.
|
|
213
|
+
|
|
214
|
+
### Instalación desde npm sin sudo en macOS / Linux
|
|
215
|
+
|
|
216
|
+
1. Instala la versión publicada en tu carpeta de usuario:
|
|
217
|
+
|
|
218
|
+
```sh
|
|
219
|
+
npm install -g --prefix "$HOME/.local" @ing.jorgeu/wargaming-overlay
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
2. Habilita el comando en la terminal actual y comprueba la instalación:
|
|
223
|
+
|
|
224
|
+
```sh
|
|
225
|
+
export PATH="$HOME/.local/bin:$PATH"
|
|
226
|
+
wargaming-overlay --version
|
|
227
|
+
wargaming-overlay doctor
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
3. Para conservar PATH en nuevas terminales, abre el archivo de configuración de tu shell. En macOS con zsh:
|
|
231
|
+
|
|
232
|
+
```sh
|
|
233
|
+
nano ~/.zshrc
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Si usas bash, abre `nano ~/.bashrc`. Añade esta línea solo si todavía no existe:
|
|
237
|
+
|
|
238
|
+
```sh
|
|
239
|
+
export PATH="$HOME/.local/bin:$PATH"
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
En nano guarda con Ctrl+O, Enter y sal con Ctrl+X. Abre una nueva terminal y comprueba:
|
|
243
|
+
|
|
244
|
+
```sh
|
|
245
|
+
command -v wargaming-overlay
|
|
246
|
+
wargaming-overlay --version
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Con esta instalación, la ruta debería terminar en `.local/bin/wargaming-overlay`.
|
|
250
|
+
|
|
251
|
+
4. Con el teléfono preparado, inicia:
|
|
252
|
+
|
|
253
|
+
```sh
|
|
254
|
+
wargaming-overlay start
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
Si no quieres modificar PATH, también puedes ejecutar `"$HOME/.local/bin/wargaming-overlay" start` directamente.
|
|
258
|
+
|
|
259
|
+
**Diferencia entre instalar desde npm y desde el repositorio:**
|
|
260
|
+
|
|
261
|
+
| Comando | Origen |
|
|
262
|
+
| --- | --- |
|
|
263
|
+
| `npm install -g --prefix "$HOME/.local" @ing.jorgeu/wargaming-overlay` | Descarga el paquete publicado en npm. No vincula el comando a tu repositorio. |
|
|
264
|
+
| `npm install -g --prefix "$HOME/.local" .` | Instala la carpeta actual; npm puede enlazarla al repositorio local. Debes estar en la carpeta del proyecto. |
|
|
265
|
+
|
|
266
|
+
`--prefix` solo cambia el destino de esa operación; no modifica la configuración global de npm. Repite el mismo prefijo al actualizar o desinstalar:
|
|
267
|
+
|
|
268
|
+
```sh
|
|
269
|
+
npm install -g --prefix "$HOME/.local" @ing.jorgeu/wargaming-overlay@latest
|
|
270
|
+
npm uninstall -g --prefix "$HOME/.local" @ing.jorgeu/wargaming-overlay
|
|
205
271
|
```
|
|
206
272
|
|
|
207
|
-
|
|
273
|
+
Son operaciones alternativas: ejecuta la primera para actualizar o la segunda para desinstalar, no ambas seguidas. La desinstalación conserva los datos de las partidas. [Documentación npm install](https://docs.npmjs.com/cli/v8/commands/npm-install/).
|
|
208
274
|
|
|
209
|
-
###
|
|
275
|
+
### Si npm devuelve E404
|
|
210
276
|
|
|
211
|
-
|
|
277
|
+
E404 significa que el registro no encuentra el paquete o que tu sesión no tiene acceso; no es un error de permisos de tu disco. Comprueba el nombre y consulta el registro público:
|
|
212
278
|
|
|
213
279
|
```sh
|
|
214
|
-
npm
|
|
215
|
-
export PATH="$HOME/.local/bin:$PATH"
|
|
216
|
-
wargaming-overlay --version
|
|
280
|
+
npm view @ing.jorgeu/wargaming-overlay version --registry=https://registry.npmjs.org/ --prefer-online
|
|
217
281
|
```
|
|
218
282
|
|
|
219
|
-
|
|
283
|
+
Si devuelve una versión, vuelve a instalar consultando información actualizada:
|
|
284
|
+
|
|
285
|
+
```sh
|
|
286
|
+
npm install -g --prefix "$HOME/.local" @ing.jorgeu/wargaming-overlay --prefer-online
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
Si la consulta sigue fallando, revisa `npm config get registry` y la [página del paquete](https://www.npmjs.com/package/@ing.jorgeu/wargaming-overlay). No publiques otra versión ni borres la caché solo para solucionar un E404 de instalación.
|
|
220
290
|
|
|
221
291
|
## Preparar el teléfono
|
|
222
292
|
|
|
@@ -246,7 +316,7 @@ Para conservar el PATH, añade la línea `export PATH` a `~/.zshrc` si usas zsh,
|
|
|
246
316
|
```
|
|
247
317
|
|
|
248
318
|
4. Abre el panel en [http://127.0.0.1:8765](http://127.0.0.1:8765).
|
|
249
|
-
5. Configura título, nombres, facciones, disposiciones y destacamentos. Si el nombre visible del overlay es distinto al de la app, completa **Nombre en la app** para cada jugador. Pulsa **Actualizar overlay**.
|
|
319
|
+
5. Configura título, nombres, facciones, disposiciones y destacamentos. Si el nombre visible del overlay es distinto al de la app, completa **Nombre en la app** para cada jugador. Pulsa **Actualizar overlay**. Para cambiar de lado, pulsa **Intercambiar jugadores ⇄**: guarda también los cambios pendientes del formulario e intercambia ambos jugadores completos. Los VP, CP y misiones siguen vinculados por nombre; el cambio se refleja al instante en OBS y se conserva al reiniciar.
|
|
250
320
|
6. En OBS añade una fuente **Navegador**, desmarca **Archivo local** y usa:
|
|
251
321
|
|
|
252
322
|
```text
|
|
@@ -391,7 +461,7 @@ El botón **Restablecer datos de la partida** vacía los puntos y misiones guard
|
|
|
391
461
|
|
|
392
462
|
Solo lee lo visible. Un bloque sin nombre de jugador se ignora para evitar asignaciones incorrectas. Los datos conservados pueden haber cambiado fuera de pantalla y las listas de misiones pueden ser parciales. No interpreta una lista ausente como lista vacía.
|
|
393
463
|
|
|
394
|
-
Al cambiar de ronda no reutiliza CP ni misiones de la ronda anterior. Si consultas una ronda histórica en el teléfono, mostrará esa ronda.
|
|
464
|
+
Al cambiar de ronda no reutiliza CP ni misiones de la ronda anterior. Si consultas una ronda histórica en el teléfono, mostrará esa ronda. TOP/BOTTOM se infiere automáticamente al observar aumentos de puntuación o cambios de objetivos previamente leídos. No requiere seleccionar el turno en el panel. El orden se aprende a partir de la primera actividad observada tras una ronda nueva, o desde una partida observada en ronda 1 con ambos marcadores en cero. Si conectas a mitad de partida, puede quedar sin indicar hasta la siguiente ronda. La detección se guarda al reiniciar y es independiente del intercambio de lados. Es una estimación: puntos registrados fuera de turno o correcciones al alza pueden confundirla. No interpreta misiones que aparecen por primera vez al desplazar la pantalla como cartas recién robadas. Bajar puntos o gastar CP no cambia el turno; cambios simultáneos en ambos jugadores lo conservan. El nombre general de la primaria no siempre está expuesto; se muestran sus condiciones de puntuación.
|
|
395
465
|
|
|
396
466
|
## Solución de problemas
|
|
397
467
|
|
|
@@ -399,8 +469,8 @@ Al cambiar de ronda no reutiliza CP ni misiones de la ronda anterior. Si consult
|
|
|
399
469
|
| --- | --- |
|
|
400
470
|
| `wargaming-overlay: command not found` | Comprueba la instalación con `npm list -g --depth=0`. Revisa PATH y abre otra terminal. En Unix, los ejecutables están bajo `bin` del prefijo mostrado por `npm prefix -g`; en Windows, en el propio prefijo. |
|
|
401
471
|
| PowerShell bloquea un `.ps1` | Usa `npm.cmd` y `wargaming-overlay.cmd`. |
|
|
402
|
-
| npm devuelve EACCES |
|
|
403
|
-
| npm devuelve 404 |
|
|
472
|
+
| npm devuelve EACCES | Ejecuta `npm install -g --prefix "$HOME/.local" @ing.jorgeu/wargaming-overlay` y añade `$HOME/.local/bin` a PATH siguiendo los pasos anteriores. |
|
|
473
|
+
| npm devuelve 404 | Sigue la sección «Si npm devuelve E404»: comprueba nombre, registro y consulta con `--prefer-online`. |
|
|
404
474
|
| ADB no encontrado | Ejecuta `setup --install-adb`, instala Platform-Tools o pasa `--adb` con la ruta completa. |
|
|
405
475
|
| Python ausente/incompatible | Instala Python 3.8+ y usa `--python` si tienes varias instalaciones. En Windows prueba `py -3 --version`. |
|
|
406
476
|
| No aparece ningún teléfono | Revisa cable de datos, puerto USB, desbloqueo y depuración USB. En Windows comprueba drivers. |
|
|
@@ -425,7 +495,7 @@ Detén el servidor con Ctrl+C antes de actualizar. Desde el código fuente actua
|
|
|
425
495
|
npm install -g .
|
|
426
496
|
```
|
|
427
497
|
|
|
428
|
-
Desde npm
|
|
498
|
+
Desde npm:
|
|
429
499
|
|
|
430
500
|
```sh
|
|
431
501
|
npm install -g @ing.jorgeu/wargaming-overlay@latest
|
|
@@ -469,3 +539,89 @@ Consulta [PUBLISHING.md](PUBLISHING.md) para autenticarte y publicar. `npm publi
|
|
|
469
539
|
El código se distribuye bajo **[0BSD](LICENSE)**, una licencia permisiva que permite uso comercial, modificación y redistribución sin atribución obligatoria y sin garantías. [Texto oficial](https://opensource.org/license/0bsd).
|
|
470
540
|
|
|
471
541
|
Se conservan las imágenes del overlay. Su procedencia y derechos se documentan por separado en [THIRD_PARTY_ASSETS.md](THIRD_PARTY_ASSETS.md); la licencia del código no concede derechos adicionales sobre recursos de terceros. El proyecto no declara afiliación oficial con las aplicaciones o juegos a los que hace referencia.
|
|
542
|
+
|
|
543
|
+
## Caché local de estadísticas de Listhammer
|
|
544
|
+
|
|
545
|
+
Al iniciar el servidor con `wargaming-overlay start` o `npm start`, se descargan en segundo plano los datos públicos de [Listhammer Stats](https://listhammer.info/stats) y las páginas de las facciones presentes en su tabla. No requiere dependencias nuevas, cuenta ni configuración. La lectura del teléfono y OBS arrancan sin esperar esta descarga.
|
|
546
|
+
|
|
547
|
+
Se guarda `listhammer-stats.json` en la misma [carpeta de datos](#ubicación-de-los-datos) que `game-state.json`; `--data-dir` también cambia ese destino. En macOS, por defecto:
|
|
548
|
+
|
|
549
|
+
```text
|
|
550
|
+
~/Library/Application Support/wargaming-overlay/listhammer-stats.json
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
El JSON contiene:
|
|
554
|
+
|
|
555
|
+
- `factions`: victorias, derrotas, partidas, porcentaje de victorias, jugadores y demás agregados por facción.
|
|
556
|
+
- `dispositions.overall` y `dispositions.matchups`: resultados por disposición y contra cada disposición rival.
|
|
557
|
+
- `factionDetails`: resultados de cada facción contra las facciones rivales (`matchups`), diferencial de puntos y estadísticas y tamaño de muestra jugando primero cuando la fuente los ofrece. Incluye también el histórico semanal y los agregados de la página.
|
|
558
|
+
- `sourceUrl`, `fetchedAt`, `schemaVersion` y `selection`: procedencia, fecha de descarga, versión del formato, período y filtros.
|
|
559
|
+
|
|
560
|
+
Se recoge la selección inicial del sitio: **This Weekend**, sin RTTs; actualmente partidas de 40k de 11.ª edición, 2000 puntos, eventos de al menos 5 rondas y 16 jugadores, excluyendo enfrentamientos espejo. `headline` y `matchups` corresponden al período indicado; `overall` de cada facción es su histórico general y `weekly` distingue las semanas. Se conservan los porcentajes tal como los entrega Listhammer (cadenas, escala 0–100) y sus tamaños de muestra: un cero con cero partidas no representa una tasa observada. No se guardan nombres de jugadores ni sus listas.
|
|
561
|
+
|
|
562
|
+
La descarga es secuencial, con una pausa de 250 ms entre facciones y un límite de 20 segundos por petición. Se realiza una vez en cada arranque, sin consultas periódicas durante la emisión. La consola informa al terminar e indica la ruta guardada.
|
|
563
|
+
|
|
564
|
+
Si falta internet, el sitio devuelve un error o cambia de formato, aparece un aviso y se conserva la última copia completa. Si es el primer arranque y falla, aún no habrá archivo. Para reintentar, reinicia el servidor. El reemplazo es atómico y solo ocurre después de validar todas las facciones y comprobar que sus resultados y períodos coinciden con la tabla principal.
|
|
565
|
+
|
|
566
|
+
Estos datos alimentan los avisos opcionales descritos abajo. El archivo completo no tiene una ruta HTTP pública. Restablecer la partida no borra esta caché. No se incluyen copias descargadas en el paquete npm. La disponibilidad y los datos dependen de Listhammer; la licencia del código no concede derechos sobre sus datos.
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
## Mensajes, curiosidades y estadísticas en pantalla
|
|
570
|
+
|
|
571
|
+
En el panel encontrarás **Mensajes en pantalla**. Los avisos aparecen en la parte inferior de `overlay-art.html`, sobre una franja oscura translúcida, durante **15 segundos**. Se escalan junto con el diseño original de 1440p para todas las resoluciones compatibles.
|
|
572
|
+
|
|
573
|
+
1. Elige las facciones y disposiciones de ambos jugadores y pulsa **Actualizar overlay**. Se usan esas selecciones guardadas, no el nombre del jugador, para escoger las curiosidades y estadísticas.
|
|
574
|
+
2. Para un aviso propio, escribe hasta 280 caracteres y pulsa **Mostrar mensaje · 15 s**. Por ejemplo: `¡Bienvenidos! La siguiente ronda comienza en unos minutos.` Los mensajes son texto plano; los saltos de línea se convierten en espacios.
|
|
575
|
+
3. Activa **Mostrar un dato cada 2 minutos** para iniciar la rotación. El primer aviso automático aparece después de dos minutos. Puedes incluir **Curiosidades de las facciones**, **Estadísticas y enfrentamiento actual**, o ambas. Las opciones se guardan de inmediato, sin pulsar Actualizar overlay.
|
|
576
|
+
4. **Mostrar siguiente dato** adelanta la rotación incluso cuando el modo automático está pausado. Cada pulsación reemplaza el aviso actual y usa el mismo historial que la rotación automática.
|
|
577
|
+
5. Al empezar otra transmisión, pulsa **Nueva sesión de mensajes** para reiniciar el historial. No modifica jugadores, puntos, misiones ni la caché de estadísticas.
|
|
578
|
+
|
|
579
|
+
Un mensaje manual reemplaza cualquier aviso visible, no consume curiosidades y deja dos minutos hasta el siguiente aviso automático. Los avisos automáticos solo consumen datos cuando hay al menos una fuente del overlay conectada; tener únicamente el panel abierto no consume la rotación. Varias fuentes de OBS reciben el mismo aviso, sin crear temporizadores independientes. Si OBS se reconecta mientras hay un aviso, recibe solo el tiempo restante de sus 15 segundos.
|
|
580
|
+
|
|
581
|
+
### Selección y repetición
|
|
582
|
+
|
|
583
|
+
El catálogo contiene **764 curiosidades aprobadas en español para las 28 facciones**, con al menos 20 por facción. Solo incluye entradas auditadas; cada una conserva su enlace y fecha de revisión. Genestealer Cults dispone de 32 y Thousand Sons de 20: juntas ofrecen 52 curiosidades, más las estadísticas disponibles. Custom no tiene curiosidades inventadas.
|
|
584
|
+
|
|
585
|
+
El orden es aleatorio, con más peso para las estadísticas del enfrentamiento actual. Cada dato aparece una vez por ciclo. Al completar la lista empieza otro ciclo automáticamente, sin repetir inmediatamente el último dato si hay alternativas. El botón «Mostrar siguiente dato» también inicia el siguiente ciclo. Las partidas espejo usan una sola copia del catálogo de su facción. El panel separa curiosidades y estadísticas, e indica el ciclo y los datos pendientes. La cobertura mostrada es el tiempo hasta completar el ciclo; la rotación puede continuar indefinidamente.
|
|
586
|
+
|
|
587
|
+
El historial y las opciones se guardan en `toast-session.json`, dentro de la carpeta de datos (`--data-dir`). Recargar el panel, refrescar OBS, pausar la rotación o reiniciar el servidor no reinicia ese historial. «Nueva sesión de mensajes» es independiente de «Restablecer datos de la partida».
|
|
588
|
+
|
|
589
|
+
### Qué estadísticas se muestran
|
|
590
|
+
|
|
591
|
+
La caché local de Listhammer proporciona porcentajes generales de las facciones elegidas, resultados entre ambas facciones, diferencial de puntos y resultados jugando primero cuando hay al menos 10 partidas para esos dos últimos indicadores. También aparecen las disposiciones seleccionadas y su enfrentamiento: se identifican expresamente como resultados **entre todas las facciones**, no como estadísticas particulares de los jugadores.
|
|
592
|
+
|
|
593
|
+
Cada aviso estadístico incluye período, fecha de la copia y número de partidas. Una tasa con menos de 10 partidas se marca como **muestra pequeña**. Los resultados con cero partidas se omiten. No se calcula una probabilidad de victoria para esta partida ni se inventa un 50 % para enfrentamientos espejo. Los nombres equivalentes del selector y Listhammer se reconocen, por ejemplo Space Marines / Space Marines (Astartes), Agents of the Imperium / Imperial Agents y Genestealer Cults / Genestealer Cult.
|
|
594
|
+
|
|
595
|
+
No se consulta internet cada dos minutos: las curiosidades vienen incluidas con el paquete y los avisos de estadísticas usan la copia descargada al arrancar. Si Listhammer falla, se utiliza la última copia disponible indicando su fecha. Sin ninguna copia, continúan las curiosidades habilitadas y el panel avisa de que las estadísticas no están disponibles. Las pruebas usan un reloj simulado para verificar la cadencia, la expiración y la ausencia de repeticiones dentro de cada ciclo y continuidad entre ciclos, además de probar los endpoints y la entrega de eventos.
|
|
596
|
+
|
|
597
|
+
### Si no aparece un aviso
|
|
598
|
+
|
|
599
|
+
- Reinicia el servidor con esta versión del código y refresca el panel y la fuente Navegador de OBS. La fuente debe apuntar a `/overlay-art.html` en el mismo puerto que el panel.
|
|
600
|
+
- Si ejecutas una instalación global anterior, los cambios del repositorio no la actualizan automáticamente: reinstala esa versión o prueba con `npm start` desde el repositorio.
|
|
601
|
+
- Comprueba que guardaste las facciones, que hay contenido habilitado y que el panel muestra datos disponibles. El botón «Mostrar siguiente dato» permite comprobarlo sin esperar dos minutos.
|
|
602
|
+
- «Nueva sesión» borra solo el historial de avisos, no las estadísticas descargadas.
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
### Correspondencia de facciones y cobertura de una transmisión
|
|
606
|
+
|
|
607
|
+
El selector y los mensajes usan el mismo registro, `public/factions.json`. Las equivalencias de `factions.js` traducen **Space Marines (Astartes) → Space Marines**, **Imperial Agents → Agents of the Imperium** y **Genestealer Cult → Genestealer Cults**. Se toleran diferencias de mayúsculas, espacios y puntuación. Los capítulos se mantienen separados: Blood Angels no recibe estadísticas de Space Marines. Los nombres desconocidos se señalan en el panel y no reciben contenido de otra facción por semejanza.
|
|
608
|
+
|
|
609
|
+
Las estadísticas de un enfrentamiento se toman desde la perspectiva de la facción indicada en el texto. No se calcula el porcentaje del contrario restándolo de 100. Las disposiciones globales se identifican como datos de todas las facciones.
|
|
610
|
+
|
|
611
|
+
A un aviso cada dos minutos, cuatro horas requieren **120 datos distintos**. Para garantizar esa duración con solo curiosidades en cualquier combinación, incluidas partidas espejo, hacen falta 120 por facción. **Esa ampliación general todavía está pendiente**: Adepta Sororitas y Tyranids ya tienen 120 curiosidades por facción y cubren incluso partidas espejo durante 240 minutos. El catálogo actual cubre Aeldari vs Chaos Daemons durante 248 minutos, pero un espejo de cualquiera de ellas cubre 124 minutos y otras combinaciones tienen menos contenido. Las estadísticas disponibles pueden aumentar la duración; no se cuentan como una garantía porque dependen de la copia local de Listhammer.
|
|
612
|
+
|
|
613
|
+
El panel muestra el número de datos aún no vistos y su duración a intervalos de dos minutos. Advierte si no quedan cuatro horas. Usar «Mostrar siguiente dato» consume una entrada y reduce esa cobertura; los mensajes manuales no la consumen. La estimación presupone que mantienes las facciones y opciones seleccionadas.
|
|
614
|
+
|
|
615
|
+
Para revisar el catálogo desde el repositorio:
|
|
616
|
+
|
|
617
|
+
```sh
|
|
618
|
+
npm run facts:coverage
|
|
619
|
+
```
|
|
620
|
+
|
|
621
|
+
Para exigir cuatro horas incluso en partidas espejo (termina con código 1 mientras falten curiosidades):
|
|
622
|
+
|
|
623
|
+
```sh
|
|
624
|
+
npm run facts:coverage -- --require-four-hours
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
Al ampliar `data/faction-facts.json`, usa el nombre exacto del registro, conserva los IDs existentes para respetar el historial y asigna un ID estable a cada nueva curiosidad. Incluye un enlace específico, título de fuente y fecha de revisión. No añadas reformulaciones del mismo dato para aumentar artificialmente la duración.
|
package/announcements.js
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
const {randomUUID} = require('node:crypto');
|
|
2
|
+
const catalog = require('./data/faction-facts.json').facts;
|
|
3
|
+
const DURATION = 15000;
|
|
4
|
+
const INTERVAL = 120000;
|
|
5
|
+
const key = value => String(value || '').normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
6
|
+
const {factionKey, canonicalFaction} = require('./factions');
|
|
7
|
+
function validRate(row) {
|
|
8
|
+
return row && Number.isInteger(row.total) && row.total > 0 && row.winRate != null && row.winRate !== '' && Number.isFinite(Number(row.winRate)) && Number(row.winRate) >= 0 && Number(row.winRate) <= 100;
|
|
9
|
+
}
|
|
10
|
+
const percent = n => Number(n).toLocaleString('es-ES', {maximumFractionDigits:1}) + ' %';
|
|
11
|
+
const sample = n => `${n} partidas${n < 10 ? ' · muestra pequeña' : ''}`;
|
|
12
|
+
function buildPool(layout, stats, settings) {
|
|
13
|
+
const players = [layout?.left, layout?.right].filter(p => canonicalFaction(p?.faction)).map(p=>({...p,faction:canonicalFaction(p.faction)}));
|
|
14
|
+
const selected = new Set(players.map(p => factionKey(p.faction)));
|
|
15
|
+
const pool = settings.lore ? catalog.filter(f => selected.has(factionKey(f.faction))).map(f => ({...f, kind:'lore', weight:2, title:'¿Sabías qué?:', footer:f.sourceTitle})) : [];
|
|
16
|
+
if (!settings.meta || !stats?.period || !Array.isArray(stats.factions) || !Array.isArray(stats.factionDetails)) return pool;
|
|
17
|
+
const sourceDate = /^\d{4}-\d{2}-\d{2}/.exec(stats.fetchedAt || '')?.[0];
|
|
18
|
+
const footer = `Listhammer · ${stats.period}${sourceDate ? ' · copia ' + sourceDate : ''}`;
|
|
19
|
+
function add(id, text, row, sourceUrl, weight = 1) {
|
|
20
|
+
if (!validRate(row)) return;
|
|
21
|
+
pool.push({id:'meta:'+id, kind:'meta', title:'¿Sabías qué?:', text, footer:footer+' · '+sample(row.total), sourceTitle:'Listhammer', sourceUrl:sourceUrl || stats.sourceUrl, weight});
|
|
22
|
+
}
|
|
23
|
+
for (const id of selected) {
|
|
24
|
+
const row = stats.factions.find(f => factionKey(f.faction) === id);
|
|
25
|
+
if (validRate(row)) add('overall:'+id, `${canonicalFaction(row.faction)} registra un ${percent(row.winRate)} de victorias en el período de Listhammer.`, row, row.sourceUrl);
|
|
26
|
+
}
|
|
27
|
+
if (players.length === 2) {
|
|
28
|
+
const [left, right] = players;
|
|
29
|
+
const a = factionKey(left.faction), b = factionKey(right.faction);
|
|
30
|
+
const detail = stats.factionDetails.find(f => factionKey(f.faction) === a);
|
|
31
|
+
const matchup = detail?.matchups?.find(m => factionKey(m.opponentFaction) === b);
|
|
32
|
+
// A mirrored matchup is omitted by the source, never inferred as 50%.
|
|
33
|
+
if (a !== b && validRate(matchup)) {
|
|
34
|
+
const pair = [a,b].sort().join(':');
|
|
35
|
+
add('matchup:'+pair, `En este cruce, ${left.faction} obtuvo un ${percent(matchup.winRate)} de victorias frente a ${right.faction}.`, matchup, detail.sourceUrl, 4);
|
|
36
|
+
if (Number.isFinite(matchup.avgDifferential) && matchup.differentialGames >= 10) {
|
|
37
|
+
const diff = matchup.avgDifferential;
|
|
38
|
+
const value = Math.abs(diff).toLocaleString('es-ES',{maximumFractionDigits:1});
|
|
39
|
+
add('differential:'+pair, `${left.faction} promedió ${value} puntos ${diff < 0 ? 'menos' : 'más'} que ${right.faction} en sus enfrentamientos.`, {...matchup,total:matchup.differentialGames}, detail.sourceUrl, 3);
|
|
40
|
+
}
|
|
41
|
+
if (validRate(matchup.goingFirst) && matchup.goingFirst.total >= 10) {
|
|
42
|
+
add('first:'+a+':'+b, `Cuando jugó primero, ${left.faction} ganó el ${percent(matchup.goingFirst.winRate)} de sus partidas contra ${right.faction}.`, matchup.goingFirst, detail.sourceUrl, 3);
|
|
43
|
+
}
|
|
44
|
+
// Also retain the other faction's first-turn record when the source has a sufficient sample.
|
|
45
|
+
const reverse = stats.factionDetails.find(f => factionKey(f.faction) === b);
|
|
46
|
+
const first = reverse?.matchups?.find(m => factionKey(m.opponentFaction) === a)?.goingFirst;
|
|
47
|
+
if (validRate(first) && first.total >= 10) add('first:'+b+':'+a, `Cuando jugó primero, ${right.faction} ganó el ${percent(first.winRate)} de sus partidas contra ${left.faction}.`, first, reverse.sourceUrl, 3);
|
|
48
|
+
}
|
|
49
|
+
const formations = [...new Set(players.map(p => p.formation || 'Take and Hold'))];
|
|
50
|
+
for (const formation of formations) {
|
|
51
|
+
const row = stats.dispositions?.overall?.find(r => r.disposition === formation);
|
|
52
|
+
if (validRate(row)) add('disposition:'+key(formation), `La disposición ${formation} registra un ${percent(row.winRate)} de victorias entre todas las facciones.`, row);
|
|
53
|
+
}
|
|
54
|
+
if (formations.length === 2) {
|
|
55
|
+
const row = stats.dispositions?.matchups?.find(r => r.disposition === formations[0] && r.opponentDisposition === formations[1]);
|
|
56
|
+
if (validRate(row)) add('dispositions:'+formations.map(key).sort().join(':'), `${formations[0]} obtuvo un ${percent(row.winRate)} de victorias contra ${formations[1]}, contando todas las facciones.`, row, null, 2);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return pool;
|
|
60
|
+
}
|
|
61
|
+
class Announcements {
|
|
62
|
+
constructor({layout, stats, saved = {}, save = () => {}, emit = () => {}, now = Date.now, random = Math.random}) {
|
|
63
|
+
Object.assign(this, {layout, stats, save, emit, now, random});
|
|
64
|
+
this.settings = {enabled: saved.settings?.enabled === true, lore:saved.settings?.lore !== false, meta:saved.settings?.meta !== false};
|
|
65
|
+
this.seen = new Set(Array.isArray(saved.seen) ? saved.seen.filter(s => typeof s === 'string') : []);
|
|
66
|
+
this.lastId = saved.lastId || null;
|
|
67
|
+
this.sessionId = saved.sessionId || randomUUID();
|
|
68
|
+
this.cycle = Number.isInteger(saved.cycle) && saved.cycle > 0 ? saved.cycle : 1;
|
|
69
|
+
this.active = null;
|
|
70
|
+
this.nextAt = this.settings.enabled ? now() + INTERVAL : null;
|
|
71
|
+
}
|
|
72
|
+
pool() { return buildPool(this.layout(), this.stats(), this.settings); }
|
|
73
|
+
persist() { this.save({schemaVersion:1,sessionId:this.sessionId,settings:this.settings,seen:[...this.seen],lastId:this.lastId,cycle:this.cycle}); }
|
|
74
|
+
status() {
|
|
75
|
+
const pool = this.pool();
|
|
76
|
+
const remaining = pool.filter(f=>!this.seen.has(f.id)).length;
|
|
77
|
+
const selected = [this.layout()?.left?.faction, this.layout()?.right?.faction].filter(Boolean);
|
|
78
|
+
const unmatchedFactions = [...new Set(selected.filter(name=>!canonicalFaction(name)))];
|
|
79
|
+
return {unmatchedFactions, exhausted:pool.length > 0 && remaining === 0, cyclic:true, loreTotal:pool.filter(f=>f.kind==='lore').length, metaTotal:pool.filter(f=>f.kind==='meta').length, fourHourReady:remaining >= 120, settings:this.settings,sessionId:this.sessionId,cycle:this.cycle,total:pool.length,remaining:pool.filter(f=>!this.seen.has(f.id)).length,coverageMinutes:pool.length*INTERVAL/60000,remainingMinutes:pool.filter(f=>!this.seen.has(f.id)).length*INTERVAL/60000,nextAt:this.nextAt,active:this.current(),statsAvailable:!!this.stats()?.factions?.length};
|
|
80
|
+
}
|
|
81
|
+
current() { return this.active?.expiresAt > this.now() ? {...this.active,remainingMs:this.active.expiresAt-this.now()} : null; }
|
|
82
|
+
refresh() { this.emit('status',this.status()); }
|
|
83
|
+
configure(input) {
|
|
84
|
+
if (!input || typeof input !== 'object' || Object.keys(input).some(k => !['enabled','lore','meta'].includes(k) || typeof input[k] !== 'boolean')) throw new Error('Configuración de mensajes no válida.');
|
|
85
|
+
const enabled = this.settings.enabled;
|
|
86
|
+
this.settings = {...this.settings,...input};
|
|
87
|
+
if (this.settings.enabled && !this.settings.lore && !this.settings.meta) { this.settings.enabled = false; }
|
|
88
|
+
if (enabled !== this.settings.enabled) this.nextAt = this.settings.enabled ? this.now()+INTERVAL : null;
|
|
89
|
+
this.persist(); this.refresh();
|
|
90
|
+
}
|
|
91
|
+
show(item) {
|
|
92
|
+
const now = this.now();
|
|
93
|
+
this.active = {...item,token:randomUUID(),startedAt:now,expiresAt:now+DURATION};
|
|
94
|
+
if (this.settings.enabled) this.nextAt = now+INTERVAL;
|
|
95
|
+
this.emit('toast',this.current()); this.refresh();
|
|
96
|
+
return this.current();
|
|
97
|
+
}
|
|
98
|
+
manual(text) {
|
|
99
|
+
if (typeof text !== 'string' || !text.trim() || text.trim().length > 280) throw new Error('Escribe un mensaje de 1 a 280 caracteres.');
|
|
100
|
+
return this.show({kind:'manual',title:'EN DIRECTO',text:text.trim().replace(/\s+/g, ' '),footer:''});
|
|
101
|
+
}
|
|
102
|
+
next() {
|
|
103
|
+
const pool = this.pool();
|
|
104
|
+
if (!pool.length) throw new Error('No hay datos disponibles para estas facciones y opciones. Activa curiosidades o estadísticas y guarda las facciones del encuentro.');
|
|
105
|
+
let choices = pool.filter(f=>!this.seen.has(f.id));
|
|
106
|
+
if (!choices.length) {
|
|
107
|
+
// Start another pass through the current pool, retaining history for other matchups.
|
|
108
|
+
for (const item of pool) this.seen.delete(item.id);
|
|
109
|
+
this.cycle += 1;
|
|
110
|
+
choices = pool.length > 1 ? pool.filter(f=>f.id !== this.lastId) : pool;
|
|
111
|
+
}
|
|
112
|
+
let draw = this.random()*choices.reduce((sum,f)=>sum+f.weight,0);
|
|
113
|
+
const chosen = choices.find(f=>(draw-=f.weight)<0) || choices.at(-1);
|
|
114
|
+
this.seen.add(chosen.id); this.lastId = chosen.id;
|
|
115
|
+
this.persist();
|
|
116
|
+
return this.show(chosen);
|
|
117
|
+
}
|
|
118
|
+
tick(hasAudience) {
|
|
119
|
+
if (!this.settings.enabled || this.now() < this.nextAt) return;
|
|
120
|
+
this.nextAt = this.now()+INTERVAL;
|
|
121
|
+
if (hasAudience && !this.current() && this.pool().length > 0) this.next();
|
|
122
|
+
else this.refresh();
|
|
123
|
+
}
|
|
124
|
+
reset() {
|
|
125
|
+
this.seen.clear(); this.lastId=null; this.cycle=1; this.sessionId=randomUUID();
|
|
126
|
+
this.active=null; this.nextAt=this.settings.enabled ? this.now()+INTERVAL : null;
|
|
127
|
+
this.persist(); this.emit('toast',null); this.refresh();
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
module.exports = {Announcements,buildPool,factionKey,DURATION,INTERVAL};
|
package/battle_reader.py
CHANGED
|
@@ -13,7 +13,16 @@ def parse(xml):
|
|
|
13
13
|
players = [dict(name=header[i+3], score=int(header[i+1]), cp=None, primary=None, secondary=None, secondaries=None, primaryObjectives=None) for i in range(2)]
|
|
14
14
|
names = [p['name'] for p in players]
|
|
15
15
|
if names[0] == names[1]: raise ValueError('Usa nombres distintos para identificar a los jugadores.')
|
|
16
|
+
# A scrolled two-player round can omit the first player's heading while
|
|
17
|
+
# exposing its scores/missions followed by the other player's heading.
|
|
18
|
+
# That next heading bounds the leading block and identifies its owner.
|
|
19
|
+
headings = [(i, t) for i, t in enumerate(texts) if t in names]
|
|
16
20
|
current = None
|
|
21
|
+
if headings and len({name for _, name in headings}) == 1:
|
|
22
|
+
first_index, next_name = headings[0]
|
|
23
|
+
leading = texts[:first_index]
|
|
24
|
+
if any(re.fullmatch(r'(PRIMARY|SECONDARY):\s*\d+/\d+', t) for t in leading):
|
|
25
|
+
current = players[1 - names.index(next_name)]
|
|
17
26
|
section = None
|
|
18
27
|
for i, t in enumerate(texts):
|
|
19
28
|
if t in names:
|