
Godot Genre Visual Novel
- 182 installs
- 454 repo stars
- Updated July 28, 2026
- thedivergentai/gd-agentic-skills
Use godot-genre-visual-novel for development tasks
About
godot-genre-visual-novel: A skill for development. This provides functionality for development workflows.
- godot-genre-visual-novel
Godot Genre Visual Novel by the numbers
- 182 all-time installs (skills.sh)
- +8 installs in the week ending Jul 27, 2026 (Skillselion tracking)
- Ranked #2,179 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/thedivergentai/gd-agentic-skills --skill godot-genre-visual-novelAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 182 |
|---|---|
| repo stars | ★ 454 |
| Last updated | July 28, 2026 |
| Repository | thedivergentai/gd-agentic-skills ↗ |
What it does
Use godot-genre-visual-novel for development tasks
Files
Genre: Visual Novel
Branching narratives, meaningful choices, and quality-of-life features define visual novels.
Core Loop
1. Read: Consume narrative text and character dialogue 2. Decide: Choose at key moments 3. Branch: Story diverges based on choice 4. Consequence: Immediate reaction or long-term flag changes 5. Conclude: Reach one of multiple endings
NEVER Do (Expert Anti-Patterns)
Narrative & Flow
- NEVER create the "Illusion of Choice" exclusively; strictly provide Immediate Dialogue Variations or Flag Changes even if the plot converges later.
- NEVER skip mandatory QoL features; strictly implement Auto-Play, Fast-Forward, and Backlog/History for replayability.
- NEVER display "Walls of Text"; strictly limit dialogue boxes to 3-4 Lines max to avoid intimidating the reader.
- NEVER hardcode dialogue text inside GDScripts; strictly store narrative scripts in External Files (JSON, CSV, or custom Resources) for iteration.
- NEVER ignore the Rollback mechanic; strictly maintain a history stack so players can undo miss-clicks or reread missed lines.
Technical & UI
- NEVER use plain text for emotional beats; strictly use RichTextLabel BBCode (e.g.,
[shake],[wave]) to add visual weight. - NEVER parse massive narrative files on the main thread; strictly use `ResourceLoader.load_threaded_request()` to prevent transition stutters.
- NEVER use standard Strings for frequently accessed game flags; strictly use `StringName` (&"met_alice") for faster dictionary lookups.
- NEVER use
_processfor letter-by-letter animation; strictly use a Tween on `visible_ratio` for smooth, frame-independent reveals. - NEVER neglect character Z-ordering; strictly ensure the active speaker is brought to the front (highest
z_index) for visual clarity. - NEVER use
z_indexforControlnode priority if input handling is required; strictly usemove_to_front()to ensure draw order and input propagation match. - NEVER use absolute pixel positioning for character sprites; strictly rely on Anchors & Percent-based Offsets for responsive scaling.
- NEVER allow text animations to continue when the player skips; strictly set `visible_ratio` to 1.0 instantly on input.
- NEVER leave orphaned character sprites; strictly use `queue_free()` when actors exit the stage to prevent memory leaks.
---
🛠 Expert Components (scripts/)
Original Expert Patterns
- story_manager.gd - Flag-aware dialog orchestrator with branching logic and character state persistence.
- dialogue_ui.gd - Presentation layer with typewriter tweens and choice-window generation.
- vn_rollback_manager.gd - History stack maintenance for state rollback (flags/backgrounds/index).
Modular Components
- visual_novel_patterns.gd - Reusable patterns: BBCode effects, choice filtering, and sprite layering.
- dialogue_ui.gd - Base UI core for dialogue and character management.
---
| Phase | Skills | Purpose |
|---|---|---|
| 1. Text & UI | ui-system, rich-text-label | Dialogue box, bbcode effects, typewriting |
| 2. Logic | json-parsing, resource-management | Loading scripts, managing character data |
| 3. State | godot-save-load-systems, dictionaries | Flags, history, persistent data |
| 4. Audio | audio-system | Voice acting, background music transitions |
| 5. Polish | godot-tweening, shaders | Character transitions, background effects |
Architecture Overview
1. Story Manager (The Driver)
Parses the script and directs the other systems.
# story_manager.gd
extends Node
var current_script: Dictionary
var current_line_index: int = 0
var flags: Dictionary = {}
func load_script(script_path: String) -> void:
var file = FileAccess.open(script_path, FileAccess.READ)
current_script = JSON.parse_string(file.get_as_text())
current_line_index = 0
display_next_line()
func display_next_line() -> void:
if current_line_index >= current_script["lines"].size():
return
var line_data = current_script["lines"][current_line_index]
if line_data.has("choice"):
present_choices(line_data["choice"])
else:
CharacterManager.show_character(line_data.get("character"), line_data.get("expression"))
DialogueUI.show_text(line_data["text"])
current_line_index += 12. Dialogue UI (Typewriter Effect)
Displaying text character by character.
# dialogue_ui.gd
func show_text(text: String) -> void:
rich_text_label.text = text
rich_text_label.visible_ratio = 0.0
var tween = create_tween()
tween.tween_property(rich_text_label, "visible_ratio", 1.0, text.length() * 0.05)3. History & Rollback
Essential VN feature. Store the state before every line.
var history: Array[Dictionary] = []
func save_state_to_history() -> void:
history.append({
"line_index": current_line_index,
"flags": flags.duplicate(),
"background": current_background,
"music": current_music
})
func rollback() -> void:
if history.is_empty(): return
var trusted_state = history.pop_back()
restore_state(trusted_state)Key Mechanics Implementation
Branching Paths (Flags)
Track decisions to influence future scenes.
func make_choice(choice_id: String) -> void:
match choice_id:
"be_nice":
flags["relationship_alice"] += 1
jump_to_label("alice_happy")
"be_mean":
flags["relationship_alice"] -= 1
jump_to_label("alice_sad")
### 4. Speaker Z-Ordering (Dynamic Focus)
Bring the active speaker to the front and dim others.
actor_manager.gd
func focus_speaker(active_name: String) -> void: for actor in get_children(): if not actor is CanvasItem: continue
if actor.name == active_name:
Move to bottom of tree to render on top & catch input events
actor.move_to_front() actor.modulate = Color.WHITE else:
Dim inactive actors
actor.modulate = Color(0.5, 0.5, 0.5, 1.0)
### 5. Asynchronous Background Loading
Prevent stutters when switching high-res assets.
background_streamer.gd
var _pending_path: String = ""
func load_background(path: String) -> void: _pending_path = path ResourceLoader.load_threaded_request(path) set_process(true)
func _process(_delta: float) -> void: var status = ResourceLoader.load_threaded_get_status(_pending_path) if status == ResourceLoader.THREAD_LOAD_LOADED: texture = ResourceLoader.load_threaded_get(_pending_path) set_process(false)
### 6. Emotional BBCode Effects
Use `RichTextLabel` with performance-first `append_text`.
dialogue_printer.gd
func print_line(speaker: String, text: String, emotion: String) -> void: var bb: String = text match emotion: "angry": bb = "[shake rate=30.0 level=8]%s[/shake]" % text "sad": bb = "[wave amp=20.0 freq=2.0]%s[/wave]" % text
Use append_text to avoid rebuilding the entire tag stack
append_text("[b]%s:[/b] %s\n" % [speaker, bb])
Script Format (JSON vs Resource)
- JSON: Easy to write externally, standard format.
- Custom Resource: Typosafe, editable in Inspector.
- Text Parsers: (e.g., Markdown-like syntax) simpler for writers.
Common Pitfalls
1. Too Much Text: Walls of text are intimidating. Break it up. Fix: Limit lines to 3-4 rows max. 2. Illusion of Choice: Choices that lead to the same outcome immediately feel cheap. Fix: Use small variations in dialogue even if the main plot converges. 3. Missing Quality of Life: No Skip, No Auto, No Save. Fix: These are mandatory features for the genre.
Godot-Specific Tips
- RichTextLabel: Use BBCode for
[wave],[shake],[color]effects to add emotion to text. - Resource Preloader: Visual Novels have heavy assets (4K backgrounds). Load scenes asynchronously or use a loading screen between chapters.
- Dialogic: Mentioning this plugin is important—it's the industry standard for Godot VNs. Use it if you want a full suite of tools, or build your own for lightweight needs.
Reference
- Master Skill: godot-master
# skills/genre-visual-novel/scripts/dialogue_ui.gd
extends Control
## Dialogue UI (Expert Pattern)
## Handles typewriter effect, BBCode tags, and user input (skip/advance).
class_name DialogueUI
signal animation_finished
@export var rich_text_label: RichTextLabel
@export var name_label: Label
@export var type_speed: float = 0.05
var full_text: String = ""
var is_typing: bool = false
func show_line(text: String, character_name: String) -> void:
full_text = text
if name_label: name_label.text = character_name
rich_text_label.text = text # Parse BBCode immediately
rich_text_label.visible_ratio = 0.0
is_typing = true
var tween = create_tween()
var duration = text.length() * type_speed
tween.tween_property(rich_text_label, "visible_ratio", 1.0, duration)
tween.finished.connect(_on_tween_finished)
func _on_tween_finished() -> void:
is_typing = false
animation_finished.emit()
func instant_finish() -> void:
if is_typing:
# Kill running tweens on label
var tweens = get_tree().get_processed_tweens()
# Find which tween targets the label? Complex.
# Simpler: unique tween stored
# But for now, just force visible ratio
rich_text_label.visible_ratio = 1.0
is_typing = false
animation_finished.emit()
func _input(event: InputEvent) -> void:
if event.is_action_pressed("ui_accept"):
if is_typing:
instant_finish()
get_viewport().set_input_as_handled()
## EXPERT USAGE:
## Connect to Story Manager. Call show_line().
## Handles input to skip typing.
# skills/genre-visual-novel/scripts/story_manager.gd
extends Node
## Story Manager (Expert Pattern)
## Driver for Visual Novels. Parses scripts, manages state, and directs UI.
class_name StoryManager
signal line_advanced(text: String, character: String)
signal options_presented(choices: Array)
signal scene_changed(background: String)
var script_data: Dictionary = {}
var current_index: int = 0
var flags: Dictionary = {}
var history: Array[Dictionary] = []
func load_script_from_json(path: String) -> void:
var file = FileAccess.open(path, FileAccess.READ)
if file:
var json = JSON.new()
if json.parse(file.get_as_text()) == OK:
script_data = json.data
current_index = 0
_process_current_line()
else:
printerr("Invalid JSON script")
func advance() -> void:
current_index += 1
_process_current_line()
func _process_current_line() -> void:
if not script_data.has("lines") or current_index >= script_data["lines"].size():
return
var line = script_data["lines"][current_index]
# Save history state before processing
history.append({
"index": current_index,
"flags": flags.duplicate(true)
})
if line.has("background"):
scene_changed.emit(line["background"])
if line.has("choices"):
options_presented.emit(line["choices"])
else:
var char_name = line.get("character", "")
var text = line.get("text", "")
line_advanced.emit(text, char_name)
func select_choice(choice_index: int) -> void:
var line = script_data["lines"][current_index]
var choices = line["choices"]
var selected = choices[choice_index]
if selected.has("flag_updates"):
for key in selected["flag_updates"]:
flags[key] = selected["flag_updates"][key]
if selected.has("jump_to"):
_jump_to_label(selected["jump_to"])
else:
advance()
func _jump_to_label(label: String) -> void:
# Simple linear search for label
for i in range(script_data["lines"].size()):
if script_data["lines"][i].get("label") == label:
current_index = i
_process_current_line()
return
## EXPERT USAGE:
## Call load_script_from_json() with a path to a JSON file.
## Connect UI to signals to display text/choices.
# visual_novel_patterns.gd
extends Node
# 1. BBCode-Aware Character Display
# EXPERT NOTE: Animate the visible_ratio while preserving BBCode tags for modern UI feel.
func animate_text(label: RichTextLabel, duration: float) -> void:
label.visible_ratio = 0.0
var tween := create_tween()
tween.tween_property(label, "visible_ratio", 1.0, duration)
# 2. Functional Choice Filtering
# EXPERT NOTE: Efficiently filter available dialogue options based on player flags.
func get_valid_choices(choices: Array, flags: Dictionary) -> Array:
return choices.filter(func(c): return flags.get(c.get(&"required_flag"), true))
# 3. Dynamic Sprite Layering (Z-Index)
# EXPERT NOTE: Adjust z_index programmatically to ensure the speaking character is always on top.
func focus_character(sprite: Sprite2D) -> void:
sprite.z_index = 10
sprite.modulate = Color.WHITE
# 4. Global State Persistence (Config)
# EXPERT NOTE: Save world-flags and relationship stats to a persistent config file.
func save_vn_flags(flags: Dictionary) -> void:
var cfg := ConfigFile.new()
for key in flags:
cfg.set_value("Flags", key, flags[key])
cfg.save("user://save_data.cfg")
# 5. Resource-Based Dialogue Trees
# EXPERT NOTE: Use custom resources to define branching dialogue nodes without messy JSON.
func process_dialogue_node(node: Resource) -> void:
var text: String = node.get(&"dialogue_text")
var choices: Array = node.get(&"choices")
# Display logic here
# 6. Smooth Background Crossfades
# EXPERT NOTE: Blend between background scenes using a shader or global modulate tween.
func transition_background(bg: CanvasItem, next_tex: Texture2D) -> void:
var tween := create_tween()
tween.tween_property(bg, "modulate:a", 0.0, 0.5)
tween.tween_callback(func(): bg.set(&"texture", next_tex))
tween.tween_property(bg, "modulate:a", 1.0, 0.5)
# 7. Localized StringName Keys
# EXPERT NOTE: Use StringName for dictionary lookups in high-frequency dialogue systems.
func get_line(id: StringName) -> String:
return tr(id) # Built-in translation lookup
# 8. Handling Skip/Fast-Forward Input
# EXPERT NOTE: Check for "skip" action to instantly complete text animations.
func _input(event: InputEvent) -> void:
if event.is_action_pressed(&"ui_accept"):
# label.visible_ratio = 1.0
pass
# 9. Autoload Manager for Global State
# EXPERT NOTE: Use a Singleton (Autoload) to keep track of the current active node and flags.
func jump_to_node(id: String) -> void:
# StateManager.current_node = id
pass
# 10. Node Cleaning for Scene Transitions
# EXPERT NOTE: Always use queue_free() during scene changes to avoid physics/signal errors.
func clear_stage(container: Node) -> void:
for child in container.get_children():
child.queue_free()
# skills/genre-visual-novel/code/vn_rollback_manager.gd
extends Node
## VN Rollback Expert Pattern
## Manages state snapshots for multi-level history undo.
@export var max_history: int = 50
# Array of Dictionaries representing state at each dialogue step
var _history: Array[Dictionary] = []
# Current live state
var game_state: Dictionary = {
"dialogue_id": "start",
"flags": {},
"current_bg": "park",
"character_positions": {}
}
func take_snapshot() -> void:
# 1. Recursive Deep Copy of State
var snapshot = game_state.duplicate(true)
_history.append(snapshot)
if _history.size() > max_history:
_history.remove_at(0)
func rollback() -> bool:
# 2. Multi-Level Undo
if _history.is_empty():
return false
game_state = _history.pop_back()
_apply_state()
return true
func _apply_state() -> void:
# Logic to restore the world based on game_state
# E.g. Update UI, hide/show characters, change background
print("Rollback to: ", game_state.dialogue_id)
func set_flag(flag: String, value: Variant) -> void:
# 3. Predictable State Mutation
# Always take a snapshot BEFORE changing global flags.
take_snapshot()
game_state.flags[flag] = value
## EXPERT NOTE:
## Use the 'Command Pattern' for non-trivial state changes (like inventory).
## For 'genre-visual-novel', implement 'Character Sprite Layering' where a
## single Actor node can have its 'base', 'eyes', 'mouth', and 'clothes'
## textures swapped independently via game_state variables.
## NEVER hardcode dialogue; use a JSON parser to map game_state.dialogue_id
## to specific lines in a localization-ready data file.