anima-core 0.3.0 → 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 +4 -0
- data/README.md +219 -25
- 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 +76 -28
- data/app/jobs/agent_request_job.rb +24 -0
- data/app/jobs/analytical_brain_job.rb +33 -0
- data/app/jobs/count_event_tokens_job.rb +1 -1
- data/app/models/concerns/event/broadcasting.rb +20 -2
- data/app/models/event.rb +1 -1
- data/app/models/goal.rb +91 -0
- data/app/models/session.rb +347 -22
- data/config/application.rb +2 -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 -9
- 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 +4 -0
- 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/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 +8 -7
- 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 +3 -4
- data/lib/tools/mcp_tool.rb +114 -0
- data/lib/tools/read.rb +15 -16
- 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/tui/app.rb +332 -43
- data/lib/tui/message_store.rb +20 -0
- data/lib/tui/screens/chat.rb +207 -20
- 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 +284 -1
|
@@ -0,0 +1,516 @@
|
|
|
1
|
+
# Entities Domain Reference
|
|
2
|
+
|
|
3
|
+
Game entity management: storage, spawning, collision detection, and lifecycle.
|
|
4
|
+
|
|
5
|
+
## Entity Storage
|
|
6
|
+
|
|
7
|
+
Store entities in `args.state` arrays with lazy initialization:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
def tick(args)
|
|
11
|
+
args.state.enemies ||= []
|
|
12
|
+
args.state.bullets ||= []
|
|
13
|
+
args.state.particles ||= []
|
|
14
|
+
end
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
**Naming convention:** Use descriptive plurals (enemies, bullets, projectiles).
|
|
18
|
+
|
|
19
|
+
## Entity Representation
|
|
20
|
+
|
|
21
|
+
### Hash-Based Entities (Recommended for most games)
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
entity = {
|
|
25
|
+
# Required for rendering
|
|
26
|
+
x: 100, y: 100, w: 40, h: 40,
|
|
27
|
+
path: 'sprites/enemy.png',
|
|
28
|
+
|
|
29
|
+
# Optional visual properties
|
|
30
|
+
r: 255, g: 255, b: 255, a: 255,
|
|
31
|
+
angle: 0,
|
|
32
|
+
flip_horizontally: false,
|
|
33
|
+
|
|
34
|
+
# Custom properties
|
|
35
|
+
speed: 5,
|
|
36
|
+
health: 100,
|
|
37
|
+
dead: false
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Tracked Entities (with automatic metadata)
|
|
42
|
+
|
|
43
|
+
```ruby
|
|
44
|
+
args.state.new_entity(:enemy) do |e|
|
|
45
|
+
e.x = 100
|
|
46
|
+
e.y = 200
|
|
47
|
+
e.w = 40
|
|
48
|
+
e.h = 40
|
|
49
|
+
e.path = 'sprites/enemy.png'
|
|
50
|
+
end
|
|
51
|
+
# Adds: entity_id, created_at, created_at_elapsed
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Class-Based Entities (for 1000+ entities)
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
class Enemy
|
|
58
|
+
attr_sprite # Enables x, y, w, h, path, etc.
|
|
59
|
+
|
|
60
|
+
def initialize(x, y)
|
|
61
|
+
@x, @y = x, y
|
|
62
|
+
@w, @h = 40, 40
|
|
63
|
+
@path = 'sprites/enemy.png'
|
|
64
|
+
@speed = 5
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def update
|
|
68
|
+
@x += @speed
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Spawning Patterns
|
|
74
|
+
|
|
75
|
+
### Factory Methods
|
|
76
|
+
|
|
77
|
+
```ruby
|
|
78
|
+
def spawn_enemy(args)
|
|
79
|
+
size = 40
|
|
80
|
+
{
|
|
81
|
+
x: rand(args.grid.w - size),
|
|
82
|
+
y: rand(args.grid.h - size),
|
|
83
|
+
w: size,
|
|
84
|
+
h: size,
|
|
85
|
+
path: 'sprites/enemy.png',
|
|
86
|
+
speed: rand(3) + 1,
|
|
87
|
+
dead: false
|
|
88
|
+
}
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Usage
|
|
92
|
+
args.state.enemies << spawn_enemy(args)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Random Positioning with Gutters
|
|
96
|
+
|
|
97
|
+
Prevent entities from spawning partially off-screen:
|
|
98
|
+
|
|
99
|
+
```ruby
|
|
100
|
+
def spawn_in_bounds(args)
|
|
101
|
+
size = 64
|
|
102
|
+
{
|
|
103
|
+
# Gutter formula: rand(max - size * 2) + size
|
|
104
|
+
x: rand(args.grid.w - size * 2) + size,
|
|
105
|
+
y: rand(args.grid.h - size * 2) + size,
|
|
106
|
+
w: size,
|
|
107
|
+
h: size
|
|
108
|
+
}
|
|
109
|
+
end
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
**Formula breakdown:**
|
|
113
|
+
- `args.grid.h - size * 2` = available height (subtract top + bottom gutters)
|
|
114
|
+
- `rand(available)` = random position in safe zone
|
|
115
|
+
- `+ size` = offset for bottom/left gutter
|
|
116
|
+
|
|
117
|
+
### Spawn in Specific Region
|
|
118
|
+
|
|
119
|
+
```ruby
|
|
120
|
+
# Right 40% of screen
|
|
121
|
+
x: rand(args.grid.w * 0.4) + args.grid.w * 0.6
|
|
122
|
+
|
|
123
|
+
# Top half with padding
|
|
124
|
+
y: rand(args.grid.h / 2 - 50) + args.grid.h / 2 + 25
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Batch Spawning
|
|
128
|
+
|
|
129
|
+
```ruby
|
|
130
|
+
def spawn_wave(args, count)
|
|
131
|
+
count.times.map { spawn_enemy(args) }
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
args.state.enemies += spawn_wave(args, 10)
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Collision Detection
|
|
138
|
+
|
|
139
|
+
### Basic Rectangle Collision
|
|
140
|
+
|
|
141
|
+
```ruby
|
|
142
|
+
# Instance method
|
|
143
|
+
if player.intersect_rect?(enemy)
|
|
144
|
+
handle_collision
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Module method
|
|
148
|
+
if Geometry.intersect_rect?(player, enemy)
|
|
149
|
+
handle_collision
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# With tolerance (default 0.1)
|
|
153
|
+
if player.intersect_rect?(enemy, 0.5)
|
|
154
|
+
handle_collision
|
|
155
|
+
end
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Find First Collision
|
|
159
|
+
|
|
160
|
+
```ruby
|
|
161
|
+
# Returns first intersecting entity or nil
|
|
162
|
+
hit = Geometry.find_intersect_rect(bullet, args.state.enemies)
|
|
163
|
+
if hit
|
|
164
|
+
hit.dead = true
|
|
165
|
+
end
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Find All Collisions
|
|
169
|
+
|
|
170
|
+
```ruby
|
|
171
|
+
# Returns array (empty if no collisions)
|
|
172
|
+
hits = Geometry.find_all_intersect_rect(explosion, args.state.enemies)
|
|
173
|
+
hits.each { |enemy| enemy.health -= 50 }
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Many-to-Many Collision
|
|
177
|
+
|
|
178
|
+
```ruby
|
|
179
|
+
# Iterate through all collision pairs
|
|
180
|
+
Geometry.each_intersect_rect(bullets, enemies) do |bullet, enemy|
|
|
181
|
+
bullet.dead = true
|
|
182
|
+
enemy.health -= bullet.damage
|
|
183
|
+
end
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Nested Loop Pattern
|
|
187
|
+
|
|
188
|
+
```ruby
|
|
189
|
+
args.state.bullets.each do |bullet|
|
|
190
|
+
args.state.enemies.each do |enemy|
|
|
191
|
+
if Geometry.intersect_rect?(bullet, enemy)
|
|
192
|
+
bullet.dead = true
|
|
193
|
+
enemy.dead = true
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### Quad Trees (100+ entities)
|
|
200
|
+
|
|
201
|
+
```ruby
|
|
202
|
+
# Create once for static/semi-static entities
|
|
203
|
+
args.state.quad_tree ||= Geometry.quad_tree_create(args.state.terrain)
|
|
204
|
+
|
|
205
|
+
# Fast collision lookup
|
|
206
|
+
hit = Geometry.find_intersect_rect_quad_tree(
|
|
207
|
+
args.state.player,
|
|
208
|
+
args.state.quad_tree
|
|
209
|
+
)
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## Entity Lifecycle
|
|
213
|
+
|
|
214
|
+
### Two-Phase Removal Pattern
|
|
215
|
+
|
|
216
|
+
**Phase 1:** Mark entities as dead during update loops:
|
|
217
|
+
|
|
218
|
+
```ruby
|
|
219
|
+
args.state.bullets.each do |bullet|
|
|
220
|
+
bullet.x += bullet.speed
|
|
221
|
+
|
|
222
|
+
# Mark off-screen bullets
|
|
223
|
+
if bullet.x > args.grid.w
|
|
224
|
+
bullet.dead = true
|
|
225
|
+
next # Skip further processing
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# Mark on collision
|
|
229
|
+
args.state.enemies.each do |enemy|
|
|
230
|
+
if Geometry.intersect_rect?(bullet, enemy)
|
|
231
|
+
bullet.dead = true
|
|
232
|
+
enemy.dead = true
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
**Phase 2:** Reject dead entities after all processing:
|
|
239
|
+
|
|
240
|
+
```ruby
|
|
241
|
+
args.state.bullets.reject! { |b| b.dead }
|
|
242
|
+
args.state.enemies.reject! { |e| e.dead }
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### Off-Screen Removal
|
|
246
|
+
|
|
247
|
+
```ruby
|
|
248
|
+
# Remove bullets past screen edge
|
|
249
|
+
args.state.bullets.reject! { |b| b.x > args.grid.w }
|
|
250
|
+
|
|
251
|
+
# Remove any entity outside bounds
|
|
252
|
+
args.state.entities.reject! do |e|
|
|
253
|
+
e.x < -100 || e.x > 1380 || e.y < -100 || e.y > 820
|
|
254
|
+
end
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### Age-Based Removal
|
|
258
|
+
|
|
259
|
+
```ruby
|
|
260
|
+
# Mark particles after certain age
|
|
261
|
+
args.state.particles.each do |p|
|
|
262
|
+
p.age ||= 0
|
|
263
|
+
p.age += 1
|
|
264
|
+
p.dead = true if p.age > 60 # 1 second at 60 FPS
|
|
265
|
+
end
|
|
266
|
+
args.state.particles.reject!(&:dead)
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
## Collection Rendering
|
|
270
|
+
|
|
271
|
+
### Basic Rendering
|
|
272
|
+
|
|
273
|
+
```ruby
|
|
274
|
+
# Render entire collection
|
|
275
|
+
args.outputs.sprites << args.state.enemies
|
|
276
|
+
|
|
277
|
+
# DragonRuby flattens nested arrays automatically
|
|
278
|
+
args.outputs.sprites << [
|
|
279
|
+
args.state.player,
|
|
280
|
+
args.state.enemies,
|
|
281
|
+
args.state.bullets
|
|
282
|
+
]
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
### Conditional Rendering
|
|
286
|
+
|
|
287
|
+
```ruby
|
|
288
|
+
# Only render alive entities
|
|
289
|
+
args.outputs.sprites << args.state.enemies.select(&:alive)
|
|
290
|
+
|
|
291
|
+
# Simple frustum culling
|
|
292
|
+
args.outputs.sprites << args.state.entities.select do |e|
|
|
293
|
+
e.x.between?(-100, 1380) && e.y.between?(-100, 820)
|
|
294
|
+
end
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
## Geometry API Reference
|
|
298
|
+
|
|
299
|
+
Access via `Geometry.*` (preferred) or `args.geometry.*`. Instance methods also available as mixins on Hash/Array/Entity.
|
|
300
|
+
|
|
301
|
+
| Method | Returns | Use Case |
|
|
302
|
+
|--------|---------|----------|
|
|
303
|
+
| `intersect_rect?(other)` | `true/false` | Basic collision check |
|
|
304
|
+
| `inside_rect?(other)` | `true/false` | Fully contained check |
|
|
305
|
+
| `find_intersect_rect(rect, coll)` | `rect/nil` | First collision |
|
|
306
|
+
| `find_all_intersect_rect(rect, coll)` | `Array` | All collisions |
|
|
307
|
+
| `each_intersect_rect(c1, c2) {}` | — | Iterate pairs |
|
|
308
|
+
| `quad_tree_create(coll)` | `quad_tree` | Build spatial index |
|
|
309
|
+
| `find_intersect_rect_quad_tree(r, tree)` | `rect/nil` | Fast lookup |
|
|
310
|
+
| `distance(p1, p2)` | `Float` | Distance between points |
|
|
311
|
+
| `angle_to(from, to)` | `Float` | Angle in degrees |
|
|
312
|
+
|
|
313
|
+
## Performance Tiers
|
|
314
|
+
|
|
315
|
+
| Entity Count | Recommended Approach |
|
|
316
|
+
|--------------|---------------------|
|
|
317
|
+
| < 100 | Hash-based entities |
|
|
318
|
+
| 100-500 | `args.state.new_entity` or classes |
|
|
319
|
+
| 500-1000 | Classes with `attr_sprite` |
|
|
320
|
+
| 1000+ | `static_sprites` + `draw_override` |
|
|
321
|
+
|
|
322
|
+
For collision detection:
|
|
323
|
+
|
|
324
|
+
| Entity Count | Recommended Approach |
|
|
325
|
+
|--------------|---------------------|
|
|
326
|
+
| < 100 | `intersect_rect?` / nested loops |
|
|
327
|
+
| 100-500 | `find_intersect_rect` / `each_intersect_rect` |
|
|
328
|
+
| 500+ | Quad trees |
|
|
329
|
+
|
|
330
|
+
## Common Antipatterns
|
|
331
|
+
|
|
332
|
+
### ❌ Modifying Collection While Iterating
|
|
333
|
+
|
|
334
|
+
```ruby
|
|
335
|
+
# BUG: Modifying during iteration
|
|
336
|
+
args.state.enemies.each do |enemy|
|
|
337
|
+
if enemy.health <= 0
|
|
338
|
+
args.state.enemies.delete(enemy) # WRONG
|
|
339
|
+
end
|
|
340
|
+
end
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
**Fix:** Mark and reject in separate phases.
|
|
344
|
+
|
|
345
|
+
### ❌ Keeping Dead Entities
|
|
346
|
+
|
|
347
|
+
```ruby
|
|
348
|
+
# Memory leak: dead entities accumulate
|
|
349
|
+
args.state.bullets.each do |b|
|
|
350
|
+
b.x += b.speed
|
|
351
|
+
# Never removed when off-screen
|
|
352
|
+
end
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
**Fix:** Always reject off-screen and dead entities.
|
|
356
|
+
|
|
357
|
+
### ❌ Using Arrays Instead of Hashes
|
|
358
|
+
|
|
359
|
+
```ruby
|
|
360
|
+
# Unreadable: what is [0]?
|
|
361
|
+
bullet = [x, y, w, h, 'sprites/bullet.png']
|
|
362
|
+
bullet[0] += speed
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
**Fix:** Use hashes with named properties.
|
|
366
|
+
|
|
367
|
+
### ❌ Forgetting Gutters
|
|
368
|
+
|
|
369
|
+
```ruby
|
|
370
|
+
# Can spawn partially off-screen
|
|
371
|
+
y: rand(args.grid.h)
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
**Fix:** Use `rand(max - size * 2) + size` formula.
|
|
375
|
+
|
|
376
|
+
### ❌ Collision After Death
|
|
377
|
+
|
|
378
|
+
```ruby
|
|
379
|
+
args.state.bullets.each do |bullet|
|
|
380
|
+
bullet.x += bullet.speed
|
|
381
|
+
# No check if already dead - wastes cycles
|
|
382
|
+
args.state.enemies.each do |enemy|
|
|
383
|
+
if Geometry.intersect_rect?(bullet, enemy)
|
|
384
|
+
bullet.dead = true
|
|
385
|
+
enemy.dead = true
|
|
386
|
+
end
|
|
387
|
+
end
|
|
388
|
+
end
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
**Fix:** Use `next if bullet.dead` after marking.
|
|
392
|
+
|
|
393
|
+
## Complete Entity System
|
|
394
|
+
|
|
395
|
+
```ruby
|
|
396
|
+
def tick(args)
|
|
397
|
+
defaults(args)
|
|
398
|
+
update_entities(args)
|
|
399
|
+
check_collisions(args)
|
|
400
|
+
cleanup_entities(args)
|
|
401
|
+
render_entities(args)
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
def defaults(args)
|
|
405
|
+
args.state.player ||= { x: 100, y: 360, w: 40, h: 40, path: 'sprites/player.png' }
|
|
406
|
+
args.state.enemies ||= []
|
|
407
|
+
args.state.bullets ||= []
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
def update_entities(args)
|
|
411
|
+
args.state.bullets.each do |b|
|
|
412
|
+
b.x += 10
|
|
413
|
+
b.dead = true if b.x > args.grid.w
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
args.state.enemies.each do |e|
|
|
417
|
+
e.x -= e.speed
|
|
418
|
+
e.dead = true if e.x < -e.w
|
|
419
|
+
end
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
def check_collisions(args)
|
|
423
|
+
args.state.bullets.each do |bullet|
|
|
424
|
+
next if bullet.dead
|
|
425
|
+
|
|
426
|
+
args.state.enemies.each do |enemy|
|
|
427
|
+
next if enemy.dead
|
|
428
|
+
|
|
429
|
+
if Geometry.intersect_rect?(bullet, enemy)
|
|
430
|
+
bullet.dead = true
|
|
431
|
+
enemy.dead = true
|
|
432
|
+
args.state.score ||= 0
|
|
433
|
+
args.state.score += 100
|
|
434
|
+
end
|
|
435
|
+
end
|
|
436
|
+
end
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
def cleanup_entities(args)
|
|
440
|
+
args.state.bullets.reject!(&:dead)
|
|
441
|
+
args.state.enemies.reject!(&:dead)
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
def render_entities(args)
|
|
445
|
+
args.outputs.sprites << [
|
|
446
|
+
args.state.player,
|
|
447
|
+
args.state.enemies,
|
|
448
|
+
args.state.bullets
|
|
449
|
+
]
|
|
450
|
+
end
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
## Decision Tree
|
|
454
|
+
|
|
455
|
+
**How should I store game objects?**
|
|
456
|
+
```
|
|
457
|
+
Need to track many objects of same type?
|
|
458
|
+
├─ Yes → Use args.state arrays with ||= initialization
|
|
459
|
+
│ See: examples/entities/entity_storage.rb
|
|
460
|
+
└─ No (single object like player) → Use args.state.player directly
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
**Which entity representation?**
|
|
464
|
+
```
|
|
465
|
+
How many entities?
|
|
466
|
+
├─ < 100 → Hash-based entities (simplest)
|
|
467
|
+
│ See: examples/entities/entity_storage.rb
|
|
468
|
+
├─ 100-1000 → args.state.new_entity (tracking) or classes
|
|
469
|
+
│ See: examples/entities/factory_methods.rb
|
|
470
|
+
└─ 1000+ → Classes with attr_sprite
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
**How should I spawn entities?**
|
|
474
|
+
```
|
|
475
|
+
Creating entities in multiple places?
|
|
476
|
+
├─ Yes → Use factory methods (spawn_enemy, spawn_bullet)
|
|
477
|
+
│ See: examples/entities/factory_methods.rb
|
|
478
|
+
└─ No → Inline creation is fine
|
|
479
|
+
|
|
480
|
+
Need random positions?
|
|
481
|
+
├─ Yes, anywhere on screen → Use gutter formula
|
|
482
|
+
│ See: examples/entities/random_spawning.rb
|
|
483
|
+
└─ Yes, specific region → Calculate bounds manually
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
**Which collision method?**
|
|
487
|
+
```
|
|
488
|
+
How many entities to check?
|
|
489
|
+
├─ Single entity vs single entity → intersect_rect?
|
|
490
|
+
│ See: examples/entities/collision_detection.rb
|
|
491
|
+
├─ Single entity vs collection (find first) → find_intersect_rect
|
|
492
|
+
├─ Single entity vs collection (find all) → find_all_intersect_rect
|
|
493
|
+
├─ Collection vs collection → each_intersect_rect or nested loops
|
|
494
|
+
│ See: examples/entities/entity_lifecycle.rb
|
|
495
|
+
└─ 100+ static entities → quad_tree
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
**How should I remove entities?**
|
|
499
|
+
```
|
|
500
|
+
When to remove?
|
|
501
|
+
├─ On collision → Mark dead, reject after loop
|
|
502
|
+
│ See: examples/entities/entity_lifecycle.rb
|
|
503
|
+
├─ Off-screen → Check bounds, mark dead
|
|
504
|
+
├─ After time → Track age, mark when expired
|
|
505
|
+
└─ All at once → Use reject! after all updates
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
## Examples
|
|
509
|
+
|
|
510
|
+
| File | Demonstrates |
|
|
511
|
+
|------|--------------|
|
|
512
|
+
| `examples/entities/entity_storage.rb` | Arrays in args.state, ||= initialization |
|
|
513
|
+
| `examples/entities/factory_methods.rb` | spawn_* patterns, new_entity |
|
|
514
|
+
| `examples/entities/collision_detection.rb` | intersect_rect?, find methods |
|
|
515
|
+
| `examples/entities/entity_lifecycle.rb` | Create → update → mark → reject |
|
|
516
|
+
| `examples/entities/random_spawning.rb` | Gutter formula, bounded positions |
|