new-component
Create a new ECS component in rogueverse. Use when adding components, creating component classes, or extending the ECS system with new data types.
Installation
Follow the repository instructions for this skill and your agent. Review the files and dependencies before adding it to your workflow.
View source ↗GuidesSkill instructions
New Component Creation Skill
This skill guides you through creating a new ECS component for the rogueverse project, ensuring all patterns and conventions are followed.
Overview
Creating a new component involves these steps:
- Add the component class to
lib/ecs/components.dart - Run
build_runnerto generate the mapper - Create inspector metadata in
lib/app/widgets/overlays/inspector/sections/ - Register the metadata in
properties_panel.dart - Consider if a new System is needed
- Consider if an Interaction Definition is needed (for player-interactable components)
Step 1: Component Class Pattern
All components must follow this pattern in lib/ecs/components.dart:
Simple Marker Component (no data):
/// Brief description of what this marker indicates.
@MappableClass()
class MyMarker with MyMarkerMappable implements Component {
@override
String get componentType => "MyMarker";
}
Data Component:
/// Description of the component's purpose.
@MappableClass()
class MyComponent with MyComponentMappable implements Component {
final int someValue;
final String someName;
MyComponent({required this.someValue, required this.someName});
@override
String get componentType => "MyComponent";
}
Intent Component (for player/AI actions):
/// Description of what action this intent represents.
@MappableClass()
class MyActionIntent extends IntentComponent with MyActionIntentMappable {
final int targetId;
MyActionIntent({required this.targetId});
@override
String get componentType => "MyActionIntent";
}
Event Component (cleared before/after tick):
/// Component added when [event description].
@MappableClass()
class DidSomething extends BeforeTick with DidSomethingMappable implements Component {
final int relevantData;
DidSomething({required this.relevantData}) : super(1);
@override
String get componentType => "DidSomething";
}
Key Rules for Components
- Data-only: Components should only contain data, no behavior/logic
-
Immutable fields preferred: Use
finalfor fields when possible -
Required annotation: Always use
@MappableClass()annotation -
Mixin naming: The mixin name is
{ClassName}Mappable - componentType getter: Must return the class name as a string
- Documentation: Add a doc comment explaining the component's purpose
Step 2: Run Build Runner
After adding the component, run:
dart run build_runner build --delete-conflicting-outputs
This generates the components.mapper.dart file with serialization code.
Step 3: Inspector Metadata
Create a metadata class for the inspector UI. Choose the appropriate pattern:
For Marker Components (no editable fields):
Add to an existing *_sections.dart file or create a new one:
/// Metadata for the MyMarker marker component.
class MyMarkerMetadata extends MarkerComponentMetadata {
@override
String get componentName => 'MyMarker';
@override
bool hasComponent(Entity entity) => entity.has<MyMarker>();
@override
Component createDefault() => MyMarker();
@override
void removeComponent(Entity entity) => entity.remove<MyMarker>();
}
For Data Components (with editable fields):
/// Metadata for the MyComponent component.
class MyComponentMetadata extends ComponentMetadata {
static const _theme = PropertyPanelThemeData(labelColumnWidth: 140);
@override
String get componentName => 'MyComponent';
@override
bool hasComponent(Entity entity) => entity.has<MyComponent>();
@override
Widget buildContent(Entity entity) {
return StreamBuilder<Change>(
stream: entity.parentCell.componentChanges.onEntityOnComponent<MyComponent>(entity.id),
builder: (context, snapshot) {
final comp = entity.get<MyComponent>();
if (comp == null) return const SizedBox.shrink();
return Column(
children: [
PropertyRow(
key: ValueKey('mycomp_someValue_${comp.someValue}'),
item: IntPropertyItem(
id: "someValue",
label: "Some Value",
value: comp.someValue,
onChanged: (int newVal) {
entity.upsert<MyComponent>(comp.copyWith(someValue: newVal));
},
),
theme: _theme,
),
// Add more PropertyRow widgets for each field...
],
);
},
);
}
@override
Component createDefault() => MyComponent(someValue: 0, someName: 'default');
@override
void removeComponent(Entity entity) => entity.remove<MyComponent>();
}
Property Types Available:
-
IntPropertyItem- for integers -
StringPropertyItem- for strings -
BoolPropertyItem- for booleans -
DoublePropertyItem- for doubles -
EnumPropertyItem<T>- for enums
Step 4: Register in Inspector
- If you created a new section file, export it in
sections/sections.dart:
export 'my_new_section.dart';
- Register the metadata in
properties_panel.dart's_registerAllComponents():
// Add under appropriate category comment
ComponentRegistry.register(MyComponentMetadata());
Step 5: Consider System Needs
ASK THE USER if they need a new System to process this component.
Systems are needed when:
- The component represents an action/intent that needs processing
- The component data needs to be updated each tick
- The component triggers side effects on other entities
Systems are NOT needed for:
- Pure data storage components (like Name, Renderable)
- Marker/tag components that are just checked by other systems
If a System is needed, follow the pattern in lib/ecs/systems.dart:
/// Description of what this system does.
@MappableClass()
class MySystem extends System with MySystemMappable {
@override
int get priority => 100; // Default priority
@override
void update(World world) {
Timeline.timeSync("MySystem: update", () {
final components = world.get<MyComponent>();
for (final entry in components.entries) {
final entity = world.getEntity(entry.key);
// Process entity...
}
});
}
}
Step 6: Consider Interaction Definition
ASK THE USER if this component represents something the player can interact with.
The game uses a context menu system for player interactions (triggered by E key or right-click). If the new component represents an interactable entity, you may need to register an InteractionDefinition in the interaction registry.
Interaction definitions are needed when:
- The component represents something the player can interact with (doors, items, controls, resources)
- There's already an Intent component for the action (e.g.,
OpenIntent,PickupIntent) - The player needs to be able to trigger this action via the E key or right-click menu
Interaction definitions are NOT needed for:
- Components that are purely internal state
- Actions that happen automatically (e.g., walking into a door auto-opens it)
- AI-only behaviors
Adding an Interaction Definition
Add to lib/game/interaction/interaction_registry.dart:
InteractionDefinition(
actionName: 'My Action', // Display name in menu (e.g., "Open", "Pick up")
actionVerb: 'Doing action', // Present participle for feedback
genericLabel: 'Thing', // Fallback if entity has no Name component
range: 1, // Manhattan distance: 0=same tile, 1=adjacent
isAvailable: (e) => e.has<MyComponent>() && /* any state checks */,
createIntent: (e) => MyActionIntent(targetEntityId: e.id),
),
Key Fields:
- actionName: What appears in the context menu
- genericLabel: Fallback entity name (e.g., "Door", "Item", "Resource")
-
range: How far the player can be to interact
-
0= must be on the same tile (pickup, take control) -
1= adjacent tiles (open door, mine) -
N= up to N tiles away (ranged interactions)
-
- isAvailable: Function that checks if interaction is valid for this entity
- createIntent: Function that creates the Intent component to execute
Related Files:
-
lib/game/interaction/interaction_definition.dart- InteractionDefinition class -
lib/game/interaction/interaction_registry.dart- Register new interactions here -
lib/game/interaction/nearby_entity_finder.dart- Finds interactable entities -
lib/app/widgets/overlays/interaction_context_menu.dart- The UI widget
File Locations Summary
| Type | Location |
|---|---|
| Component class | lib/ecs/components.dart |
| Generated mapper | lib/ecs/components.mapper.dart (auto-generated) |
| Inspector metadata | lib/app/widgets/overlays/inspector/sections/*.dart |
| Sections barrel | lib/app/widgets/overlays/inspector/sections/sections.dart |
| Registry calls | lib/app/widgets/panels/properties_panel.dart |
| Systems | lib/ecs/systems.dart |
| Interaction registry | lib/game/interaction/interaction_registry.dart |
Workflow Checklist
When creating a new component:
- Define the component class with
@MappableClass()annotation - Add
with {Name}Mappable implements Component - Implement
componentTypegetter - Add constructor with appropriate fields
- Run
dart run build_runner build --delete-conflicting-outputs - Create inspector metadata class (marker or data pattern)
- Export in
sections/sections.dartif new file created - Register in
properties_panel.dart_registerAllComponents() - Ask user if a System is needed for this component
- Ask user if an Interaction Definition is needed (for player-interactable components)
Examples
Read the usage examples in the source repository.
View source ↗Source history
- Indexed on
- 2026-01-20
- Index updated
- 2026-01-26
- Branch
- main
Index and sync dates describe this directory. View the source repository for code history.
GitHub ↗chat Comments (0)
Sign in to join the discussion and leave a comment.
No comments yet. Be the first to share your thoughts!