From book
Generates Cairo Dojo system contracts for implementing game logic, modifying model state, handling player actions, and emitting events in Starknet games.
How this skill is triggered — by the user, by Claude, or both
Slash command
/book:dojo-systemThis skill is limited to the following tools:
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
Create Dojo systems (smart contracts) that implement your game's logic and modify model state.
Create Dojo systems (smart contracts) that implement your game's logic and modify model state.
Copy these imports for any Dojo system:
// Core Dojo imports - ALWAYS needed for systems
use dojo::model::{ModelStorage, ModelValueStorage};
use dojo::event::EventStorage;
// Starknet essentials
use starknet::{ContractAddress, get_caller_address, get_block_timestamp};
self.world_default() come from?self.world_default() is provided automatically by #[dojo::contract] - no import needed!
#[dojo::contract] // <-- This macro provides world_default()
mod my_system {
use dojo::model::{ModelStorage, ModelValueStorage};
use dojo::event::EventStorage;
#[abi(embed_v0)]
impl MyImpl of IMySystem<ContractState> {
fn my_function(ref self: ContractState) {
// world_default() is available because of #[dojo::contract]
let mut world = self.world_default();
// Now use world for all operations...
}
}
}
Requires: use dojo::event::EventStorage;
// 1. Define the event (outside impl block)
#[derive(Copy, Drop, Serde)]
#[dojo::event]
struct PlayerMoved {
#[key]
player: ContractAddress,
from_x: u32,
from_y: u32,
to_x: u32,
to_y: u32,
}
// 2. Emit it (inside a function)
fn move_player(ref self: ContractState, direction: u8) {
let mut world = self.world_default();
// ... game logic ...
// Emit event - note the @ for snapshot
world.emit_event(@PlayerMoved {
player: get_caller_address(),
from_x: 0,
from_y: 0,
to_x: 1,
to_y: 1,
});
}
| You want to use | Import this |
|---|---|
world.read_model() | use dojo::model::ModelStorage; |
world.write_model() | use dojo::model::ModelStorage; |
world.emit_event() | use dojo::event::EventStorage; |
self.world_default() | Nothing! Provided by #[dojo::contract] |
get_caller_address() | use starknet::get_caller_address; |
Generates Cairo system contracts with:
#[dojo::contract] attribute#[starknet::interface]world.read_model(), world.write_model())#[dojo::event]Interactive mode:
"Create a system for player movement"
I'll ask about:
Direct mode:
"Create a move system that updates Position based on Direction"
A Dojo contract consists of an interface trait and a contract module:
use dojo_starter::models::{Direction, Position};
// Define the interface
#[starknet::interface]
trait IActions<T> {
fn spawn(ref self: T);
fn move(ref self: T, direction: Direction);
}
// Dojo contract
#[dojo::contract]
pub mod actions {
use super::{IActions, Direction, Position};
use starknet::{ContractAddress, get_caller_address};
use dojo_starter::models::{Vec2, Moves};
use dojo::model::{ModelStorage, ModelValueStorage};
use dojo::event::EventStorage;
// Define a custom event
#[derive(Copy, Drop, Serde)]
#[dojo::event]
pub struct Moved {
#[key]
pub player: ContractAddress,
pub direction: Direction,
}
#[abi(embed_v0)]
impl ActionsImpl of IActions<ContractState> {
fn spawn(ref self: ContractState) {
let mut world = self.world_default();
let player = get_caller_address();
// Read current position (defaults to zero if not set)
let position: Position = world.read_model(player);
// Set initial position
let new_position = Position {
player,
vec: Vec2 { x: position.vec.x + 10, y: position.vec.y + 10 }
};
world.write_model(@new_position);
// Set initial moves
let moves = Moves {
player,
remaining: 100,
last_direction: Direction::None(()),
can_move: true
};
world.write_model(@moves);
}
fn move(ref self: ContractState, direction: Direction) {
let mut world = self.world_default();
let player = get_caller_address();
// Read current state
let position: Position = world.read_model(player);
let mut moves: Moves = world.read_model(player);
// Update moves
moves.remaining -= 1;
moves.last_direction = direction;
// Calculate next position
let next = next_position(position, direction);
// Write updated state
world.write_model(@next);
world.write_model(@moves);
// Emit event
world.emit_event(@Moved { player, direction });
}
}
// Internal helper to get world with namespace
#[generate_trait]
impl InternalImpl of InternalTrait {
fn world_default(self: @ContractState) -> dojo::world::WorldStorage {
self.world(@"dojo_starter")
}
}
}
// Helper function outside the contract
fn next_position(mut position: Position, direction: Direction) -> Position {
match direction {
Direction::None => { return position; },
Direction::Left => { position.vec.x -= 1; },
Direction::Right => { position.vec.x += 1; },
Direction::Up => { position.vec.y -= 1; },
Direction::Down => { position.vec.y += 1; },
};
position
}
Get the world storage using your namespace:
let mut world = self.world(@"my_namespace");
Create a helper function to avoid repeating the namespace:
#[generate_trait]
impl InternalImpl of InternalTrait {
fn world_default(self: @ContractState) -> dojo::world::WorldStorage {
self.world(@"my_namespace")
}
}
let position: Position = world.read_model(player);
world.write_model(@Position { player, vec: Vec2 { x: 10, y: 20 } });
Define events with #[dojo::event]:
#[derive(Copy, Drop, Serde)]
#[dojo::event]
pub struct PlayerMoved {
#[key]
pub player: ContractAddress,
pub from: Vec2,
pub to: Vec2,
}
// Emit in your function
world.emit_event(@PlayerMoved { player, from: old_pos, to: new_pos });
use starknet::get_caller_address;
let player = get_caller_address();
let entity_id = world.uuid();
Each system should have one clear purpose:
MovementSystem: Handles player/entity movementCombatSystem: Manages battles and damageInventorySystem: Manages itemsSystems should be stateless, reading state from models:
fn attack(ref self: ContractState, target: ContractAddress) {
let mut world = self.world_default();
let attacker = get_caller_address();
// Read current state
let attacker_stats: Combat = world.read_model(attacker);
let mut target_stats: Combat = world.read_model(target);
// Apply logic
target_stats.health -= attacker_stats.damage;
// Write updated state
world.write_model(@target_stats);
}
Validate inputs before modifying state:
fn move(ref self: ContractState, direction: Direction) {
let mut world = self.world_default();
let player = get_caller_address();
let moves: Moves = world.read_model(player);
assert(moves.remaining > 0, 'No moves remaining');
assert(moves.can_move, 'Movement disabled');
// Proceed with movement
}
Systems need writer permission to modify models.
Configure in dojo_dev.toml:
[writers]
"my_namespace" = ["my_namespace-actions"]
Or grant specific model access:
[writers]
"my_namespace-Position" = ["my_namespace-actions"]
"my_namespace-Moves" = ["my_namespace-actions"]
After creating systems:
dojo-test skill to test system logicdojo-review skill to check for issuesdojo-deploy skill to deploy your worlddojo-client skill to call systems from frontendnpx claudepluginhub dojoengine/bookGenerates Cairo structs as Dojo models for game state storage with ECS patterns, #[key] fields, and traits like Drop and Serde. Use for entities, components, or state.
Authors Cairo smart contracts on Starknet, including storage, events, interfaces, and OpenZeppelin component composition with security patterns.
Provides architecture patterns and best practices for browser games with Three.js or Phaser. Use for designing game systems, project structure, and code organization.