Shared library
Optional. An NPC that says its own lines needs nothing on this page.
A TalkKitDatabase is a plain resource holding conversations and character speakers, for content reused across NPCs or scenes — a shopkeeper greeting that twelve shopkeepers share.
Lookup order
extends Node
## Docs: /guide/library — reusing conversations across NPCs. Optional.
func _ready() -> void:
_build()
#region lookup
# play() checks the node first, then the library. So an NPC overrides a shared
# conversation simply by owning one with the same name — there is no alias
# concept to learn.
func who_wins() -> StringName:
return $NPCTalkKit.find_conversation(&"greeting").conversation_id
#endregion
#region list
# Everything this NPC can play, node content shadowing the library.
func every_name() -> Array[StringName]:
return $NPCTalkKit.list_conversations()
#endregion
func _build() -> void:
var box := _instant_box()
var shared := TalkConversation.new()
shared.conversation_id = &"greeting"
shared.default_dialogue_box = box
var shared_line := TalkLine.new()
shared_line.text = "Welcome to Oakhollow."
shared.lines = [shared_line]
var farewell := TalkConversation.new()
farewell.conversation_id = &"farewell"
farewell.default_dialogue_box = box
var bye := TalkLine.new()
bye.text = "Safe roads."
farewell.lines = [bye]
var library := TalkKitDatabase.new()
library.conversations = [shared, farewell]
var own := TalkConversation.new()
own.conversation_id = &"greeting"
own.default_dialogue_box = box
var own_line := TalkLine.new()
own_line.text = "Mind the anvil, it is still hot."
own.lines = [own_line]
var talk := NPCTalkKit.new()
talk.name = "NPCTalkKit"
talk.dialogue_boxes = [box]
talk.library = library
talk.conversations = [own]
add_child(talk)
func _verify() -> Array[String]:
var failures: Array[String] = []
var talk: NPCTalkKit = $NPCTalkKit
var spoken: Array[String] = []
talk.line_started.connect(func(line: TalkLine) -> void: spoken.append(line.text))
talk.play(&"greeting")
if spoken.is_empty() or not spoken[0].begins_with("Mind the anvil"):
failures.append("library: the node's own conversation must win, got %s" % str(spoken))
talk.stop()
spoken.clear()
talk.play(&"farewell")
if spoken.is_empty() or spoken[0] != "Safe roads.":
failures.append("library: a library-only conversation must still play")
talk.stop()
var names := every_name()
if not (names.has(&"greeting") and names.has(&"farewell")) or names.size() != 2:
failures.append("library: listing must merge both without duplicates, got %s" % str(names))
return failures
## Typing speed lives on the dialogue box now, so turning it off for a scripted
## run means handing the node a box that types instantly.
func _instant_box() -> TalkBoxTemplate:
var template := TalkBoxTemplate.new()
template.typewriter_speed = 0.0
return templateplay() checks the node first, then the library. So an NPC overrides shared content by owning a conversation with the same name. There is no alias concept, because there does not need to be one.
extends Node
## Docs: /guide/library — reusing conversations across NPCs. Optional.
func _ready() -> void:
_build()
#region lookup
# play() checks the node first, then the library. So an NPC overrides a shared
# conversation simply by owning one with the same name — there is no alias
# concept to learn.
func who_wins() -> StringName:
return $NPCTalkKit.find_conversation(&"greeting").conversation_id
#endregion
#region list
# Everything this NPC can play, node content shadowing the library.
func every_name() -> Array[StringName]:
return $NPCTalkKit.list_conversations()
#endregion
func _build() -> void:
var box := _instant_box()
var shared := TalkConversation.new()
shared.conversation_id = &"greeting"
shared.default_dialogue_box = box
var shared_line := TalkLine.new()
shared_line.text = "Welcome to Oakhollow."
shared.lines = [shared_line]
var farewell := TalkConversation.new()
farewell.conversation_id = &"farewell"
farewell.default_dialogue_box = box
var bye := TalkLine.new()
bye.text = "Safe roads."
farewell.lines = [bye]
var library := TalkKitDatabase.new()
library.conversations = [shared, farewell]
var own := TalkConversation.new()
own.conversation_id = &"greeting"
own.default_dialogue_box = box
var own_line := TalkLine.new()
own_line.text = "Mind the anvil, it is still hot."
own.lines = [own_line]
var talk := NPCTalkKit.new()
talk.name = "NPCTalkKit"
talk.dialogue_boxes = [box]
talk.library = library
talk.conversations = [own]
add_child(talk)
func _verify() -> Array[String]:
var failures: Array[String] = []
var talk: NPCTalkKit = $NPCTalkKit
var spoken: Array[String] = []
talk.line_started.connect(func(line: TalkLine) -> void: spoken.append(line.text))
talk.play(&"greeting")
if spoken.is_empty() or not spoken[0].begins_with("Mind the anvil"):
failures.append("library: the node's own conversation must win, got %s" % str(spoken))
talk.stop()
spoken.clear()
talk.play(&"farewell")
if spoken.is_empty() or spoken[0] != "Safe roads.":
failures.append("library: a library-only conversation must still play")
talk.stop()
var names := every_name()
if not (names.has(&"greeting") and names.has(&"farewell")) or names.size() != 2:
failures.append("library: listing must merge both without duplicates, got %s" % str(names))
return failures
## Typing speed lives on the dialogue box now, so turning it off for a scripted
## run means handing the node a box that types instantly.
func _instant_box() -> TalkBoxTemplate:
var template := TalkBoxTemplate.new()
template.typewriter_speed = 0.0
return templatePromoting a conversation
Authoring starts inline. Moving one into the library is an explicit action, not something that happens behind your back:
- Assign a library to the node, and save that library to a file.
- Select the conversation in the TalkKit workspace and choose ⋮ → Save to Library.
The conversation is saved as its own .tres, the node's slot is repointed at the file, and the library gains an entry.
Nothing auto-syncs. Syncing on scene save would dirty a shared file on every scene edit — an invisible side effect and a merge-conflict generator.
Conversation ids are per node
play("greeting") is called on a specific node, so twenty NPCs may each own a greeting. Only the library is a single namespace. The validator follows that:
| Situation | Reported as |
|---|---|
| the library declares an id twice | error — genuinely ambiguous |
| one node declares an id twice | error — the second can never play |
| two different nodes share an id | warning — normal, but promoting both would collide |
| a node shadows a library id | nothing — that is the override above |
Validating a whole scene
TalkValidator and TalkKitSerializer take plain arrays, never a database, so inline content is a first-class citizen rather than a special case:
var report := TalkKitSceneCollector.validate(scene_root, library)It walks every NPCTalkKit in the scene and checks it alongside the library.
JSON
TalkKitSerializer exports and imports conversations as JSON, for a translation pipeline, an external tool, or an AI agent writing content. There is no LLM anywhere in the addon — it is structured data that happens to be easy for one to produce.