API reference
Everything public, on one page, because the surface is small enough to fit on one. Use Ctrl+F.
NPCTalkKit
The node you add to an NPC. extends Node.
Methods
play(conversation_id: StringName, params: Dictionary = {}) -> bool
say(content: Variant, params: Dictionary = {}, options: Dictionary = {}) -> bool
advance() -> void
resume() -> void
stop() -> void
is_running() -> bool
is_blocked() -> bool
find_conversation(conversation_id: StringName) -> TalkConversation
list_conversations() -> Array[StringName]
build_context(line: TalkLine = null, conversation: TalkConversation = null) -> TalkContext
static transient_conversation(content: Variant) -> TalkConversation
static default_template() -> TalkBoxTemplate
static substitute(text: String, params: Dictionary) -> Stringplay() returns false and never throws when the name is unknown, the content fails validation, or a conversation is already being read. It is accepted while blocked — see branching.
say() takes a String, a TalkLine, or an Array mixing the two, and plays it without saving anything to conversations. A node with zero conversations is a working node.
options names what the content does not: speaker, dialogue_box, placement. Each may be a resource or the id of one this node carries.
$NPCTalkKit.say("Stop!", {}, {"speaker": &"ren", "dialogue_box": &"warning"})
$NPCTalkKit.say("Three days later…", {}, {"speaker": null}) # narratorThe key is what counts, not the value: {"speaker": null} says there is no speaker, while leaving the key out says nothing and falls through to quick_say_speaker. A null that meant both would make a narrator impossible to ask for.
Options only ever select. say() cannot build a speaker or restyle a box — if you need one that does not exist yet, make it and add it to the node's collection.
Resolution
Every rule about who is speaking, what the box looks like and where it goes is decided here and nowhere else. A renderer consumes the answer; it never re-derives one.
resolve_speaker(line, conversation = null) -> TalkSpeaker
resolve_dialogue_box(line, conversation = null) -> TalkBoxTemplate
resolve_placement(line, conversation = null) -> TalkAnchor
resolve_typing_speed(line, conversation = null) -> float
resolve_voice(line, conversation = null) -> TalkSpeaker
resolve_dialogue_visual(for_speaker: TalkSpeaker, line: TalkLine = null) -> TalkDialogueVisual
resolve_display_name(for_speaker: TalkSpeaker, line: TalkLine = null) -> String
resolve_follow_target(line: TalkLine = null) -> Node
find_speaker(speaker_id: StringName) -> TalkSpeaker
find_dialogue_box(box_id: StringName) -> TalkBoxTemplate
known_speakers() -> Array[TalkSpeaker]Omitting conversation asks about the one that is playing.
Two levels, and only two.
line override → conversation default| Question | Line says | Otherwise |
|---|---|---|
| Who is speaking | speaker_mode + speaker | conversation.default_speaker |
| Which box | line.dialogue_box | conversation.default_dialogue_box |
| Where | line.placement | conversation.default_placement |
| Which face | line.visual_variant | the speaker's default visual |
| How fast | line.characters_per_second | the box's typewriter_speed |
There is no node-wide fallback. The node holds collections to pick from; it does not sit underneath a conversation deciding what it forgot to say. The Quick Say Defaults are for say() alone and never touch a saved conversation.
A conversation with no default_dialogue_box is reported by the validator. The runtime still falls back to the shipped Classic Panel so a half-finished conversation cannot crash — that is a crash guard, not a tier of the model.
No speaker is a valid answer. The line still types, events still fire; the name row and the visual stay empty. That is a narrator, a sign, a system message.
Properties
| Property | Type | Notes |
|---|---|---|
conversations | Array[TalkConversation] | this NPC's conversations; empty is valid, say() still works |
speakers | Array[TalkSpeaker] | the speakers this node can use — a collection, not a default |
dialogue_boxes | Array[TalkBoxTemplate] | the boxes this node can use |
quick_say_speaker | TalkSpeaker | what say() falls back to; never reaches a saved conversation. Under Quick Say in the compact Inspector |
quick_say_dialogue_box | TalkBoxTemplate | the same, for the box |
quick_say_placement | TalkAnchor | the same, for placement |
library | TalkKitDatabase | optional, searched after conversations |
default_params | Dictionary | merged under the params passed to play() |
conversation_target | Node | defaults to the parent; a position source only |
box_parent_path | NodePath | where the box is added |
request_lock_on_start | bool | default true |
request_face_on_start | bool | default false |
request_camera_on_start | bool | default false |
state is read-only playback state: enum State { IDLE, OPENING, DISPLAYING, BLOCKED, FINISHING, CANCELLING }. Prefer is_running() and is_blocked() in game code.
Constants: BOX_SCENE_PATH (the shipped renderer scene) and DEFAULT_TEMPLATE_PATH (the Classic Panel used as the crash-guard fallback).
Signals
conversation_started(conversation_id: StringName)
line_started(line: TalkLine)
line_finished(line: TalkLine)
event_triggered(event_id: StringName, payload: Variant)
event_blocked(event_id: StringName, payload: Variant)
conversation_finished(conversation_id: StringName)
conversation_cancelled(conversation_id: StringName)
speech_started(line: TalkLine)
speech_progress(normalized_progress: float)
speech_finished(line: TalkLine)
request_player_lock(locked: bool)
request_face_target(target: Node)
request_camera_focus(target: Node)The three request_* signals are the entire integration surface. TalkKit asks; it never takes. Ignore them and the addon still works.
Signals carry the rendered line
line_started, line_finished and speech_* hand you the line after substitution — a copy, so shared content is never mutated. line_id is preserved, so host code matching on it is unaffected.
TalkConversation
| Property | Type |
|---|---|
conversation_id | StringName |
lines | Array[TalkLine] |
default_speaker | TalkSpeaker — who speaks the lines that say nothing. Empty means nobody, which is a narrator conversation |
default_dialogue_box | TalkBoxTemplate — which box paints them |
default_placement | TalkAnchor — where they appear |
event_id, event_payload | fired on completion, non-blocking |
is_empty() -> bool
get_line_by_id(line_id: StringName) -> TalkLine
speakers() -> Array[TalkSpeaker]The defaults are the reason a conversation is a resource rather than a bare array: a scene where Haru speaks twelve times in the Classic Panel says so once here, and only the line where Ren interrupts says anything at all.
One conversation may contain lines from several speakers. That is not branching — it is still one straight sequence; only the name on the box changes.
TalkLine
| Property | Type | Notes |
|---|---|---|
text | String | rich text, and supports |
speaker_mode | SpeakerMode | INHERIT, NONE, or SPEAKER |
speaker | TalkSpeaker | who says this line, when the mode is SPEAKER |
dialogue_box | TalkBoxTemplate | a different box for this one line; empty inherits |
placement | TalkAnchor | a different place for this one line; empty inherits |
event_id, event_payload | told to the game | |
event_blocking | bool | pauses until the game answers |
line_id | StringName | optional; for host code and localisation |
speaker_id | StringName | names a speaker by id instead of pointing at one, for JSON content |
display_name_override | String | a different name for this line — "???" before a reveal. The voice and the face stay whoever is speaking |
visual_variant | StringName | picks one of the speaker's named visuals; an unknown name falls back |
characters_per_second | float | 0 inherits |
voice_clip | AudioStream |
enum SpeakerMode { INHERIT, NONE, SPEAKER }
duplicate_line() -> TalkLine
inherits_speaker() -> boolThree states, not two, because "say nothing and inherit" and "say that nobody speaks this line" are different instructions. A narrator line inside Haru's conversation is NONE; a line that simply does not care is INHERIT. A null speaker cannot express both.
A line stores only what differs from its conversation's defaults.
TalkBoxTemplate
Everything about a dialogue box: its parts, its paint, its animation. See Dialogue box.
| Group | Properties |
|---|---|
| Panel | base — a TalkBoxLayer, the Panel's Base Appearance |
| Text | font, text_size, text_color, line_spacing, text_min_size, typewriter_speed |
| Speaker Name | name_shown, name_placement, name_align, name_offset, name_font, name_size, name_color |
| Dialogue Visual | visual_shown, visual_size, visual_side, visual_spacing, visual_filter |
| Tail | tail_shown, tail_look, tail_color, tail_texture, tail_size, tail_overlap |
| Continue Indicator | continue_shown, continue_look, continue_text, continue_texture, continue_filter, continue_motion |
| Transitions | open_transition, close_transition, line_transition, transition_duration, transition_curve, transition_easing |
| Advanced | custom_transition, theme, renderer |
| Layers | layers — Array[TalkBoxLayer], edited in the workspace's Layers view |
enum VisualSide { LEFT, RIGHT }
enum NamePlacement { INLINE, TOP_EDGE }
enum NameAlign { LEFT, CENTER, RIGHT }
enum TailLook { COLOR, IMAGE }
enum IndicatorLook { GLYPH, IMAGE }
enum IndicatorMotion { NONE, BOUNCE, BLINK }
static default_base() -> TalkBoxLayer # the classic flat panel a new box starts with
build_panel_style() -> StyleBox # the box's own StyleBox, padding applied
drawn_panel_style() -> StyleBox # what paints the Panel, at its scale
style_margins() -> Vector4 # what "Use style margins" gives
effective_padding() -> Vector4 # the padding that actually applies
is_textured() -> bool # anything but a flat style
base_draws_in_view() -> bool # an Image or textured base, drawn like a layer
image_filter() -> CanvasItem.TextureFilter # the Panel's, which tail and indicator images follow
panel_color() -> Color # what a flat tail and "Sample panel colour" use
layers_for(slot: TalkBoxLayer.Slot) -> Array[TalkBoxLayer]
apply_to(box: Control, text: RichTextLabel = null, name_label: Label = null,
visual: TextureRect = null, indicator: Label = null,
tail: Polygon2D = null, content: BoxContainer = null,
indicator_image: TextureRect = null) -> void
transition_for(slot: String) -> TalkTransition # "open", "close", "line"The Panel's look is base, a TalkBoxLayer with the slot, Position and depth of a Panel fill. It is edited with the same Appearance controls as a name box, a frame or any layer: the Panel group shows the base's Appearance rows, and the Speaker Name and Dialogue Visual groups show a single name box's or frame's the same way. While theme is set, the font, size and colour settings stand down; the Panel, padding and layers still apply. A text_size of 0 inherits from the theme instead of overriding it. typewriter_speed of 0 shows the whole line at once — typing pace belongs to the box because it is part of how a box behaves: a terminal types deliberately, a comic bubble snaps in.
Both built-in boxes draw whoever is speaking and nothing else. The renderer contract carries visible as an array so that a box keeping a cast on screen can be written without rebuilding the speaker model — but that is a renderer concern, with nothing to configure here.
TalkBoxLayer
One decoration on a part of the box: a name box, a portrait frame, a corner ornament, an edge strip or an extra background. See Dialogue Box Anatomy.
enum Slot { PANEL, SPEAKER_NAME, DIALOGUE_VISUAL }
enum Position { FILL, TOP, BOTTOM, LEFT, RIGHT, TOP_LEFT, TOP_RIGHT, BOTTOM_LEFT, BOTTOM_RIGHT }
enum Depth { BEHIND, ABOVE }
enum Source { FLAT_STYLE, NINE_SLICE, IMAGE, RESOURCE }
enum Fit { STRETCH, TILE, TILE_FIT, KEEP_SIZE }
enum PaddingMode { STYLE_MARGINS, CUSTOM }
look_properties() -> Array[Dictionary] # the shared Appearance editor
holds_content() -> bool # the base, a name box or a frame
style_margins() -> Vector4
effective_padding() -> Vector4
rect_in(slot_rect: Rect2) -> Rect2 # where it lands on its slot
painted_rect_in(slot_rect: Rect2) -> Rect2 # including a StyleBox's expand margins
own_size() -> Vector2
draws_texture() -> bool
is_empty() -> bool
is_corner() -> bool
is_edge() -> bool
draw(canvas: CanvasItem, slot_rect: Rect2) -> void
static scaled_style(original: StyleBox, factor: int, smooth := false) -> StyleBox
static is_pixel_filter(texture_filter: CanvasItem.TextureFilter) -> boolProperties: name, enabled, slot, position, depth, offset, size, edge_inset, tint, and the appearance: visual_source, style, texture, scale, fit, filter, padding_mode, padding. Padding is the layer's own and is never written into its StyleBox.
A layer draws style (Flat Style, Nine-slice Texture, or a StyleBox resource) or texture (Image, or a Texture2D resource; an AtlasTexture draws its region). Corners centre on the corner point, so they overhang by default; edges run between their edge_insets. Text, Tail and Continue Indicator take no layers.
TalkAnchor
Where the box sits. See Anchoring.
enum Mode { SCREEN, NODE_FOLLOW }
enum Spot { TOP_LEFT, TOP, TOP_RIGHT, LEFT, CENTER, RIGHT,
BOTTOM_LEFT, BOTTOM, BOTTOM_RIGHT, ABSOLUTE }
enum Offscreen { CLAMP, HIDE, FREE }
resolve(box_node: Node, box: Vector2, fallback_target: Node = null) -> TalkAnchorResult
forced_size(view: Vector2) -> Vector2 # the least size this placement gives
room(view: Vector2) -> Vector2 # the most room on screen, margins excluded
stretches(view: Vector2) -> bool # fills the screen's width
wants_physics_frame() -> bool| Group | Properties |
|---|---|
| Screen | screen_spot, screen_margin, screen_position, stretch_horizontal, box_size |
| Follow | follow_target, follow_offset, pivot, when_offscreen, viewport_margin |
| Follow 3D | follow_offset_3d, max_distance, scale_with_distance, reference_distance, min_scale, max_scale, hide_when_occluded, occlusion_mask |
Subclass and override resolve() for a custom position provider — every built-in mode goes through the same method.
TalkAnchorResult
visible, position, scale, size, focus, has_focus. focus is the tracked point in screen space, which is how the bubble aims its tail, and has_focus says there is one. size is zero on any axis the anchor leaves to the box's natural size. TalkAnchorResult.hidden() returns a result with visible = false.
TalkTransition
enum Kind { NONE, FADE, SLIDE, SCALE_POP }
static builtin(of_kind: Kind, seconds := 0.18) -> TalkTransition
is_instant() -> bool
play_in(box: Node) -> void # await it
play_out(box: Node) -> void # await it
rest_state() -> Dictionary # {alpha, offset, scale} when fully shown
away_state() -> Dictionary # the same, fully hidden, for this kindProperties: kind, duration, curve, easing, slide_offset, scale_from.
A transition never writes to the box directly — the anchor rewrites position and scale every frame — so it animates transition_alpha, transition_offset and transition_scale on the box, which folds them into the anchored result. Subclass and override play_in / play_out for anything else.
TalkBoxRenderer
The advanced escape hatch, for a box the template settings cannot describe. extends CanvasLayer. Override these:
get_text_label() -> RichTextLabel # a line is rich text
get_name_label() -> Label
get_visual_rect() -> TextureRect
get_continue_label() -> Label
get_continue_image() -> TextureRect # the indicator as an image, or null
get_continue_indicator() -> Control # shown only when the player can advance
get_tail() -> Polygon2D
get_content_container() -> BoxContainer
get_box_control() -> Control # required for anchoring and transitionsAnd these are provided:
setup(context: TalkContext) -> void
show_line(line: TalkLine) -> void
finish_line() -> void
close() -> void
open() -> void
reopen() -> void
is_typing() -> bool
request_advance() -> void
apply_template() -> void
apply_layers() -> void
apply_anchor() -> void
can_advance() -> bool
get_slot_control(slot: TalkBoxLayer.Slot) -> Control
exposed_slots() -> Array[TalkBoxLayer.Slot] # layers on other slots are skipped
layer_view(layer: TalkBoxLayer) -> Control
layer_rect(layer: TalkBoxLayer) -> Rect2
slot_rects() -> Dictionary # each part, for the Anatomy Map
layer_bleed() -> Vector4 # how far layers overhang the box
name_width() -> float # the name's width shown whole
const WRAP_WIDTH := 440.0 # where text wraps when nothing sets a width
freeze() -> void
effective_template() -> TalkBoxTemplate
effective_anchor() -> TalkAnchor
visible_length() -> int # characters to reveal, tags excluded
visible_text() -> String # the text as the player reads it
enum Phase { HIDDEN, OPENING, SHOWN, CHANGING, CLOSING }
signal advance_requested
signal finished # the current line finished typing
signal closed # close transition done; the box is about to free
signal speech_started(line: TalkLine)
signal speech_progress(line: TalkLine, normalized_progress: float)
signal speech_finished(line: TalkLine)State the renderer keeps: context, current_line, last_anchor (the most recent TalkAnchorResult), phase, and the transition channels transition_alpha, transition_offset, transition_scale.
A renderer consumes resolved state. It must not re-derive who is speaking, which box to use or how fast to type — the resolver has already combined the line exception and conversation default and handed the result over whole in the TalkContext.
Exported: default_anchor — a renderer's own placement.
TalkSpeaker
Who is speaking: identity, how they look inside the box, how they sound.
A speaker is a dialogue identity, not a gameplay character. It carries no NodePath, no AnimationPlayer, no world sprite, and TalkKit never maps one onto a node in your scene — so a narrator, a radio voice, a sign, or a character standing in a level that is not loaded are all ordinary speakers.
| Group | Property | Type |
|---|---|---|
| Identity | speaker_id | StringName — only needed for library or JSON lookup |
display_name | String — empty hides the name row | |
| Dialogue Visual | visual | TalkDialogueVisual |
variants | Dictionary[StringName, TalkDialogueVisual] | |
| Voice | voice_enabled | bool |
voice_blip | AudioStream | |
voice_blip_interval | float | |
voice_pitch_min, voice_pitch_max | float — randomised per blip |
normalized_pitch_range() -> Vector2
resolve_visual(variant: StringName = &"") -> TalkDialogueVisualIf you want the world character to animate while it talks, your game does that itself:
$NPCTalkKit.play(&"greeting")
$AnimationPlayer.play("talk")TalkDialogueVisual
How a speaker looks inside the dialogue box.
| Property | Type | Notes |
|---|---|---|
texture | Texture2D | a still image — a portrait, or a full bust |
sprite_frames | SpriteFrames | an animated visual; ignored while texture is set |
animation | StringName | which animation of those frames |
flip_h | bool | for an asset drawn facing the other way |
is_empty() -> bool
frame_texture(elapsed := 0.0) -> Texture2DSeparate from the node is not the same as separate from the asset. Pointing this at the very SpriteFrames your player already uses is the intended way to make the player a speaker:
World Player TalkSpeaker "Player"
└─ AnimatedSprite2D └─ visual
└─ player_frames.tres └─ sprite_frames = player_frames.tresTalkKit never looks for $Player/AnimatedSprite2D and never copies its runtime state.
TalkRenderParticipant
One speaker as the box should draw them, every rule already applied. extends RefCounted.
Internal, and deliberately so: there is no authoring concept behind it. A line names a speaker, the runtime works out what that means, and this is the answer travelling to the renderer.
| Property | Type |
|---|---|
speaker | TalkSpeaker — may be null |
display_name | String — after display_name_override |
visual | TalkDialogueVisual — after the line's variant |
active | bool — whether this is the speaker of the line being shown |
TalkInteractionArea2D / TalkInteractionArea3D
Optional proximity triggers. extends Area2D / Area3D.
| Property | Type |
|---|---|
talkkit_component | NPCTalkKit |
conversation_id | StringName |
input_action | StringName, default ui_accept |
accepted_body_group | StringName, default player |
Signal: availability_changed(available: bool).
Not required — calling play() from your own interaction code is the preferred integration.
TalkKitDatabase
Optional. See Shared library. Exported speakers and conversations, plus:
list_speakers() -> Array[StringName]
get_speaker(speaker_id: StringName) -> TalkSpeaker
upsert_speaker(entry: TalkSpeaker, overwrite := false) -> bool
list_conversations() -> Array[StringName]
get_conversation(conversation_id: StringName) -> TalkConversation
create_conversation(data: Dictionary) -> TalkConversation
update_conversation(conversation_id: StringName, data: Dictionary) -> bool
delete_conversation(conversation_id: StringName) -> bool
validate_speaker(speaker_id: StringName) -> TalkValidationReport
validate_conversation(conversation_id: StringName) -> TalkValidationReport
validate_renderer(scene: PackedScene) -> TalkValidationReport
validate_all() -> TalkValidationReport
export_speaker_json(speaker_id: StringName) -> String
export_conversation_json(conversation_id: StringName) -> String
import_speaker_json(json_text: String, mode := "safe") -> TalkValidationReport
import_conversation_json(json_text: String, mode := "safe") -> TalkValidationReportImport mode is "safe" (refuses to replace an existing id), "overwrite", or "preview" (parses and validates, changes nothing). The parsed resource is in the report's value.
TalkValidator / TalkKitSceneCollector / TalkKitSerializer
TalkValidator.validate_conversation(conversation, speakers = []) -> TalkValidationReport
TalkValidator.validate_against_collection(conversation, available_speakers, available_boxes) -> TalkValidationReport
TalkValidator.validate_speaker(speaker, require_id := false) -> TalkValidationReport
TalkValidator.validate_renderer(scene: PackedScene) -> TalkValidationReport
TalkValidator.validate_all(speakers, conversations) -> TalkValidationReport
TalkKitSceneCollector.collect(root: Node, library = null) -> Collected
TalkKitSceneCollector.validate(root: Node, library = null) -> TalkValidationReport
TalkKitSerializer.export_conversation_json(conversation) -> String
TalkKitSerializer.export_speaker_json(speaker) -> String
TalkKitSerializer.parse_conversation_json(json_text, speakers = []) -> TalkValidationReport
TalkKitSerializer.parse_speaker_json(json_text) -> TalkValidationReport
TalkKitSerializer.conversation_to_dictionary(conversation) -> Dictionary
TalkKitSerializer.conversation_from_dictionary(data, speakers = []) -> TalkConversation
TalkKitSerializer.speaker_to_dictionary(speaker) -> Dictionary
TalkKitSerializer.speaker_from_dictionary(data) -> TalkSpeakerAll of them take plain arrays, never a database, so inline content is a first-class citizen. validate_against_collection reports lines that point at a speaker or box the node does not carry — valid, but invisible in every dropdown. The serializer's parse_* functions never touch a library; the parsed resource is the report's value. The database's import_conversation_json and import_speaker_json are the versions that file the result.
TalkValidationReport
var issues: Array[TalkValidationIssue]
var intended_changes: Array[Dictionary]
var value: Variant # what a parse produced, when it produced something
is_valid() -> bool # no errors; warnings are allowed
error_count() -> int
warning_count() -> int
add(code: StringName, severity: StringName, message: String, context := {}) -> void
add_issue(issue: TalkValidationIssue) -> void
merge(other: TalkValidationReport) -> void
to_dictionary(include_issues := true) -> DictionaryTalkValidationIssue
code, severity (&"error" or &"warning"), message, and where it is: speaker_id, conversation_id, line_id, line_index (-1 when not a line), field. TalkValidationIssue.create(code, severity, message, context) builds one; to_dictionary() serializes it.
TalkKitConstants
SCHEMA_VERSION ("4.0"), the format written into exported JSON; ADDON_VERSION ("2.0.0"), the same version as plugin.cfg, shown in the workspace footer; DOCS_URL and SUPPORT_URL, the footer's Docs and Support links.
TalkContext
Handed to a renderer's setup(). Everything the box needs to draw one line, already resolved — the renderer runs no chains of its own.
| Property | Type | Notes |
|---|---|---|
component | NPCTalkKit | |
target | Node | what a following anchor tracks; a position source |
conversation | TalkConversation | |
anchor | TalkAnchor | null falls back to the renderer's own |
template | TalkBoxTemplate | the box this line resolved to |
typewriter_speed | float | characters per second; 0 shows the line at once |
active | TalkRenderParticipant | who is speaking; null for a narrator |
visible | Array[TalkRenderParticipant] | everyone the box should keep on screen |
display_name() -> String # "" hides the name row
visual_texture(elapsed := 0.0) -> Texture2D # null draws none
voice_speaker() -> TalkSpeaker # null means silenceBoth built-in boxes draw active and nothing else, so visible holds at most one entry today. It is an array because a renderer that keeps a cast on screen is a renderer concern — not an authoring feature, and nothing to configure.