Event Creator
Los eventos son escenas narrativas con opciones y resultados aleatorios ponderados. La lógica pura
vive en src/game/systems/EventSystem.ts; los datos de contenido en src/game/constants/events/*.ts.
Esta skill documenta el modelo REAL (extendido en T-008) para escribir contenido nuevo que tipe
y funcione. Usa la fuente de verdad, no inventes campos.
Fuentes de verdad (léelas si dudas de un campo):
- Tipos:
src/game/types.ts(GameEvent,EventChoice,EventOutcome,RequirementCheck,EventCost,RiskLevel,Player.eventFlags).- Resolución:
src/game/systems/EventSystem.ts(resolveEventChoice,applyOutcomeEffects,rollOutcome,checkEventFlags,getAvailableChoices,getAvailableEventsForPlayer).- Selección/flujo:
src/game/systems/LocationSystem.ts(generateEventActivity→ filtra la oferta por arco y por flags víagetAvailableEventsForPlayer),src/hooks/useActivityHandlers.ts(handleEventChoice→ cadena/combate/cierre),src/scenes/activities/Event.tsx(UI: oculta opciones gateadas por flags).- Ejemplos vivos:
src/game/constants/events/{genericEvents,wavesArcEvents,academyArcEvents}.ts.
1. Cuándo usarla
- Añadir un evento nuevo a un arco (o genérico a todos los arcos).
- Editar outcomes, requisitos, costos o pesos de un evento existente.
- Construir una cadena de escenas (
chainTo+setFlags) o un evento gateado por progreso. - Usar los efectos T-008 (
grantSkillById,curse,removeRandomItem) con trade-off legible.
Contenido puro de datos: no toques EventSystem.ts salvo que cambies la mecánica del motor
(eso es un topic de combat/architecture, no de events).
2. Modelo de datos
GameEvent
| Campo | Tipo | Obl. | Para qué |
|-------|------|------|----------|
| id | string | Sí | Identificador único. Lo referencia chainTo y EVENTS.find(e => e.id === ...). |
| title | string | Sí | Cabecera en la UI del evento. |
| description | string | Sí | Texto narrativo del cuerpo. |
| allowedArcs | string[] | No | Arcos donde aparece (p.ej. ['WAVES_ARC']). [] u omitido = todos los arcos. Se compara contra region.arc en LocationSystem. |
| rarity | Rarity | No | Pondera la aparición (T-016). generateEventActivity (LocationSystem) hace una tirada ponderada por rareza vía selectWeightedEvent: tiers más raros salen menos. Pesos en EVENT_RARITY_WEIGHTS (src/game/constants/index.ts; hoy COMMON 100 / RARE 45 / EPIC 18 / LEGENDARY·CURSED 7). Omitido = tratado como COMMON. |
| requiresFlags | Record<string, number> | No | T-008. El evento solo se ofrece si todo flag ≥ su valor. |
| excludesFlags | Record<string, number> | No | T-008. El evento se oculta si algún flag ≥ su valor. |
| choices | EventChoice[] | Sí | 1+ opciones del jugador. |
EventChoice
| Campo | Tipo | Obl. | Para qué |
|-------|------|------|----------|
| label | string | Sí | Texto del botón. |
| description | string | Sí | Subtexto bajo el botón (telegrafía riesgo/costo/requisito). |
| riskLevel | RiskLevel | Sí | SAFE \| LOW \| MEDIUM \| HIGH \| EXTREME. Indicador visible; debe casar con la varianza real. |
| hintText | string | No | Pista vaga del resultado. |
| requirements | RequirementCheck | No | minStat y/o requiredClan. Si no se cumple, la opción se muestra deshabilitada (no se oculta). |
| requiresFlags | Record<string, number> | No | T-008. La opción se elimina de la oferta salvo que todo flag ≥ su valor. |
| excludesFlags | Record<string, number> | No | T-008. La opción se elimina si algún flag ≥ su valor. |
| costs | EventCost | No | { ryo?: number }. Se paga al elegir; opción deshabilitada si no alcanza. |
| outcomes | EventOutcome[] | Sí | Tabla de resultados ponderados. |
Agencia de clan: no hay un sesgo probabilístico por clan. Para dar a un clan una vía propia usa
requirements.requiredClanen unEventChoice(opción exclusiva/legible), que es la ruta viva. El antiguoclanBonusse retiró en T-016 por ser un no-op (ver Notas de discrepancia).
RequirementCheck: minStat?: { stat: PrimaryStat; value: number } (compara contra
player.primaryStats[stat]), requiredClan?: Clan.
EventCost: { ryo?: number }.
EventOutcome
| Campo | Tipo | Obl. | Para qué |
|-------|------|------|----------|
| weight | number | Sí | Peso de probabilidad. Convención: los pesos de un choice suman 100. |
| effects | object | Sí | Efectos aplicados al resolver (ver tabla). |
EventOutcome.effects
| Campo | Tipo | Aplicado por | Notas |
|-------|------|--------------|-------|
| statChanges | Partial<PrimaryAttributes> | applyOutcomeEffects | +/- stats primarios; piso 1. |
| exp | number | applyOutcomeEffects | Otorga XP. |
| ryo | number | applyOutcomeEffects | Suma (puede ser negativo). |
| hpChange | number \| { percent: number } | applyOutcomeEffects | Plano o % de maxHp; clamp [1, maxHp]. |
| chakraChange | number \| { percent: number } | applyOutcomeEffects | Plano o % de maxChakra; clamp [0, maxChakra]. |
| buffs | Buff[] | applyOutcomeEffects | Se añaden a player.activeBuffs. |
| upgradeTreasureQuality | boolean | applyOutcomeEffects | Un paso BROKEN→COMMON→RARE. |
| addMerchantSlot | boolean | applyOutcomeEffects | +1 hasta MAX_MERCHANT_SLOTS. |
| intelGain | number | handleEventOutcomeClose | Reemplaza el Intel por defecto de la sala (INTEL_GAIN.EVENT_DEFAULT), no se suma; aplica solo en el outcome terminal que la cierra (no en combate ni en cadena). Un terminal sin intelGain igual otorga el default. Rango típico 0–40. |
| triggerCombat | { floor; difficulty; archetype; name? } | handleEventChoice | Inicia pelea tras el evento. floor→dangerLevel = clamp(ceil(floor/3),1,7); floor:0 = danger actual. |
| chainTo | string | handleEventChoice | T-008. Id del siguiente GameEvent a abrir en vez de volver a exploración. |
| setFlags | Record<string, number> | applyOutcomeEffects | T-008. Merge-asigna en player.eventFlags (sobrescribe, no incrementa). |
| grantSkillById | string | applyOutcomeEffects | T-008. Otorga la skill cuyo Skill.id coincide en SKILLS. Dedupe por id (repetir = no-op). |
| curse | { value?; duration? } | applyOutcomeEffects | T-008. Añade Buff EffectType.CURSE a activeBuffs. value = fracción de daño extra recibido (def 0.5); duration turnos (def 3). |
| removeRandomItem | boolean | applyOutcomeEffects | T-008. Vacía un slot lleno aleatorio del bag (PRNG del juego). |
| logMessage | string | — | Obligatorio. Línea de log al resolver. |
| logType | 'gain' \| 'danger' \| 'info' \| 'loot' | — | Obligatorio. Categoría/color del log. |
Gating por flags (semántica real, checkEventFlags): los flags viven en player.eventFlags
como enteros (0 = ausente). requiresFlags pasa si flag ≥ valor; excludesFlags bloquea si
flag ≥ valor. Gates undefined siempre pasan (los eventos/opciones existentes no se ven afectados).
La escritura es vía setFlags (sobrescribe al valor dado). Para un contador, escribe el valor
explícito que quieras (p.ej. setFlags: { favor: 2 }).
3. Reglas de oro
- Pesos suman 100 por choice.
rollOutcomenormaliza por el total igualmente, pero 100 es la convención legible y lo que verificaeventContent.test.ts. riskLevelacorde a la varianza.SAFE= un outcome benigno de peso 100 o sin castigo real;HIGH/EXTREME= puede disparar combate o perder HP/objeto significativo. NuncaSAFEen un choice que puedatriggerCombat.- Siempre
logMessage+logType(el tipo los exige; sin ellos no compila). - Requisitos/costos legibles: que el
description/hintTexttelegrafíe elminStat,costso el peligro. El jugador debe entender por qué ganó o perdió (pilar de VISION). - Siempre una salida. Deja al menos un choice
SAFEde "marcharse/declinar" sin requisitos ni flags: el jugador conserva agencia, sin muertes injustas. - Cadenas (
chainTo): apunta a unidreal presente enEVENTS. El eslabón final NO llevachainTonitriggerCombat, para que la sala se cierre por el flujo normal (handleEventOutcomeClose). Persiste el estado de la historia consetFlagsen el outcome que ramifica, y gatea las escenas siguientes conrequiresFlags/excludesFlags. - Efectos T-008 con trade-off legible (pilar VISION + advisory de balance): no regales una skill
dominante gratis con
grantSkillById;curse/removeRandomItemdeben venir con recompensa o contrajugada. El tuning fino de números queda para el topic de balance (T-012), no lo cierres aquí. intelGainsolo cuenta en el outcome terminal (el que cierra la sala). No lo pongas esperando efecto en un outcome contriggerCombatochainTo.
4. Plantillas
Tres plantillas listas para copiar y tipar contra el modelo real (evento simple, cadena multi-escena
con flags, y evento gateado con requiresFlags/excludesFlags) están en
references/templates.md. Todas usan import { GameEvent, PrimaryStat, Rarity, RiskLevel } from '../../types'
como los archivos de arco reales. Léelo antes de escribir contenido nuevo.
5. Checklist de registro
- Añade el objeto
GameEvental array del arco correcto:WAVES_ARC_EVENTSenwavesArcEvents.ts,ACADEMY_ARC_EVENTSenacademyArcEvents.ts, etc., oGENERIC_EVENTS(genericEvents.ts) si es de todos los arcos. QueallowedArcscase con el arco. - Los arrays de arco ya están spread en la constante agregadora
EVENTSdesrc/game/constants/index.ts(se llamaEVENTS, NOALL_EVENTS). Añadir al array del arco basta; no hace falta registro extra salvo que crees un archivo de arco nuevo → entonces impórtalo y hazle spread enEVENTS. - Cadenas: el evento destino de
chainTodebe existir en algún array spread enEVENTS(el lookup esEVENTS.find). Las restricciones de arco no bloquean el destino de una cadena (se busca por id directo), pero mantenlo en el mismo archivo de arco por cohesión. - Todo flag usado en
requiresFlags/excludesFlagsdebe escribirse en algúnsetFlags, o nunca se activará. - Gates deterministas:
npx tsc --noEmit(0 err) ynpm test(verde). Las suites deEventSystem.test.tscubren la resolución; no las rompas.
6. Errores comunes / YAGNI
- No hay sistema de aliados/compañeros — no inventes uno.
- Pesos que no suman 100 — funciona pero es engañoso y lo marca
eventContent.test.ts. effects.items/effects.skillsya no existen (retirados en T-016). Para dar un jutsu usagrantSkillById(la única vía viva). No hay ruta de outcome que otorgue objetos: no la inventes.- Olvidar registrar un archivo de arco nuevo (no spread en
EVENTS) → el evento nunca aparece. - Gatear TODOS los choices sin fallback → hay una guarda anti-softlock que reabre la lista completa, pero depender de ella es un olor: deja siempre un choice sin gatear.
- Cadena que nunca termina (cada eslabón lleva
chainTo) → la sala nunca se completa. Asegura un eslabón terminal sinchainTonitriggerCombat. setFlagstratado como incremento — es sobrescritura (merge-asigna). Escribe el valor final.intelGainen un outcome de combate/cadena esperando que aplique — solo cuenta en el terminal.
Notas de discrepancia (código real vs. brief)
- El agregador se llama
EVENTS(src/game/constants/index.ts), noALL_EVENTS. - Resuelto (T-016):
effects.itemsyeffects.skillsestaban declarados pero nunca se aplicaban (dead code) → se retiraron del tipo. La vía viva para otorgar una skill esgrantSkillById; no hay ruta para otorgar objetos. intelGainse consume enhandleEventOutcomeClose(cierre de sala), no enapplyOutcomeEffects.- Resuelto (T-016):
GameEvent.rarityahora sí pondera la selección —generateEventActivityusaselectWeightedEventcon pesos deEVENT_RARITY_WEIGHTS. Ya no es aleatoria uniforme. - Resuelto (T-016):
clanBonusera un no-op (escalaba todos los pesos y renormalizaba) → se retiró el campo. Para agencia de clan usarequirements.requiredClanen un choice.
Recursos del skill
references/templates.md— evento simple, cadena multi-escena (chainTo+ flags) y evento gateado, como TypeScript copiable que tipa contra el modelo real.