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.
- checksums.yaml +4 -4
- data/.reek.yml +27 -1
- data/CHANGELOG.md +19 -0
- data/README.md +213 -43
- data/agents/codebase-analyzer.md +88 -0
- data/agents/codebase-pattern-finder.md +83 -0
- data/agents/documentation-researcher.md +59 -0
- data/agents/thoughts-analyzer.md +102 -0
- data/agents/web-search-researcher.md +71 -0
- data/anima-core.gemspec +3 -0
- data/app/channels/session_channel.rb +195 -45
- data/app/decorators/user_message_decorator.rb +16 -5
- data/app/jobs/agent_request_job.rb +55 -2
- data/app/jobs/analytical_brain_job.rb +33 -0
- data/app/jobs/count_event_tokens_job.rb +15 -4
- data/app/models/concerns/event/broadcasting.rb +81 -0
- data/app/models/event.rb +20 -1
- data/app/models/goal.rb +91 -0
- data/app/models/session.rb +366 -21
- data/config/application.rb +2 -0
- data/config/initializers/event_subscribers.rb +0 -1
- data/config/routes.rb +0 -6
- data/db/migrate/20260313010000_add_status_to_events.rb +8 -0
- data/db/migrate/20260313020000_add_processing_to_sessions.rb +7 -0
- data/db/migrate/20260314075248_add_subagent_support_to_sessions.rb +6 -0
- data/db/migrate/20260314112417_add_granted_tools_to_sessions.rb +5 -0
- data/db/migrate/20260314140000_add_name_to_sessions.rb +7 -0
- data/db/migrate/20260314150000_add_viewport_event_ids_to_sessions.rb +7 -0
- data/db/migrate/20260315100000_add_active_skills_to_sessions.rb +7 -0
- data/db/migrate/20260315140843_create_goals.rb +16 -0
- data/db/migrate/20260315144837_add_completed_at_to_goals.rb +5 -0
- data/db/migrate/20260315191105_add_active_workflow_to_sessions.rb +5 -0
- data/lib/agent_loop.rb +65 -6
- data/lib/agents/definition.rb +116 -0
- data/lib/agents/registry.rb +106 -0
- data/lib/analytical_brain/runner.rb +276 -0
- data/lib/analytical_brain/tools/activate_skill.rb +52 -0
- data/lib/analytical_brain/tools/deactivate_skill.rb +43 -0
- data/lib/analytical_brain/tools/deactivate_workflow.rb +34 -0
- data/lib/analytical_brain/tools/everything_is_ready.rb +28 -0
- data/lib/analytical_brain/tools/finish_goal.rb +62 -0
- data/lib/analytical_brain/tools/read_workflow.rb +58 -0
- data/lib/analytical_brain/tools/rename_session.rb +63 -0
- data/lib/analytical_brain/tools/set_goal.rb +60 -0
- data/lib/analytical_brain/tools/update_goal.rb +60 -0
- data/lib/analytical_brain.rb +23 -0
- data/lib/anima/cli/mcp/secrets.rb +76 -0
- data/lib/anima/cli/mcp.rb +197 -0
- data/lib/anima/cli.rb +5 -40
- data/lib/anima/installer.rb +168 -0
- data/lib/anima/settings.rb +226 -0
- data/lib/anima/version.rb +1 -1
- data/lib/anima.rb +9 -0
- data/lib/credential_store.rb +103 -0
- data/lib/environment_probe.rb +232 -0
- data/lib/events/subscribers/persister.rb +1 -0
- data/lib/events/user_message.rb +17 -0
- data/lib/llm/client.rb +29 -10
- data/lib/mcp/client_manager.rb +86 -0
- data/lib/mcp/config.rb +213 -0
- data/lib/mcp/health_check.rb +77 -0
- data/lib/mcp/secrets.rb +73 -0
- data/lib/mcp/stdio_transport.rb +206 -0
- data/lib/providers/anthropic.rb +11 -20
- data/lib/shell_session.rb +11 -10
- data/lib/skills/definition.rb +97 -0
- data/lib/skills/registry.rb +105 -0
- data/lib/tools/edit.rb +226 -0
- data/lib/tools/mcp_tool.rb +114 -0
- data/lib/tools/read.rb +151 -0
- data/lib/tools/registry.rb +14 -12
- data/lib/tools/request_feature.rb +121 -0
- data/lib/tools/return_result.rb +81 -0
- data/lib/tools/spawn_specialist.rb +109 -0
- data/lib/tools/spawn_subagent.rb +111 -0
- data/lib/tools/subagent_prompts.rb +12 -0
- data/lib/tools/web_get.rb +8 -9
- data/lib/tools/write.rb +86 -0
- data/lib/tui/app.rb +985 -26
- data/lib/tui/cable_client.rb +69 -31
- data/lib/tui/message_store.rb +103 -8
- data/lib/tui/screens/chat.rb +293 -45
- data/lib/workflows/definition.rb +97 -0
- data/lib/workflows/registry.rb +89 -0
- data/skills/activerecord/SKILL.md +255 -0
- data/skills/activerecord/examples/associations/association_extensions.rb +298 -0
- data/skills/activerecord/examples/associations/basic_associations.rb +118 -0
- data/skills/activerecord/examples/associations/counter_caches.rb +215 -0
- data/skills/activerecord/examples/associations/polymorphic_associations.rb +217 -0
- data/skills/activerecord/examples/associations/self_referential.rb +302 -0
- data/skills/activerecord/examples/associations/through_associations.rb +203 -0
- data/skills/activerecord/examples/basics/crud_operations.rb +209 -0
- data/skills/activerecord/examples/basics/dirty_tracking.rb +218 -0
- data/skills/activerecord/examples/basics/inheritance.rb +377 -0
- data/skills/activerecord/examples/basics/type_casting.rb +317 -0
- data/skills/activerecord/examples/callbacks/alternatives_to_callbacks.rb +447 -0
- data/skills/activerecord/examples/callbacks/conditional_callbacks.rb +353 -0
- data/skills/activerecord/examples/callbacks/lifecycle_callbacks.rb +280 -0
- data/skills/activerecord/examples/callbacks/transaction_callbacks.rb +340 -0
- data/skills/activerecord/examples/migrations/indexes_and_constraints.rb +337 -0
- data/skills/activerecord/examples/migrations/reversible_patterns.rb +403 -0
- data/skills/activerecord/examples/migrations/safe_patterns.rb +420 -0
- data/skills/activerecord/examples/migrations/schema_changes.rb +277 -0
- data/skills/activerecord/examples/querying/batch_processing.rb +226 -0
- data/skills/activerecord/examples/querying/eager_loading.rb +259 -0
- data/skills/activerecord/examples/querying/finder_methods.rb +170 -0
- data/skills/activerecord/examples/querying/optimization.rb +275 -0
- data/skills/activerecord/examples/querying/scopes.rb +260 -0
- data/skills/activerecord/examples/validations/built_in_validators.rb +277 -0
- data/skills/activerecord/examples/validations/conditional_validations.rb +288 -0
- data/skills/activerecord/examples/validations/custom_validators.rb +381 -0
- data/skills/activerecord/examples/validations/database_constraints.rb +432 -0
- data/skills/activerecord/examples/validations/validation_contexts.rb +367 -0
- data/skills/activerecord/references/associations.md +709 -0
- data/skills/activerecord/references/basics.md +622 -0
- data/skills/activerecord/references/callbacks.md +738 -0
- data/skills/activerecord/references/migrations.md +657 -0
- data/skills/activerecord/references/querying.md +655 -0
- data/skills/activerecord/references/validations.md +596 -0
- data/skills/dragonruby/SKILL.md +250 -0
- data/skills/dragonruby/examples/audio/audio_events.rb +55 -0
- data/skills/dragonruby/examples/audio/background_music.rb +29 -0
- data/skills/dragonruby/examples/audio/crossfade.rb +51 -0
- data/skills/dragonruby/examples/audio/music_controls.rb +51 -0
- data/skills/dragonruby/examples/audio/sound_effects.rb +30 -0
- data/skills/dragonruby/examples/core/coordinate_system.rb +27 -0
- data/skills/dragonruby/examples/core/hello_world.rb +24 -0
- data/skills/dragonruby/examples/core/labels.rb +22 -0
- data/skills/dragonruby/examples/core/sprites.rb +35 -0
- data/skills/dragonruby/examples/core/state_management.rb +29 -0
- data/skills/dragonruby/examples/distribution/background_pause.rb +42 -0
- data/skills/dragonruby/examples/distribution/build_workflow.sh +26 -0
- data/skills/dragonruby/examples/distribution/cvars_production.txt +16 -0
- data/skills/dragonruby/examples/distribution/game_metadata_hd.txt +23 -0
- data/skills/dragonruby/examples/distribution/game_metadata_minimal.txt +9 -0
- data/skills/dragonruby/examples/distribution/game_metadata_mobile.txt +31 -0
- data/skills/dragonruby/examples/distribution/platform_detection.rb +36 -0
- data/skills/dragonruby/examples/distribution/steam_metadata.txt +19 -0
- data/skills/dragonruby/examples/entities/collision_detection.rb +43 -0
- data/skills/dragonruby/examples/entities/entity_lifecycle.rb +68 -0
- data/skills/dragonruby/examples/entities/entity_storage.rb +38 -0
- data/skills/dragonruby/examples/entities/factory_methods.rb +45 -0
- data/skills/dragonruby/examples/entities/random_spawning.rb +50 -0
- data/skills/dragonruby/examples/game-logic/reset_patterns.rb +98 -0
- data/skills/dragonruby/examples/game-logic/save_load.rb +101 -0
- data/skills/dragonruby/examples/game-logic/scoring.rb +104 -0
- data/skills/dragonruby/examples/game-logic/state_transitions.rb +103 -0
- data/skills/dragonruby/examples/game-logic/timers.rb +87 -0
- data/skills/dragonruby/examples/input/action_triggers.rb +36 -0
- data/skills/dragonruby/examples/input/analog_movement.rb +28 -0
- data/skills/dragonruby/examples/input/controller_input.rb +28 -0
- data/skills/dragonruby/examples/input/directional_input.rb +24 -0
- data/skills/dragonruby/examples/input/keyboard_input.rb +28 -0
- data/skills/dragonruby/examples/input/mouse_click.rb +26 -0
- data/skills/dragonruby/examples/input/movement_with_bounds.rb +22 -0
- data/skills/dragonruby/examples/input/normalized_movement.rb +32 -0
- data/skills/dragonruby/examples/rendering/frame_animation.rb +32 -0
- data/skills/dragonruby/examples/rendering/labels.rb +32 -0
- data/skills/dragonruby/examples/rendering/layering.rb +51 -0
- data/skills/dragonruby/examples/rendering/solids.rb +61 -0
- data/skills/dragonruby/examples/rendering/sprites.rb +33 -0
- data/skills/dragonruby/examples/rendering/spritesheet_animation.rb +39 -0
- data/skills/dragonruby/examples/scenes/case_dispatch.rb +60 -0
- data/skills/dragonruby/examples/scenes/class_based.rb +150 -0
- data/skills/dragonruby/examples/scenes/pause_overlay.rb +100 -0
- data/skills/dragonruby/examples/scenes/safe_transitions.rb +68 -0
- data/skills/dragonruby/examples/scenes/scene_transitions.rb +98 -0
- data/skills/dragonruby/examples/scenes/send_dispatch.rb +88 -0
- data/skills/dragonruby/references/audio.md +396 -0
- data/skills/dragonruby/references/core.md +385 -0
- data/skills/dragonruby/references/distribution.md +434 -0
- data/skills/dragonruby/references/entities.md +516 -0
- data/skills/dragonruby/references/game-logic/persistence.md +386 -0
- data/skills/dragonruby/references/game-logic/state.md +389 -0
- data/skills/dragonruby/references/input.md +414 -0
- data/skills/dragonruby/references/rendering/animation.md +467 -0
- data/skills/dragonruby/references/rendering/primitives.md +403 -0
- data/skills/dragonruby/references/scenes.md +443 -0
- data/skills/draper-decorators/SKILL.md +344 -0
- data/skills/draper-decorators/examples/application_decorator.rb +61 -0
- data/skills/draper-decorators/examples/decorator_spec.rb +253 -0
- data/skills/draper-decorators/examples/model_decorator.rb +152 -0
- data/skills/draper-decorators/references/anti-patterns.md +640 -0
- data/skills/draper-decorators/references/patterns.md +507 -0
- data/skills/draper-decorators/references/testing.md +559 -0
- data/skills/gh-issue.md +182 -0
- data/skills/mcp-server/SKILL.md +177 -0
- data/skills/mcp-server/examples/dynamic_tools.rb +36 -0
- data/skills/mcp-server/examples/file_manager_tool.rb +85 -0
- data/skills/mcp-server/examples/http_client.rb +48 -0
- data/skills/mcp-server/examples/http_server.rb +97 -0
- data/skills/mcp-server/examples/rails_integration.rb +88 -0
- data/skills/mcp-server/examples/stdio_server.rb +108 -0
- data/skills/mcp-server/examples/streaming_client.rb +95 -0
- data/skills/mcp-server/references/gotchas.md +183 -0
- data/skills/mcp-server/references/prompts.md +98 -0
- data/skills/mcp-server/references/resources.md +53 -0
- data/skills/mcp-server/references/server.md +140 -0
- data/skills/mcp-server/references/tools.md +146 -0
- data/skills/mcp-server/references/transport.md +104 -0
- data/skills/ratatui-ruby/SKILL.md +315 -0
- data/skills/ratatui-ruby/references/core-concepts.md +340 -0
- data/skills/ratatui-ruby/references/events.md +387 -0
- data/skills/ratatui-ruby/references/frameworks.md +522 -0
- data/skills/ratatui-ruby/references/layout.md +423 -0
- data/skills/ratatui-ruby/references/styling.md +268 -0
- data/skills/ratatui-ruby/references/testing.md +433 -0
- data/skills/ratatui-ruby/references/widgets.md +532 -0
- data/skills/rspec/SKILL.md +340 -0
- data/skills/rspec/examples/core/basic_structure.rb +69 -0
- data/skills/rspec/examples/core/configuration.rb +126 -0
- data/skills/rspec/examples/core/hooks.rb +126 -0
- data/skills/rspec/examples/core/memoized_helpers.rb +139 -0
- data/skills/rspec/examples/core/metadata_filtering.rb +144 -0
- data/skills/rspec/examples/core/shared_examples.rb +145 -0
- data/skills/rspec/examples/factory_bot/associations.rb +314 -0
- data/skills/rspec/examples/factory_bot/build_strategies.rb +272 -0
- data/skills/rspec/examples/factory_bot/callbacks.rb +320 -0
- data/skills/rspec/examples/factory_bot/custom_construction.rb +328 -0
- data/skills/rspec/examples/factory_bot/factory_definition.rb +191 -0
- data/skills/rspec/examples/factory_bot/inheritance.rb +314 -0
- data/skills/rspec/examples/factory_bot/traits.rb +293 -0
- data/skills/rspec/examples/factory_bot/transients.rb +229 -0
- data/skills/rspec/examples/matchers/change.rb +115 -0
- data/skills/rspec/examples/matchers/collections.rb +154 -0
- data/skills/rspec/examples/matchers/comparisons.rb +79 -0
- data/skills/rspec/examples/matchers/composing.rb +155 -0
- data/skills/rspec/examples/matchers/custom_matchers.rb +197 -0
- data/skills/rspec/examples/matchers/equality.rb +58 -0
- data/skills/rspec/examples/matchers/errors.rb +136 -0
- data/skills/rspec/examples/matchers/output.rb +103 -0
- data/skills/rspec/examples/matchers/predicates.rb +87 -0
- data/skills/rspec/examples/matchers/truthiness.rb +101 -0
- data/skills/rspec/examples/matchers/types.rb +82 -0
- data/skills/rspec/examples/matchers/yield.rb +147 -0
- data/skills/rspec/examples/mocks/any_instance.rb +172 -0
- data/skills/rspec/examples/mocks/argument_matchers.rb +206 -0
- data/skills/rspec/examples/mocks/constants.rb +177 -0
- data/skills/rspec/examples/mocks/doubles.rb +139 -0
- data/skills/rspec/examples/mocks/expectations.rb +137 -0
- data/skills/rspec/examples/mocks/message_chains.rb +173 -0
- data/skills/rspec/examples/mocks/ordering.rb +144 -0
- data/skills/rspec/examples/mocks/receive_counts.rb +181 -0
- data/skills/rspec/examples/mocks/responses.rb +223 -0
- data/skills/rspec/examples/mocks/spies.rb +149 -0
- data/skills/rspec/examples/mocks/stubbing.rb +133 -0
- data/skills/rspec/examples/rails/channels.rb +250 -0
- data/skills/rspec/examples/rails/controller_specs.rb +302 -0
- data/skills/rspec/examples/rails/helper_specs.rb +245 -0
- data/skills/rspec/examples/rails/job_specs.rb +256 -0
- data/skills/rspec/examples/rails/mailer_specs.rb +228 -0
- data/skills/rspec/examples/rails/matchers.rb +374 -0
- data/skills/rspec/examples/rails/model_specs.rb +193 -0
- data/skills/rspec/examples/rails/request_specs.rb +275 -0
- data/skills/rspec/examples/rails/routing_specs.rb +276 -0
- data/skills/rspec/examples/rails/system_specs.rb +294 -0
- data/skills/rspec/examples/rails/transactions.rb +254 -0
- data/skills/rspec/examples/rails/view_specs.rb +252 -0
- data/skills/rspec/references/core.md +816 -0
- data/skills/rspec/references/factory_bot.md +641 -0
- data/skills/rspec/references/matchers.md +516 -0
- data/skills/rspec/references/mocks.md +381 -0
- data/skills/rspec/references/rails.md +528 -0
- data/templates/soul.md +40 -0
- data/workflows/commit.md +45 -0
- data/workflows/create_handoff.md +98 -0
- data/workflows/create_note.md +82 -0
- data/workflows/create_plan.md +457 -0
- data/workflows/decompose_ticket.md +109 -0
- data/workflows/feature.md +91 -0
- data/workflows/implement_plan.md +87 -0
- data/workflows/iterate_plan.md +247 -0
- data/workflows/research_codebase.md +210 -0
- data/workflows/resume_handoff.md +217 -0
- data/workflows/review_pr.md +320 -0
- data/workflows/thoughts_init.md +71 -0
- data/workflows/validate_plan.md +166 -0
- metadata +290 -3
- data/app/controllers/api/sessions_controller.rb +0 -25
- 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 |
|