Funpix Kit
Inicio / Componentes / fpx · dialog

fpx · dialog

Grafos de diálogo ramificado con condiciones (nivel, quests) contra un contexto inyectado. Runtime puro: jamás toca estado global.

capa 0 v0.1.0 sin dependencias

El ZIP incluye: addons/fpx/dialog + sus tests.

Qué es

Diálogo ramificado como grafo de datos (nodos con texto del NPC + opciones del jugador que navegan o disparan acciones de vocabulario congelado) y DialogRuntime PURO: filtra opciones visibles por condiciones (nivel mínimo, quest hecha/activa, auto-ocultar al completar) contra un context INYECTADO, y devuelve las acciones para que la UI las ejecute — jamás toca estado global.

Instalación

  1. Descarga fpx-dialog-v0.1.0.zip (botón de arriba) y extráelo en la raíz de tu proyecto Godot — deja la estructura tal cual:
text
tu-juego/
├── project.godot
├── addons/
│   └── fpx/
│       └── dialog/
└── tests/          ← opcional: bórralo si no quieres los tests
  1. Abre el proyecto (o corre --import headless). Sin plugins ni autoloads que activar: las clases quedan disponibles en cualquier script.
  2. Opcional — verifica con la suite (necesita GdUnit4, ver la guía): -a tests/dialog.

Ejemplo de uso

Usa las fixtures incluidas en el componente para probar en segundos; en tu juego, sustitúyelas por tus datos.

dialog · ejemplo
# El grafo dorado de la fixture (misma estructura que maestro_roderic de
# Stremus). Tus grafos reales son .tres en res://data/dialogs.
var runtime := DialogRuntime.new()
runtime.start(DialogFixtures.golden_graph())

# El contexto se INYECTA: el runtime jamás toca estado global.
var context := {
	"level": 3,
	"quest_done": func(id: StringName) -> bool:
		return id == &"reconocimiento_del_perimetro",
	"quest_active": func(_id: StringName) -> bool:
		return false,
}

print(runtime.current_text())  # el texto del NPC en el nodo actual

# Opciones filtradas por condiciones (nivel mínimo, quest hecha, auto-ocultar).
var options := runtime.visible_options(context)
for i in options.size():
	print(i, ": ", options[i].text)

# choose() navega el grafo o te DEVUELVE la acción — ejecutarla es cosa tuya.
var result := runtime.choose(0)
match result["kind"]:
	&"goto":    # ya movió el nodo actual
		print(runtime.current_text())
	&"action":  # p. ej. offer_quest / open_shop: la ejecuta tu juego
		start_quest(result["quest_id"])
	&"end":     # cerrar el panel
		pass
	&"none":    # índice inválido: jamás crashea
		pass

Demo en vivo

El componente REAL corriendo en tu navegador — Godot 4.7 exportado a Web sobre las fixtures del kit. La primera carga baja el motor (~10 MB comprimido) y se comparte entre todas las demos.

abrir a pantalla completa ↗

API pública

ClaseQué da
DialogGraphDefinition (Resource)id, start_node, nodes · node_by_id(id) · by_id(dialog_id) desde DIALOGS_DIR
DialogNodeDefinition / DialogOptionDefinitiontexto, goto, action, quest_id, required_level, required_quest_done, hide_if_quest_done
DialogRuntimestart(graph) · current_node() / current_text() · visible_options(context) · choose(index) -> Dictionary · goto_node(id) · option_visible(option, context) estática

context = {level: int, quest_done: Callable(id) -> bool, quest_active: Callable(id) -> bool}.

Firmas completas (del código)

DialogGraphDefinitionextends Resourcedialog_graph_definition.gd

Grafo de DIÁLOGO RAMIFICADO como datos (ED-3, plan_editor_contenido.md §4.1): opciones y sub-opciones a N niveles, con condiciones y acciones. Se resuelve por id contra data/dialogs/ (mismo "Db" mínimo que QuestDefinition.by_id / MapDefinition.by_id). Un NpcDefinition apunta a un grafo con su campo dialog; sin grafo, el NPC sigue con dialog_lines (greybox intacto — compatibilidad TOTAL).

const DIALOGS_DIR: String = "res://data/dialogs"
@export var id: StringName
@export var display_name: String = ""
@export var start_node: StringName

Id del nodo por el que ARRANCA la conversación (debe existir en nodes).

@export var nodes: Array[DialogNodeDefinition] = []
func node_by_id(node_id: StringName) -> DialogNodeDefinition

Nodo con ese id, o null si no existe (el runtime/validador deciden qué hacer).

static func by_id(dialog_id: StringName) -> DialogGraphDefinition

Resolución por id contra data/dialogs (patrón QuestDefinition.by_id): null + warning si no existe — el llamador decide el fallback (dialog_lines).

DialogNodeDefinitionextends Resourcedialog_node_definition.gd

Un NODO del grafo de diálogo ramificado (ED-3, plan_editor_contenido.md §4.1): el texto que dice el NPC en este punto de la conversación y las OPCIONES del jugador (aristas hacia otros nodos o acciones). El grafo lo recorre DialogRuntime (núcleo puro) y lo pinta DialogPanel.

@export var id: StringName

Id ÚNICO del nodo dentro del grafo (destino de goto de las opciones y posible start_node del grafo). Congelado como los demás ids del pipeline.

@export var options: Array[DialogOptionDefinition] = []

Opciones del jugador en este nodo (vacío = nodo terminal; la UI ofrece «Marcharte.» para cerrar de todas formas).

DialogOptionDefinitionextends Resourcedialog_option_definition.gd

Una OPCIÓN del jugador dentro de un nodo de diálogo ramificado (ED-3, plan_editor_contenido.md §4.1). Es una arista del grafo: o navega a otro nodo (goto) o dispara una ACCIÓN que la UI ejecuta (el runtime jamás toca GameState — mismo espíritu que QuestOfferFlow/QuestLog puros). Compatibilidad: los NPC sin grafo siguen usando NpcDefinition.dialog_lines; este esquema solo entra en juego cuando NpcDefinition.dialog != null.

const ACTIONS: Array[StringName] = …

Vocabulario CONGELADO de acciones (solo se AMPLÍA al final, regla dura del pipeline de contenido). &"" = la opción solo navega (usa goto). - &"end" : cierra el diálogo (goto se ignora). - &"offer_quest" : arranca la conversación de OFERTA de quest_id (reusa el QuestOfferFlow existente). - &"open_shop" : abre la tienda del NPC (si es vendedor). - &"promote" : muestra las opciones de promoción del NPC (P-22). - &"reset" : muestra los resets del Reconfigurador (P-26).

@export var text: String = ""

Texto que ve el jugador en la fila de opción.

@export var goto: StringName = &""

Id del nodo destino al elegir esta opción; &"" si la acción cierra el diálogo (o si la opción SOLO dispara una acción sin navegar).

@export var action: StringName = &""

Acción a ejecutar (vocabulario ACTIONS). &"" = solo navega con goto.

@export var quest_id: StringName = &""

Quest a ofrecer cuando action es &"offer_quest" (id contra data/quests).

@export var required_level: int = 0

Nivel MÍNIMO del personaje para ver la opción; 0 = sin requisito.

@export var required_quest_done: StringName = &""

La opción SOLO se muestra si esta quest está COMPLETADA; &"" = sin requisito.

@export var required_quest_active: StringName = &""

La opción SOLO se muestra si esta quest está ACTIVA; &"" = sin requisito.

@export var hide_if_quest_done: StringName = &""

La opción se OCULTA si esta quest está completada; &"" = sin requisito (natural para no re-ofrecer una quest ya cumplida).

func has_action() -> bool

¿Esta opción dispara una acción (distinta de la mera navegación)?

DialogRuntimeextends RefCounteddialog_runtime.gd

Recorrido PURO de un grafo de diálogo ramificado (ED-3, plan_editor_contenido.md §4.1): mantiene el nodo actual, filtra las opciones visibles por sus condiciones y resuelve la elección. NO ejecuta acciones ni toca GameState — las DEVUELVE para que la UI las ejecute (mismo espíritu que QuestLog/QuestOfferFlow puros). Testeable headless. El context es un Diccionario INYECTADO (desacople total de GameState): { "level": int, "quest_done": Callable(StringName) -> bool, "quest_active": Callable(StringName) -> bool } Cualquier clave ausente se trata como "sin dato" (nivel 0, checks falsos).

func start(graph: DialogGraphDefinition) -> void

Arranca el recorrido en el start_node del grafo. context es opcional aquí (la visibilidad se evalúa al pedir visible_options).

func current_node() -> DialogNodeDefinition

Nodo actual (null si el grafo no arrancó o el start_node no existe).

func current_text() -> String

Texto del NPC en el nodo actual ("" si no hay nodo).

func visible_options(context: Dictionary = {}) -> Array[DialogOptionDefinition]

Opciones VISIBLES del nodo actual según las condiciones del context. Cachea el resultado para que choose(index) opere sobre el mismo listado que la UI acaba de pintar.

func choose(index: int) -> Dictionary

Resuelve la elección del índice index sobre las ÚLTIMAS opciones visibles (las de la llamada previa a visible_options). Devuelve el INTENTO, sin ejecutar nada: { "kind": &"goto", "node": StringName } → ya movió el nodo actual { "kind": &"end" } → cerrar el diálogo { "kind": &"action", "action": StringName, "quest_id": StringName, "goto": StringName } → la UI ejecuta la acción { "kind": &"none" } → índice inválido / opción vacía Un índice fuera de rango JAMÁS crashea (kind &"none").

func goto_node(node_id: StringName) -> bool

Navega DIRECTO a un nodo por id (lo usa la UI para «Volver» tras una acción que no movió el grafo). Falso si el nodo no existe (no crashea).

static func option_visible(option: DialogOptionDefinition, context: Dictionary) -> bool

¿La opción es visible con este contexto? PURA y estática (testeable sin instanciar el runtime). Cada condición ausente (&"" o 0) no filtra.

Qué configura el juego

Sus grafos .tres (o en memoria), el context y la ejecución real de las acciones (offer_quest, open_shop, …).

Fixture mínima

testing/dialog_fixtures.gd — DialogFixtures.golden_graph(): 3 nodos, 2 niveles de opciones y una oferta condicionada (misma estructura que maestro_roderic).

dialog_fixtures.gdcódigo completo
dialog_fixtures.gd
# res://addons/fpx/dialog/testing/dialog_fixtures.gd
class_name DialogFixtures
extends RefCounted
## Fixture mínima del componente `dialog`: un grafo dorado EN MEMORIA con la
## misma estructura que el ejemplar `maestro_roderic` de Stremus (start →
## entrenamiento → prueba, con la oferta de quest condicionada por nivel,
## quest previa hecha y auto-ocultación al completarla) — cero `res://data`.
## Los tests del kit recorren ESTE grafo; el `.tres` real de Stremus sigue
## siendo el ejemplar oro de contenido y lo cubren sus tests de contenido.


static func _option(text: String, goto: StringName = &"", action: StringName = &"",
		quest_id: StringName = &"") -> DialogOptionDefinition:
	var option := DialogOptionDefinition.new()
	option.text = text
	option.goto = goto
	option.action = action
	option.quest_id = quest_id
	return option


static func _node(id: StringName, text: String,
		options: Array[DialogOptionDefinition]) -> DialogNodeDefinition:
	var node := DialogNodeDefinition.new()
	node.id = id
	node.text = text
	node.options = options
	return node


## Grafo dorado: 3 nodos, 2 niveles de opciones y una oferta condicionada.
static func golden_graph() -> DialogGraphDefinition:
	var offer := _option("Estoy listo para la prueba.", &"", &"offer_quest",
			&"la_senda_del_espadachin")
	offer.required_level = 2
	offer.required_quest_done = &"reconocimiento_del_perimetro"
	offer.hide_if_quest_done = &"la_senda_del_espadachin"

	var start := _node(&"saludo", "Bienvenido, recluta. ¿Qué buscas?", [
		_option("Quiero aprender la espada.", &"entrenamiento"),
		_option("Nada, sigo mi camino.", &"", &"end"),
	])
	var training := _node(&"entrenamiento",
			"El acero exige disciplina. Pregunta lo que necesites.", [
		_option("Háblame de la prueba.", &"prueba"),
		_option("Volvamos al principio.", &"saludo"),
	])
	var trial := _node(&"prueba",
			"Solo quien ha reconocido el perímetro puede intentarlo.", [
		offer,
		_option("Aún no estoy listo.", &"entrenamiento"),
	])

	var graph := DialogGraphDefinition.new()
	graph.id = &"grafo_dorado"
	graph.display_name = "Grafo dorado del kit"
	graph.start_node = &"saludo"
	var nodes: Array[DialogNodeDefinition] = [start, training, trial]
	graph.nodes = nodes
	return graph

Cómo se prueba

tests/dialog/dialog_runtime_test.gd — recorrido dorado, matriz de condiciones, índices inválidos sin crash.

Ejemplar oro (Stremus)

scripts/resources/dialog_*_definition.gd + scripts/core/dialog/dialog_runtime.gd (main@0be95b4); el grafo maestro_roderic de data/dialogs/.

Deuda declarada

  • FLAG-DIALOG-UI: el panel de diálogo del ejemplar no se extrae; cada juego pinta el suyo (o llega con ui_kit, FK-7).
  • FLAG-I18N (D-FW-8): los textos viven en el .tres.
Generado el 2026-08-31 · Funpix Kit — componentes extraídos de Stremus: El Tercer Despertar (ejemplar oro) · Funpixart