Add spine and dialogic museums
This commit is contained in:
60
.agents/skills/developing-godot-gdscript/SKILL.md
Normal file
60
.agents/skills/developing-godot-gdscript/SKILL.md
Normal file
@@ -0,0 +1,60 @@
|
||||
---
|
||||
name: developing-godot-gdscript
|
||||
description: Builds and refactors Godot 4 projects with idiomatic GDScript, scene composition, signals, and autoload patterns. Use when implementing gameplay, UI, tools, or architecture changes in Godot/GDScript repositories.
|
||||
---
|
||||
|
||||
# Developing Godot With GDScript
|
||||
|
||||
Implements production-friendly Godot 4 changes using typed GDScript, scene composition, and low-coupling architecture.
|
||||
|
||||
## Use This Skill When
|
||||
|
||||
- Adding or refactoring gameplay systems, UI flows, or content pipelines in Godot.
|
||||
- Debugging node lifecycle, signals, resources, or autoload behavior.
|
||||
- Improving maintainability, style consistency, or data flow in GDScript code.
|
||||
|
||||
## Core Rules
|
||||
|
||||
1. Prefer composition over deep inheritance: split reusable behavior into scenes/resources/scripts with focused responsibilities.
|
||||
2. Choose scenes for reusable node trees and editor-authored structure; choose scripts/resources for logic and data that do not need node hierarchy.
|
||||
3. Use autoloads only for truly global state/services (save systems, global config, cross-scene coordinators), not as default dependency injection.
|
||||
4. Favor signals over direct cross-tree mutation to reduce coupling between gameplay, UI, and services.
|
||||
5. Use typed GDScript where possible: annotate variables, params, and return types for safer refactors.
|
||||
|
||||
## GDScript Implementation Checklist
|
||||
|
||||
1. Keep naming idiomatic: `snake_case` for vars/functions/signals, `PascalCase` for `class_name`, `UPPER_SNAKE_CASE` for constants.
|
||||
2. Use `@export` for editor-facing configuration and `@onready` only for scene-dependent node lookups.
|
||||
3. Respect lifecycle intent: `_init` for construction data, `_enter_tree` for tree attachment concerns, `_ready` for node wiring, `_process`/`_physics_process` for frame loops.
|
||||
4. Distinguish frame loops clearly: use `_physics_process` for deterministic physics movement and `_process` for visual or non-physics updates.
|
||||
5. Use `match` for clear branching on enums/states and keep functions short with early returns.
|
||||
|
||||
## Data, Loading, And Error Handling
|
||||
|
||||
1. Guard all file access (`FileAccess.open`) and JSON parsing before indexing into parsed values.
|
||||
2. Validate dynamic data types (`is Dictionary`, `is Array`) before field access to avoid runtime surprises.
|
||||
3. Use `preload` for stable, frequently used compile-time resources; use `load` for optional or dynamic runtime assets.
|
||||
4. Prefer `Resource`/`RefCounted` for plain data models when node features are unnecessary.
|
||||
5. Use `assert`/`push_error` for invalid states that should fail loudly during development.
|
||||
|
||||
## Delivery Workflow
|
||||
|
||||
1. Read `project.godot` first to identify autoloads, rendering/physics settings, and project conventions.
|
||||
2. Inspect relevant `.tscn` + `.gd` pairs together before editing to preserve scene-script contracts.
|
||||
3. Implement minimal, targeted changes that keep public APIs/signals stable unless change is requested.
|
||||
4. Run headless validation and script checks for touched scripts; run project smoke test when feasible.
|
||||
5. Summarize changed files, behavior impact, and any follow-up risks.
|
||||
|
||||
## Validation Commands
|
||||
|
||||
Use project-provided commands when available. Typical Godot checks:
|
||||
|
||||
```bash
|
||||
"$GODOT_BIN" --headless --path . --quit
|
||||
"$GODOT_BIN" --headless --check-only --script res://path/to/changed_script.gd
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
- https://docs.godotengine.org/en/stable/tutorials/best_practices/index.html
|
||||
- https://docs.godotengine.org/en/stable/tutorials/scripting/gdscript/gdscript_basics.html
|
||||
564
.agents/skills/dialogic/SKILL.md
Normal file
564
.agents/skills/dialogic/SKILL.md
Normal file
@@ -0,0 +1,564 @@
|
||||
---
|
||||
name: dialogic
|
||||
description: Dialogic 2 addon for Godot 4 — a dialog system with timelines, events, characters, subsystems, styles, layouts, variables, save/load, and text effects. Use when working with dialog trees, visual novel scenes, character portraits, branching conversations, or any Dialogic-related code in this project.
|
||||
---
|
||||
|
||||
# Dialogic 2 Skill
|
||||
|
||||
Dialogic is a Godot 4 addon (`addons/dialogic/`) providing a full dialog and visual novel system. It is an autoload (`Dialogic`) with a modular subsystem/event architecture.
|
||||
|
||||
> **Official docs:** https://docs.dialogic.pro/
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
```
|
||||
DialogicGameHandler (autoload "Dialogic")
|
||||
├── Subsystems (modular, extendable)
|
||||
│ ├── Animations — tweening/transition helpers
|
||||
│ ├── Audio — sound effects, music, channels
|
||||
│ ├── Backgrounds — background images/scenes with transitions
|
||||
│ ├── Choices — branching choice menus
|
||||
│ ├── Expressions — expression/variable evaluation
|
||||
│ ├── Glossary — hover-to-inspect word definitions
|
||||
│ ├── History — conversation log
|
||||
│ ├── Inputs — keyboard/mouse input handling
|
||||
│ ├── Jump — label navigation & jump/return logic
|
||||
│ ├── PortraitContainers — layout slots for portraits
|
||||
│ ├── Portraits — character portrait display & animation
|
||||
│ ├── Save — built-in save/load system
|
||||
│ ├── Settings — runtime settings
|
||||
│ ├── Styles — UI layout/style management
|
||||
│ ├── Text — text display with BBCode effects
|
||||
│ ├── TextInput — player text input prompts
|
||||
│ ├── VAR — variable storage & manipulation
|
||||
│ └── Voice — voice/audio per character line
|
||||
```
|
||||
|
||||
## Core Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `addons/dialogic/Core/DialogicGameHandler.gd` | Autoload: manages state, timeline execution, subsystems |
|
||||
| `addons/dialogic/Core/DialogicUtil.gd` | Static helpers (filesystem, variables, conversions, suggestions) |
|
||||
| `addons/dialogic/Core/DialogicResourceUtil.gd` | Resource directory management, caches, lookups |
|
||||
| `addons/dialogic/Core/Dialogic_Subsystem.gd` | Base class for all subsystems |
|
||||
| `addons/dialogic/Resources/event.gd` | Base class for all events |
|
||||
| `addons/dialogic/Resources/timeline.gd` | Timeline resource |
|
||||
| `addons/dialogic/Resources/character.gd` | Character resource |
|
||||
| `addons/dialogic/Resources/dialogic_style.gd` | Style resource |
|
||||
| `addons/dialogic/Resources/dialogic_layout_base.gd` | Layout resource |
|
||||
| `addons/dialogic/Resources/dialogic_layout_layer.gd` | Layout layer resource |
|
||||
| `addons/dialogic/Resources/dialogic_style_layer.gd` | Style layer resource |
|
||||
|
||||
## DialogicGameHandler API (`Dialogic` autoload)
|
||||
|
||||
Access via `Dialogic` (global autoload name) or `DialogicUtil.autoload()`.
|
||||
|
||||
### States (enum `States`)
|
||||
|
||||
- `IDLE` — awaiting input to advance
|
||||
- `REVEALING_TEXT` — currently revealing text
|
||||
- `ANIMATING` — an animation is playing
|
||||
- `AWAITING_CHOICE` — awaiting choice selection
|
||||
- `WAITING` — awaiting something (e.g., timer)
|
||||
|
||||
`current_state` property; `state_changed(new_state)` signal.
|
||||
|
||||
### Pausing
|
||||
|
||||
```gdscript
|
||||
Dialogic.paused = true # pauses all subsystems (not the scene tree)
|
||||
Dialogic.paused = false # resumes
|
||||
```
|
||||
|
||||
Signals: `dialogic_paused`, `dialogic_resumed`
|
||||
|
||||
### Starting/Ending Timelines
|
||||
|
||||
```gdscript
|
||||
# Start a timeline AND load a layout scene (use for first dialog)
|
||||
var layout_node = Dialogic.start(timeline, label_or_idx)
|
||||
# timeline: DialogicTimeline resource OR string path OR identifier name
|
||||
# label_or_idx: optional label (string) or event index (int) to jump to
|
||||
|
||||
# Start without loading layout (use when layout already exists)
|
||||
Dialogic.start_timeline(timeline, label_or_idx)
|
||||
|
||||
# Preload (prepare without playing)
|
||||
var preloaded = Dialogic.preload_timeline(timeline_resource)
|
||||
|
||||
# End the current timeline
|
||||
Dialogic.end_timeline(skip_ending:=false)
|
||||
|
||||
# Check if timeline exists
|
||||
if Dialogic.timeline_exists(timeline): ...
|
||||
|
||||
# Only start dialog if none is active
|
||||
if Dialogic.current_timeline == null:
|
||||
Dialogic.start("my_timeline")
|
||||
```
|
||||
|
||||
Signals: `timeline_started`, `timeline_ended`, `event_handled(resource)`
|
||||
|
||||
### Timeline Properties
|
||||
|
||||
```gdscript
|
||||
Dialogic.current_timeline # DialogicTimeline resource
|
||||
Dialogic.current_timeline_events # Array of event resources
|
||||
Dialogic.current_event_idx # int
|
||||
Dialogic.current_state_info # Dictionary of state info
|
||||
```
|
||||
|
||||
### Clear States (enum `ClearFlags`)
|
||||
|
||||
```gdscript
|
||||
Dialogic.clear(Dialogic.ClearFlags.FULL_CLEAR) # clear everything
|
||||
Dialogic.clear(Dialogic.ClearFlags.KEEP_VARIABLES) # keep variables
|
||||
Dialogic.clear(Dialogic.ClearFlags.TIMELINE_INFO_ONLY) # only clear timeline/index
|
||||
```
|
||||
|
||||
### Save/Load Full State
|
||||
|
||||
```gdscript
|
||||
var state = Dialogic.get_full_state() # returns Dictionary
|
||||
Dialogic.load_full_state(state) # restores full dialog state
|
||||
```
|
||||
|
||||
### Event Handling
|
||||
|
||||
```gdscript
|
||||
Dialogic.handle_next_event() # advance to next event
|
||||
Dialogic.handle_event(event_index) # jump to specific event index
|
||||
```
|
||||
|
||||
### Subsystem Access
|
||||
|
||||
All subsystems are accessible as properties:
|
||||
|
||||
```gdscript
|
||||
Dialogic.Animations
|
||||
Dialogic.Audio
|
||||
Dialogic.Backgrounds
|
||||
Dialogic.Choices
|
||||
Dialogic.Expressions
|
||||
Dialogic.History
|
||||
Dialogic.Inputs
|
||||
Dialogic.Jump
|
||||
Dialogic.PortraitContainers
|
||||
Dialogic.Portraits
|
||||
Dialogic.Save
|
||||
Dialogic.Settings
|
||||
Dialogic.Styles
|
||||
Dialogic.Text
|
||||
Dialogic.TextInput
|
||||
Dialogic.VAR
|
||||
Dialogic.Voice
|
||||
Dialogic.Glossary
|
||||
```
|
||||
|
||||
Check existence: `Dialogic.has_subsystem("SubsystemName")`
|
||||
|
||||
## Timeline Events
|
||||
|
||||
Events are scripts in `addons/dialogic/Modules/<Module>/event_<name>.gd`. They extend `DialogicEvent` (base class in `Resources/event.gd`).
|
||||
|
||||
### Event Types
|
||||
|
||||
| Module | Event | Purpose |
|
||||
|--------|-------|---------|
|
||||
| Text | `event_text` | Display dialog text with character portrait |
|
||||
| Character | `event_character` | Join/leave/update character portraits |
|
||||
| Choice | `event_choice` | Present branching choices |
|
||||
| Condition | `event_condition` | Branch based on variable conditions |
|
||||
| Audio | `event_audio` | Play sound effects / music on channels |
|
||||
| Background | `event_background` | Change background with transition |
|
||||
| Jump | `event_jump` | Jump to a label |
|
||||
| Jump | `event_label` | Define a label target |
|
||||
| Jump | `event_return` | Return from a jump |
|
||||
| Signal | `event_signal` | Emit custom signals |
|
||||
| Variable | `event_variable` | Set/modify variables |
|
||||
| Wait | `event_wait` | Wait for a duration |
|
||||
| WaitInput | `event_wait_input` | Wait for player input |
|
||||
| Clear | `event_clear` | Clear subsystems |
|
||||
| Comment | `event_comment` | Editor-only comments |
|
||||
| End | `event_end` | End the timeline |
|
||||
| Save | `event_save` | Trigger save |
|
||||
| Settings | `event_setting` | Change runtime settings |
|
||||
| Style | `event_style` | Switch style/layout |
|
||||
| Glossary | `event_glossary` | Manage glossary |
|
||||
| History | `event_history` | Manage history |
|
||||
| TextInput | `event_text_input` | Prompt player for text |
|
||||
| Voice | `event_voice` | Play voice clip |
|
||||
| Call | `event_call` | Call a method on an autoload |
|
||||
| Core | `event_end_branch` | Internal branch-ending marker |
|
||||
|
||||
### Timeline Text Format
|
||||
|
||||
Timelines are saved in text format (`.dtl` files). They use TAB indentation to know what events belong to a choice or condition block. Only changes in indentation matter — be consistent within one block.
|
||||
|
||||
#### Custom-syntax events (bare text, no brackets):
|
||||
|
||||
```
|
||||
# Comments (lines starting with #)
|
||||
# Comment Event — editor-only, ignored at runtime
|
||||
|
||||
# Text Event
|
||||
CharacterName: Dialog text here.
|
||||
CharacterName (portrait): Dialog text with portrait change.
|
||||
NoName: Text said by no one in particular.
|
||||
Long text ending with \ will include the next line too.\
|
||||
This line is part of the same text event.
|
||||
|
||||
# Character Event
|
||||
join CharacterName (portrait) position [animation="Bounce In"]
|
||||
leave CharacterName [animation="Bounce Out" length="0.3"]
|
||||
update CharacterName (portrait) position [animation="Tada" wait="true"]
|
||||
|
||||
# Condition Event (indentation-based blocks)
|
||||
if {coins} == 0:
|
||||
# indented events run when true
|
||||
elif {health} <= 10:
|
||||
# another branch
|
||||
else:
|
||||
# fallback
|
||||
|
||||
# Choice Event (each option starts with -)
|
||||
- Yes, let's go!
|
||||
# indented events fire when chosen
|
||||
- No, not now. | [if {relationship} > 23]
|
||||
# conditional choice
|
||||
- Maybe | [if {charisma} > 10] [else="disable" alt_text="Maybe [too insecure]"]
|
||||
|
||||
# Set Variable Event
|
||||
set {MyVariable} += 10
|
||||
set {flag} = true
|
||||
|
||||
# Label Event
|
||||
label LabelIdentifier
|
||||
label LabelIdentifier (Display Name)
|
||||
|
||||
# Jump Event
|
||||
jump LabelIdentifier
|
||||
jump TimelineName/LabelIdentifier # jump to label in another timeline
|
||||
jump TimelineName/ # jump to beginning of another timeline
|
||||
|
||||
# Return Event
|
||||
return
|
||||
|
||||
# Do/Call Event
|
||||
do AutoloadName.method("argument")
|
||||
```
|
||||
|
||||
#### Shortcode events (wrapped in `[...]`):
|
||||
|
||||
```
|
||||
[background path="res://icon.png" fade="1.0"]
|
||||
[audio path="res://music.ogg" channel="music" fade="1.0"]
|
||||
[signal arg="activate_something"]
|
||||
[end_timeline]
|
||||
[wait time=1.5]
|
||||
[clear time=0.5]
|
||||
[style name="MyStyle"]
|
||||
[save slot="auto"]
|
||||
```
|
||||
|
||||
Shortcode parameter values must be in double quotes. Order doesn't matter.
|
||||
|
||||
## Characters
|
||||
|
||||
Characters are `.dch` resources managed via `DialogicResourceUtil`:
|
||||
|
||||
```gdscript
|
||||
var char = DialogicResourceUtil.get_character_resource("CharacterName")
|
||||
var all_chars = DialogicResourceUtil.get_character_directory()
|
||||
```
|
||||
|
||||
### Character Properties
|
||||
|
||||
- **Display Name**: Shown on name labels. Can contain `{variables}`.
|
||||
- **Nicknames**: Used for autocolor names feature.
|
||||
- **Default Portrait**: Used when no portrait is specified (Character join, `[portrait]` text effect). Does NOT affect Text events without a portrait.
|
||||
- **Portraits**: Dictionary of portrait name → scene settings. A portrait is a scene (`.tscn`) instanced by Dialogic. The default portrait scene uses an image. Custom portraits extend `DialogicPortrait`.
|
||||
- **Style**: Optional custom style used when this character speaks.
|
||||
- **Sound Moods**: Random typing sounds per portrait.
|
||||
- **Prefix/Suffix**: Text segments wrapped around each text section (e.g., quotation marks).
|
||||
- **Scale, Offset, Mirror**: Per-character and per-portrait transform settings.
|
||||
|
||||
### Character lookup
|
||||
|
||||
Characters are found by identifier string in the `dch_directory`. The directory maps identifiers to file paths. By default, the identifier is derived from the filename (e.g., `Miko.dch` → `"Miko"`). The lookup is **case-sensitive** — ensure timeline text uses the exact same case as the `.dch` filename.
|
||||
|
||||
If a character is not found at runtime, Dialogic creates a temporary character with no portraits. Portraits won't display because `change_speaker()` skips characters with empty portraits.
|
||||
|
||||
## Variables
|
||||
|
||||
Variables are stored in `ProjectSettings` under `dialogic/variables` as a nested Dictionary. Access/manage via `Dialogic.VAR` subsystem.
|
||||
|
||||
**Important:** Variables must be pre-declared in the Dialogic Variables editor (or `project.godot`) before they can be set by timeline `set` commands. The Variable event reads the current value first and fails if the variable doesn't exist.
|
||||
|
||||
```gdscript
|
||||
DialogicUtil.get_default_variables() # returns the full variable dict
|
||||
DialogicUtil.list_variables(dict, path) # flatten to array of paths
|
||||
DialogicUtil.get_variable_type(path, dict) # get VarTypes enum value
|
||||
|
||||
# VarTypes enum: ANY, STRING, FLOAT, INT, BOOL
|
||||
```
|
||||
|
||||
### Variable references in expressions
|
||||
|
||||
Dialogic variables are referenced with **curly braces** in expressions (conditions, set values, choice conditions):
|
||||
|
||||
```
|
||||
if {coins} == 0:
|
||||
set {coins} += 10
|
||||
- Option | [if {player_has_key}]
|
||||
```
|
||||
|
||||
Bare names like `coins` are NOT resolved as variables — they're treated as autoload properties by Godot's `Expression` class.
|
||||
|
||||
## Text Effects (inline BBCode)
|
||||
|
||||
Text effects are tags within TextEvent text that trigger actions when reached during text reveal:
|
||||
|
||||
| Effect | Syntax | Purpose |
|
||||
|--------|--------|---------|
|
||||
| Speed | `[speed=x]` / `[speed]` | Set speed multiplier (1 = normal, 0 = instant). Resets per event. |
|
||||
| Letter Speed | `[lspeed=x]` / `[lspeed=x!]` | Set letter reveal time in seconds. `!` makes it absolute (ignores multiplier). |
|
||||
| Pause | `[pause=x]` / `[pause=x!]` | Pause reveal for x seconds. `!` makes it absolute. |
|
||||
| Portrait | `[portrait=name]` | Switch speaker's portrait mid-text. |
|
||||
| Signal | `[signal=arg]` | Emit `Dialogic.text_signal` with argument. |
|
||||
| Mood | `[mood=name]` | Change typing sound mood. |
|
||||
| Extra Data | `[extra_data=value]` | Set extra data on speaker's portrait. |
|
||||
| New Event | `[n]` / `[n+]` | Split text into manual-advance sections. `+` appends to previous without clearing. |
|
||||
| Input | `[input]` | Await any input (can be skipped). |
|
||||
| Auto-Advance | `[aa]` / `[aa=x]` / `[aa=x?]` | Enable auto-advance. `x` sets time. `?` sets time without enabling. |
|
||||
| No Skip | `[ns]` / `[ns=x]` | Disable text skipping for this event. `x` sets auto-advance time. |
|
||||
|
||||
**Important:** Text effects are NOT paired — there is no `[/speed]` closing tag. Effects change behavior from that point until the next text event resets them. Writing `[/speed]` will appear as visible text.
|
||||
|
||||
## Text Modifiers (preprocessors)
|
||||
|
||||
Text modifiers run on text before display:
|
||||
|
||||
| Modifier | Syntax | Purpose |
|
||||
|----------|--------|---------|
|
||||
| Line Break | `[br]` | Insert line break (useful in choices or with "New Lines as New events") |
|
||||
| Random Selection | `<Option1/Option2/Option3>` | Random variation for repeated text |
|
||||
| Conditional Text | `[if {cond} Text if true./Text if false.]` | Inline conditional text |
|
||||
| Autopauses | (automatic) | Insert `[pause]` after configured punctuation characters |
|
||||
|
||||
## Styles & Layouts
|
||||
|
||||
Styles define the visual presentation. Layouts define the scene structure.
|
||||
|
||||
- **Dialogic Nodes**: UI nodes (DialogText, NameLabel, PortraitContainer, ChoiceButton, etc.) that display things. They register themselves in groups.
|
||||
- **Layout Scenes**: Scenes containing Dialogic Nodes and custom scripts. Base layout extends `DialogicLayoutBase`, layers extend `DialogicLayoutLayer`.
|
||||
- **Styles**: Resources combining one base layout + list of layer layouts with overridable settings. Can inherit from other styles.
|
||||
|
||||
```gdscript
|
||||
Dialogic.Styles.load_style("StyleName") # load/switch to a style
|
||||
Dialogic.Styles.has_active_layout_node() # check if layout is present
|
||||
Dialogic.Styles.get_layout_node() # get the layout scene node
|
||||
Dialogic.Styles.change_style("StyleName") # change style, preserving state
|
||||
```
|
||||
|
||||
## Signals & Signal Events
|
||||
|
||||
### Signal Event
|
||||
|
||||
Emits `Dialogic.signal_event` with an argument:
|
||||
|
||||
```gdscript
|
||||
Dialogic.signal_event.connect(_on_dialogic_signal)
|
||||
|
||||
func _on_dialogic_signal(argument: String):
|
||||
if argument == "activate_something":
|
||||
print("Activated!")
|
||||
```
|
||||
|
||||
### Text Signal (via `[signal=arg]` text effect)
|
||||
|
||||
Emits `Dialogic.text_signal`:
|
||||
|
||||
```gdscript
|
||||
Dialogic.text_signal.connect(_on_dialogic_text_signal)
|
||||
```
|
||||
|
||||
### Timeline Start/End
|
||||
|
||||
```gdscript
|
||||
Dialogic.timeline_started.connect(_on_timeline_started)
|
||||
Dialogic.timeline_ended.connect(_on_timeline_ended)
|
||||
```
|
||||
|
||||
### Key Subsystem Signals
|
||||
|
||||
```gdscript
|
||||
Dialogic.Text.about_to_show_text(info: Dictionary)
|
||||
Dialogic.Text.text_finished(info: Dictionary)
|
||||
Dialogic.Text.speaker_updated(character: DialogicCharacter)
|
||||
Dialogic.Text.textbox_visibility_changed(visible: bool)
|
||||
Dialogic.Portraits.character_joined(info: Dictionary)
|
||||
Dialogic.Portraits.character_left(info: Dictionary)
|
||||
Dialogic.Portraits.character_portrait_changed(info: Dictionary)
|
||||
Dialogic.VAR.variable_changed(info: Dictionary)
|
||||
Dialogic.VAR.variable_was_set(info: Dictionary) # only from set variable events
|
||||
```
|
||||
|
||||
## Saving & Loading
|
||||
|
||||
```gdscript
|
||||
# Simple save/load (takes screenshots too)
|
||||
Dialogic.Save.save("slot_name")
|
||||
Dialogic.Save.load("slot_name")
|
||||
|
||||
# Save without screenshot
|
||||
Dialogic.Save.save("slot_name", false, Dialogic.Save.ThumbnailMode.NONE)
|
||||
|
||||
# Save with extra info (date, custom data)
|
||||
var extra = {"text": Dialogic.current_state_info["text"], "date": Time.get_datetime_string_from_system(false, true)}
|
||||
Dialogic.Save.save("slot_name", false, Dialogic.Save.ThumbnailMode.STORE_ONLY, extra)
|
||||
|
||||
# Check if slot exists
|
||||
if Dialogic.Save.has_slot("slot_name"):
|
||||
var info = Dialogic.Save.get_slot_info("slot_name")
|
||||
```
|
||||
|
||||
Savegames store Dialogic's timeline state and variables. Extra info is stored per-slot but not loaded into Dialogic state.
|
||||
|
||||
## Audio Channels
|
||||
|
||||
Audio plays on named channels. Default channels defined in project settings:
|
||||
|
||||
```gdscript
|
||||
DialogicUtil.get_audio_channel_defaults() # returns Dictionary
|
||||
DialogicUtil.get_audio_channel_suggestions("search")
|
||||
DialogicUtil.validate_audio_channel_name("name")
|
||||
```
|
||||
|
||||
Channel names: `""` (default), `"music"` (looping by default), plus custom.
|
||||
|
||||
## Resource Management (DialogicResourceUtil)
|
||||
|
||||
```gdscript
|
||||
# Directories (map identifier -> path)
|
||||
DialogicResourceUtil.get_directory('dtl') # timelines
|
||||
DialogicResourceUtil.get_directory('dch') # characters
|
||||
DialogicResourceUtil.update_directory(extension) # rescan
|
||||
|
||||
# Lookups
|
||||
DialogicResourceUtil.get_timeline_resource("name")
|
||||
DialogicResourceUtil.get_character_resource("name")
|
||||
DialogicResourceUtil.get_unique_identifier_by_path("res://...")
|
||||
DialogicResourceUtil.get_resource_path_from_identifier("name", "dtl")
|
||||
|
||||
# Label cache (editor only)
|
||||
DialogicResourceUtil.get_label_cache()
|
||||
DialogicResourceUtil.update_label_cache()
|
||||
|
||||
# Event cache
|
||||
DialogicResourceUtil.get_event_cache() # all event types
|
||||
DialogicResourceUtil.update_event_cache()
|
||||
```
|
||||
|
||||
## Utility Helpers (DialogicUtil)
|
||||
|
||||
```gdscript
|
||||
# Variables
|
||||
DialogicUtil.VarTypes # ANY, STRING, FLOAT, INT, BOOL
|
||||
DialogicUtil.get_default_variables()
|
||||
DialogicUtil.list_variables(dict, path, type)
|
||||
DialogicUtil.get_variable_value_type(value)
|
||||
DialogicUtil.get_variable_type(path, dict)
|
||||
DialogicUtil.logical_convert(value) # auto-convert string to int/float/bool
|
||||
|
||||
# Suggestions (for editor fields)
|
||||
DialogicUtil.get_character_suggestions(search, current, allow_none, allow_all)
|
||||
DialogicUtil.get_portrait_suggestions(search, character, allow_empty)
|
||||
DialogicUtil.get_autoload_suggestions(filter)
|
||||
DialogicUtil.get_autoload_method_suggestions(filter, autoload_name)
|
||||
DialogicUtil.get_autoload_property_suggestions(filter, autoload_name)
|
||||
DialogicUtil.get_audio_bus_suggestions(filter)
|
||||
DialogicUtil.get_audio_channel_suggestions(search)
|
||||
|
||||
# Timer
|
||||
DialogicUtil.is_physics_timer()
|
||||
DialogicUtil.update_timer_process_callback(timer)
|
||||
```
|
||||
|
||||
## Project Settings
|
||||
|
||||
Key settings (under `dialogic/`):
|
||||
- `dialogic/variables` — default variable dictionary
|
||||
- `dialogic/layout/default_style` — default style name
|
||||
- `dialogic/layout/end_behaviour` — 0=free, 1=hide, 2=keep layout on end
|
||||
- `dialogic/text/letter_speed` — default letter reveal time
|
||||
- `dialogic/text/autocolor_names` — auto-color character names in text
|
||||
- `dialogic/text/split_at_new_lines` — treat newlines as `[n]`
|
||||
- `dialogic/text/autopauses` — auto-insert pauses after punctuation
|
||||
- `dialogic/extensions_folder` — custom modules path (default: `res://addons/dialogic_additions/`)
|
||||
- `dialogic/directories/*_directory` — resource directories (dch, dtl, style)
|
||||
- `dialogic/audio/channel_defaults` — default audio channels
|
||||
|
||||
## Working with Dialogic in This Project
|
||||
|
||||
### Common Patterns
|
||||
|
||||
**Starting dialog (guard against double-start):**
|
||||
```gdscript
|
||||
func _input(event):
|
||||
if player_is_in_area and Input.is_action_pressed("start_interaction"):
|
||||
if Dialogic.current_timeline == null:
|
||||
Dialogic.start("my_timeline")
|
||||
```
|
||||
|
||||
**Reacting to dialog signals:**
|
||||
```gdscript
|
||||
Dialogic.signal_event.connect(_on_dialogic_signal)
|
||||
Dialogic.timeline_ended.connect(_on_dialog_finished)
|
||||
func _on_dialog_finished():
|
||||
Dialogic.timeline_ended.disconnect(_on_dialog_finished)
|
||||
# game logic here
|
||||
```
|
||||
|
||||
**Setting variables from code:**
|
||||
```gdscript
|
||||
Dialogic.VAR.set_variable("player_name", "Miko")
|
||||
Dialogic.VAR.set_variable("coins", 42)
|
||||
```
|
||||
|
||||
**Creating timelines in code:**
|
||||
```gdscript
|
||||
# From strings (follow text syntax)
|
||||
var events: Array = """
|
||||
Jowan (Surprised): Wow this is interesting!
|
||||
- Yes
|
||||
set {MyAutoload.excitement} += 20
|
||||
- No
|
||||
set {MyAutoload.excitement} -= 10
|
||||
""".split('\n')
|
||||
var timeline = DialogicTimeline.new()
|
||||
timeline.events = events
|
||||
Dialogic.start(timeline)
|
||||
```
|
||||
|
||||
**Hiding/showing textbox:**
|
||||
```gdscript
|
||||
if Dialogic.Text.is_textbox_visible():
|
||||
Dialogic.Text.hide_textbox()
|
||||
else:
|
||||
Dialogic.Text.show_textbox()
|
||||
```
|
||||
|
||||
**Getting current speaker:**
|
||||
```gdscript
|
||||
var speaker: DialogicCharacter = Dialogic.Text.get_current_speaker()
|
||||
var portrait_info := Dialogic.Portraits.get_character_info(speaker)
|
||||
```
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
- **Portraits not showing**: Characters must join the scene with a Character Event (Join mode) before their portraits appear. Ensure `.dch` filename case matches timeline text exactly.
|
||||
- **Wrong text displayed**: If translation is enabled, CSV rows take priority. Disable translation during development or update CSVs.
|
||||
- **Lag on dialog start**: Preload styles with `style.prepare()` or preload an empty timeline during your loading screen.
|
||||
- **Input conflicts**: If your interaction key matches the dialogic input action, guard with `if Dialogic.current_timeline == null:` before starting new dialog.
|
||||
642
.agents/skills/godot-test-instrumentation/SKILL.md
Normal file
642
.agents/skills/godot-test-instrumentation/SKILL.md
Normal file
@@ -0,0 +1,642 @@
|
||||
---
|
||||
name: godot-test-instrumentation
|
||||
description: Automates Godot testing for coding agents via headless execution, method simulation, and stdout validation.
|
||||
---
|
||||
|
||||
# Godot Test Instrumentation
|
||||
|
||||
Enables coding agents to automatically test game changes by running Godot in headless mode, simulating user actions via method calls, and validating results through structured stdout output.
|
||||
|
||||
## Use This Skill When
|
||||
|
||||
- A coding agent needs to verify gameplay changes work correctly
|
||||
- Testing generator purchases, currency gains, prestige mechanics, or buff systems
|
||||
- Running automated validation in CI/CD pipelines
|
||||
- Debugging features without manual GUI interaction
|
||||
|
||||
## Core Workflow
|
||||
|
||||
```
|
||||
1. Agent writes/modifies game code
|
||||
↓
|
||||
2. Agent creates or updates test script in res://tests/
|
||||
↓
|
||||
3. Agent runs: godot --headless --path . -s res://tests/test_x.gd
|
||||
↓
|
||||
4. Test loads gym scene, simulates actions, prints [TAG] results
|
||||
↓
|
||||
5. Agent parses stdout for [PASS]/[FAIL]/[RESULT]
|
||||
↓
|
||||
6. Exit code 0 = PASS, Exit code 1 = FAIL
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
res://
|
||||
├── tests/
|
||||
│ ├── test_runner.gd # Cross-platform test discovery & execution
|
||||
│ ├── test_utils.gd # Shared assertion helpers
|
||||
│ ├── test_gold_mine_click.gd # Example: generator click → currency gain
|
||||
│ └── test_prestige.gd # Example: prestige mechanics validation
|
||||
├── docs/gyms/ # Test scenes (e.g., tiny_sword.tscn)
|
||||
└── core/ # Game logic (LevelGameState, generators, etc.)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 1. Test Utils (res://tests/test_utils.gd)
|
||||
|
||||
Shared assertion helpers with structured stdout output.
|
||||
|
||||
```gdscript
|
||||
class_name TestUtils
|
||||
extends RefCounted
|
||||
|
||||
static var _test_name: String = ""
|
||||
static var _passed: int = 0
|
||||
static var _failed: int = 0
|
||||
|
||||
static func set_test_name(name: String) -> void:
|
||||
_test_name = name
|
||||
_passed = 0
|
||||
_failed = 0
|
||||
|
||||
static func assert_equals(expected: Variant, actual: Variant, message: String) -> void:
|
||||
if expected == actual:
|
||||
print("[PASS] %s: %s" % [_test_name, message])
|
||||
_passed += 1
|
||||
else:
|
||||
print("[FAIL] %s: %s (expected %s, got %s)" % [
|
||||
_test_name, message, str(expected), str(actual)
|
||||
])
|
||||
_failed += 1
|
||||
|
||||
static func assert_true(condition: bool, message: String) -> void:
|
||||
if condition:
|
||||
print("[PASS] %s: %s" % [_test_name, message])
|
||||
_passed += 1
|
||||
else:
|
||||
print("[FAIL] %s: %s" % [_test_name, message])
|
||||
_failed += 1
|
||||
|
||||
static func assert_greater_than(a: float, b: float, message: String) -> void:
|
||||
if a > b:
|
||||
print("[PASS] %s: %s" % [_test_name, message])
|
||||
_passed += 1
|
||||
else:
|
||||
print("[FAIL] %s: %s (expected %s > %s)" % [_test_name, message, str(a), str(b)])
|
||||
_failed += 1
|
||||
|
||||
static func print_result() -> void:
|
||||
print("[SUMMARY] %s: %d passed, %d failed" % [_test_name, _passed, _failed])
|
||||
if _failed == 0:
|
||||
print("[RESULT] PASS")
|
||||
else:
|
||||
print("[RESULT] FAIL")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Test Runner (res://tests/test_runner.gd)
|
||||
|
||||
Cross-platform test discovery and execution. Runs all `test_*.gd` scripts in the tests directory.
|
||||
|
||||
```gdscript
|
||||
extends SceneTree
|
||||
|
||||
var test_scripts: Array[String] = []
|
||||
var test_index: int = 0
|
||||
var total_passed: int = 0
|
||||
var total_failed: int = 0
|
||||
|
||||
func _init():
|
||||
print("\n=== GODOT TEST RUNNER ===\n")
|
||||
_discover_tests()
|
||||
|
||||
if test_scripts.is_empty():
|
||||
print("[ERROR] No test scripts found in res://tests/")
|
||||
quit(1)
|
||||
return
|
||||
|
||||
print("Found %d test(s): %s\n" % [test_scripts.size(), str(test_scripts)])
|
||||
await _run_next_test()
|
||||
|
||||
print("\n=== TEST SUMMARY ===")
|
||||
print("Total passed: %d" % total_passed)
|
||||
print("Total failed: %d" % total_failed)
|
||||
|
||||
if total_failed == 0:
|
||||
print("[OVERALL_RESULT] PASS")
|
||||
quit(0)
|
||||
else:
|
||||
print("[OVERALL_RESULT] FAIL")
|
||||
quit(1)
|
||||
|
||||
func _discover_tests() -> void:
|
||||
var dir = DirAccess.open("res://tests")
|
||||
if dir:
|
||||
dir.list_dir_begin()
|
||||
var file_name = dir.get_next()
|
||||
while file_name != "":
|
||||
if file_name.ends_with(".gd") and file_name.begins_with("test_") and file_name != "test_runner.gd" and file_name != "test_utils.gd":
|
||||
test_scripts.append("res://tests/%s" % file_name)
|
||||
file_name = dir.get_next()
|
||||
dir.list_dir_end()
|
||||
test_scripts.sort()
|
||||
|
||||
func _run_next_test() -> void:
|
||||
if test_index >= test_scripts.size():
|
||||
return
|
||||
|
||||
var test_script = test_scripts[test_index]
|
||||
print("--- Running test %d/%d: %s ---" % [test_index + 1, test_scripts.size(), test_script])
|
||||
|
||||
var test_instance = load(test_script).new()
|
||||
await test_instance.run()
|
||||
|
||||
total_passed += test_instance.passed
|
||||
total_failed += test_instance.failed
|
||||
|
||||
test_index += 1
|
||||
await _run_next_test()
|
||||
|
||||
class TestScript:
|
||||
var passed: int = 0
|
||||
var failed: int = 0
|
||||
|
||||
func run():
|
||||
pass
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
```bash
|
||||
# Run all tests via test runner
|
||||
"$GODOT_BIN" --headless --path . -s res://tests/test_runner.gd
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Test Script Template
|
||||
|
||||
Each test extends `SceneTree` and implements a `run()` method.
|
||||
|
||||
```gdscript
|
||||
extends SceneTree
|
||||
|
||||
var passed: int = 0
|
||||
var failed: int = 0
|
||||
|
||||
func _init():
|
||||
await run()
|
||||
quit(0 if failed == 0 else 1)
|
||||
|
||||
func run():
|
||||
# Setup
|
||||
TestUtils.set_test_name("<test_name>")
|
||||
|
||||
# Load scene
|
||||
var scene = load("<path_to_gym_scene>") # e.g., "res://docs/gyms/tiny_sword/tiny_sword.tscn"
|
||||
var root_node = scene.instantiate()
|
||||
add_root_node(root_node)
|
||||
|
||||
# Wait for scene initialization
|
||||
await _wait()
|
||||
|
||||
# Get references
|
||||
var game_state: LevelGameState = root_node.find_child("LevelGameState")
|
||||
var generator = root_node.find_child("<GeneratorNodeName>") as CurrencyGenerator
|
||||
|
||||
if game_state == null:
|
||||
print("[ERROR] LevelGameState not found")
|
||||
_print_summary()
|
||||
return
|
||||
|
||||
# Test logic
|
||||
var initial_value = game_state.get_currency_amount_by_id(&"<currency_id>")
|
||||
print("[TARGET] initial_<currency>: %s" % initial_value.to_string_suffix(2))
|
||||
|
||||
# Simulate action (method call, not mouse click)
|
||||
generator._on_pressed()
|
||||
|
||||
# Wait for signal propagation
|
||||
await _wait()
|
||||
|
||||
# Validate
|
||||
var final_value = game_state.get_currency_amount_by_id(&"<currency_id>")
|
||||
print("[TARGET] final_<currency>: %s" % final_value.to_string_suffix(2))
|
||||
|
||||
TestUtils.assert_greater_than(
|
||||
final_value.mantissa,
|
||||
initial_value.mantissa,
|
||||
"<currency> increased after action"
|
||||
)
|
||||
|
||||
_print_summary()
|
||||
|
||||
func _wait() -> void:
|
||||
await get_tree().create_timer(0.5).timeout
|
||||
|
||||
func _print_summary():
|
||||
TestUtils.print_result()
|
||||
passed = _get_passed_count()
|
||||
failed = _get_failed_count()
|
||||
|
||||
func _get_passed_count() -> int:
|
||||
# Extract from TestUtils or track locally
|
||||
return 0
|
||||
|
||||
func _get_failed_count() -> int:
|
||||
return 0
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Example Test: Gold Mine Click
|
||||
|
||||
Tests that clicking a generator grants currency.
|
||||
|
||||
```gdscript
|
||||
extends SceneTree
|
||||
|
||||
var passed: int = 0
|
||||
var failed: int = 0
|
||||
var _local_passed: int = 0
|
||||
var _local_failed: int = 0
|
||||
|
||||
func _init():
|
||||
await run()
|
||||
quit(failed == 0 ? 0 : 1)
|
||||
|
||||
func run():
|
||||
print("\n=== TEST: Gold Mine Click ===\n")
|
||||
TestUtils.set_test_name("gold_mine_click")
|
||||
|
||||
# Load tiny_sword scene
|
||||
var scene = load("res://docs/gyms/tiny_sword/tiny_sword.tscn")
|
||||
var root_node = scene.instantiate()
|
||||
add_root_node(root_node)
|
||||
|
||||
await _wait()
|
||||
|
||||
var game_state: LevelGameState = root_node.find_child("LevelGameState")
|
||||
var gold_mine = root_node.find_child("GoldMine") as CurrencyGenerator
|
||||
|
||||
if game_state == null or gold_mine == null:
|
||||
print("[ERROR] Missing LevelGameState or GoldMine node")
|
||||
_print_summary()
|
||||
return
|
||||
|
||||
# Record initial gold
|
||||
var initial_gold: BigNumber = game_state.get_currency_amount_by_id(&"gold")
|
||||
print("[TARGET] initial_gold: %s" % initial_gold.to_string_suffix(2))
|
||||
|
||||
# Simulate clicking the generator
|
||||
gold_mine._on_pressed()
|
||||
|
||||
await _wait()
|
||||
|
||||
# Validate gold increased
|
||||
var final_gold: BigNumber = game_state.get_currency_amount_by_id(&"gold")
|
||||
print("[TARGET] final_gold: %s" % final_gold.to_string_suffix(2))
|
||||
|
||||
TestUtils.assert_greater_than(
|
||||
final_gold.mantissa,
|
||||
initial_gold.mantissa,
|
||||
"Gold increased after generator click"
|
||||
)
|
||||
|
||||
# Check specific value (adjust based on click_mantissa in data)
|
||||
var expected_min: float = initial_gold.mantissa + 1.0
|
||||
TestUtils.assert_true(
|
||||
final_gold.mantissa >= expected_min,
|
||||
"Gold increased by at least 1.0"
|
||||
)
|
||||
|
||||
_print_summary()
|
||||
|
||||
func _wait() -> void:
|
||||
await get_tree().create_timer(0.5).timeout
|
||||
|
||||
func _print_summary():
|
||||
TestUtils.print_result()
|
||||
# Note: For accurate counts, modify TestUtils to return counts or track locally
|
||||
_local_failed = 0 # Update based on actual assertions
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Example Test: Prestige Mechanics
|
||||
|
||||
Tests prestige threshold and currency gain.
|
||||
|
||||
```gdscript
|
||||
extends SceneTree
|
||||
|
||||
var passed: int = 0
|
||||
var failed: int = 0
|
||||
|
||||
func _init():
|
||||
await run()
|
||||
quit(failed == 0 ? 0 : 1)
|
||||
|
||||
func run():
|
||||
print("\n=== TEST: Prestige Mechanics ===\n")
|
||||
TestUtils.set_test_name("prestige")
|
||||
|
||||
var scene = load("res://docs/gyms/tiny_sword/tiny_sword.tscn")
|
||||
var root_node = scene.instantiate()
|
||||
add_root_node(root_node)
|
||||
|
||||
await _wait()
|
||||
|
||||
var game_state: LevelGameState = root_node.find_child("LevelGameState")
|
||||
var prestige_manager = root_node.find_child("PrestigeManager") as PrestigeManager
|
||||
|
||||
if game_state == null or prestige_manager == null:
|
||||
print("[ERROR] Missing LevelGameState or PrestigeManager")
|
||||
_print_summary()
|
||||
return
|
||||
|
||||
# Setup: Accumulate currencies to meet prestige threshold
|
||||
game_state.add_currency_by_id(&"gold", BigNumber.from_float(100000.0))
|
||||
game_state.add_currency_by_id(&"gems", BigNumber.from_float(100.0))
|
||||
|
||||
await _wait()
|
||||
|
||||
# Check if can prestige
|
||||
var can_prestige: bool = prestige_manager.can_prestige()
|
||||
print("[TARGET] can_prestige: %s" % str(can_prestige))
|
||||
|
||||
TestUtils.assert_true(can_prestige, "Should be able to prestige after accumulating currencies")
|
||||
|
||||
if can_prestige:
|
||||
# Perform prestige
|
||||
prestige_manager.perform_prestige()
|
||||
|
||||
await _wait()
|
||||
|
||||
# Check ascension currency
|
||||
var ascension: BigNumber = game_state.get_currency_amount_by_id(&"ascension")
|
||||
print("[TARGET] ascension_currency: %s" % ascension.to_string_suffix(2))
|
||||
|
||||
TestUtils.assert_true(
|
||||
ascension.mantissa > 0,
|
||||
"Should receive ascension currency after prestige"
|
||||
)
|
||||
|
||||
# Check prestige multiplier increased
|
||||
var total_multiplier = prestige_manager.get_total_multiplier()
|
||||
print("[TARGET] total_multiplier: %s" % str(total_multiplier))
|
||||
|
||||
TestUtils.assert_greater_than(
|
||||
total_multiplier if total_multiplier is float else float(total_multiplier),
|
||||
1.0,
|
||||
"Prestige multiplier should be > 1.0"
|
||||
)
|
||||
|
||||
_print_summary()
|
||||
|
||||
func _wait() -> void:
|
||||
await get_tree().create_timer(0.5).timeout
|
||||
|
||||
func _print_summary():
|
||||
TestUtils.print_result()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Shell Scripts
|
||||
|
||||
### run_test.sh
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# Usage: ./run_test.sh <test_script_path>
|
||||
# Example: ./run_test.sh res://tests/test_gold_mine_click.gd
|
||||
|
||||
set -e
|
||||
|
||||
TEST_SCRIPT="$1"
|
||||
GODOT_BIN="${GODOT_BIN:-godot}"
|
||||
|
||||
if [ -z "$TEST_SCRIPT" ]; then
|
||||
echo "Usage: $0 <test_script_path>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Running test: $TEST_SCRIPT"
|
||||
echo ""
|
||||
|
||||
"$GODOT_BIN" --headless --path . -s "$TEST_SCRIPT"
|
||||
EXIT_CODE=$?
|
||||
|
||||
echo ""
|
||||
if [ $EXIT_CODE -eq 0 ]; then
|
||||
echo "✓ Test passed"
|
||||
else
|
||||
echo "✗ Test failed with exit code: $EXIT_CODE"
|
||||
fi
|
||||
|
||||
exit $EXIT_CODE
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### run_all.sh
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# Usage: ./run_all.sh
|
||||
|
||||
set -e
|
||||
|
||||
GODOT_BIN="${GODOT_BIN:-godot}"
|
||||
TEST_DIR="res://tests"
|
||||
|
||||
echo "=== Running All Tests ==="
|
||||
echo ""
|
||||
|
||||
# Run test runner (discovers all tests automatically)
|
||||
"$GODOT_BIN" --headless --path . -s "$TEST_DIR/test_runner.gd"
|
||||
EXIT_CODE=$?
|
||||
|
||||
echo ""
|
||||
if [ $EXIT_CODE -eq 0 ]; then
|
||||
echo "✓ All tests passed"
|
||||
else
|
||||
echo "✗ Some tests failed"
|
||||
fi
|
||||
|
||||
exit $EXIT_CODE
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### check_syntax.sh
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# Usage: ./check_syntax.sh <script_path>
|
||||
|
||||
SCRIPT="$1"
|
||||
GODOT_BIN="${GODOT_BIN:-godot}"
|
||||
|
||||
if [ -z "$SCRIPT" ]; then
|
||||
echo "Usage: $0 <script_path>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Checking syntax: $SCRIPT"
|
||||
"$GODOT_BIN" --headless --path . --check-only --script "$SCRIPT"
|
||||
EXIT_CODE=$?
|
||||
|
||||
if [ $EXIT_CODE -eq 0 ]; then
|
||||
echo "✓ Syntax valid"
|
||||
else
|
||||
echo "✗ Syntax errors found"
|
||||
fi
|
||||
|
||||
exit $EXIT_CODE
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Godot CLI Command Reference
|
||||
|
||||
| Command | Purpose |
|
||||
|---------|---------|
|
||||
| `"$GODOT_BIN" --headless --path . -s res://tests/test_x.gd` | Run single test in headless mode |
|
||||
| `"$GODOT_BIN" --headless --path . -s res://tests/test_runner.gd` | Run all tests via test runner |
|
||||
| `"$GODOT_BIN" --headless --path . --check-only --script res://test.gd` | Syntax validation only |
|
||||
| `"$GODOT_BIN" --headless --verbose --path . -s res://tests/test_x.gd` | Verbose output for debugging |
|
||||
| `"$GODOT_BIN" --headless --path . --quit-after 100 -s res://tests/test_x.gd` | Run for 100 frames then quit |
|
||||
| `"$GODOT_BIN" --headless --path . -s res://tests/test_x.gd --log-file /tmp/test.log` | Log output to file |
|
||||
|
||||
**Important:** Always use `--headless` for automated testing. This sets `--display-driver headless --audio-driver Dummy`.
|
||||
|
||||
---
|
||||
|
||||
## 8. Output Parsing Protocol
|
||||
|
||||
Agents should parse stdout for these tags:
|
||||
|
||||
| Tag | Purpose | Example |
|
||||
|-----|---------|---------|
|
||||
| `[TARGET]` | Values to validate | `[TARGET] gold: 100.00` |
|
||||
| `[PASS]` | Assertion passed | `[PASS] gold_mine_click: Gold increased]` |
|
||||
| `[FAIL]` | Assertion failed | `[FAIL] gold_mine_click: Expected gold > 0]` |
|
||||
| `[SUMMARY]` | Test completion stats | `[SUMMARY] gold_mine_click: 2 passed, 0 failed]` |
|
||||
| `[RESULT]` | Overall test result | `[RESULT] PASS` or `[RESULT] FAIL]` |
|
||||
| `[ERROR]` | Setup/runtime error | `[ERROR] Missing LevelGameState node]` |
|
||||
| `[OVERALL_RESULT]` | Test runner summary | `[OVERALL_RESULT] PASS` or `[OVERALL_RESULT] FAIL]` |
|
||||
|
||||
**Exit Codes:**
|
||||
- `0` = All assertions passed
|
||||
- `1` = One or more assertions failed or error occurred
|
||||
|
||||
---
|
||||
|
||||
## 9. Best Practices
|
||||
|
||||
### Simulating User Actions
|
||||
|
||||
In headless mode, **do not** simulate mouse clicks with `InputEventMouseButton`. Instead, call methods directly:
|
||||
|
||||
```gdscript
|
||||
# ✅ DO: Call the method directly
|
||||
generator._on_pressed()
|
||||
game_state.add_currency_by_id(&"gold", BigNumber.from_float(100.0))
|
||||
prestige_manager.perform_prestige()
|
||||
|
||||
# ❌ DON'T: Try to synthesize mouse events (won't work in headless)
|
||||
var event = InputEventMouseButton.new()
|
||||
Input.parse_input_event(event) # Ineffective without GUI
|
||||
```
|
||||
|
||||
### Async Handling
|
||||
|
||||
Use standard wait time for signal propagation:
|
||||
|
||||
```gdscript
|
||||
await get_tree().create_timer(0.5).timeout
|
||||
```
|
||||
|
||||
Adjust based on test needs:
|
||||
- `0.1s` - Simple state changes
|
||||
- `0.5s` - Signal propagation (recommended default)
|
||||
- `1.0s+` - Complex async operations
|
||||
|
||||
### Scene Selection
|
||||
|
||||
Use gym scenes from `res://docs/gyms/`:
|
||||
- `res://docs/gyms/tiny_sword/tiny_sword.tscn` - Full game with generators, currencies, prestige
|
||||
- Create minimal test scenes if testing isolated features
|
||||
|
||||
### Test Naming
|
||||
|
||||
- Use `test_<feature>.gd` naming convention
|
||||
- Test name in `TestUtils.set_test_name()` should match filename (without `.gd`)
|
||||
|
||||
### Assertions
|
||||
|
||||
- One assertion per line for clear parsing
|
||||
- Include descriptive messages
|
||||
- Use `[TARGET]` to log values before/after actions
|
||||
|
||||
---
|
||||
|
||||
## 10. Quick Start for Agents
|
||||
|
||||
1. **Create test file:**
|
||||
```bash
|
||||
touch res://tests/test_my_feature.gd
|
||||
```
|
||||
|
||||
2. **Copy template:**
|
||||
```gdscript
|
||||
extends SceneTree
|
||||
var passed: int = 0
|
||||
var failed: int = 0
|
||||
|
||||
func _init():
|
||||
await run()
|
||||
quit(failed == 0 ? 0 : 1)
|
||||
|
||||
func run():
|
||||
print("\n=== TEST: My Feature ===\n")
|
||||
TestUtils.set_test_name("my_feature")
|
||||
|
||||
var scene = load("res://docs/gyms/tiny_sword/tiny_sword.tscn")
|
||||
var root = scene.instantiate()
|
||||
add_root_node(root)
|
||||
|
||||
await get_tree().create_timer(0.5).timeout
|
||||
|
||||
# Your test logic here
|
||||
|
||||
TestUtils.print_result()
|
||||
```
|
||||
|
||||
3. **Run test:**
|
||||
```bash
|
||||
godot --headless --path . -s res://tests/test_my_feature.gd
|
||||
```
|
||||
|
||||
4. **Parse output:**
|
||||
- Look for `[PASS]`, `[FAIL]`, `[RESULT]` tags
|
||||
- Check exit code (0 = pass, 1 = fail)
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- [Godot CLI Tutorial](https://docs.godotengine.org/en/latest/tutorials/editor/command_line_tutorial.html)
|
||||
- [Godot Headless Mode](https://docs.godotengine.org/en/latest/tutorials/editor/command_line_tutorial.html#run-options)
|
||||
- [InputEvent Documentation](https://docs.godotengine.org/en/stable/tutorials/inputs/inputevent.html)
|
||||
|
||||
</content>
|
||||
<parameter=filePath>
|
||||
/home/mikymod/work/jmp/idle/.agents/skills/godot-test-instrumentation/SKILL.md
|
||||
Reference in New Issue
Block a user