# Sprint 3d Research — Animation Intelligence

## Part 1: Codebase Research

### Tool Structure Pattern

All tools follow a split pattern:
1. **Private helper** `_build_*()` — takes session + params, does computation, returns dict
2. **Public async tool** with `@mcp.tool()` decorator — validates session, calls helper

```python
from godotiq.server import mcp
from mcp.server.fastmcp import Context

def _build_animation_info(session: GodotIQSession, scene: str, ...) -> dict:
    # computation here
    return result

@mcp.tool(name="godotiq_animation_info", annotations={...})
async def godotiq_animation_info(scene: str, ..., ctx: Context = None) -> dict:
    session = ctx.request_context.lifespan_context["session"]
    if session is None:
        return {"error": "No Godot project loaded"}
    return _build_animation_info(session, scene, ...)
```

### Tool Registration

In `src/godotiq/server.py`, tools are imported after `mcp` creation:
```python
from godotiq.tools import memory, code, spatial, assets  # noqa: E402, F401
```
New animation module must be added here.

### Session API (Key Methods)

```python
session._index: ProjectIndex        # Cross-reference data
session._gd_scripts: dict[str, GdScript]  # Parsed scripts by res:// path
session.get_script(res_path) -> GdScript | None
session.get_scene(res_path) -> TscnScene | None
session.resolve_file_path(path) -> str | None
session._resolve_res_path(res_path) -> Path  # res:// to absolute path
session.to_res_path(path) -> str  # absolute to res:// path
```

### Parser Data Structures

**TscnScene:**
```python
scene.sub_resources: dict[str, SubResource]  # id -> SubResource
scene.nodes: list[TscnNode]  # flat list
scene.ext_resources: dict[str, ExtResource]  # id -> ExtResource
scene.connections: list[Connection]
scene.get_nodes_by_type(type_name) -> list[TscnNode]
scene.get_all_scripts() -> list[str]  # unique script paths
```

**SubResource:**
```python
sub.id: str
sub.type: str          # "Animation", "AnimationLibrary", "AnimationNodeStateMachine", etc.
sub.properties: dict[str, object]  # parsed property values
```

**TscnNode:**
```python
node.name: str
node.type: str         # "AnimationPlayer", "AnimationTree", etc.
node.parent: str
node.children: list[TscnNode]
node.script: str | None  # ExtResource id
node.properties: dict[str, object]
```

**Key detail:** Property values are mixed types — strings, numbers, lists, dicts. SubResource references in properties appear as strings like `SubResource("id")` that need regex parsing.

### Test Setup Pattern

From `tests/test_tools/conftest.py`:
```python
@pytest.fixture
def sample_root() -> Path:
    return Path(__file__).resolve().parent.parent / "fixtures" / "sample_project"

@pytest.fixture
def session(sample_root: Path) -> GodotIQSession:
    s = GodotIQSession(sample_root)
    s.load()
    return s
```

Tests import private helpers directly:
```python
from godotiq.tools.assets import _build_asset_registry
result = _build_asset_registry(session)
```

### Existing Fixtures

```
tests/fixtures/sample_project/
├── project.godot
├── .godotiq.json
├── scenes/main.tscn, equipment/fdm_printer.tscn
├── scripts/autoloads/*, entities/printer.gd, worker.gd
└── assets/models/printers/*.glb
```

No animation fixtures exist yet — all new for Sprint 3d.

---

## Part 2: Web Research — Godot 4 Animation Format

### AnimationNodeStateMachine .tscn Serialization

**Confirmed from engine source code** (`animation_node_state_machine.cpp`):

States use per-state named properties:
```
states/StateName/node = SubResource("id")
states/StateName/position = Vector2(x, y)
```

Transitions use a **single flat array** (every 3 elements = one transition):
```
transitions = ["FromState", "ToState", SubResource("id"), "FromState2", "ToState2", SubResource("id2")]
```

**Real-world .tscn example** (from GitHub issue #61500):
```
[sub_resource type="AnimationNodeStateMachine" id=16]
states/Attack/node = SubResource( 33 )
states/Attack/position = Vector2( 282, 178.562 )
states/Idle/node = SubResource( 15 )
states/Idle/position = Vector2( 287, 79.3281 )
states/Run/node = SubResource( 22 )
states/Run/position = Vector2( 548, 78.75 )
transitions = [ "Idle", "Run", SubResource( 23 ), "Run", "Idle", SubResource( 24 ) ]
start_node = "Idle"
graph_offset = Vector2( -1740, -525 )
```

**IMPORTANT:** The spec's fixture uses per-transition properties (`transitions/0/from`, `transitions/0/to`) which is NOT how Godot actually serializes. The real format uses `states/NAME/node` for states and a flat `transitions` array. This is critical for the implementation.

### AnimationNodeAnimation sub_resource

```
[sub_resource type="AnimationNodeAnimation" id="15"]
animation = &"idle"
```
- `animation` property uses `&"name"` StringName syntax
- Optional: `play_mode` (0=forward, 1=backward)

### AnimationNodeStateMachineTransition sub_resource

```
[sub_resource type="AnimationNodeStateMachineTransition" id="23"]
switch_mode = 1        # 0=Immediate, 1=Sync, 2=AtEnd
advance_mode = 2       # 0=Disabled, 1=Enabled, 2=Auto
xfade_time = 0.1
reset = true
```

### AnimationNodeBlendTree Serialization

Nodes: `nodes/NodeName/node = SubResource("id")`, `nodes/NodeName/position = Vector2(x, y)`
Connections: flat array `node_connections = ["Blend2", 0, "idle", ...]` (every 3 = input_node, input_index, output_node)

### GDScript Animation Patterns

**AnimationPlayer patterns:**
```gdscript
anim_player.play("idle")
anim_player.play("walk", 0.2, 1.5)  # blend_time, speed
anim_player.queue("idle")
await anim_player.animation_finished
```

**AnimationTree state machine control:**
```gdscript
@onready var state_machine = anim_tree["parameters/playback"]
state_machine.travel("walk")
state_machine.start("idle", true)
var current = state_machine.get_current_node()
```

**Enum-based animation organization:**
```gdscript
const ANIM_NAMES := {
    AnimState.IDLE: "idle",
    AnimState.WALK: "walk",
}
```

### Testing Implications

For animation fixture files:
1. `AnimationNodeStateMachine` sub_resource must use `states/NAME/node` format (not `transitions/N/from`)
2. `transitions` is a flat array, not indexed properties
3. `AnimationNodeAnimation` uses `animation = &"name"` with StringName syntax
4. AnimationTree node needs `tree_root = SubResource("id")` and `anim_player = NodePath("...")`

### Fixture Format Correction

The spec's `animated_scene_tree.tscn` uses incorrect transition format. The correct format based on engine source:

```
[sub_resource type="AnimationNodeStateMachine" id="StateMachine_main"]
states/idle/node = SubResource("AnimNode_idle")
states/idle/position = Vector2(300, 100)
states/walk/node = SubResource("AnimNode_walk")
states/walk/position = Vector2(500, 100)
transitions = [&"idle", &"walk", SubResource("Trans_idle_walk"), &"walk", &"idle", SubResource("Trans_walk_idle")]
```

Note: State names in the transitions array may use `&"name"` StringName syntax or plain `"name"` strings depending on Godot version.
