一键导入
dojo
Dojo Engine framework patterns — World, Systems, Models, Events, Components, Store, permissions, testing with spawn_test_world, and deployment with sozo.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Dojo Engine framework patterns — World, Systems, Models, Events, Components, Store, permissions, testing with spawn_test_world, and deployment with sozo.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Shinigami architecture for fully onchain Dojo games — Elements, Types, Models, Components, Systems, Helpers, Store, Events, Interfaces. Use when structuring a new game, adding modules, understanding the codebase hierarchy, or implementing new game mechanics.
Add SVG icons to the Scoundrel game client — convert SVG, create component, export, update storybook. Use when adding, modifying, or removing icon components.
UI component patterns for the Scoundrel game client — Radix primitives, elements, containers, theming, Storybook conventions. Use when creating or modifying UI components, adding storybook stories, or working with the design system.
| name | dojo |
| description | Dojo Engine framework patterns — World, Systems, Models, Events, Components, Store, permissions, testing with spawn_test_world, and deployment with sozo. |
This skill covers Dojo Engine patterns for building fully on-chain games on Starknet. For pure Cairo language and Starknet contract patterns, see the cairo skill.
#[dojo::contract])#[dojo::model]) or events (#[dojo::event])spawn_test_worldDojo is an Entity Component System (ECS) framework:
#[dojo::model])#[dojo::contract])#[dojo::event])#[starknet::component])System (#[dojo::contract])
└── self.world(@NAMESPACE()) → WorldStorage
├── world.read_model(key) → Model
├── world.write_model(@model)
└── world.emit_event(@event)
// In systems (always needed)
use dojo::model::{ModelStorage, ModelValueStorage};
use dojo::event::EventStorage;
use dojo::world::WorldStorage;
// In models
use starknet::ContractAddress;
// For nested structs in models
use dojo::meta::Introspect;
// In tests
use dojo_cairo_test::{
ContractDef, ContractDefTrait, NamespaceDef, TestResource,
WorldStorageTestTrait, spawn_test_world,
};
#[dojo::model])Models are the state. Structs annotated with #[dojo::model] are stored in the World.
#[derive(Drop, Serde)]
#[dojo::model]
pub struct Game {
#[key]
pub id: u64, // Primary key — used for lookup
pub level: u8, // Data fields
pub score: u32,
pub slots: felt252, // Can store packed data
}
#[key] field requiredDrop, SerdeCopy (for primitive types)#[derive(Copy, Drop, Serde)]
#[dojo::model]
pub struct VaultPosition {
#[key]
pub user: felt252, // First key
pub shares: u256,
pub lockup: u64,
}
// Read with single key
let position: VaultPosition = world.read_model(user_felt);
// For multiple keys, use tuple
let resource: GameResource = world.read_model((player, location));
Must derive Introspect:
#[derive(Drop, Copy, Serde, Introspect)]
pub struct Vec2 {
pub x: u32,
pub y: u32,
}
let mut world = self.world(@"NUMS");
// Read (returns default/zero if not set)
let game: Game = world.read_model(game_id);
// Write / Update
world.write_model(@game);
// Generate unique ID
let entity_id = world.uuid();
// Delete
world.erase_model(@game);
#[dojo::event])Events are automatically indexed by Torii (Dojo's off-chain indexer).
#[derive(Copy, Drop, Serde)]
#[dojo::event]
pub struct Purchased {
#[key]
pub player_id: felt252, // Indexed field for filtering
pub starterpack_id: u32,
pub quantity: u32,
pub time: u64,
}
// Emit in a system or component
world.emit_event(@Purchased {
player_id: caller.into(),
starterpack_id: 1,
quantity: 1,
time: get_block_timestamp(),
});
#[dojo::contract])Systems are thin entry points. They get WorldStorage and delegate to components.
#[starknet::interface]
pub trait IPlay<T> {
fn set(ref self: T, game_id: u64, index: u8) -> u16;
fn select(ref self: T, game_id: u64, index: u8);
}
#[dojo::contract]
pub mod Play {
use dojo::model::ModelStorage;
use crate::components::playable::PlayableComponent;
use crate::constants::NAMESPACE;
// Embed components
component!(path: PlayableComponent, storage: playable, event: PlayableEvent);
impl PlayableInternalImpl = PlayableComponent::InternalImpl<ContractState>;
#[storage]
struct Storage {
#[substorage(v0)]
playable: PlayableComponent::Storage,
}
#[event]
#[derive(Drop, starknet::Event)]
enum Event {
#[flat]
PlayableEvent: PlayableComponent::Event,
}
#[abi(embed_v0)]
impl PlayImpl of super::IPlay<ContractState> {
fn set(ref self: ContractState, game_id: u64, index: u8) -> u16 {
// Get world for namespace
let world = self.world(@NAMESPACE());
// Delegate to component
self.playable.set(world, game_id, index)
}
}
}
// With explicit namespace (preferred in this project)
let world = self.world(@NAMESPACE()); // NAMESPACE() returns "NUMS"
// With default namespace
let world = self.world_default();
// With inline namespace
let world = self.world(@"my_namespace");
dojo_init)Called once on contract deployment:
fn dojo_init(ref self: ContractState, owner: ContractAddress, entry_price: u128) {
let mut world = self.world(@NAMESPACE());
let mut store = StoreImpl::new(world);
// Create initial config
let config = ConfigTrait::new(owner, entry_price);
store.set_config(config);
// Initialize components
self.initializable.initialize(world);
}
Components contain the business logic and receive WorldStorage as a parameter.
#[starknet::component]
pub mod PlayableComponent {
use dojo::world::WorldStorage;
use crate::{StoreImpl, StoreTrait};
#[storage]
pub struct Storage {}
#[event]
#[derive(Drop, starknet::Event)]
pub enum Event {}
#[generate_trait]
pub impl InternalImpl<
TContractState,
+HasComponent<TContractState>,
+Drop<TContractState>,
> of InternalTrait<TContractState> {
fn set(
ref self: ComponentState<TContractState>,
world: WorldStorage,
game_id: u64,
index: u8,
) -> u16 {
// [Setup] Store
let mut store = StoreImpl::new(world);
// [Check] Game state
let caller = starknet::get_caller_address();
let mut game = store.game(game_id);
game.assert_does_exist();
game.assert_not_over();
// [Effect] Place number
game.place(game.number, index, ref rand);
// [Interaction] Write back
store.set_game(@game);
game.number
}
}
}
// Immutable access
let questable = get_dep_component!(@self, Quest);
questable.progress(world, player, task_id, count, true);
// Mutable access
let mut rankable = get_dep_component_mut!(ref self, Rankable);
rankable.submit(world: world, leaderboard_id: 1, score: 42);
// 1. Declare component
component!(path: PlayableComponent, storage: playable, event: PlayableEvent);
impl PlayableInternalImpl = PlayableComponent::InternalImpl<ContractState>;
// 2. Add to storage
#[storage]
struct Storage {
#[substorage(v0)]
playable: PlayableComponent::Storage,
}
// 3. Add to events
#[event]
#[derive(Drop, starknet::Event)]
enum Event {
#[flat]
PlayableEvent: PlayableComponent::Event,
}
OZ components compose with Dojo contracts using the same component!() pattern:
#[dojo::contract]
pub mod Vault {
use openzeppelin::token::erc20::extensions::erc4626::ERC4626Component;
use openzeppelin::access::accesscontrol::AccessControlComponent;
component!(path: ERC4626Component, storage: erc4626, event: ERC4626Event);
component!(path: AccessControlComponent, storage: accesscontrol, event: AccessControlEvent);
// ERC4626 hooks for custom deposit/withdraw logic
impl ERC4626HooksImpl of ERC4626Component::ERC4626HooksTrait<ContractState> {
fn before_deposit(ref self: ERC4626Component::ComponentState<ContractState>, ...) {
let mut contract_state = self.get_contract_mut();
contract_state.pausable.assert_not_paused();
let world = contract_state.world(@NAMESPACE());
// ... custom logic using world
}
}
}