anima-core 0.2.1 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (280) hide show
  1. checksums.yaml +4 -4
  2. data/.reek.yml +27 -1
  3. data/CHANGELOG.md +19 -0
  4. data/README.md +213 -43
  5. data/agents/codebase-analyzer.md +88 -0
  6. data/agents/codebase-pattern-finder.md +83 -0
  7. data/agents/documentation-researcher.md +59 -0
  8. data/agents/thoughts-analyzer.md +102 -0
  9. data/agents/web-search-researcher.md +71 -0
  10. data/anima-core.gemspec +3 -0
  11. data/app/channels/session_channel.rb +195 -45
  12. data/app/decorators/user_message_decorator.rb +16 -5
  13. data/app/jobs/agent_request_job.rb +55 -2
  14. data/app/jobs/analytical_brain_job.rb +33 -0
  15. data/app/jobs/count_event_tokens_job.rb +15 -4
  16. data/app/models/concerns/event/broadcasting.rb +81 -0
  17. data/app/models/event.rb +20 -1
  18. data/app/models/goal.rb +91 -0
  19. data/app/models/session.rb +366 -21
  20. data/config/application.rb +2 -0
  21. data/config/initializers/event_subscribers.rb +0 -1
  22. data/config/routes.rb +0 -6
  23. data/db/migrate/20260313010000_add_status_to_events.rb +8 -0
  24. data/db/migrate/20260313020000_add_processing_to_sessions.rb +7 -0
  25. data/db/migrate/20260314075248_add_subagent_support_to_sessions.rb +6 -0
  26. data/db/migrate/20260314112417_add_granted_tools_to_sessions.rb +5 -0
  27. data/db/migrate/20260314140000_add_name_to_sessions.rb +7 -0
  28. data/db/migrate/20260314150000_add_viewport_event_ids_to_sessions.rb +7 -0
  29. data/db/migrate/20260315100000_add_active_skills_to_sessions.rb +7 -0
  30. data/db/migrate/20260315140843_create_goals.rb +16 -0
  31. data/db/migrate/20260315144837_add_completed_at_to_goals.rb +5 -0
  32. data/db/migrate/20260315191105_add_active_workflow_to_sessions.rb +5 -0
  33. data/lib/agent_loop.rb +65 -6
  34. data/lib/agents/definition.rb +116 -0
  35. data/lib/agents/registry.rb +106 -0
  36. data/lib/analytical_brain/runner.rb +276 -0
  37. data/lib/analytical_brain/tools/activate_skill.rb +52 -0
  38. data/lib/analytical_brain/tools/deactivate_skill.rb +43 -0
  39. data/lib/analytical_brain/tools/deactivate_workflow.rb +34 -0
  40. data/lib/analytical_brain/tools/everything_is_ready.rb +28 -0
  41. data/lib/analytical_brain/tools/finish_goal.rb +62 -0
  42. data/lib/analytical_brain/tools/read_workflow.rb +58 -0
  43. data/lib/analytical_brain/tools/rename_session.rb +63 -0
  44. data/lib/analytical_brain/tools/set_goal.rb +60 -0
  45. data/lib/analytical_brain/tools/update_goal.rb +60 -0
  46. data/lib/analytical_brain.rb +23 -0
  47. data/lib/anima/cli/mcp/secrets.rb +76 -0
  48. data/lib/anima/cli/mcp.rb +197 -0
  49. data/lib/anima/cli.rb +5 -40
  50. data/lib/anima/installer.rb +168 -0
  51. data/lib/anima/settings.rb +226 -0
  52. data/lib/anima/version.rb +1 -1
  53. data/lib/anima.rb +9 -0
  54. data/lib/credential_store.rb +103 -0
  55. data/lib/environment_probe.rb +232 -0
  56. data/lib/events/subscribers/persister.rb +1 -0
  57. data/lib/events/user_message.rb +17 -0
  58. data/lib/llm/client.rb +29 -10
  59. data/lib/mcp/client_manager.rb +86 -0
  60. data/lib/mcp/config.rb +213 -0
  61. data/lib/mcp/health_check.rb +77 -0
  62. data/lib/mcp/secrets.rb +73 -0
  63. data/lib/mcp/stdio_transport.rb +206 -0
  64. data/lib/providers/anthropic.rb +11 -20
  65. data/lib/shell_session.rb +11 -10
  66. data/lib/skills/definition.rb +97 -0
  67. data/lib/skills/registry.rb +105 -0
  68. data/lib/tools/edit.rb +226 -0
  69. data/lib/tools/mcp_tool.rb +114 -0
  70. data/lib/tools/read.rb +151 -0
  71. data/lib/tools/registry.rb +14 -12
  72. data/lib/tools/request_feature.rb +121 -0
  73. data/lib/tools/return_result.rb +81 -0
  74. data/lib/tools/spawn_specialist.rb +109 -0
  75. data/lib/tools/spawn_subagent.rb +111 -0
  76. data/lib/tools/subagent_prompts.rb +12 -0
  77. data/lib/tools/web_get.rb +8 -9
  78. data/lib/tools/write.rb +86 -0
  79. data/lib/tui/app.rb +985 -26
  80. data/lib/tui/cable_client.rb +69 -31
  81. data/lib/tui/message_store.rb +103 -8
  82. data/lib/tui/screens/chat.rb +293 -45
  83. data/lib/workflows/definition.rb +97 -0
  84. data/lib/workflows/registry.rb +89 -0
  85. data/skills/activerecord/SKILL.md +255 -0
  86. data/skills/activerecord/examples/associations/association_extensions.rb +298 -0
  87. data/skills/activerecord/examples/associations/basic_associations.rb +118 -0
  88. data/skills/activerecord/examples/associations/counter_caches.rb +215 -0
  89. data/skills/activerecord/examples/associations/polymorphic_associations.rb +217 -0
  90. data/skills/activerecord/examples/associations/self_referential.rb +302 -0
  91. data/skills/activerecord/examples/associations/through_associations.rb +203 -0
  92. data/skills/activerecord/examples/basics/crud_operations.rb +209 -0
  93. data/skills/activerecord/examples/basics/dirty_tracking.rb +218 -0
  94. data/skills/activerecord/examples/basics/inheritance.rb +377 -0
  95. data/skills/activerecord/examples/basics/type_casting.rb +317 -0
  96. data/skills/activerecord/examples/callbacks/alternatives_to_callbacks.rb +447 -0
  97. data/skills/activerecord/examples/callbacks/conditional_callbacks.rb +353 -0
  98. data/skills/activerecord/examples/callbacks/lifecycle_callbacks.rb +280 -0
  99. data/skills/activerecord/examples/callbacks/transaction_callbacks.rb +340 -0
  100. data/skills/activerecord/examples/migrations/indexes_and_constraints.rb +337 -0
  101. data/skills/activerecord/examples/migrations/reversible_patterns.rb +403 -0
  102. data/skills/activerecord/examples/migrations/safe_patterns.rb +420 -0
  103. data/skills/activerecord/examples/migrations/schema_changes.rb +277 -0
  104. data/skills/activerecord/examples/querying/batch_processing.rb +226 -0
  105. data/skills/activerecord/examples/querying/eager_loading.rb +259 -0
  106. data/skills/activerecord/examples/querying/finder_methods.rb +170 -0
  107. data/skills/activerecord/examples/querying/optimization.rb +275 -0
  108. data/skills/activerecord/examples/querying/scopes.rb +260 -0
  109. data/skills/activerecord/examples/validations/built_in_validators.rb +277 -0
  110. data/skills/activerecord/examples/validations/conditional_validations.rb +288 -0
  111. data/skills/activerecord/examples/validations/custom_validators.rb +381 -0
  112. data/skills/activerecord/examples/validations/database_constraints.rb +432 -0
  113. data/skills/activerecord/examples/validations/validation_contexts.rb +367 -0
  114. data/skills/activerecord/references/associations.md +709 -0
  115. data/skills/activerecord/references/basics.md +622 -0
  116. data/skills/activerecord/references/callbacks.md +738 -0
  117. data/skills/activerecord/references/migrations.md +657 -0
  118. data/skills/activerecord/references/querying.md +655 -0
  119. data/skills/activerecord/references/validations.md +596 -0
  120. data/skills/dragonruby/SKILL.md +250 -0
  121. data/skills/dragonruby/examples/audio/audio_events.rb +55 -0
  122. data/skills/dragonruby/examples/audio/background_music.rb +29 -0
  123. data/skills/dragonruby/examples/audio/crossfade.rb +51 -0
  124. data/skills/dragonruby/examples/audio/music_controls.rb +51 -0
  125. data/skills/dragonruby/examples/audio/sound_effects.rb +30 -0
  126. data/skills/dragonruby/examples/core/coordinate_system.rb +27 -0
  127. data/skills/dragonruby/examples/core/hello_world.rb +24 -0
  128. data/skills/dragonruby/examples/core/labels.rb +22 -0
  129. data/skills/dragonruby/examples/core/sprites.rb +35 -0
  130. data/skills/dragonruby/examples/core/state_management.rb +29 -0
  131. data/skills/dragonruby/examples/distribution/background_pause.rb +42 -0
  132. data/skills/dragonruby/examples/distribution/build_workflow.sh +26 -0
  133. data/skills/dragonruby/examples/distribution/cvars_production.txt +16 -0
  134. data/skills/dragonruby/examples/distribution/game_metadata_hd.txt +23 -0
  135. data/skills/dragonruby/examples/distribution/game_metadata_minimal.txt +9 -0
  136. data/skills/dragonruby/examples/distribution/game_metadata_mobile.txt +31 -0
  137. data/skills/dragonruby/examples/distribution/platform_detection.rb +36 -0
  138. data/skills/dragonruby/examples/distribution/steam_metadata.txt +19 -0
  139. data/skills/dragonruby/examples/entities/collision_detection.rb +43 -0
  140. data/skills/dragonruby/examples/entities/entity_lifecycle.rb +68 -0
  141. data/skills/dragonruby/examples/entities/entity_storage.rb +38 -0
  142. data/skills/dragonruby/examples/entities/factory_methods.rb +45 -0
  143. data/skills/dragonruby/examples/entities/random_spawning.rb +50 -0
  144. data/skills/dragonruby/examples/game-logic/reset_patterns.rb +98 -0
  145. data/skills/dragonruby/examples/game-logic/save_load.rb +101 -0
  146. data/skills/dragonruby/examples/game-logic/scoring.rb +104 -0
  147. data/skills/dragonruby/examples/game-logic/state_transitions.rb +103 -0
  148. data/skills/dragonruby/examples/game-logic/timers.rb +87 -0
  149. data/skills/dragonruby/examples/input/action_triggers.rb +36 -0
  150. data/skills/dragonruby/examples/input/analog_movement.rb +28 -0
  151. data/skills/dragonruby/examples/input/controller_input.rb +28 -0
  152. data/skills/dragonruby/examples/input/directional_input.rb +24 -0
  153. data/skills/dragonruby/examples/input/keyboard_input.rb +28 -0
  154. data/skills/dragonruby/examples/input/mouse_click.rb +26 -0
  155. data/skills/dragonruby/examples/input/movement_with_bounds.rb +22 -0
  156. data/skills/dragonruby/examples/input/normalized_movement.rb +32 -0
  157. data/skills/dragonruby/examples/rendering/frame_animation.rb +32 -0
  158. data/skills/dragonruby/examples/rendering/labels.rb +32 -0
  159. data/skills/dragonruby/examples/rendering/layering.rb +51 -0
  160. data/skills/dragonruby/examples/rendering/solids.rb +61 -0
  161. data/skills/dragonruby/examples/rendering/sprites.rb +33 -0
  162. data/skills/dragonruby/examples/rendering/spritesheet_animation.rb +39 -0
  163. data/skills/dragonruby/examples/scenes/case_dispatch.rb +60 -0
  164. data/skills/dragonruby/examples/scenes/class_based.rb +150 -0
  165. data/skills/dragonruby/examples/scenes/pause_overlay.rb +100 -0
  166. data/skills/dragonruby/examples/scenes/safe_transitions.rb +68 -0
  167. data/skills/dragonruby/examples/scenes/scene_transitions.rb +98 -0
  168. data/skills/dragonruby/examples/scenes/send_dispatch.rb +88 -0
  169. data/skills/dragonruby/references/audio.md +396 -0
  170. data/skills/dragonruby/references/core.md +385 -0
  171. data/skills/dragonruby/references/distribution.md +434 -0
  172. data/skills/dragonruby/references/entities.md +516 -0
  173. data/skills/dragonruby/references/game-logic/persistence.md +386 -0
  174. data/skills/dragonruby/references/game-logic/state.md +389 -0
  175. data/skills/dragonruby/references/input.md +414 -0
  176. data/skills/dragonruby/references/rendering/animation.md +467 -0
  177. data/skills/dragonruby/references/rendering/primitives.md +403 -0
  178. data/skills/dragonruby/references/scenes.md +443 -0
  179. data/skills/draper-decorators/SKILL.md +344 -0
  180. data/skills/draper-decorators/examples/application_decorator.rb +61 -0
  181. data/skills/draper-decorators/examples/decorator_spec.rb +253 -0
  182. data/skills/draper-decorators/examples/model_decorator.rb +152 -0
  183. data/skills/draper-decorators/references/anti-patterns.md +640 -0
  184. data/skills/draper-decorators/references/patterns.md +507 -0
  185. data/skills/draper-decorators/references/testing.md +559 -0
  186. data/skills/gh-issue.md +182 -0
  187. data/skills/mcp-server/SKILL.md +177 -0
  188. data/skills/mcp-server/examples/dynamic_tools.rb +36 -0
  189. data/skills/mcp-server/examples/file_manager_tool.rb +85 -0
  190. data/skills/mcp-server/examples/http_client.rb +48 -0
  191. data/skills/mcp-server/examples/http_server.rb +97 -0
  192. data/skills/mcp-server/examples/rails_integration.rb +88 -0
  193. data/skills/mcp-server/examples/stdio_server.rb +108 -0
  194. data/skills/mcp-server/examples/streaming_client.rb +95 -0
  195. data/skills/mcp-server/references/gotchas.md +183 -0
  196. data/skills/mcp-server/references/prompts.md +98 -0
  197. data/skills/mcp-server/references/resources.md +53 -0
  198. data/skills/mcp-server/references/server.md +140 -0
  199. data/skills/mcp-server/references/tools.md +146 -0
  200. data/skills/mcp-server/references/transport.md +104 -0
  201. data/skills/ratatui-ruby/SKILL.md +315 -0
  202. data/skills/ratatui-ruby/references/core-concepts.md +340 -0
  203. data/skills/ratatui-ruby/references/events.md +387 -0
  204. data/skills/ratatui-ruby/references/frameworks.md +522 -0
  205. data/skills/ratatui-ruby/references/layout.md +423 -0
  206. data/skills/ratatui-ruby/references/styling.md +268 -0
  207. data/skills/ratatui-ruby/references/testing.md +433 -0
  208. data/skills/ratatui-ruby/references/widgets.md +532 -0
  209. data/skills/rspec/SKILL.md +340 -0
  210. data/skills/rspec/examples/core/basic_structure.rb +69 -0
  211. data/skills/rspec/examples/core/configuration.rb +126 -0
  212. data/skills/rspec/examples/core/hooks.rb +126 -0
  213. data/skills/rspec/examples/core/memoized_helpers.rb +139 -0
  214. data/skills/rspec/examples/core/metadata_filtering.rb +144 -0
  215. data/skills/rspec/examples/core/shared_examples.rb +145 -0
  216. data/skills/rspec/examples/factory_bot/associations.rb +314 -0
  217. data/skills/rspec/examples/factory_bot/build_strategies.rb +272 -0
  218. data/skills/rspec/examples/factory_bot/callbacks.rb +320 -0
  219. data/skills/rspec/examples/factory_bot/custom_construction.rb +328 -0
  220. data/skills/rspec/examples/factory_bot/factory_definition.rb +191 -0
  221. data/skills/rspec/examples/factory_bot/inheritance.rb +314 -0
  222. data/skills/rspec/examples/factory_bot/traits.rb +293 -0
  223. data/skills/rspec/examples/factory_bot/transients.rb +229 -0
  224. data/skills/rspec/examples/matchers/change.rb +115 -0
  225. data/skills/rspec/examples/matchers/collections.rb +154 -0
  226. data/skills/rspec/examples/matchers/comparisons.rb +79 -0
  227. data/skills/rspec/examples/matchers/composing.rb +155 -0
  228. data/skills/rspec/examples/matchers/custom_matchers.rb +197 -0
  229. data/skills/rspec/examples/matchers/equality.rb +58 -0
  230. data/skills/rspec/examples/matchers/errors.rb +136 -0
  231. data/skills/rspec/examples/matchers/output.rb +103 -0
  232. data/skills/rspec/examples/matchers/predicates.rb +87 -0
  233. data/skills/rspec/examples/matchers/truthiness.rb +101 -0
  234. data/skills/rspec/examples/matchers/types.rb +82 -0
  235. data/skills/rspec/examples/matchers/yield.rb +147 -0
  236. data/skills/rspec/examples/mocks/any_instance.rb +172 -0
  237. data/skills/rspec/examples/mocks/argument_matchers.rb +206 -0
  238. data/skills/rspec/examples/mocks/constants.rb +177 -0
  239. data/skills/rspec/examples/mocks/doubles.rb +139 -0
  240. data/skills/rspec/examples/mocks/expectations.rb +137 -0
  241. data/skills/rspec/examples/mocks/message_chains.rb +173 -0
  242. data/skills/rspec/examples/mocks/ordering.rb +144 -0
  243. data/skills/rspec/examples/mocks/receive_counts.rb +181 -0
  244. data/skills/rspec/examples/mocks/responses.rb +223 -0
  245. data/skills/rspec/examples/mocks/spies.rb +149 -0
  246. data/skills/rspec/examples/mocks/stubbing.rb +133 -0
  247. data/skills/rspec/examples/rails/channels.rb +250 -0
  248. data/skills/rspec/examples/rails/controller_specs.rb +302 -0
  249. data/skills/rspec/examples/rails/helper_specs.rb +245 -0
  250. data/skills/rspec/examples/rails/job_specs.rb +256 -0
  251. data/skills/rspec/examples/rails/mailer_specs.rb +228 -0
  252. data/skills/rspec/examples/rails/matchers.rb +374 -0
  253. data/skills/rspec/examples/rails/model_specs.rb +193 -0
  254. data/skills/rspec/examples/rails/request_specs.rb +275 -0
  255. data/skills/rspec/examples/rails/routing_specs.rb +276 -0
  256. data/skills/rspec/examples/rails/system_specs.rb +294 -0
  257. data/skills/rspec/examples/rails/transactions.rb +254 -0
  258. data/skills/rspec/examples/rails/view_specs.rb +252 -0
  259. data/skills/rspec/references/core.md +816 -0
  260. data/skills/rspec/references/factory_bot.md +641 -0
  261. data/skills/rspec/references/matchers.md +516 -0
  262. data/skills/rspec/references/mocks.md +381 -0
  263. data/skills/rspec/references/rails.md +528 -0
  264. data/templates/soul.md +40 -0
  265. data/workflows/commit.md +45 -0
  266. data/workflows/create_handoff.md +98 -0
  267. data/workflows/create_note.md +82 -0
  268. data/workflows/create_plan.md +457 -0
  269. data/workflows/decompose_ticket.md +109 -0
  270. data/workflows/feature.md +91 -0
  271. data/workflows/implement_plan.md +87 -0
  272. data/workflows/iterate_plan.md +247 -0
  273. data/workflows/research_codebase.md +210 -0
  274. data/workflows/resume_handoff.md +217 -0
  275. data/workflows/review_pr.md +320 -0
  276. data/workflows/thoughts_init.md +71 -0
  277. data/workflows/validate_plan.md +166 -0
  278. metadata +290 -3
  279. data/app/controllers/api/sessions_controller.rb +0 -25
  280. data/lib/events/subscribers/action_cable_bridge.rb +0 -59
@@ -0,0 +1,467 @@
1
+ # DragonRuby Animation
2
+
3
+ Animation timing, sprite sequences, and easing. DragonRuby runs at 60 FPS (60 ticks/second).
4
+
5
+ ## frame_index Method
6
+
7
+ Core animation API. Called on a timestamp, returns current frame index.
8
+
9
+ ```ruby
10
+ # Basic usage - looping animation
11
+ frame = 0.frame_index(count: 6, hold_for: 8, repeat: true)
12
+ args.state.player.path = "sprites/dragon-#{frame}.png"
13
+ ```
14
+
15
+ ### Parameters
16
+
17
+ ```ruby
18
+ timestamp.frame_index(
19
+ count: 6, # number of frames
20
+ hold_for: 4, # ticks per frame (4 ticks = ~67ms)
21
+ repeat: true, # loop forever or play once
22
+ repeat_index: 0, # frame to loop back to (skip intro frames)
23
+ tick_count_override: Kernel.tick_count
24
+ )
25
+ ```
26
+
27
+ **Returns**: Integer (0 to count-1) or `nil` if animation completed and `repeat: false`
28
+
29
+ ### Timing Math
30
+
31
+ ```
32
+ Animation duration = count × hold_for ticks
33
+ Duration in seconds = (count × hold_for) / 60
34
+
35
+ Example: 6 frames, hold_for: 8
36
+ Duration = 6 × 8 = 48 ticks = 0.8 seconds
37
+ Cycles per second = 60 / 48 = 1.25
38
+ ```
39
+
40
+ ### Animation from Event
41
+
42
+ ```ruby
43
+ # Trigger animation on keypress
44
+ if args.inputs.keyboard.key_down.space
45
+ args.state.attack_at = Kernel.tick_count
46
+ end
47
+
48
+ # Play attack animation once
49
+ if args.state.attack_at
50
+ frame = args.state.attack_at.frame_index(count: 4, hold_for: 6, repeat: false)
51
+ frame ||= 3 # stay on last frame when done
52
+ end
53
+ ```
54
+
55
+ ### Skip Intro Frames with repeat_index
56
+
57
+ ```ruby
58
+ # Frames 0-2 are "launch", frames 3-8 are "loop"
59
+ frame = args.state.action_at.frame_index(
60
+ count: 9,
61
+ hold_for: 8,
62
+ repeat: true,
63
+ repeat_index: 3 # after first playthrough, loop starts at frame 3
64
+ )
65
+ ```
66
+
67
+ ## Spritesheet Animation
68
+
69
+ Use `tile_*` or `source_*` to crop frames from a single image.
70
+
71
+ ### Horizontal Strip
72
+
73
+ ```ruby
74
+ frame = args.state.started_at.frame_index(count: 6, hold_for: 4, repeat: true)
75
+ frame ||= 0
76
+
77
+ args.outputs.sprites << {
78
+ x: 100, y: 100, w: 64, h: 64,
79
+ path: 'sprites/player-run.png',
80
+ tile_x: frame * 64, # offset by frame width
81
+ tile_y: 0,
82
+ tile_w: 64,
83
+ tile_h: 64
84
+ }
85
+ ```
86
+
87
+ ### Grid Spritesheet
88
+
89
+ ```ruby
90
+ def frame_from_grid(index, cols:, tile_size:)
91
+ row = index.idiv(cols)
92
+ col = index % cols
93
+
94
+ {
95
+ tile_x: col * tile_size,
96
+ tile_y: row * tile_size,
97
+ tile_w: tile_size,
98
+ tile_h: tile_size
99
+ }
100
+ end
101
+
102
+ frame = args.state.started_at.frame_index(count: 12, hold_for: 4, repeat: true)
103
+ sprite = frame_from_grid(frame, cols: 4, tile_size: 32)
104
+ sprite.merge!(x: 100, y: 100, w: 64, h: 64, path: 'sprites/sheet.png')
105
+ args.outputs.sprites << sprite
106
+ ```
107
+
108
+ ### tile_* vs source_*
109
+
110
+ ```ruby
111
+ # tile_* - origin at TOP-LEFT of image (common for texture atlases)
112
+ { tile_x: 0, tile_y: 0, tile_w: 64, tile_h: 64 }
113
+
114
+ # source_* - origin at BOTTOM-LEFT of image
115
+ { source_x: 0, source_y: 0, source_w: 64, source_h: 64 }
116
+ ```
117
+
118
+ ## Direction with Flipping
119
+
120
+ Mirror sprites instead of separate images per direction:
121
+
122
+ ```ruby
123
+ args.state.player.direction ||= 1 # 1=right, -1=left
124
+
125
+ if args.inputs.left
126
+ args.state.player.direction = -1
127
+ elsif args.inputs.right
128
+ args.state.player.direction = 1
129
+ end
130
+
131
+ args.outputs.sprites << {
132
+ x: args.state.player.x, y: args.state.player.y,
133
+ w: 64, h: 64,
134
+ path: 'sprites/player-walk.png',
135
+ flip_horizontally: args.state.player.direction < 0
136
+ }
137
+ ```
138
+
139
+ ## Rotation Animation
140
+
141
+ ```ruby
142
+ # Continuous rotation
143
+ args.state.angle ||= 0
144
+ args.state.angle += 2 # 2 degrees per frame
145
+
146
+ args.outputs.sprites << {
147
+ x: 640, y: 360, w: 100, h: 100,
148
+ path: 'sprites/wheel.png',
149
+ angle: args.state.angle,
150
+ angle_anchor_x: 0.5, # rotate around center
151
+ angle_anchor_y: 0.5
152
+ }
153
+ ```
154
+
155
+ ### Movement in Direction of Angle
156
+
157
+ ```ruby
158
+ # Move in direction sprite is facing
159
+ args.state.player.x += args.state.player.angle.vector_x * speed
160
+ args.state.player.y += args.state.player.angle.vector_y * speed
161
+ ```
162
+
163
+ ## State-Based Animation
164
+
165
+ ```ruby
166
+ def tick args
167
+ args.state.player.action ||= :idle
168
+ args.state.player.action_at ||= 0
169
+
170
+ # Change state
171
+ if args.inputs.keyboard.key_down.space
172
+ args.state.player.action = :attack
173
+ args.state.player.action_at = Kernel.tick_count
174
+ end
175
+
176
+ # Get frame based on state
177
+ frame = case args.state.player.action
178
+ when :idle
179
+ args.state.player.action_at.frame_index(count: 4, hold_for: 12, repeat: true)
180
+ when :attack
181
+ idx = args.state.player.action_at.frame_index(count: 6, hold_for: 4, repeat: false)
182
+ if idx.nil?
183
+ args.state.player.action = :idle
184
+ args.state.player.action_at = Kernel.tick_count
185
+ 0
186
+ else
187
+ idx
188
+ end
189
+ else
190
+ 0
191
+ end
192
+
193
+ args.state.player.path = "sprites/player-#{args.state.player.action}-#{frame}.png"
194
+ end
195
+ ```
196
+
197
+ ## Timing Helpers
198
+
199
+ ### elapsed_time - Ticks Since Event
200
+
201
+ ```ruby
202
+ args.state.last_hit_at ||= 0
203
+ time_since_hit = args.state.last_hit_at.elapsed_time
204
+
205
+ if time_since_hit < 30 # flash for 0.5 seconds
206
+ args.state.player.a = (Kernel.tick_count % 10 < 5) ? 255 : 128
207
+ end
208
+ ```
209
+
210
+ ### elapsed? - Check If Duration Passed
211
+
212
+ ```ruby
213
+ # Check if 2 seconds have passed
214
+ if args.state.spawn_at.elapsed?(120)
215
+ spawn_enemy
216
+ args.state.spawn_at = Kernel.tick_count
217
+ end
218
+ ```
219
+
220
+ ### Numeric.frame - Extended Info
221
+
222
+ ```ruby
223
+ info = Numeric.frame(start_at: 0, count: 6, hold_for: 4, repeat: true)
224
+ # Returns:
225
+ # {
226
+ # frame_index: 2,
227
+ # frame_count: 6,
228
+ # frames_left: 16,
229
+ # started: true,
230
+ # completed: false,
231
+ # elapsed_time: 8,
232
+ # duration: 24
233
+ # }
234
+ ```
235
+
236
+ ## Easing Functions
237
+
238
+ Smooth animation curves instead of linear movement.
239
+
240
+ ### smooth_start - Accelerating
241
+
242
+ ```ruby
243
+ progress = Easing.smooth_start(
244
+ start_at: args.state.tween_start,
245
+ end_at: args.state.tween_start + 60,
246
+ tick_count: Kernel.tick_count,
247
+ power: 2 # 2=quadratic, 3=cubic
248
+ )
249
+
250
+ args.state.player.x = 100 + (500 * progress) # starts slow, ends fast
251
+ ```
252
+
253
+ ### smooth_stop - Decelerating
254
+
255
+ ```ruby
256
+ progress = Easing.smooth_stop(start_at: 0, end_at: 60, tick_count: t, power: 2)
257
+ # starts fast, ends slow
258
+ ```
259
+
260
+ ### smooth_step - Ease In/Out
261
+
262
+ ```ruby
263
+ progress = Easing.smooth_step(start_at: 0, end_at: 60, tick_count: t, power: 2)
264
+ # slow start, fast middle, slow end
265
+ ```
266
+
267
+ ### lerp - Linear Interpolation
268
+
269
+ ```ruby
270
+ # Smoothly approach target
271
+ args.state.camera_x = args.state.camera_x.lerp(target_x, 0.1) # 10% per frame
272
+ ```
273
+
274
+ ### remap - Range Mapping
275
+
276
+ ```ruby
277
+ # Convert 0-1 to screen coordinates
278
+ x = progress.remap(0, 1, 100, 1180)
279
+ ```
280
+
281
+ ## Performance Tips
282
+
283
+ ### Cache Calculations
284
+
285
+ ```ruby
286
+ # Precompute tile positions
287
+ args.state.tile_positions ||= 6.map { |i| i * 64 }
288
+ sprite.tile_x = args.state.tile_positions[frame]
289
+ ```
290
+
291
+ ### Skip When Not Animating
292
+
293
+ ```ruby
294
+ if args.state.player.moving
295
+ frame = args.state.player.move_at.frame_index(count: 6, hold_for: 4, repeat: true)
296
+ else
297
+ frame = 0 # idle frame, no calculation
298
+ end
299
+ ```
300
+
301
+ ### Use Classes for Many Sprites
302
+
303
+ ```ruby
304
+ class AnimatedSprite
305
+ attr_sprite
306
+
307
+ def initialize(x, y, path_prefix, frame_count)
308
+ @x, @y, @w, @h = x, y, 64, 64
309
+ @path_prefix = path_prefix
310
+ @frame_count = frame_count
311
+ @started_at = Kernel.tick_count
312
+ end
313
+
314
+ def update
315
+ frame = @started_at.frame_index(count: @frame_count, hold_for: 4, repeat: true)
316
+ @path = "#{@path_prefix}#{frame}.png"
317
+ end
318
+ end
319
+ ```
320
+
321
+ ## Best Practices
322
+
323
+ 1. **Store animation start time** - use `Kernel.tick_count` when action begins
324
+ 2. **Handle nil from frame_index** - returned when one-shot animation ends
325
+ 3. **Use flip_horizontally** - instead of separate left/right sprites
326
+ 4. **Use spritesheets** - fewer files, better performance
327
+ 5. **Cache tile positions** - avoid multiplication every frame
328
+ 6. **Reset action_at on state change** - ensures animation restarts
329
+
330
+ ## Common Antipatterns
331
+
332
+ ### Recalculating From Zero
333
+
334
+ ```ruby
335
+ # WRONG - animation never progresses for one-shots
336
+ def tick(args)
337
+ frame = 0.frame_index(count: 6, hold_for: 4, repeat: false)
338
+ # frame is always 0 because we start from 0 every tick
339
+ end
340
+
341
+ # CORRECT - store animation start time
342
+ def tick(args)
343
+ args.state.attack_at ||= Kernel.tick_count
344
+ frame = args.state.attack_at.frame_index(count: 6, hold_for: 4, repeat: false)
345
+ end
346
+ ```
347
+
348
+ **Why:** `frame_index` calculates based on elapsed time since the timestamp; using 0 resets every frame.
349
+
350
+ ### Not Handling Nil
351
+
352
+ ```ruby
353
+ # WRONG - crashes when animation completes
354
+ frame = args.state.attack_at.frame_index(count: 6, hold_for: 4, repeat: false)
355
+ path = "sprite-#{frame}.png" # NoMethodError when frame is nil!
356
+
357
+ # CORRECT - handle nil return
358
+ frame = args.state.attack_at.frame_index(count: 6, hold_for: 4, repeat: false)
359
+ frame ||= 5 # stay on last frame, or switch state
360
+ path = "sprite-#{frame}.png"
361
+ ```
362
+
363
+ **Why:** `frame_index` returns nil when `repeat: false` animation completes.
364
+
365
+ ### Separate Direction Images
366
+
367
+ ```ruby
368
+ # WRONG - requires two sets of sprite assets
369
+ path = player.facing_left ? 'player-left.png' : 'player-right.png'
370
+
371
+ # CORRECT - use flip_horizontally
372
+ args.outputs.sprites << {
373
+ path: 'player.png',
374
+ flip_horizontally: player.direction < 0
375
+ }
376
+ ```
377
+
378
+ **Why:** Flipping reduces asset count and ensures consistency.
379
+
380
+ ### Hardcoded Frame Timing
381
+
382
+ ```ruby
383
+ # WRONG - magic numbers
384
+ args.state.walk_at.frame_index(count: 6, hold_for: 8, repeat: true)
385
+ args.state.attack_at.frame_index(count: 4, hold_for: 4, repeat: false)
386
+
387
+ # CORRECT - named constants
388
+ WALK_FRAME_SPEED = 8
389
+ ATTACK_FRAME_SPEED = 4
390
+
391
+ args.state.walk_at.frame_index(count: 6, hold_for: WALK_FRAME_SPEED, repeat: true)
392
+ args.state.attack_at.frame_index(count: 4, hold_for: ATTACK_FRAME_SPEED, repeat: false)
393
+ ```
394
+
395
+ **Why:** Constants make animation timing easier to tune and understand.
396
+
397
+ ## Quick Reference
398
+
399
+ | Need | Solution |
400
+ |------|----------|
401
+ | Loop animation forever | `repeat: true` |
402
+ | Play animation once | `repeat: false`, handle `nil` return |
403
+ | Skip intro frames on loop | `repeat_index: N` |
404
+ | Flip sprite direction | `flip_horizontally: true` |
405
+ | Rotate sprite | `angle: degrees`, `angle_anchor_x/y: 0.5` |
406
+ | Smooth movement | `value.lerp(target, 0.1)` |
407
+ | Accelerate in | `Easing.smooth_start` |
408
+ | Decelerate out | `Easing.smooth_stop` |
409
+ | Ease both | `Easing.smooth_step` |
410
+ | Time since event | `timestamp.elapsed_time` |
411
+ | Check if time passed | `timestamp.elapsed?(duration)` |
412
+
413
+ ## Decision Tree
414
+
415
+ ```
416
+ How are your animation frames stored?
417
+
418
+ ├─ Separate PNG files (dragon-0.png, dragon-1.png...)?
419
+ │ └─ Use frame_index with string interpolation → examples/rendering/frame_animation.rb
420
+
421
+ └─ Single spritesheet image?
422
+ └─ Use frame_index + tile_x/tile_y → examples/rendering/spritesheet_animation.rb
423
+
424
+ Animation behavior?
425
+
426
+ ├─ Loop forever?
427
+ │ └─ repeat: true
428
+
429
+ ├─ Play once and stop?
430
+ │ └─ repeat: false, handle nil return (stay on last frame or switch state)
431
+
432
+ ├─ Loop but skip intro frames?
433
+ │ └─ Use repeat_index: N
434
+
435
+ └─ Different speeds per action?
436
+ └─ Different hold_for values per state
437
+
438
+ Character direction?
439
+
440
+ ├─ Left/right facing?
441
+ │ └─ flip_horizontally: direction < 0
442
+
443
+ └─ Angle-based movement?
444
+ └─ Use angle + vector_x/vector_y
445
+
446
+ Smooth movement needed?
447
+
448
+ ├─ Gradually approach target?
449
+ │ └─ value.lerp(target, 0.1)
450
+
451
+ ├─ Accelerate from stop?
452
+ │ └─ Easing.smooth_start
453
+
454
+ ├─ Decelerate to stop?
455
+ │ └─ Easing.smooth_stop
456
+
457
+ └─ Smooth in and out?
458
+ └─ Easing.smooth_step
459
+ ```
460
+
461
+ ## Examples Index
462
+
463
+ | Example | Purpose |
464
+ |---------|---------|
465
+ | `examples/rendering/frame_animation.rb` | Separate files, looping + one-shot |
466
+ | `examples/rendering/spritesheet_animation.rb` | Single sheet with tile_x/tile_y |
467
+ | `examples/rendering/sprites.rb` | Rotation, flipping, tinting |