Skip to content

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 ​

gdscript
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) -> String

play() 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.

gdscript
$NPCTalkKit.say("Stop!", {}, {"speaker": &"ren", "dialogue_box": &"warning"})
$NPCTalkKit.say("Three days later…", {}, {"speaker": null})   # narrator

The 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.

gdscript
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
QuestionLine saysOtherwise
Who is speakingspeaker_mode + speakerconversation.default_speaker
Which boxline.dialogue_boxconversation.default_dialogue_box
Whereline.placementconversation.default_placement
Which faceline.visual_variantthe speaker's default visual
How fastline.characters_per_secondthe 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 ​

PropertyTypeNotes
conversationsArray[TalkConversation]this NPC's conversations; empty is valid, say() still works
speakersArray[TalkSpeaker]the speakers this node can use — a collection, not a default
dialogue_boxesArray[TalkBoxTemplate]the boxes this node can use
quick_say_speakerTalkSpeakerwhat say() falls back to; never reaches a saved conversation. Under Quick Say in the compact Inspector
quick_say_dialogue_boxTalkBoxTemplatethe same, for the box
quick_say_placementTalkAnchorthe same, for placement
libraryTalkKitDatabaseoptional, searched after conversations
default_paramsDictionarymerged under the params passed to play()
conversation_targetNodedefaults to the parent; a position source only
box_parent_pathNodePathwhere the box is added
request_lock_on_startbooldefault true
request_face_on_startbooldefault false
request_camera_on_startbooldefault 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 ​

gdscript
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 ​

PropertyType
conversation_idStringName
linesArray[TalkLine]
default_speakerTalkSpeaker — who speaks the lines that say nothing. Empty means nobody, which is a narrator conversation
default_dialogue_boxTalkBoxTemplate — which box paints them
default_placementTalkAnchor — where they appear
event_id, event_payloadfired on completion, non-blocking
gdscript
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 ​

PropertyTypeNotes
textStringrich text, and supports
speaker_modeSpeakerModeINHERIT, NONE, or SPEAKER
speakerTalkSpeakerwho says this line, when the mode is SPEAKER
dialogue_boxTalkBoxTemplatea different box for this one line; empty inherits
placementTalkAnchora different place for this one line; empty inherits
event_id, event_payloadtold to the game
event_blockingboolpauses until the game answers
line_idStringNameoptional; for host code and localisation
speaker_idStringNamenames a speaker by id instead of pointing at one, for JSON content
display_name_overrideStringa different name for this line — "???" before a reveal. The voice and the face stay whoever is speaking
visual_variantStringNamepicks one of the speaker's named visuals; an unknown name falls back
characters_per_secondfloat0 inherits
voice_clipAudioStream
gdscript
enum SpeakerMode { INHERIT, NONE, SPEAKER }

duplicate_line() -> TalkLine
inherits_speaker() -> bool

Three 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.

GroupProperties
Panelbase — a TalkBoxLayer, the Panel's Base Appearance
Textfont, text_size, text_color, line_spacing, text_min_size, typewriter_speed
Speaker Namename_shown, name_placement, name_align, name_offset, name_font, name_size, name_color
Dialogue Visualvisual_shown, visual_size, visual_side, visual_spacing, visual_filter
Tailtail_shown, tail_look, tail_color, tail_texture, tail_size, tail_overlap
Continue Indicatorcontinue_shown, continue_look, continue_text, continue_texture, continue_filter, continue_motion
Transitionsopen_transition, close_transition, line_transition, transition_duration, transition_curve, transition_easing
Advancedcustom_transition, theme, renderer
Layerslayers — Array[TalkBoxLayer], edited in the workspace's Layers view
gdscript
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.

gdscript
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) -> bool

Properties: 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.

gdscript
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
GroupProperties
Screenscreen_spot, screen_margin, screen_position, stretch_horizontal, box_size
Followfollow_target, follow_offset, pivot, when_offscreen, viewport_margin
Follow 3Dfollow_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 ​

gdscript
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 kind

Properties: 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:

gdscript
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 transitions

And these are provided:

gdscript
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.

GroupPropertyType
Identityspeaker_idStringName — only needed for library or JSON lookup
display_nameString — empty hides the name row
Dialogue VisualvisualTalkDialogueVisual
variantsDictionary[StringName, TalkDialogueVisual]
Voicevoice_enabledbool
voice_blipAudioStream
voice_blip_intervalfloat
voice_pitch_min, voice_pitch_maxfloat — randomised per blip
gdscript
normalized_pitch_range() -> Vector2
resolve_visual(variant: StringName = &"") -> TalkDialogueVisual

If you want the world character to animate while it talks, your game does that itself:

gdscript
$NPCTalkKit.play(&"greeting")
$AnimationPlayer.play("talk")

TalkDialogueVisual ​

How a speaker looks inside the dialogue box.

PropertyTypeNotes
textureTexture2Da still image — a portrait, or a full bust
sprite_framesSpriteFramesan animated visual; ignored while texture is set
animationStringNamewhich animation of those frames
flip_hboolfor an asset drawn facing the other way
gdscript
is_empty() -> bool
frame_texture(elapsed := 0.0) -> Texture2D

Separate 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.tres

TalkKit 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.

PropertyType
speakerTalkSpeaker — may be null
display_nameString — after display_name_override
visualTalkDialogueVisual — after the line's variant
activebool — whether this is the speaker of the line being shown

TalkInteractionArea2D / TalkInteractionArea3D ​

Optional proximity triggers. extends Area2D / Area3D.

PropertyType
talkkit_componentNPCTalkKit
conversation_idStringName
input_actionStringName, default ui_accept
accepted_body_groupStringName, 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:

gdscript
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") -> TalkValidationReport

Import 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 ​

gdscript
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) -> TalkSpeaker

All 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 ​

gdscript
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) -> Dictionary

TalkValidationIssue ​

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.

PropertyTypeNotes
componentNPCTalkKit
targetNodewhat a following anchor tracks; a position source
conversationTalkConversation
anchorTalkAnchornull falls back to the renderer's own
templateTalkBoxTemplatethe box this line resolved to
typewriter_speedfloatcharacters per second; 0 shows the line at once
activeTalkRenderParticipantwho is speaking; null for a narrator
visibleArray[TalkRenderParticipant]everyone the box should keep on screen
gdscript
display_name() -> String                       # "" hides the name row
visual_texture(elapsed := 0.0) -> Texture2D    # null draws none
voice_speaker() -> TalkSpeaker                 # null means silence

Both 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.

GDScript-first. No telemetry, no network requests, no AI service dependency in the shipped addon.