Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction

MIT licensed and AI-native

Build worlds where intelligent agents are a core system, not an afterthought.

A free, MIT-licensed Rust game engine where AI agents are a first-class runtime system. Deterministic simulation, tool-validated behavior, and Criterion-backed performance — inspectable, reproducible, and open.

MIT license Free to use, fork, and modify Built for engine developers
AstraWeave nebula logo
Scope Deterministic ECS, AI planning and validation, replay-safe simulation.
12,700+ agents at 60 FPS 977 Miri tests, 0 undefined behavior 71+ Kani proof harnesses
12,700+ validated agents at 60 FPS on modest consumer hardware
39,000+ workspace tests across 128 packages
977 Miri tests with zero undefined behavior
2.70 ms frame time at 1,000 entities in the current benchmark baseline
71+ Kani proof harnesses across safety-critical crates
Quick navigation

Jump straight to the area you need.

Why AstraWeave

Architecture, safety, and performance you can verify.

AI-first architecture

Perception → reasoning → planning → action, built into the runtime.

World snapshots, plan intents, tool validation, behavior trees, GOAP, and LLM-backed planning all plug into a deterministic loop.

Deterministic and safe

Bit-identical replay, validator-gated actions, and formal verification.

Core unsafe paths are exercised under Miri. Kani proofs back critical ECS, math, and SDK invariants. Replay validation is built in, not bolted on.

Benchmarked subsystems

Every performance claim links to a reproducible Criterion measurement.

ECS, AI planning, physics, rendering frame times, and SIMD throughput are all measured with specific numbers, not broad adjectives.

Engine loop

Perception → reasoning → planning → validation → action.

Perception World snapshots from deterministic ECS state.
Reasoning Behavior trees, utility systems, GOAP, or LLM logic.
Planning Action sequences with costs, priorities, and fallbacks.
Validation Cooldowns, LOS, pathing, and sandbox constraints.
Action Approved commands flow into simulation, physics, and rendering.
What ships today

A focused stack for intelligent, simulation-heavy games.

AI orchestration

Six validated modes plus hybrid arbiters.

Classical planners, behavior trees, utility logic, LLM orchestration, ensemble patterns, and hybrid arbiters.

Deterministic ECS

Ordered simulation for replay, tooling, and scale.

Archetype storage, system staging, iteration guarantees, and event channels form a reproducible simulation backbone.

Rendering

wgpu-based rendering with real engine workloads.

PBR materials, clustered lighting, GPU skinning, post-processing, and LOD tooling.

Physics and movement

Character control, spatial hashing, fluids, and more.

Collision, character motion, destructibles, ragdolls, vehicles, gravity zones, and fluid simulation.

Navigation and world systems

Navmesh, terrain, scene streaming, and gameplay layers.

Terrain generation, navigation meshes, crafting, quests, dialogue, and procedural content.

Tooling and integration

Example suite, editor tooling, and a C ABI for embedding.

A large example suite, editor workflows, and a stable C SDK layer.

Evidence-backed metrics

Current measurements from the codebase.

  • Agent capacity at 60 FPS12,700+
  • AI validation throughput6.48M checks/sec
  • Frame time at 1,000 entities2.70 ms
  • ECS world creation25.8 ns
  • Character move cost114 ns
  • SIMD batch over 10k entities9.879 us
Top Benchmarks · Latest Run
Criterion.rs statistical benchmarking · Feb 2026
Stable baseline
Bake 10K Triangles E2E Plan Gen · Cache Miss Cache Latency · 200ms Light Binning · High Bloom Upsample · Mip 0 Light Binning · 5K Cache Latency · 100ms Game Loop · 5K Stress
100 ms 200 ms 300 ms 400 ms 500 ms Bake 10K Triangles 548.16 ms E2E Plan Gen · Cache Miss 218.79 ms Cache Latency · 200ms 209.26 ms Light Binning · High 176.06 ms Bloom Upsample · Mip 0 172.55 ms Light Binning · 5K 113.34 ms Cache Latency · 100ms 108.52 ms Game Loop · 5K Stress 93.91 ms
Quality posture

Verification and testing across the stack.

  • Weighted line coverage59.3% across measured crates
  • High-coverage crates14 crates at 85%+
  • Miri validation977 tests, 0 UB
  • Kani verification71+ harnesses
  • Prompt mutation testing100% adjusted kill rate
  • Desktop targetsWindows, Linux, macOS
Use cases

Where AstraWeave fits best.

RPGs and immersive sims

Companions, directors, and systemic encounters.

Projects where NPCs need to observe, plan, and react with more depth than state-machine scripting allows.

Server-authoritative multiplayer

Validation and replayability matter.

Deterministic simulation and tool-gated actions for anti-cheat, reproducibility, and replay validation.

Research and prototyping

Benchmarkable AI-native architecture.

Test agent scale, planning strategies, and hybrid AI control under measurable conditions.

Embedded engine teams

Rust core with a C ABI.

Adopt focused subsystems through the modular crate structure instead of committing to the whole stack.

Design lineages

Games this engine could help realize.

Colony and world simulation

In the lineage of Dwarf Fortress or RimWorld.

Agent autonomy, world-state memory, logistics, and emergent story generation.

4X and grand strategy

In the lineage of Civilization or Crusader Kings.

Multi-agent diplomacy, advisor systems, strategic planners, and explainable AI.

Tactical command games

In the lineage of X-COM or Battle Brothers.

Tool validation, cover awareness, action planning, and replay-safe combat loops.

Systemic sandboxes

In the lineage of Kenshi or Mount and Blade.

Large numbers of autonomous actors with persistent world consequences.

Immersive sims and party RPGs

In the lineage of Deus Ex or Dragon Age.

Companion decisions, quest reactivity, systemic encounters, and director-style orchestration.

Rights-holder remakes

Licensed ports or original successors.

Rebuild ambitious systemic designs with modern AI-native architecture.

Next step

Clone, build, and decide from evidence.

AstraWeave is free and MIT licensed. Evaluate, adopt subsystems, or contribute back without platform lock-in.

Quick Start

Get up and running with AstraWeave in minutes! This guide will help you install the engine, run your first AI companion, and understand the basic concepts.

Prerequisites

  • Rust: 1.89.0+ (managed automatically via rust-toolchain.toml)
  • Platform: Linux, macOS, or Windows
  • GPU: Vulkan-compatible graphics card
  • Memory: 4GB+ RAM recommended for AI models

Installation

1. Clone the Repository

git clone https://github.com/lazyxeon/AstraWeave-AI-Native-Gaming-Engine.git
cd AstraWeave-AI-Native-Gaming-Engine

2. System Dependencies (Linux)

If you’re on Linux, install the required system packages:

sudo apt-get update
sudo apt-get install -y build-essential pkg-config cmake ninja-build \
  libx11-dev libxi-dev libxcursor-dev libxrandr-dev libxinerama-dev \
  libxkbcommon-dev libxkbcommon-x11-dev libx11-xcb-dev libxcb1-dev \
  libxcb-randr0-dev libxcb-xfixes0-dev libxcb-shape0-dev libxcb-xkb-dev \
  libgl1-mesa-dev libegl1-mesa-dev wayland-protocols libwayland-dev \
  libasound2-dev libpulse-dev libudev-dev mesa-vulkan-drivers vulkan-tools

3. Build Core Components

Build the stable, working core components:

cargo build -p astraweave-core -p astraweave-ai -p astraweave-physics \
            -p astraweave-nav -p astraweave-render -p hello_companion

This typically takes 8-15 seconds after initial dependency download.

Your First AI Companion

Let’s run the most basic example to see AstraWeave in action:

cargo run -p hello_companion --release

What You’ll See

The demo will show:

  1. AI Perception: The companion perceives the world state
  2. Planning: AI generates a plan using its understanding
  3. Tool Validation: The engine validates what the AI wants to do
  4. Expected Panic: The demo will panic with “LosBlocked” - this is expected behavior demonstrating the validation system

Example Output

[INFO] AI Companion initialized
[INFO] Perception snapshot captured: 1 entities
[INFO] Planning phase: generating intent for companion
[INFO] Generated plan: MoveTo(target_position)
[INFO] Validating tool usage: MovementTool
[ERROR] Validation failed: LosBlocked
thread 'main' panicked at 'LOS validation failed'

This panic is intentional! It demonstrates AstraWeave’s core principle: the AI can only do what the engine validates as possible.

Understanding What Happened

The hello_companion example showcases AstraWeave’s fundamental architecture:

  1. Fixed-Tick Simulation: The world runs at deterministic 60Hz
  2. AI Perception: AI agents receive structured world snapshots
  3. Planning Layer: AI generates intentions using LLM-based planning
  4. Tool Validation: Engine validates every AI action before execution
  5. Safety First: Invalid actions are rejected, maintaining game integrity

Next Steps

Now that you’ve seen the core loop in action:

Troubleshooting

Build Errors

If you encounter build errors:

  • Make sure you have the correct Rust version (check rust-toolchain.toml)
  • Install system dependencies for your platform
  • Some examples have known compilation issues - stick to the working core components listed above

Runtime Issues

  • Graphics errors: Ensure you have Vulkan drivers installed
  • Audio errors: Install audio system dependencies (ALSA/PulseAudio on Linux)
  • Permission errors: Make sure your user can access graphics and audio devices

For more help, see Troubleshooting.


🎉 Congratulations! You’ve successfully run your first AstraWeave AI companion. The engine validated the AI’s actions and maintained world integrity, just as it should.

Installation Guide

This guide covers detailed installation instructions for AstraWeave on different platforms, including all dependencies and troubleshooting common issues.

System Requirements

Minimum Requirements

  • CPU: x64 processor with SSE2 support
  • Memory: 4GB RAM (8GB+ recommended for AI models)
  • GPU: Vulkan 1.0 compatible graphics card
  • Storage: 2GB free space (more for AI models)
  • Rust: 1.89.0+ (managed via rust-toolchain.toml)
  • CPU: Multi-core x64 processor (4+ cores)
  • Memory: 16GB+ RAM for development and multiple AI models
  • GPU: Modern Vulkan 1.2+ compatible GPU with 2GB+ VRAM
  • Storage: SSD with 10GB+ free space

Platform-Specific Installation

Linux

Ubuntu/Debian

# Update package lists
sudo apt-get update

# Install build essentials
sudo apt-get install -y build-essential pkg-config cmake ninja-build

# Install graphics dependencies
sudo apt-get install -y libx11-dev libxi-dev libxcursor-dev libxrandr-dev \
  libxinerama-dev libxkbcommon-dev libxkbcommon-x11-dev libx11-xcb-dev \
  libxcb1-dev libxcb-randr0-dev libxcb-xfixes0-dev libxcb-shape0-dev \
  libxcb-xkb-dev

# Install rendering dependencies
sudo apt-get install -y libgl1-mesa-dev libegl1-mesa-dev wayland-protocols \
  libwayland-dev mesa-vulkan-drivers vulkan-tools

# Install audio dependencies  
sudo apt-get install -y libasound2-dev libpulse-dev

# Install additional system dependencies
sudo apt-get install -y libudev-dev

Arch Linux

# Install base development tools
sudo pacman -S base-devel cmake ninja

# Install graphics and audio
sudo pacman -S vulkan-devel mesa alsa-lib libpulse wayland wayland-protocols

# Install X11 dependencies
sudo pacman -S libx11 libxcb libxrandr libxinerama libxcursor libxi

Fedora/RHEL

# Install development tools
sudo dnf groupinstall "Development Tools"
sudo dnf install cmake ninja-build pkg-config

# Install graphics dependencies
sudo dnf install libX11-devel libXi-devel libXcursor-devel libXrandr-devel \
  libXinerama-devel libxkbcommon-devel libxkbcommon-x11-devel

# Install Vulkan and Mesa
sudo dnf install vulkan-devel mesa-dri-drivers

# Install audio
sudo dnf install alsa-lib-devel pulseaudio-libs-devel

macOS

Prerequisites

First, install Xcode Command Line Tools:

xcode-select --install
# Install Homebrew if not already installed
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

# Install dependencies
brew install cmake ninja pkg-config

# For Intel Macs, ensure MoltenVK is installed
brew install molten-vk

Manual Installation

  • Download and install Xcode from the App Store
  • Install CMake from cmake.org
  • Ensure MoltenVK is available for Vulkan support

Windows

  1. Install Visual Studio 2019 or later with C++ build tools
  2. Install Git for Windows
  3. Install CMake (either standalone or via Visual Studio Installer)

Using MSYS2/MinGW

# Install MSYS2 from https://www.msys2.org/
# Then in MSYS2 terminal:
pacman -S mingw-w64-x86_64-cmake mingw-w64-x86_64-ninja
pacman -S mingw-w64-x86_64-vulkan-devel

Rust Installation

AstraWeave uses a specific Rust version defined in rust-toolchain.toml. The installation process will automatically use the correct version.

Install Rust

# Install rustup (Rust installer)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# Follow the prompts, then restart your terminal or run:
source ~/.cargo/env

# Verify installation
rustc --version
cargo --version

Rust Components

The following components will be installed automatically when needed:

  • cargo - Package manager and build tool
  • clippy - Linter for catching common mistakes
  • rustfmt - Code formatter
  • rust-analyzer - Language server for IDE support

Clone and Build

1. Clone the Repository

git clone https://github.com/lazyxeon/AstraWeave-AI-Native-Gaming-Engine.git
cd AstraWeave-AI-Native-Gaming-Engine

2. Verify Rust Toolchain

The correct Rust version will be installed automatically:

# This will show the version from rust-toolchain.toml
rustc --version

3. Build Core Components

Start with the stable, working components:

cargo build -p astraweave-core -p astraweave-ai -p astraweave-physics \
            -p astraweave-nav -p astraweave-render -p hello_companion

4. Run Tests

Verify the installation with tests:

cargo test -p astraweave-input

5. Run Example

Test the installation:

cargo run -p hello_companion --release

Verification

Check GPU Support

# Linux: Check Vulkan
vulkaninfo | grep "deviceName"

# macOS: Check Metal
system_profiler SPDisplaysDataType

# Windows: Use dxdiag or GPU-Z

Check Audio

# Linux: Test audio devices
aplay -l

# macOS: Check audio
system_profiler SPAudioDataType  

# Windows: Check audio devices in Device Manager

Development Environment Setup

IDE Recommendations

Install these extensions:

  • rust-analyzer - Rust language support
  • CodeLLDB - Debugging support
  • Even Better TOML - TOML file support
  • Error Lens - Inline error display

Other IDEs

  • CLion: Has good Rust support with the Rust plugin
  • Vim/Neovim: Use with rust-analyzer LSP
  • Emacs: Use with rust-analyzer and rustic-mode

Performance Considerations

Release Builds

For better performance during development:

# Always use release mode for examples
cargo run -p hello_companion --release

# Build in release mode
cargo build --release

Parallel Compilation

Speed up builds by using multiple CPU cores:

# Set in ~/.cargo/config.toml
[build]
jobs = 4  # or number of CPU cores

Target Directory

Use a shared target directory to reduce disk usage:

# Set CARGO_TARGET_DIR environment variable
export CARGO_TARGET_DIR=/path/to/shared/target

Troubleshooting

Common Build Errors

“linker not found”

  • Linux: Install build-essential or gcc
  • macOS: Install Xcode Command Line Tools
  • Windows: Install Visual Studio with C++ tools

Vulkan errors

  • Linux: Install mesa-vulkan-drivers and vulkan-tools
  • macOS: Ensure MoltenVK is installed
  • Windows: Update graphics drivers

Audio errors

  • Linux: Install libasound2-dev and libpulse-dev
  • macOS: Usually works out of the box
  • Windows: Ensure Windows Audio service is running

Performance Issues

Slow Compilation

  • Use cargo build --release for better runtime performance
  • Consider using sccache to cache compilation results
  • Increase parallel build jobs in Cargo config

Runtime Performance

  • Always use --release flag for examples and demos
  • Ensure GPU drivers are up to date
  • Check system has adequate RAM (4GB minimum)

Platform-Specific Issues

Linux Wayland vs X11

AstraWeave supports both Wayland and X11:

# Force X11 if needed
export WAYLAND_DISPLAY=""

# Force Wayland if needed  
export DISPLAY=""

macOS Code Signing

For distribution on macOS, you may need to sign binaries:

codesign --force --deep --sign - target/release/hello_companion

Windows Antivirus

Some antivirus software may flag Rust binaries. Add exclusions for:

  • The project directory
  • ~/.cargo directory
  • target/ build directory

Next Steps

With AstraWeave installed:

  1. Run through the Quick Start Guide
  2. Explore Working Examples
  3. Read about Architecture
  4. Build Your First Game

For ongoing development, see the Contributing Guide and Building from Source.

Your First AI Companion

This tutorial will guide you through creating your first AI companion in AstraWeave. By the end, you’ll have a companion that can perceive its environment, experience emotions, and exhibit autonomous behaviors.

Table of Contents

Prerequisites

Before starting, ensure you have:

New to Rust? Check out [The Rust Book](https://doc.rust-lang.org/book/) for a comprehensive introduction.

Project Setup

Create a New Project

# Create a new Rust project
cargo new my_first_companion
cd my_first_companion

Add AstraWeave Dependencies

Edit Cargo.toml to add AstraWeave dependencies:

[package]
name = "my_first_companion"
version = "0.1.0"
edition = "2021"

[dependencies]
# Core AstraWeave crates
astraweave-ai = "0.1.0"
astraweave-render = "0.1.0"
bevy = "0.12"

# Utilities
anyhow = "1.0"
Version numbers may change. Check [crates.io](https://crates.io) for the latest versions.

Verify Setup

# Build to ensure dependencies are resolved
cargo build

Creating a Basic Companion

Let’s start with a simple companion that just exists in the world.

Basic Structure

Edit src/main.rs:

use bevy::prelude::*;
use astraweave_ai::prelude::*;
use anyhow::Result;

fn main() -> Result<()> {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_plugins(AstraWeaveAIPlugin)
        .add_systems(Startup, setup_companion)
        .add_systems(Update, update_companion)
        .run();
    
    Ok(())
}

fn setup_companion(mut commands: Commands) {
    // Create a basic companion
    let companion = CompanionBuilder::new()
        .with_name("Buddy")
        .build();
    
    commands.spawn(companion);
    
    info!("Spawned companion: Buddy");
}

fn update_companion(
    mut companions: Query<&mut Companion>,
    time: Res<Time>,
) {
    for mut companion in companions.iter_mut() {
        companion.update(time.delta_seconds());
    }
}

Run Your First Companion

cargo run

You should see log output indicating your companion was spawned!

Congratulations! You've created your first AI companion. Now let's make it more interesting.

Adding Perception

Companions need to perceive their environment to interact meaningfully.

Visual Perception

Add visual perception to detect nearby entities:

#![allow(unused)]
fn main() {
use astraweave_ai::perception::*;

fn setup_companion(mut commands: Commands) {
    let companion = CompanionBuilder::new()
        .with_name("Buddy")
        .with_perception(PerceptionConfig {
            visual_range: 10.0,
            visual_fov: 120.0,      // 120-degree field of view
            update_rate: 10.0,       // 10 updates per second
            ..default()
        })
        .build();
    
    commands.spawn(companion);
}
}

Adding Stimuli

Create entities that the companion can perceive:

#![allow(unused)]
fn main() {
fn setup_companion(
    mut commands: Commands,
    mut meshes: ResMut<Assets<Mesh>>,
    mut materials: ResMut<Assets<StandardMaterial>>,
) {
    // Spawn the companion
    let companion = CompanionBuilder::new()
        .with_name("Buddy")
        .with_perception(PerceptionConfig {
            visual_range: 10.0,
            visual_fov: 120.0,
            update_rate: 10.0,
            ..default()
        })
        .build();
    
    commands.spawn((
        companion,
        Transform::from_xyz(0.0, 0.0, 0.0),
    ));
    
    // Spawn a ball that the companion can see
    commands.spawn((
        PbrBundle {
            mesh: meshes.add(Sphere::new(0.5)),
            material: materials.add(Color::rgb(0.8, 0.2, 0.2)),
            transform: Transform::from_xyz(3.0, 0.5, 0.0),
            ..default()
        },
        Stimulus::new(StimulusType::Visual, 1.0),
    ));
    
    // Add camera
    commands.spawn(Camera3dBundle {
        transform: Transform::from_xyz(0.0, 5.0, 10.0)
            .looking_at(Vec3::ZERO, Vec3::Y),
        ..default()
    });
    
    // Add light
    commands.spawn(PointLightBundle {
        point_light: PointLight {
            intensity: 1500.0,
            shadows_enabled: true,
            ..default()
        },
        transform: Transform::from_xyz(4.0, 8.0, 4.0),
        ..default()
    });
}
}

Reacting to Perception

Add a system to log what the companion perceives:

#![allow(unused)]
fn main() {
fn update_companion(
    mut companions: Query<(&mut Companion, &Transform)>,
    stimuli: Query<(&Stimulus, &Transform)>,
    time: Res<Time>,
) {
    for (mut companion, companion_transform) in companions.iter_mut() {
        companion.update(time.delta_seconds());
        
        // Check what the companion can see
        if let Some(perception) = companion.perception() {
            for (stimulus, stimulus_transform) in stimuli.iter() {
                let distance = companion_transform
                    .translation
                    .distance(stimulus_transform.translation);
                
                if distance <= perception.visual_range {
                    info!(
                        "{} sees {} stimulus at distance {:.2}",
                        companion.name(),
                        stimulus.stimulus_type,
                        distance
                    );
                }
            }
        }
    }
}
}

Adding Emotions

Emotions make companions feel alive and responsive.

Emotion System

Add emotion processing to your companion:

#![allow(unused)]
fn main() {
use astraweave_ai::emotion::*;

fn setup_companion(
    mut commands: Commands,
    mut meshes: ResMut<Assets<Mesh>>,
    mut materials: ResMut<Assets<StandardMaterial>>,
) {
    let companion = CompanionBuilder::new()
        .with_name("Buddy")
        .with_perception(PerceptionConfig {
            visual_range: 10.0,
            visual_fov: 120.0,
            update_rate: 10.0,
            ..default()
        })
        .with_emotions(vec![
            EmotionConfig::new("joy", 0.5, 0.95),
            EmotionConfig::new("curiosity", 0.3, 0.98),
            EmotionConfig::new("calm", 0.7, 0.99),
        ])
        .build();
    
    commands.spawn((
        companion,
        Transform::from_xyz(0.0, 0.0, 0.0),
    ));
    
    // ... spawn stimuli and camera as before
}
}

Emotion Responses

Make emotions respond to stimuli:

fn process_emotions(
    mut companions: Query<&mut Companion>,
    stimuli: Query<(&Stimulus, &Transform)>,
) {
    for mut companion in companions.iter_mut() {
        if let Some(emotion_system) = companion.emotion_system_mut() {
            // Process nearby stimuli
            for (stimulus, _) in stimuli.iter() {
                match stimulus.stimulus_type {
                    StimulusType::Visual => {
                        emotion_system.increase_emotion("curiosity", 0.1);
                    }
                    StimulusType::Positive => {
                        emotion_system.increase_emotion("joy", 0.2);
                    }
                    StimulusType::Negative => {
                        emotion_system.decrease_emotion("calm", 0.15);
                    }
                    _ => {}
                }
            }
            
            // Log dominant emotion
            if let Some(dominant) = emotion_system.dominant_emotion() {
                info!(
                    "{} feels {} (intensity: {:.2})",
                    companion.name(),
                    dominant.name,
                    dominant.intensity
                );
            }
        }
    }
}

// Add this system to your app
fn main() -> Result<()> {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_plugins(AstraWeaveAIPlugin)
        .add_systems(Startup, setup_companion)
        .add_systems(Update, (update_companion, process_emotions))
        .run();
    
    Ok(())
}

Adding Behaviors

Behaviors allow companions to act autonomously based on their internal state.

Basic Movement Behavior

Add a simple behavior that makes the companion wander:

#![allow(unused)]
fn main() {
use astraweave_ai::behavior::*;

fn setup_companion(
    mut commands: Commands,
    mut meshes: ResMut<Assets<Mesh>>,
    mut materials: ResMut<Assets<StandardMaterial>>,
) {
    let companion = CompanionBuilder::new()
        .with_name("Buddy")
        .with_perception(PerceptionConfig::default())
        .with_emotions(vec![
            EmotionConfig::new("joy", 0.5, 0.95),
            EmotionConfig::new("curiosity", 0.3, 0.98),
            EmotionConfig::new("calm", 0.7, 0.99),
        ])
        .with_behavior(BehaviorConfig {
            wander_speed: 2.0,
            wander_radius: 5.0,
            ..default()
        })
        .build();
    
    commands.spawn((
        companion,
        PbrBundle {
            mesh: meshes.add(Capsule3d::new(0.5, 1.0)),
            material: materials.add(Color::rgb(0.3, 0.5, 0.8)),
            transform: Transform::from_xyz(0.0, 0.5, 0.0),
            ..default()
        },
    ));
    
    // ... rest of scene setup
}
}

Emotion-Driven Behavior

Make behavior change based on emotions:

fn emotion_driven_behavior(
    mut companions: Query<(&mut Companion, &mut Transform)>,
    time: Res<Time>,
) {
    for (mut companion, mut transform) in companions.iter_mut() {
        if let Some(emotion_system) = companion.emotion_system() {
            let curiosity = emotion_system
                .get_emotion("curiosity")
                .map(|e| e.intensity)
                .unwrap_or(0.0);
            
            let calm = emotion_system
                .get_emotion("calm")
                .map(|e| e.intensity)
                .unwrap_or(0.0);
            
            // High curiosity = move around more
            if curiosity > 0.6 {
                let speed = 2.0 * curiosity;
                let direction = Vec3::new(
                    (time.elapsed_seconds() * 0.5).sin(),
                    0.0,
                    (time.elapsed_seconds() * 0.5).cos(),
                );
                
                transform.translation += direction * speed * time.delta_seconds();
                
                info!("{} is exploring curiously!", companion.name());
            }
            // High calm = stay still
            else if calm > 0.7 {
                info!("{} is resting calmly.", companion.name());
            }
        }
    }
}

// Update main to include the behavior system
fn main() -> Result<()> {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_plugins(AstraWeaveAIPlugin)
        .add_systems(Startup, setup_companion)
        .add_systems(Update, (
            update_companion,
            process_emotions,
            emotion_driven_behavior,
        ))
        .run();
    
    Ok(())
}

Testing Your Companion

Complete Example

Here’s the full code with all features:

use bevy::prelude::*;
use astraweave_ai::prelude::*;
use anyhow::Result;

fn main() -> Result<()> {
    App::new()
        .add_plugins(DefaultPlugins.set(WindowPlugin {
            primary_window: Some(Window {
                title: "My First Companion".to_string(),
                resolution: (1280.0, 720.0).into(),
                ..default()
            }),
            ..default()
        }))
        .add_plugins(AstraWeaveAIPlugin)
        .add_systems(Startup, setup_companion)
        .add_systems(Update, (
            update_companion,
            process_emotions,
            emotion_driven_behavior,
        ))
        .run();
    
    Ok(())
}

fn setup_companion(
    mut commands: Commands,
    mut meshes: ResMut<Assets<Mesh>>,
    mut materials: ResMut<Assets<StandardMaterial>>,
) {
    // Create companion
    let companion = CompanionBuilder::new()
        .with_name("Buddy")
        .with_perception(PerceptionConfig {
            visual_range: 10.0,
            visual_fov: 120.0,
            update_rate: 10.0,
            ..default()
        })
        .with_emotions(vec![
            EmotionConfig::new("joy", 0.5, 0.95),
            EmotionConfig::new("curiosity", 0.3, 0.98),
            EmotionConfig::new("calm", 0.7, 0.99),
        ])
        .with_behavior(BehaviorConfig::default())
        .build();
    
    commands.spawn((
        companion,
        PbrBundle {
            mesh: meshes.add(Capsule3d::new(0.5, 1.0)),
            material: materials.add(Color::rgb(0.3, 0.5, 0.8)),
            transform: Transform::from_xyz(0.0, 0.5, 0.0),
            ..default()
        },
    ));
    
    // Add ground plane
    commands.spawn(PbrBundle {
        mesh: meshes.add(Plane3d::new(Vec3::Y, Vec2::new(20.0, 20.0))),
        material: materials.add(Color::rgb(0.3, 0.5, 0.3)),
        ..default()
    });
    
    // Add interactive objects
    for i in 0..5 {
        let angle = (i as f32 / 5.0) * std::f32::consts::TAU;
        let pos = Vec3::new(angle.cos() * 4.0, 0.5, angle.sin() * 4.0);
        
        commands.spawn((
            PbrBundle {
                mesh: meshes.add(Sphere::new(0.5)),
                material: materials.add(Color::rgb(0.8, 0.2, 0.2)),
                transform: Transform::from_translation(pos),
                ..default()
            },
            Stimulus::new(StimulusType::Visual, 1.0),
        ));
    }
    
    // Add camera
    commands.spawn(Camera3dBundle {
        transform: Transform::from_xyz(0.0, 8.0, 12.0)
            .looking_at(Vec3::ZERO, Vec3::Y),
        ..default()
    });
    
    // Add light
    commands.spawn(PointLightBundle {
        point_light: PointLight {
            intensity: 1500.0,
            shadows_enabled: true,
            ..default()
        },
        transform: Transform::from_xyz(4.0, 8.0, 4.0),
        ..default()
    });
}

fn update_companion(
    mut companions: Query<&mut Companion>,
    time: Res<Time>,
) {
    for mut companion in companions.iter_mut() {
        companion.update(time.delta_seconds());
    }
}

fn process_emotions(
    mut companions: Query<(&mut Companion, &Transform)>,
    stimuli: Query<(&Stimulus, &Transform)>,
) {
    for (mut companion, companion_transform) in companions.iter_mut() {
        if let Some(emotion_system) = companion.emotion_system_mut() {
            for (stimulus, stimulus_transform) in stimuli.iter() {
                let distance = companion_transform
                    .translation
                    .distance(stimulus_transform.translation);
                
                if distance <= 5.0 {
                    emotion_system.increase_emotion("curiosity", 0.05);
                    emotion_system.increase_emotion("joy", 0.02);
                }
            }
        }
    }
}

fn emotion_driven_behavior(
    mut companions: Query<(&mut Companion, &mut Transform)>,
    time: Res<Time>,
) {
    for (companion, mut transform) in companions.iter_mut() {
        if let Some(emotion_system) = companion.emotion_system() {
            let curiosity = emotion_system
                .get_emotion("curiosity")
                .map(|e| e.intensity)
                .unwrap_or(0.0);
            
            if curiosity > 0.4 {
                let speed = 1.5 * curiosity;
                let direction = Vec3::new(
                    (time.elapsed_seconds() * 0.8).sin(),
                    0.0,
                    (time.elapsed_seconds() * 0.8).cos(),
                );
                
                transform.translation += direction * speed * time.delta_seconds();
                transform.translation.y = 0.5; // Keep on ground
            }
        }
    }
}

Run and Observe

cargo run --release

Watch your companion:

  • Wander around the scene
  • React to nearby objects
  • Show emotional responses
  • Exhibit autonomous behavior
You've created a fully functional AI companion with perception, emotions, and behaviors!

Next Steps

Now that you have a working companion, try:

  1. Add more emotions: Experiment with fear, excitement, anger
  2. Complex behaviors: Implement approach/avoid behaviors based on emotions
  3. Social interactions: Add multiple companions that interact
  4. Save/Load: Persist companion state between runs
  5. Visual feedback: Change companion color based on dominant emotion

Advanced Topics

Example Projects

Check out more examples in the repository:

# Clone AstraWeave
git clone https://github.com/verdentlabs/astraweave.git
cd astraweave

# Run advanced examples
cargo run --release --example companion_emotions
cargo run --release --example multi_companion
cargo run --release --example behavior_showcase

Join the Community

Share your companion creations in our Discord showcase channel!

Happy companion building!

System Requirements

This page outlines the hardware and software requirements for developing and running AstraWeave applications.

Table of Contents

Minimum Requirements

These are the bare minimum specifications to run AstraWeave applications:

Hardware

ComponentSpecification
CPUQuad-core processor (Intel i5-6600K / AMD Ryzen 3 1300X or equivalent)
RAM8 GB
GPUNVIDIA GTX 1060 (6GB) / AMD RX 580 (8GB) / Intel Arc A380
VRAM4 GB
Storage2 GB available space (SSD recommended)
Display1920x1080 resolution
Minimum requirements may result in reduced performance. For optimal experience, see recommended specifications.

Operating System

  • Windows: Windows 10 (64-bit) version 1909 or later
  • Linux: Any modern distribution with kernel 5.4+ (Ubuntu 20.04, Fedora 33, etc.)
  • macOS: macOS 10.15 (Catalina) or later

For optimal performance and development experience:

Hardware

ComponentSpecification
CPU8-core processor (Intel i7-9700K / AMD Ryzen 7 3700X or better)
RAM16 GB or more
GPUNVIDIA RTX 3060 / AMD RX 6700 XT / Intel Arc A750 or better
VRAM8 GB or more
Storage10 GB available space on NVMe SSD
Display2560x1440 or higher resolution

Operating System

  • Windows: Windows 11 (64-bit)
  • Linux: Ubuntu 22.04 LTS / Fedora 38 or later
  • macOS: macOS 13 (Ventura) or later
More RAM and VRAM allow for larger scenes, more AI companions, and higher-quality assets.

GPU Requirements

AstraWeave is GPU-accelerated and requires modern graphics hardware with compute shader support.

Graphics API Support

Your GPU must support one of the following:

PlatformGraphics APIMinimum Version
WindowsDirectX 12Feature Level 12.0
Windows/LinuxVulkan1.2
macOSMetalMetal 2

Supported GPUs

NVIDIA

Minimum:

  • GTX 1060 (6GB)
  • GTX 1070
  • GTX 1660 Ti

Recommended:

  • RTX 3060 or better
  • RTX 4060 or better
  • RTX A2000 or better (workstation)

Optimal:

  • RTX 4070 Ti or better
  • RTX 4080 / 4090
  • RTX A4000 or better (workstation)

AMD

Minimum:

  • RX 580 (8GB)
  • RX 5500 XT (8GB)
  • RX 6600

Recommended:

  • RX 6700 XT or better
  • RX 7600 XT or better

Optimal:

  • RX 7800 XT or better
  • RX 7900 XTX
  • Radeon Pro W6800 or better (workstation)

Intel

Minimum:

  • Arc A380

Recommended:

  • Arc A750 or better
  • Arc A770

Optimal:

  • Arc A770 (16GB)
Intel Arc GPUs require the latest drivers for optimal performance. Visit [Intel's driver page](https://www.intel.com/content/www/us/en/download/726609/intel-arc-graphics-windows-dch-driver.html) for updates.

Integrated Graphics

Integrated GPUs are not recommended but may work for basic development:

  • Intel Iris Xe (11th gen or later)
  • AMD Radeon Graphics (Ryzen 5000 series or later)
Integrated graphics will have significant performance limitations. Dedicated GPU strongly recommended.

Verifying GPU Support

Windows

# Check DirectX version
dxdiag

# Check GPU with PowerShell
Get-WmiObject Win32_VideoController | Select-Object Name, DriverVersion

Linux

# Check Vulkan support
vulkaninfo | grep "deviceName"

# Check GPU details
lspci | grep -i vga
nvidia-smi  # For NVIDIA GPUs

macOS

# Check Metal support
system_profiler SPDisplaysDataType

Software Dependencies

Development Dependencies

Required

  • Rust: Version 1.75.0 or later

    • Install via rustup
    • Verify: rustc --version
  • Git: Any recent version

Platform-Specific

Windows
  • Visual Studio Build Tools 2022 or Visual Studio 2022

  • Vulkan SDK (optional, for Vulkan backend)

Linux

Debian/Ubuntu:

sudo apt install -y \
  build-essential \
  pkg-config \
  libx11-dev \
  libxcursor-dev \
  libxrandr-dev \
  libxi-dev \
  libasound2-dev \
  libudev-dev \
  vulkan-tools \
  libvulkan-dev

Fedora/RHEL:

sudo dnf install -y \
  gcc gcc-c++ \
  pkg-config \
  libX11-devel \
  libXcursor-devel \
  libXrandr-devel \
  libXi-devel \
  alsa-lib-devel \
  systemd-devel \
  vulkan-tools \
  vulkan-loader-devel

Arch Linux:

sudo pacman -S --needed \
  base-devel \
  libx11 \
  libxcursor \
  libxrandr \
  libxi \
  alsa-lib \
  vulkan-tools \
  vulkan-headers
macOS
  • Xcode Command Line Tools

    xcode-select --install
    
  • Homebrew (recommended for package management)

    /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
    

GPU Drivers

Always use the latest drivers for optimal performance:

NVIDIA

  • Download from NVIDIA Driver Downloads
  • Linux: Use distribution’s package manager or NVIDIA’s .run installer
  • Minimum driver version: 525.60.11 (Linux) / 528.24 (Windows)

AMD

  • Windows: AMD Software Adrenalin Edition
  • Linux: Mesa drivers (usually pre-installed)
    # Ubuntu/Debian
    sudo apt install mesa-vulkan-drivers
    
    # Fedora
    sudo dnf install mesa-vulkan-drivers
    

Intel

Platform-Specific Notes

Windows

  • Windows Defender: May slow initial builds. Add exclusion for project directory:

    Settings > Windows Security > Virus & threat protection > Exclusion settings
    
  • Long Path Support: Enable for deep project structures:

    # Run as Administrator
    New-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem" `
      -Name "LongPathsEnabled" -Value 1 -PropertyType DWORD -Force
    
  • Power Settings: Use “High Performance” or “Ultimate Performance” power plan for best results

Linux

  • GPU Permissions: Ensure user is in the video group:

    sudo usermod -a -G video $USER
    # Log out and back in
    
  • Wayland: Some features work better with X11. Switch if experiencing issues:

    # Edit /etc/gdm3/custom.conf (Ubuntu/Debian)
    # Uncomment: WaylandEnable=false
    
  • Memory Limits: Increase limits for large projects:

    # Add to ~/.bashrc
    ulimit -n 4096
    

macOS

  • Metal Support: Requires macOS 10.15+ and Metal-compatible GPU

    • Check: system_profiler SPDisplaysDataType | grep Metal
  • Apple Silicon (M1/M2/M3): Fully supported with native ARM builds

    # Verify ARM toolchain
    rustc --version --verbose | grep host
    # Should show: aarch64-apple-darwin
    
  • Rosetta 2: Not required for M1/M2/M3 Macs (native ARM support)

  • Security Settings: May need to allow apps in System Preferences on first run

Performance Expectations

Development Workloads

SpecificationCompile Time (full rebuild)Test Suite RuntimeEditor Performance
Minimum5-10 minutes2-3 minutesPlayable (30+ FPS)
Recommended2-4 minutes1-2 minutesSmooth (60+ FPS)
Optimal1-2 minutes<1 minuteExcellent (120+ FPS)

Runtime Performance

Example scene: 10 AI companions with full perception and emotion systems

SpecificationAverage FPS1% Low FPSMax Companions
Minimum30-4525-3010-20
Recommended60-9050-6050-100
Optimal120-144+90-100200-500+
Performance varies based on scene complexity, AI behavior count, and graphics settings.

Checking Your System

Run this command to verify your setup:

# Clone and run the system check example
git clone https://github.com/verdentlabs/astraweave.git
cd astraweave
cargo run --example system_check

The system check will report:

  • Rust version
  • GPU capabilities
  • Graphics API support
  • Available memory
  • Recommended settings
If all checks pass, your system is ready for AstraWeave development!

Upgrading Your System

If your system doesn’t meet requirements:

Priority Upgrades

  1. GPU: Biggest impact on runtime performance
  2. RAM: Enables larger scenes and faster compilation
  3. SSD: Dramatically reduces build times
  4. CPU: Improves compilation speed and AI performance

Budget Recommendations

  • Entry Level ($300-500): Used RTX 3060 or RX 6700 XT + 16GB RAM
  • Mid Range ($800-1200): RTX 4060 Ti or RX 7700 XT + 32GB RAM + NVMe SSD
  • High End ($2000+): RTX 4080 or RX 7900 XTX + 64GB RAM + Fast CPU

Next Steps

Once your system meets the requirements:

  1. Install AstraWeave
  2. Build from source
  3. Create your first companion

If you have questions about requirements, join our Discord community.

Architecture Overview

AstraWeave represents a fundamental shift in game engine design: instead of treating AI as an afterthought, it places intelligent agents at the core of the architecture. This document explains the key architectural decisions and how they enable truly AI-native gameplay.

Core Philosophy: AI-First Design

Traditional game engines follow this pattern:

Game Logic → AI System → Scripted Behaviors

AstraWeave inverts this relationship:

AI Agents ← Tool Validation ← Engine Authority

Why This Matters

  1. No Cheating AI: AI can only act through validated game systems
  2. Emergent Behavior: Complex interactions emerge from simple, validated tools
  3. Multiplayer Ready: Same validation works for human players and AI
  4. Predictable Performance: Deterministic simulation enables reliable testing

High-Level Architecture

graph TB
    subgraph "Fixed-Tick Simulation (60Hz)"
        ECS[ECS World]
        Physics[Physics System]
        Audio[Audio System]
        Render[Render System]
    end
    
    subgraph "AI Pipeline"
        Perception[Perception Bus]
        Planning[AI Planning]
        Tools[Tool Sandbox]
        Validation[Engine Validation]
    end
    
    subgraph "Game Layer"
        Gameplay[Gameplay Systems]
        Content[Content Systems]
        UI[UI Systems]
    end
    
    ECS --> Perception
    Perception --> Planning
    Planning --> Tools
    Tools --> Validation
    Validation --> ECS
    
    ECS --> Physics
    ECS --> Audio
    ECS --> Render
    
    Gameplay --> ECS
    Content --> ECS
    UI --> ECS

The Deterministic Core

Fixed-Tick Simulation

AstraWeave runs the simulation at exactly 60Hz, regardless of rendering framerate:

#![allow(unused)]
fn main() {
const TICK_RATE: f64 = 60.0;
const TICK_DURATION: Duration = Duration::from_nanos(16_666_667); // 1/60 second

// Simulation always advances by exactly this amount
fn tick_simulation(&mut self) {
    self.world.step(TICK_DURATION);
}
}

Benefits:

  • Deterministic physics and AI behavior
  • Consistent timing across different hardware
  • Reliable networking and replay systems
  • Predictable performance testing

Entity-Component-System (ECS)

AstraWeave uses an archetype-based ECS for cache-friendly performance:

#![allow(unused)]
fn main() {
// Components are pure data
#[derive(Component)]
struct Position(Vec3);

#[derive(Component)]  
struct AIAgent {
    perception_range: f32,
    planning_cooldown: Duration,
}

// Systems operate on component combinations
fn ai_perception_system(
    query: Query<(&Position, &AIAgent, &mut PerceptionState)>
) {
    for (pos, agent, mut perception) in query.iter_mut() {
        // Update agent perception based on world state
    }
}
}

Key Benefits:

  • Cache-friendly data layout
  • Parallel system execution
  • Clean separation of data and logic
  • Easy to reason about and test

The AI Pipeline

1. Perception Bus

AI agents receive structured snapshots of the world state:

#![allow(unused)]
fn main() {
#[derive(Serialize, Deserialize)]
pub struct PerceptionSnapshot {
    pub timestamp: u64,
    pub agent_id: EntityId,
    pub visible_entities: Vec<EntityData>,
    pub audio_events: Vec<AudioEvent>,
    pub world_state: WorldState,
}
}

Design Principles:

  • Filtered Information: Agents only perceive what they should be able to see/hear
  • Structured Data: JSON-serializable for easy AI model consumption
  • Temporal Consistency: Snapshots include timing information
  • Bandwidth Efficient: Only relevant changes are included

2. Planning Layer

AI models generate plans using the perception data:

#![allow(unused)]
fn main() {
pub struct AIPlan {
    pub agent_id: EntityId,
    pub intent: Intent,
    pub tools: Vec<ToolUsage>,
    pub confidence: f32,
    pub reasoning: String, // For debugging and learning
}

pub enum Intent {
    MoveTo { target: Vec3, urgency: f32 },
    AttackTarget { target: EntityId, weapon: Option<EntityId> },
    Interact { target: EntityId, interaction_type: String },
    Communicate { target: EntityId, message: String },
}
}

Key Features:

  • High-Level Intents: AI thinks in terms of goals, not implementation
  • Tool-Based Actions: All actions go through validated tools
  • Confidence Scoring: Enables dynamic difficulty and behavior tuning
  • Explainable Reasoning: For debugging and player understanding

3. Tool Validation

Every AI action must be validated by the engine:

#![allow(unused)]
fn main() {
pub trait Tool {
    fn validate(&self, world: &World, usage: &ToolUsage) -> ValidationResult;
    fn execute(&self, world: &mut World, usage: &ToolUsage) -> ExecutionResult;
}

// Example: Movement tool
impl Tool for MovementTool {
    fn validate(&self, world: &World, usage: &ToolUsage) -> ValidationResult {
        let agent = world.get::<Position>(usage.agent_id)?;
        let target = usage.target_position;
        
        // Check line of sight
        if !world.line_of_sight(agent.0, target) {
            return ValidationResult::Blocked(BlockReason::LineOfSight);
        }
        
        // Check movement cooldown
        if !self.cooldown_ready(usage.agent_id) {
            return ValidationResult::Blocked(BlockReason::Cooldown);
        }
        
        ValidationResult::Valid
    }
    
    fn execute(&self, world: &mut World, usage: &ToolUsage) -> ExecutionResult {
        // Actually perform the movement
        world.get_mut::<Position>(usage.agent_id)?.0 = usage.target_position;
        ExecutionResult::Success
    }
}
}

Validation Categories:

  • Physics Constraints: Can the action physically happen?
  • Resource Requirements: Does the agent have what’s needed?
  • Cooldowns: Is the action available now?
  • Line of Sight: Can the agent see the target?
  • Game Rules: Does this follow the game’s rules?

Networking Architecture

Server Authority

AstraWeave uses server-authoritative validation for multiplayer:

Client AI → Intent → Server Validation → World Update → State Broadcast

Benefits:

  • No client-side cheating possible
  • Consistent world state across all clients
  • AI agents validated same as human players
  • Deterministic simulation enables easy rollback

Intent Replication

Instead of replicating low-level actions, AstraWeave replicates high-level intents:

#![allow(unused)]
fn main() {
#[derive(Serialize, Deserialize)]
pub struct NetworkIntent {
    pub player_id: PlayerId,
    pub intent: Intent,
    pub timestamp: u64,
    pub predicted_outcome: Option<PredictedResult>,
}
}

Advantages:

  • Lower bandwidth than action replication
  • Natural lag compensation through prediction
  • AI and human intents handled identically
  • Easy to implement anti-cheat

Performance Architecture

CPU Performance

  • ECS Archetype Iteration: Cache-friendly component access
  • Parallel Systems: Independent systems run in parallel
  • Incremental Updates: Only changed data is processed
  • Fixed Timestep: Predictable CPU load

Memory Management

  • Pool Allocation: Entities and components use object pools
  • Streaming: World chunks loaded/unloaded based on relevance
  • Compression: Perception data compressed before AI processing
  • Garbage Collection: Minimal allocations in hot paths

GPU Utilization

  • Deferred Rendering: Efficient handling of many lights
  • Instanced Rendering: Batch similar objects
  • Compute Shaders: Physics and AI calculations on GPU
  • Temporal Upsampling: Maintain quality at lower resolution

Modularity and Extensibility

Crate Organization

astraweave-core/     # ECS, validation, core types
astraweave-ai/       # AI planning and perception
astraweave-physics/  # Physics integration
astraweave-render/   # Rendering pipeline
astraweave-audio/    # Audio system
astraweave-nav/      # Navigation and pathfinding
astraweave-net/      # Networking layer

Plugin System

#![allow(unused)]
fn main() {
pub trait EnginePlugin {
    fn build(&self, app: &mut App);
}

pub struct CustomGamePlugin;

impl EnginePlugin for CustomGamePlugin {
    fn build(&self, app: &mut App) {
        app.add_system(custom_ai_behavior_system)
           .add_tool(CustomInteractionTool)
           .register_component::<CustomComponent>();
    }
}
}

Security Model

AI Sandboxing

AI agents cannot:

  • Access arbitrary memory
  • Execute arbitrary code
  • Bypass tool validation
  • Affect systems outside their permissions

AI agents can only:

  • Receive perception data
  • Generate high-level intents
  • Use validated tools
  • Learn from feedback

Deterministic Security

The deterministic simulation enables:

  • Replay Verification: Detect desync and cheating
  • Formal Verification: Mathematical proof of certain properties
  • Predictable Testing: Reliable automated testing
  • Audit Trails: Complete history of all decisions

Development Philosophy

Composition Over Inheritance

#![allow(unused)]
fn main() {
// Instead of inheritance hierarchies
class AIAgent extends Entity { ... }

// Use component composition
struct Entity {
    components: HashMap<ComponentId, Box<dyn Component>>
}
}

Data-Driven Design

#![allow(unused)]
fn main() {
// Behavior configured through data
#[derive(Deserialize)]
struct AIProfile {
    aggression: f32,
    curiosity: f32,
    risk_tolerance: f32,
    preferred_tools: Vec<String>,
}
}

Testable Architecture

#![allow(unused)]
fn main() {
// Every system is pure and testable
fn ai_planning_system(
    world: &World, 
    perceptions: &[PerceptionSnapshot]
) -> Vec<AIPlan> {
    // Pure function - easy to test
}
}

Comparison with Traditional Engines

AspectTraditional EngineAstraWeave
AI IntegrationBolted-on scriptingCore architecture
Action ValidationTrust-basedEngine-validated
DeterminismVariableFixed-tick
NetworkingAction replicationIntent replication
PerformanceVariablePredictable
TestingDifficultDeterministic

Next Steps

To understand specific systems:

To start building:

AI-Native Design

AstraWeave’s AI-native design represents a fundamental shift in how game engines approach artificial intelligence. Instead of treating AI as an add-on feature, we’ve built the entire engine around the principle that AI agents are first-class citizens.

The Traditional Approach (And Why It Fails)

Most game engines follow this pattern:

Game Engine → Game Logic → AI Scripting Layer → NPC Behaviors

Problems with this approach:

  • AI is disconnected from core game systems
  • AI agents can cheat (access hidden information, ignore physics)
  • Difficult to create consistent multiplayer behavior
  • AI behavior is scripted, not emergent
  • Hard to test and debug AI interactions

AstraWeave’s AI-Native Approach

AstraWeave inverts this relationship:

AI Agents ← Tool Validation ← Engine Core ← Game Logic

Benefits of this approach:

  • AI and human players use identical systems
  • No AI cheating possible
  • Emergent behavior from simple rules
  • Natural multiplayer compatibility
  • Testable and debuggable AI behavior

Core Principles

1. Perception-Based Decision Making

AI agents only know what they can perceive:

#![allow(unused)]
fn main() {
#[derive(Serialize, Deserialize)]
pub struct PerceptionSnapshot {
    // Only information the AI should have access to
    pub visible_entities: Vec<EntityData>,
    pub audible_events: Vec<AudioEvent>,
    pub remembered_information: Vec<MemoryItem>,
    pub world_constraints: Vec<Constraint>,
}
}

No omniscience: AI cannot access:

  • Hidden game state
  • Other agents’ thoughts
  • Perfect world information
  • Player UI state
  • Debug information

Realistic limitations: AI must work within:

  • Line of sight restrictions
  • Hearing range limitations
  • Memory capacity constraints
  • Processing time limits

2. Intent-Based Actions

AI generates high-level intents, not low-level commands:

#![allow(unused)]
fn main() {
pub enum Intent {
    // High-level goals
    ExploreArea { target_region: Region, curiosity: f32 },
    SeekCover { threat_direction: Vec3, urgency: f32 },
    ProtectAlly { ally_id: EntityId, commitment: f32 },
    
    // Not low-level commands like "move left 3 pixels"
}
}

Benefits:

  • AI thinks strategically, not tactically
  • Natural language mapping for LLMs
  • Easy to understand and debug
  • Platform and implementation independent

3. Tool-Based Execution

All AI actions go through validated tools:

#![allow(unused)]
fn main() {
pub trait Tool {
    // Every action must be validated first
    fn validate(&self, world: &World, usage: &ToolUsage) -> ValidationResult;
    
    // Only valid actions are executed
    fn execute(&self, world: &mut World, usage: &ToolUsage) -> ExecutionResult;
    
    // Tools have constraints and cooldowns
    fn get_constraints(&self) -> ToolConstraints;
}
}

No direct world manipulation: AI cannot:

  • Teleport entities
  • Spawn infinite resources
  • Ignore physics constraints
  • Break game rules

The AI Pipeline Architecture

Phase 1: Perception

#![allow(unused)]
fn main() {
fn perception_system(
    mut agents: Query<(&Position, &AIAgent, &mut PerceptionState)>,
    world_entities: Query<&Position, &EntityType>,
    audio_events: Res<AudioEventBuffer>,
) {
    for (pos, agent, mut perception) in agents.iter_mut() {
        let mut snapshot = PerceptionSnapshot::new();
        
        // Gather visible entities (line of sight)
        for (other_pos, entity_type) in world_entities.iter() {
            if world.line_of_sight(pos.0, other_pos.0) {
                let distance = pos.0.distance(other_pos.0);
                if distance <= agent.vision_range {
                    snapshot.visible_entities.push(EntityData {
                        position: other_pos.0,
                        entity_type: entity_type.clone(),
                        distance,
                    });
                }
            }
        }
        
        // Gather audible events
        for event in audio_events.iter() {
            let distance = pos.0.distance(event.position);
            let volume = event.calculate_volume_at_distance(distance);
            if volume > agent.hearing_threshold {
                snapshot.audible_events.push(event.clone());
            }
        }
        
        // Include relevant memories
        snapshot.remembered_information = agent.memory.query_relevant(
            &snapshot, 
            agent.memory_capacity
        );
        
        perception.current_snapshot = Some(snapshot);
    }
}
}

Phase 2: Planning

#![allow(unused)]
fn main() {
fn ai_planning_system(
    mut agents: Query<(&AIAgent, &PerceptionState, &mut PlanningState)>,
    ai_service: Res<AIService>,
) {
    for (agent, perception, mut planning) in agents.iter_mut() {
        if let Some(snapshot) = &perception.current_snapshot {
            // Prepare input for AI model
            let planning_request = PlanningRequest {
                agent_profile: agent.profile.clone(),
                perception_data: snapshot.clone(),
                available_tools: tool_registry.get_available_tools(agent.id),
                current_goals: agent.goal_stack.clone(),
                recent_memory: agent.memory.get_recent(10),
            };
            
            // Generate plan using LLM
            match ai_service.generate_plan(planning_request) {
                Ok(plan) => {
                    info!("AI generated plan: {:?}", plan);
                    planning.current_plan = Some(plan);
                },
                Err(e) => {
                    warn!("AI planning failed: {}", e);
                    // Fallback to simple behaviors
                    planning.current_plan = Some(generate_fallback_plan(agent));
                }
            }
        }
    }
}
}

Phase 3: Validation

#![allow(unused)]
fn main() {
fn tool_validation_system(
    mut agents: Query<(&PlanningState, &mut ActionQueue)>,
    tool_registry: Res<ToolRegistry>,
    world: &World,
) {
    for (planning, mut actions) in agents.iter_mut() {
        if let Some(plan) = &planning.current_plan {
            for tool_usage in &plan.tool_usages {
                let tool = tool_registry.get_tool(&tool_usage.tool_name)?;
                
                match tool.validate(world, tool_usage) {
                    ValidationResult::Valid => {
                        actions.push(ValidatedAction {
                            tool_usage: tool_usage.clone(),
                            validation_timestamp: world.current_tick(),
                        });
                    },
                    ValidationResult::Blocked(reason) => {
                        warn!("Tool validation failed: {:?}", reason);
                        // AI learns from failure
                        agent.memory.record_failure(tool_usage, reason);
                    },
                    ValidationResult::Delayed(wait_time) => {
                        actions.push_delayed(tool_usage.clone(), wait_time);
                    }
                }
            }
        }
    }
}
}

Phase 4: Execution

#![allow(unused)]
fn main() {
fn action_execution_system(
    mut agents: Query<&mut ActionQueue>,
    tool_registry: Res<ToolRegistry>,
    mut world: ResMut<World>,
) {
    for mut actions in agents.iter_mut() {
        while let Some(action) = actions.pop_ready() {
            let tool = tool_registry.get_tool(&action.tool_usage.tool_name)?;
            
            match tool.execute(&mut world, &action.tool_usage) {
                ExecutionResult::Success => {
                    // AI learns from success
                    agent.memory.record_success(&action.tool_usage);
                },
                ExecutionResult::Failed(reason) => {
                    // Even validated actions can fail during execution
                    warn!("Action execution failed: {:?}", reason);
                    agent.memory.record_execution_failure(&action.tool_usage, reason);
                }
            }
        }
    }
}
}

Tool Design Philosophy

Tools as Affordances

In AstraWeave, tools represent what an agent can do, not what it will do:

#![allow(unused)]
fn main() {
pub struct MovementTool {
    max_speed: f32,
    acceleration: f32,
    valid_surfaces: Vec<SurfaceType>,
}

impl Tool for MovementTool {
    fn validate(&self, world: &World, usage: &ToolUsage) -> ValidationResult {
        // Check if movement is physically possible
        let agent_pos = world.get::<Position>(usage.agent_id)?;
        let target_pos = usage.parameters.get_vec3("target")?;
        
        // Line of sight check
        if !world.line_of_sight(agent_pos.0, target_pos) {
            return ValidationResult::Blocked(BlockReason::ObstructedPath);
        }
        
        // Surface validity check
        let surface_type = world.get_surface_type(target_pos);
        if !self.valid_surfaces.contains(&surface_type) {
            return ValidationResult::Blocked(BlockReason::InvalidSurface);
        }
        
        // Speed limit check
        let distance = agent_pos.0.distance(target_pos);
        let time_required = distance / self.max_speed;
        if time_required > usage.max_execution_time {
            return ValidationResult::Blocked(BlockReason::TooSlow);
        }
        
        ValidationResult::Valid
    }
}
}

Tool Composition

Complex behaviors emerge from combining simple tools:

#![allow(unused)]
fn main() {
// AI plans using multiple tools in sequence
let complex_plan = AIPlan {
    steps: vec![
        ToolUsage {
            tool_name: "MovementTool",
            parameters: movement_params,
        },
        ToolUsage {
            tool_name: "InteractionTool", 
            parameters: interaction_params,
        },
        ToolUsage {
            tool_name: "CommunicationTool",
            parameters: communication_params,
        },
    ],
};
}

Learning and Adaptation

Memory System Integration

#![allow(unused)]
fn main() {
pub struct AIMemory {
    // Short-term working memory
    working_memory: VecDeque<MemoryItem>,
    
    // Long-term episodic memory
    episodic_memory: Vec<Episode>,
    
    // Learned patterns and strategies
    strategy_memory: HashMap<Situation, Strategy>,
    
    // Failed actions and why they failed
    failure_memory: Vec<FailureRecord>,
}

impl AIMemory {
    pub fn record_success(&mut self, action: &ToolUsage, outcome: &ExecutionResult) {
        // Reinforce successful strategies
        let situation = self.extract_situation_features(action);
        let strategy = self.extract_strategy_features(action);
        self.strategy_memory.entry(situation)
            .or_default()
            .reinforce(strategy, outcome.success_metric());
    }
    
    pub fn record_failure(&mut self, action: &ToolUsage, reason: &BlockReason) {
        // Learn from failures to avoid them
        self.failure_memory.push(FailureRecord {
            action: action.clone(),
            reason: reason.clone(),
            context: self.current_context.clone(),
            timestamp: Instant::now(),
        });
    }
}
}

Dynamic Behavior Adaptation

#![allow(unused)]
fn main() {
fn adaptation_system(
    mut agents: Query<(&mut AIAgent, &AIMemory)>,
) {
    for (mut agent, memory) in agents.iter_mut() {
        // Adjust behavior based on recent experiences
        let recent_failures = memory.get_recent_failures(Duration::from_secs(300));
        
        if recent_failures.iter().any(|f| matches!(f.reason, BlockReason::TooAggressive)) {
            agent.profile.aggression *= 0.9; // Become less aggressive
        }
        
        if recent_failures.iter().any(|f| matches!(f.reason, BlockReason::TooSlow)) {
            agent.profile.urgency *= 1.1; // Become more urgent
        }
        
        // Adapt strategy preferences
        let successful_strategies = memory.get_successful_strategies();
        for (situation, strategy) in successful_strategies {
            agent.strategy_preferences.insert(situation, strategy);
        }
    }
}
}

Emergent Behavior Examples

Cooperative Pathfinding

When multiple AI agents need to navigate through a narrow passage:

#![allow(unused)]
fn main() {
// No explicit coordination code needed
// Emergent behavior arises from:
// 1. Each agent perceives others as obstacles
// 2. Movement tool validates non-collision
// 3. Agents naturally take turns or find alternate routes
}

Dynamic Alliance Formation

#![allow(unused)]
fn main() {
// Agents can form alliances based on shared threats
fn threat_response_planning(
    agent: &AIAgent,
    perception: &PerceptionSnapshot,
) -> Intent {
    let threats = perception.identify_threats();
    let potential_allies = perception.identify_potential_allies();
    
    if threats.is_empty() {
        return Intent::Explore { target: random_area() };
    }
    
    if potential_allies.is_empty() {
        return Intent::Flee { threat_direction: threats[0].position };
    }
    
    // Emergent alliance formation
    Intent::CoordinateDefense {
        allies: potential_allies,
        threat: threats[0],
        strategy: choose_defensive_strategy(threats, potential_allies),
    }
}
}

Adaptive Combat Tactics

#![allow(unused)]
fn main() {
// AI learns and counters player strategies
fn combat_planning(
    agent: &AIAgent,
    perception: &PerceptionSnapshot,
    memory: &AIMemory,
) -> Intent {
    let player = perception.find_player()?;
    
    // Analyze player's recent tactics
    let player_patterns = memory.analyze_player_behavior(&player);
    
    // Choose counter-strategy
    let counter_strategy = match player_patterns.primary_tactic {
        PlayerTactic::RushAttack => CombatStrategy::DefensiveCounter,
        PlayerTactic::RangedKiting => CombatStrategy::ClosingPincer,
        PlayerTactic::DefensiveTurtle => CombatStrategy::AreaDenial,
        PlayerTactic::Unpredictable => CombatStrategy::AdaptiveReactive,
    };
    
    Intent::ExecuteCombatStrategy {
        target: player.entity_id,
        strategy: counter_strategy,
        commitment: calculate_commitment(player_patterns.skill_level),
    }
}
}

Performance Considerations

Computational Efficiency

#![allow(unused)]
fn main() {
// AI planning can be expensive, so we use various optimizations:

pub struct AIService {
    // LLM inference can be slow
    model_cache: LRUCache<PlanningRequest, AIPlan>,
    
    // Batch multiple planning requests
    batch_processor: BatchProcessor<PlanningRequest>,
    
    // Use cheaper models for simple decisions
    model_hierarchy: Vec<AIModel>, // Fast → Accurate
}

impl AIService {
    pub fn generate_plan(&self, request: PlanningRequest) -> Result<AIPlan> {
        // Check cache first
        if let Some(cached_plan) = self.model_cache.get(&request) {
            return Ok(cached_plan.clone());
        }
        
        // Use appropriate model based on complexity
        let model = self.select_model(request.complexity());
        
        // Generate plan
        let plan = model.generate_plan(request)?;
        
        // Cache result
        self.model_cache.insert(request, plan.clone());
        
        Ok(plan)
    }
}
}

Memory Management

#![allow(unused)]
fn main() {
// AI memory systems need careful management
impl AIMemory {
    pub fn cleanup_old_memories(&mut self) {
        // Remove memories older than threshold
        let cutoff = Instant::now() - Duration::from_secs(3600); // 1 hour
        self.episodic_memory.retain(|episode| episode.timestamp > cutoff);
        
        // Compress similar memories
        self.compress_similar_episodes();
        
        // Keep only the most important failures
        self.failure_memory.sort_by_key(|f| f.importance_score());
        self.failure_memory.truncate(100); // Keep top 100
    }
}
}

Debugging AI Behavior

Explainable AI

#![allow(unused)]
fn main() {
#[derive(Debug, Serialize)]
pub struct AIPlan {
    pub intent: Intent,
    pub reasoning: String, // Natural language explanation
    pub confidence: f32,
    pub alternative_plans: Vec<AlternativePlan>,
    pub decision_factors: Vec<DecisionFactor>,
}

// Example reasoning output:
"I can see an enemy at position (10, 5) who appears to be low on health. 
My ally is engaged in combat nearby and could use support. I have a clear 
line of sight and my weapon is ready. I'm choosing to attack rather than 
flank because the enemy seems focused on my ally and won't see me coming."
}

Debug Visualization

#![allow(unused)]
fn main() {
// In development builds, expose AI decision making
#[cfg(debug_assertions)]
impl AIAgent {
    pub fn get_debug_info(&self) -> AIDebugInfo {
        AIDebugInfo {
            current_perception: self.perception.clone(),
            active_plan: self.planning.current_plan.clone(),
            recent_decisions: self.memory.get_recent_decisions(10),
            personality_state: self.profile.clone(),
            tool_availability: self.get_available_tools(),
        }
    }
}
}

Integration with Traditional Game Systems

Physics Integration

#![allow(unused)]
fn main() {
// AI respects physics constraints
impl Tool for MovementTool {
    fn validate(&self, world: &World, usage: &ToolUsage) -> ValidationResult {
        let physics_world = world.resource::<PhysicsWorld>();
        let agent_body = physics_world.get_body(usage.agent_id)?;
        
        // Check if movement would cause collision
        let proposed_movement = usage.parameters.get_vec3("target")?;
        if physics_world.would_collide(agent_body, proposed_movement) {
            return ValidationResult::Blocked(BlockReason::PhysicsCollision);
        }
        
        ValidationResult::Valid
    }
}
}

Animation Integration

#![allow(unused)]
fn main() {
// AI actions trigger appropriate animations
impl Tool for CombatTool {
    fn execute(&self, world: &mut World, usage: &ToolUsage) -> ExecutionResult {
        let attack_type = usage.parameters.get_string("attack_type")?;
        
        // Trigger combat animation
        world.get_mut::<AnimationController>(usage.agent_id)?
             .play_animation(format!("attack_{}", attack_type));
        
        // Execute combat logic
        self.resolve_combat(world, usage)
    }
}
}

Comparison: Traditional vs AI-Native

AspectTraditional ApproachAstraWeave AI-Native
Decision MakingScripted state machinesLLM-based planning
World KnowledgeOmniscient accessPerception-limited
Action ExecutionDirect world manipulationTool-validated actions
Behavior AdaptationManual script updatesAutomatic learning
MultiplayerSeparate AI/player codeUnified validation
DebuggingComplex state inspectionNatural language reasoning
PerformancePredictable overheadVariable AI complexity
EmergenceLimited by scriptsUnbounded combinations

Best Practices for AI-Native Development

1. Design Affordances, Not Behaviors

#![allow(unused)]
fn main() {
// Good: Define what an agent CAN do
pub struct InteractionTool {
    pub interaction_range: f32,
    pub valid_targets: Vec<EntityType>,
    pub cooldown: Duration,
}

// Avoid: Scripting what an agent WILL do
// pub fn npc_behavior_script() { ... }
}

2. Embrace Failure as Learning

#![allow(unused)]
fn main() {
// AI failures are features, not bugs
if let Err(validation_error) = tool.validate(world, usage) {
    // Don't just log the error - let the AI learn from it
    agent.memory.record_lesson(validation_error, current_context);
    
    // AI will avoid this mistake in similar situations
}
}

3. Provide Rich Perception

#![allow(unused)]
fn main() {
// Give AI agents the information they need to make good decisions
pub struct PerceptionSnapshot {
    // Not just positions, but meaningful context
    pub entities: Vec<EntityPerception>,
    pub environmental_cues: Vec<EnvironmentalCue>,
    pub social_context: SocialContext,
    pub recent_events: Vec<GameEvent>,
}
}

4. Use Hierarchical Planning

#![allow(unused)]
fn main() {
// Break complex goals into manageable sub-goals
pub enum Intent {
    // High-level strategic goals
    DefendTerritory { area: Region },
    
    // Mid-level tactical goals  
    EstablishDefensivePosition { chokepoint: Vec3 },
    
    // Low-level operational goals
    MoveToCover { cover_position: Vec3 },
}
}

Future Directions

Advanced AI Architectures

  • Multi-Agent Planning: Coordinated group decision making
  • Hierarchical Temporal Memory: Better long-term memory systems
  • Causal Reasoning: Understanding cause-and-effect relationships
  • Meta-Learning: AI that learns how to learn better

Performance Optimizations

  • Neural Network Compression: Smaller, faster AI models
  • Predictive Caching: Pre-compute likely AI decisions
  • Distributed Processing: AI planning across multiple cores/machines
  • Hybrid Approaches: Combine neural networks with symbolic reasoning

AI-native design is not just about making NPCs smarter - it’s about creating fundamentally new types of interactive experiences where AI agents are true participants in the game world, subject to the same rules and constraints as human players.

ECS Architecture

AstraWeave uses an archetype-based Entity Component System (ECS) designed for deterministic, AI-native game development. The architecture provides cache-friendly iteration, deterministic execution ordering, and efficient event propagation for AI perception.

Core Concepts

Entity

An Entity is a unique identifier representing a game object. Internally, entities use a 32-bit ID and 32-bit generation counter for safe recycling:

#![allow(unused)]
fn main() {
use astraweave_ecs::*;

let mut world = World::new();

// Spawn returns a unique Entity handle
let player = world.spawn();
let enemy = world.spawn();

// Check if entity is alive (generation-safe)
assert!(world.is_alive(player));

// Despawn removes entity and all components
world.despawn(enemy);
assert!(!world.is_alive(enemy));
}

The generation counter prevents “dangling entity” bugs—if you hold a stale entity handle after despawn, operations silently fail rather than affecting the recycled entity.

Component

A Component is data attached to an entity. Any 'static + Send + Sync type automatically implements Component:

#![allow(unused)]
fn main() {
// Components are just plain structs
#[derive(Clone, Copy)]
struct Position { x: f32, y: f32 }

#[derive(Clone, Copy)]
struct Velocity { x: f32, y: f32 }

struct Health(i32);

// Insert components
let mut world = World::new();
let e = world.spawn();
world.insert(e, Position { x: 0.0, y: 0.0 });
world.insert(e, Velocity { x: 1.0, y: 0.0 });
world.insert(e, Health(100));

// Query components
if let Some(pos) = world.get::<Position>(e) {
    println!("Entity at ({}, {})", pos.x, pos.y);
}

// Mutate components
if let Some(health) = world.get_mut::<Health>(e) {
    health.0 -= 10;
}
}

Resource

A Resource is a singleton value accessible across systems—perfect for shared state like input, time, or game configuration:

#![allow(unused)]
fn main() {
struct DeltaTime(f32);
struct InputState {
    move_direction: (f32, f32),
    attack_pressed: bool,
}

let mut world = World::new();

// Insert resources
world.insert_resource(DeltaTime(1.0 / 60.0));
world.insert_resource(InputState {
    move_direction: (0.0, 0.0),
    attack_pressed: false,
});

// Query resources
let dt = world.get_resource::<DeltaTime>().unwrap();
let input = world.get_resource::<InputState>().unwrap();
}

Archetype Storage

What is an Archetype?

An Archetype groups all entities with the same set of component types. When you add or remove components, the entity moves to a different archetype.

Archetype 0: [Position]
├── Entity(1): Position(0,0)
└── Entity(4): Position(5,3)

Archetype 1: [Position, Velocity]
├── Entity(2): Position(1,1), Velocity(1,0)
└── Entity(3): Position(2,2), Velocity(0,1)

Archetype 2: [Position, Velocity, Health]
└── Entity(5): Position(3,3), Velocity(1,1), Health(100)

This design provides:

  • Cache-friendly iteration: Components of the same type are stored contiguously
  • Efficient queries: Filter by archetype signature, not per-entity checks
  • Predictable memory layout: Improves CPU prefetching

Storage Modes

AstraWeave supports two storage backends:

Box Mode (Legacy): Components stored as Box<dyn Any>. Works for any component type but has heap indirection overhead.

BlobVec Mode (Optimized): Components stored in contiguous byte arrays. Requires component registration but provides 2-10× faster iteration:

#![allow(unused)]
fn main() {
let mut world = World::new();

// Register component for optimized storage
world.register_component::<Position>();
world.register_component::<Velocity>();

// Now Position and Velocity use BlobVec storage
}

SparseSet Entity Lookup

Entity-to-archetype mapping uses a SparseSet for O(1) lookup:

Memory Layout:
sparse: [None, Some(0), None, Some(1), None, Some(2), ...]
             ↓              ↓              ↓
dense:  [Entity(1), Entity(3), Entity(5), ...]

This replaced the previous BTreeMap approach, providing 12-57× faster entity lookups.

System Architecture

System Stages

AstraWeave uses fixed stages for deterministic execution—critical for AI agents that must produce identical behavior across game sessions:

#![allow(unused)]
fn main() {
use astraweave_ecs::*;

// System stages execute in order
pub struct SystemStage;

impl SystemStage {
    pub const PRE_SIMULATION: &'static str = "pre_simulation";
    pub const PERCEPTION: &'static str = "perception";      // Build AI snapshots
    pub const SIMULATION: &'static str = "simulation";      // Game logic
    pub const AI_PLANNING: &'static str = "ai_planning";    // Generate plans
    pub const PHYSICS: &'static str = "physics";            // Apply forces
    pub const POST_SIMULATION: &'static str = "post_simulation";
    pub const PRESENTATION: &'static str = "presentation";  // Render, audio
}
}

The AI-native game loop follows: Perception → Reasoning → Planning → Action

┌───────────────────────────────────────────────────────────────────────┐
│                         Frame N                                       │
├─────────────┬─────────────┬──────────────┬─────────────┬─────────────┤
│ pre_sim     │ perception  │  simulation  │ ai_planning │  physics    │
│ (setup)     │ (sensors)   │ (game logic) │ (decide)    │ (movement)  │
├─────────────┴─────────────┴──────────────┴─────────────┴─────────────┤
│                       post_sim → presentation                         │
└───────────────────────────────────────────────────────────────────────┘

Registering Systems

Systems are functions that operate on the World:

#![allow(unused)]
fn main() {
fn movement_system(world: &mut World) {
    world.each_mut::<Position>(|entity, pos| {
        if let Some(vel) = world.get::<Velocity>(entity) {
            pos.x += vel.x;
            pos.y += vel.y;
        }
    });
}

fn ai_perception_system(world: &mut World) {
    // Build WorldSnapshot for AI agents
    // (see AI documentation for details)
}

let mut app = App::new();
app.add_system("simulation", movement_system);
app.add_system("perception", ai_perception_system);
}

Query Types

AstraWeave provides ergonomic query iterators:

#![allow(unused)]
fn main() {
// Single-component read-only query
let query = Query::<Position>::new(&world);
for (entity, pos) in query {
    println!("Entity {:?} at ({}, {})", entity, pos.x, pos.y);
}

// Two-component query
let query2 = Query2::<Position, Velocity>::new(&world);
for (entity, pos, vel) in query2 {
    println!("Entity {:?} moving at ({}, {})", entity, vel.x, vel.y);
}

// Mutable queries
let mut query = Query2Mut::<Position, Velocity>::new(&mut world);
for (entity, pos, vel) in query.iter_mut() {
    pos.x += vel.x;
    pos.y += vel.y;
}
}

Event System

Events enable reactive AI behaviors and decoupled communication between systems.

Sending Events

#![allow(unused)]
fn main() {
use astraweave_ecs::*;

// Define custom events
#[derive(Clone)]
struct DamageEvent {
    target: Entity,
    amount: i32,
    source: Option<Entity>,
}

impl Event for DamageEvent {}

// Send events via Events resource
let mut events = Events::new();
events.send(DamageEvent {
    target: player,
    amount: 25,
    source: Some(enemy),
});
}

Reading Events

#![allow(unused)]
fn main() {
// Systems read events via EventReader
fn damage_system(world: &mut World) {
    let mut events = world.get_resource_mut::<Events>().unwrap();
    
    for event in events.drain::<DamageEvent>() {
        if let Some(health) = world.get_mut::<Health>(event.target) {
            health.0 -= event.amount;
        }
    }
}
}

Built-in Events

AstraWeave provides common events for AI perception:

EventPurpose
EntitySpawnedEventEntity creation notification
EntityDespawnedEventEntity removal notification
HealthChangedEventHealth changes (for AI threat assessment)
AiPlanningFailedEventAI plan generation failures
ToolValidationFailedEventAI action validation failures

App Builder Pattern

The App struct provides a Bevy-like builder pattern:

use astraweave_ecs::*;

#[derive(Clone, Copy)]
struct Position { x: f32, y: f32 }

#[derive(Clone, Copy)]
struct Velocity { x: f32, y: f32 }

fn movement_system(world: &mut World) {
    world.each_mut::<Position>(|entity, pos| {
        if let Some(vel) = world.get::<Velocity>(entity) {
            pos.x += vel.x;
            pos.y += vel.y;
        }
    });
}

fn main() {
    let mut app = App::new();
    app.add_system("simulation", movement_system);
    
    // Spawn entities
    let e = app.world.spawn();
    app.world.insert(e, Position { x: 0.0, y: 0.0 });
    app.world.insert(e, Velocity { x: 1.0, y: 0.0 });
    
    // Run 100 simulation ticks
    app = app.run_fixed(100);
    
    // Entity moved 100 units
    let pos = app.world.get::<Position>(e).unwrap();
    assert_eq!(pos.x, 100.0);
}

Plugin Architecture

Plugins encapsulate related systems and resources:

#![allow(unused)]
fn main() {
pub trait Plugin {
    fn build(&self, app: &mut App);
}

struct PhysicsPlugin;

impl Plugin for PhysicsPlugin {
    fn build(&self, app: &mut App) {
        app.add_system("physics", physics_tick);
        app.world.insert_resource(PhysicsConfig::default());
    }
}

// Add plugins to app
let app = App::new().add_plugin(PhysicsPlugin);
}

Command Buffer

For deferred operations (avoiding borrow conflicts), use CommandBuffer:

#![allow(unused)]
fn main() {
use astraweave_ecs::*;

// Register components first
let mut world = World::new();
world.register_component::<Position>();
world.register_component::<Velocity>();

// Queue commands
let mut cmd = CommandBuffer::new();
cmd.spawn()
    .insert(Position { x: 0.0, y: 0.0 })
    .insert(Velocity { x: 1.0, y: 0.0 });

cmd.entity(existing_entity)
    .remove::<Velocity>();

// Apply all commands at once
cmd.apply(&mut world);
}

Determinism

AstraWeave ECS is designed for deterministic replay and multiplayer synchronization:

Ordered Iteration

Entities within an archetype iterate in spawn order (using packed arrays), ensuring consistent system behavior.

Seeded RNG

Use the built-in deterministic RNG:

#![allow(unused)]
fn main() {
use astraweave_ecs::Rng;

let mut rng = Rng::from_seed(42);
let damage = rng.next_f32() * 10.0;  // Same value every time with seed 42
}

Event Ordering

Events are stored in order-of-send, with frame tracking for cleanup:

#![allow(unused)]
fn main() {
let events = Events::new()
    .with_keep_frames(2);  // Keep events for 2 frames
}

Performance Characteristics

Benchmarked on the AstraWeave test suite (Week 10):

OperationTimeNotes
World creation25.8 nsEmpty world
Entity spawn420 nsIncludes archetype assignment
Component insert1-2 µsArchetype migration if needed
Entity lookupO(1)SparseSet, 12-57× faster than BTreeMap
Iteration (per entity)<1 nsPacked array iteration
Query creation50-100 nsArchetype filtering

60 FPS Budget

With 16.67 ms per frame, current performance provides:

  • 1,000 entities: 1.14 ms frame time (93% headroom)
  • 10,000 entities: ~11 ms frame time (34% headroom)
  • Movement system: 106 µs for 1,000 entities (9.4× faster post-optimization)

Advanced Topics

Custom Component Storage

For specialized use cases, you can interact with archetype storage directly:

#![allow(unused)]
fn main() {
// Access archetypes for low-level iteration
for archetype in world.archetypes().iter() {
    println!("Archetype {} has {} entities", 
             archetype.id, archetype.len());
    
    for &entity in archetype.entities_vec() {
        // Direct entity access
    }
}
}

Profiling Integration

Enable the profiling feature for Tracy integration:

[dependencies]
astraweave-ecs = { version = "0.4", features = ["profiling"] }

Key spans are automatically instrumented: ECS::World::spawn, ECS::World::get, ECS::Schedule::run.

See Also

Deterministic Simulation

AstraWeave provides bit-identical deterministic simulation across all core systems. This enables reproducible AI behavior, lockstep multiplayer, replay systems, and reliable regression testing.

Why Determinism Matters

For AI-native gameplay, determinism is critical:

#![allow(unused)]
fn main() {
// Without determinism (BAD):
// Run 1: Entity 42 attacks first (random HashMap iteration)
// Run 2: Entity 17 attacks first (different outcome!)
// Multiplayer: Host and client see different results

// With determinism (GOOD):
// Run 1, 2, 3...: Same entity order, same decisions, same outcome
// Multiplayer: All clients see identical simulation
// Replays: Record inputs, reproduce exact gameplay
}

Use Cases:

  • Reproducible AI: Same world state → same AI decisions
  • Lockstep Multiplayer: Only sync inputs, not full game state
  • Replay Systems: Record/playback for debugging and spectating
  • Regression Testing: Validate AI changes don’t break behavior
  • Competitive Gaming: Provably fair gameplay

Determinism Guarantees

What IS Guaranteed

SystemGuarantee
ECS IterationArchetypes visited in ID order (BTreeMap)
Within-ArchetypeEntities maintain relative order
Repeated IterationsSame order every time
Cross-WorldSame operations → same archetype IDs
PhysicsDeterministic parallel iteration (Rayon)
Capture/ReplayBit-identical state restoration
EventsFIFO ordering

What is NOT Guaranteed

  • Spawn order across archetypes: Entity order changes when components added/removed
  • Floating-point across platforms: Different CPUs may produce slightly different results
  • Non-deterministic RNG: Must use seeded RNG explicitly

Fixed-Tick Simulation

AstraWeave runs physics and AI at exactly 60Hz:

#![allow(unused)]
fn main() {
const TICK_RATE: f64 = 60.0;
const TICK_DURATION: Duration = Duration::from_nanos(16_666_667);

// Every simulation step advances by exactly this amount
pub fn step(world: &mut World, cfg: &SimConfig) {
    world.tick(cfg.dt);  // cfg.dt = 1/60 second
}
}

Benefits:

  • Consistent timing across different hardware
  • Decoupled from render frame rate
  • Predictable performance testing
  • Reliable networking

ECS Determinism

BTreeMap Storage

The ECS uses BTreeMap instead of HashMap for archetype storage:

#![allow(unused)]
fn main() {
/// CRITICAL: BTreeMap for deterministic iteration by ID
pub struct ArchetypeSet {
    archetypes: BTreeMap<ArchetypeId, Archetype>,
    // NOT HashMap - iteration order would be non-deterministic!
}
}

Why this matters:

  • HashMap iteration order depends on hash function and memory layout
  • BTreeMap iteration order is sorted by key (deterministic)
  • AI agents iterate entities in identical order every run

Entity Iteration Order

#![allow(unused)]
fn main() {
// Entities are stored per-archetype
let e1 = world.spawn();  // Empty archetype (ID 0)
let e2 = world.spawn();  // Empty archetype (ID 0)
world.insert(e1, Position::new(1.0, 1.0));  // Moves e1 to Position archetype (ID 1)

// Iteration order: [e2, e1]
// - Archetype 0 (empty) visited first: contains e2
// - Archetype 1 (Position) visited second: contains e1
// NOT spawn order [e1, e2] - but IS deterministic!
}

Preserving Spawn Order

If spawn order is critical, track it explicitly:

#![allow(unused)]
fn main() {
#[derive(Component, Clone, Copy)]
struct SpawnOrder(u64);

// When spawning:
let e = world.spawn();
world.insert(e, SpawnOrder(world.entity_count() as u64));

// When iterating in spawn order:
let mut entities = world.entities_with::<SpawnOrder>();
entities.sort_by_key(|&e| world.get::<SpawnOrder>(e).unwrap().0);
}

Capture & Replay

AstraWeave provides state capture for debugging and determinism validation:

#![allow(unused)]
fn main() {
use astraweave_core::{capture_state, replay_state, SimConfig};

// Capture world state at tick 100
capture_state(100, "save.json", &world)?;

// Later: Replay from saved state
let cfg = SimConfig { dt: 1.0 / 60.0 };
let world = replay_state("save.json", 100, &cfg)?;  // Run 100 ticks
}

Snapshot Format

{
  "tick": 100,
  "world": {
    "t": 1.6666,
    "next_id": 42,
    "obstacles": [[5, 10], [0, 0], [15, 20]]
  }
}

Stability guarantees:

  • Obstacles sorted for consistent serialization
  • JSON format for human readability
  • Minimal state for fast save/load

Physics Determinism

Parallel Processing

Physics uses Rayon with deterministic iteration:

#![allow(unused)]
fn main() {
/// Parallel iterator helpers for deterministic physics
impl PhysicsParallelOps {
    /// Process rigid bodies in parallel (deterministic order)
    pub fn par_process_bodies<F>(&self, bodies: &mut [RigidBody], f: F)
    where
        F: Fn(&mut RigidBody) + Sync,
    {
        // Rayon's par_iter preserves element order
        bodies.par_iter_mut().for_each(f);
    }
}
}

Fixed Timestep

Physics simulation uses fixed timesteps:

#![allow(unused)]
fn main() {
// Physics world with deterministic gravity
let physics = PhysicsWorld::new(Vec3::new(0.0, -9.81, 0.0));

// Fixed step (not variable dt!)
physics.step(1.0 / 60.0);  // Always 16.67ms
}

Testing Determinism

Validation Tests

The ECS includes comprehensive determinism tests:

#![allow(unused)]
fn main() {
#[test]
fn test_query_iteration_deterministic() {
    let mut world = World::new();
    
    // Spawn entities with components
    for i in 0..100 {
        let e = world.spawn();
        world.insert(e, Position { x: i as f32, y: 0.0 });
    }
    
    // First iteration
    let order1: Vec<Entity> = world.entities_with::<Position>().collect();
    
    // Second iteration - must be identical
    let order2: Vec<Entity> = world.entities_with::<Position>().collect();
    
    assert_eq!(order1, order2, "Iteration order must be deterministic");
}
}

Multi-Run Verification

#![allow(unused)]
fn main() {
#[test]
fn test_determinism_across_runs() {
    let results: Vec<Vec<Entity>> = (0..5)
        .map(|_| {
            let mut world = World::new();
            // Same operations each run
            setup_world(&mut world);
            world.entities_with::<AIAgent>().collect()
        })
        .collect();
    
    // All runs must produce identical results
    for run in &results[1..] {
        assert_eq!(&results[0], run);
    }
}
}

Best Practices

DO

#![allow(unused)]
fn main() {
// ✅ Use fixed timestep
world.step(FIXED_DT);

// ✅ Use seeded RNG
let mut rng = StdRng::seed_from_u64(12345);

// ✅ Use BTreeMap for deterministic iteration
let entities: BTreeMap<EntityId, Entity> = ...;

// ✅ Sort before iteration if order matters
let mut entities = query.iter().collect::<Vec<_>>();
entities.sort_by_key(|e| e.id());
}

DON’T

#![allow(unused)]
fn main() {
// ❌ Variable timestep
world.step(delta_time);

// ❌ Unseeded RNG
let mut rng = rand::thread_rng();

// ❌ HashMap for gameplay-critical iteration
let entities: HashMap<EntityId, Entity> = ...;

// ❌ Rely on spawn order across archetype changes
let first = world.entities().next();  // Order may change!
}

Performance Impact

Determinism adds minimal overhead:

OperationHashMapBTreeMapOverhead
LookupO(1)O(log n)~10-20 ns
IterationO(n)O(n)None
InsertO(1)O(log n)~10-20 ns

In practice: For typical games (<10,000 entities), the overhead is negligible (<1% of frame budget).

See Also

  • astraweave-ecs/src/determinism_tests.rs - 763 lines of determinism validation
  • astraweave-core/src/capture_replay.rs - State capture/replay implementation
  • astraweave-physics/src/async_scheduler.rs - Deterministic parallel physics

Tool Validation System

The AstraWeave LLM integration includes a comprehensive tool validation system that ensures LLM-generated plans are safe and comply with the game’s allowed actions.

Overview

The tool validation system works in three layers:

  1. Schema Validation: Ensures JSON structure matches expected format
  2. Tool Registry Validation: Verifies all actions are in the allowed tool set
  3. Engine Validation: Runtime checks for cooldowns, line-of-sight, and other constraints

Components

ToolRegistry

The ToolRegistry defines which tools are available to the LLM:

#![allow(unused)]
fn main() {
pub struct ToolRegistry {
    pub tools: Vec<ToolSpec>,
    pub constraints: Constraints,
}

pub struct ToolSpec {
    pub name: String,
    pub args: BTreeMap<String, String>, // argument name -> type
}
}

Validation Process

  1. LLM Response Parsing: Parse the JSON response into a PlanIntent
  2. Tool Allowlist Check: Verify each action is in the registry
  3. Type Validation: Ensure arguments match expected types (future enhancement)
  4. Runtime Validation: Engine checks constraints during execution

Supported Actions

  • MoveTo: Move companion to specified coordinates
  • Throw: Throw items (smoke, grenade) at target location
  • CoverFire: Provide covering fire at target for duration
  • Revive: Revive allied units

Error Handling

The system provides clear error messages for validation failures:

  • Invalid JSON format
  • Disallowed tools used by LLM
  • Missing required arguments
  • Type mismatches (future)

Security Features

  • Allowlist-only: Only pre-approved actions can be executed
  • No dynamic code execution: All actions are statically defined
  • Input sanitization: JSON parsing prevents injection attacks
  • Constraint enforcement: Runtime validation prevents illegal moves

Usage Example

#![allow(unused)]
fn main() {
use astraweave_llm::{parse_llm_plan, MockLlm, plan_from_llm};

// Create tool registry
let registry = ToolRegistry {
    tools: vec![
        ToolSpec {
            name: "move_to".into(),
            args: [("x", "i32"), ("y", "i32")].into_iter().collect(),
        }
    ],
    constraints: Constraints::default(),
};

// Parse and validate LLM response
let plan = parse_llm_plan(llm_response, &registry)?;

// Full end-to-end validation
let plan = plan_from_llm(&client, &world_snapshot, &registry).await?;
}

Testing

The validation system includes comprehensive tests covering:

  • Valid plan parsing
  • Invalid JSON handling
  • Disallowed tool detection
  • Empty plan handling
  • All action types
  • Error message verification

Run tests with:

cargo test -p astraweave-llm

Future Enhancements

  • Argument type validation
  • Parameter range checking
  • Cost/resource validation
  • Complex constraint evaluation
  • Custom validation plugins

AI System

AstraWeave’s AI system is the core differentiator of the engine. Unlike traditional game engines where AI is an afterthought, AstraWeave treats AI agents as first-class citizens that interact with the game world through validated tools.

In AstraWeave, AI agents cannot cheat. They must use the same validated game systems as players, ensuring fair and emergent gameplay.

Architecture Overview

graph TB
    subgraph "AI Pipeline"
        Perception[Perception Bus]
        Memory[Agent Memory]
        Planning[Planning Layer]
        Tools[Tool Sandbox]
        Validation[Engine Validation]
    end
    
    subgraph "Game World"
        ECS[ECS World State]
        Physics[Physics System]
        Nav[Navigation]
    end
    
    ECS --> Perception
    Perception --> Memory
    Memory --> Planning
    Planning --> Tools
    Tools --> Validation
    Validation --> ECS

Core Components

Perception Bus

The perception system provides AI agents with a filtered view of the world:

#![allow(unused)]
fn main() {
use astraweave_ai::perception::*;

#[derive(Component)]
struct AiAgent {
    perception_radius: f32,
    perception_filter: PerceptionFilter,
}

fn perception_system(
    agents: Query<(Entity, &Transform, &AiAgent)>,
    percievables: Query<(Entity, &Transform, &Percievable)>,
    mut perception_bus: ResMut<PerceptionBus>,
) {
    for (agent_entity, agent_transform, agent) in agents.iter() {
        let mut percepts = Vec::new();
        
        for (target, target_transform, percievable) in percievables.iter() {
            let distance = agent_transform.translation
                .distance(target_transform.translation);
            
            if distance <= agent.perception_radius {
                percepts.push(Percept {
                    entity: target,
                    position: target_transform.translation,
                    category: percievable.category,
                    properties: percievable.properties.clone(),
                });
            }
        }
        
        perception_bus.update(agent_entity, percepts);
    }
}
}

See Perception Bus for details.

Planning Layer

AI agents use LLM-based planning to decide actions:

#![allow(unused)]
fn main() {
use astraweave_ai::planning::*;

fn planning_system(
    mut agents: Query<(&mut AiPlanner, &PerceptionState)>,
    llm: Res<LlmClient>,
) {
    for (mut planner, perception) in agents.iter_mut() {
        if planner.needs_replan() {
            let context = build_context(perception);
            
            match llm.plan(&context, &planner.available_tools) {
                Ok(plan) => planner.set_plan(plan),
                Err(e) => planner.fallback_behavior(),
            }
        }
    }
}
}

See Planning Layer for details.

Tool Sandbox

All AI actions go through the tool sandbox for validation:

#![allow(unused)]
fn main() {
use astraweave_ai::tools::*;

fn execute_tool_system(
    mut agents: Query<&mut AiPlanner>,
    mut tool_executor: ResMut<ToolExecutor>,
    validator: Res<ToolValidator>,
) {
    for mut planner in agents.iter_mut() {
        if let Some(tool_call) = planner.next_action() {
            match validator.validate(&tool_call) {
                ValidationResult::Valid => {
                    tool_executor.execute(tool_call);
                }
                ValidationResult::Invalid(reason) => {
                    planner.action_failed(reason);
                }
            }
        }
    }
}
}

See Tool Sandbox for details.

Behavior Trees

For deterministic, reactive behaviors:

#![allow(unused)]
fn main() {
use astraweave_behavior::*;

let patrol_tree = BehaviorTree::new(
    Selector::new(vec![
        Sequence::new(vec![
            Condition::new(|ctx| ctx.enemy_visible()),
            Action::new(|ctx| ctx.engage_combat()),
        ]),
        Sequence::new(vec![
            Condition::new(|ctx| ctx.at_patrol_point()),
            Action::new(|ctx| ctx.next_patrol_point()),
        ]),
        Action::new(|ctx| ctx.move_to_patrol_point()),
    ])
);
}

See Behavior Trees for details.

AI Agent Configuration

Basic Agent Setup

#![allow(unused)]
fn main() {
fn spawn_companion(world: &mut World) -> Entity {
    world.spawn((
        Transform::default(),
        AiAgent::new()
            .with_personality("friendly and helpful")
            .with_perception_radius(15.0)
            .with_tick_budget(Duration::from_millis(8)),
        
        PerceptionState::default(),
        AiPlanner::new(vec![
            Tool::move_to(),
            Tool::attack(),
            Tool::use_item(),
            Tool::speak(),
        ]),
        
        NavAgent::default(),
        DialogueCapable::default(),
    ))
}
}

Tick Budget

AI has a strict time budget per simulation tick:

#![allow(unused)]
fn main() {
let config = AiConfig {
    tick_budget_ms: 8,
    max_concurrent_plans: 4,
    plan_cache_duration: Duration::from_secs(1),
    fallback_on_timeout: true,
};
}

If planning exceeds the budget, agents use cached plans or fallback behaviors.

LLM Integration

Ollama Setup

AstraWeave uses Ollama for local LLM inference:

ollama serve
ollama pull hermes2-pro-mistral

Configuration

#![allow(unused)]
fn main() {
let llm_config = LlmConfig {
    endpoint: "http://localhost:11434".into(),
    model: "hermes2-pro-mistral".into(),
    temperature: 0.7,
    max_tokens: 256,
    timeout: Duration::from_millis(100),
};
}

Tool Calling

AstraWeave uses structured tool calling:

#![allow(unused)]
fn main() {
let tools = vec![
    ToolDefinition {
        name: "move_to",
        description: "Move to a target location",
        parameters: json!({
            "type": "object",
            "properties": {
                "target": { "type": "array", "items": { "type": "number" } }
            }
        }),
    },
];

let response = llm.generate_with_tools(prompt, &tools).await?;
}

Performance Considerations

Batching

Group AI queries for efficiency:

#![allow(unused)]
fn main() {
let batch = agents.iter()
    .filter(|a| a.needs_replan())
    .take(4)
    .collect::<Vec<_>>();

let results = llm.batch_plan(&batch).await;
}

Caching

Cache plans to reduce LLM calls:

#![allow(unused)]
fn main() {
let planner = AiPlanner::new(tools)
    .with_plan_cache(Duration::from_secs(2))
    .with_context_hash(true);
}

Fallback Behaviors

Always define fallbacks:

#![allow(unused)]
fn main() {
impl AiAgent {
    fn fallback_behavior(&self) -> Action {
        match self.role {
            Role::Companion => Action::FollowPlayer,
            Role::Guard => Action::Patrol,
            Role::Merchant => Action::Idle,
        }
    }
}
}

Subsections

Perception Bus

Planning Layer

Tool Sandbox

Behavior Trees

AI Arbiter System

Status: Production Ready
Crate: astraweave-ai (requires llm_orchestrator feature)
Documentation: See also Complete Implementation Guide

The AIArbiter is a hybrid AI control system that combines instant tactical decisions (GOAP) with deep strategic reasoning (LLM), achieving zero user-facing latency while maintaining LLM-level intelligence.

The Problem

Traditional game AI faces a dilemma:

ApproachLatencyIntelligence
Fast AI (GOAP, BT)~100 nsLimited reasoning
Smart AI (LLM)13-21 secondsDeep understanding

Players either wait 20 seconds for smart AI or get immediate but shallow responses.

The Solution

The arbiter provides zero user-facing latency by:

  1. Instant GOAP control - Returns tactical actions in 101.7 ns
  2. Background LLM planning - Generates strategic plans asynchronously
  3. Seamless transitions - Switches to LLM plans when ready
  4. Non-blocking polling - Checks LLM completion in 104.7 ns

Performance

OperationLatencyTargetSpeedup
GOAP update101.7 ns100 µs982×
LLM polling575 ns50 µs86×
Mode transition221.9 ns10 µs45×
Full 3-step cycle313.7 ns

Scalability

AgentsOverheadFrame BudgetStatus
1,000101.7 µs0.6%
10,0001.02 ms6.1%
50,0005.09 ms30.5%⚠️

Quick Start

#![allow(unused)]
fn main() {
use astraweave_ai::{AIArbiter, LlmExecutor, GoapOrchestrator, RuleOrchestrator};
use std::sync::Arc;

// Create arbiter
let llm_orch = Arc::new(LlmOrchestrator::new(/* config */));
let runtime = tokio::runtime::Handle::current();
let llm_executor = LlmExecutor::new(llm_orch, runtime);

let goap = Box::new(GoapOrchestrator::new());
let bt = Box::new(RuleOrchestrator);

let mut arbiter = AIArbiter::new(llm_executor, goap, bt);

// Game loop
loop {
    let snapshot = build_world_snapshot(/* ... */);
    let action = arbiter.update(&snapshot);  // 101.7 ns
    execute_action(action);
}
}

Architecture

Three-Tier Control System

┌─────────────────────────────────────────────────────┐
│                   AIArbiter                         │
│  (Orchestration Layer - 101.7 ns overhead)          │
└────┬────────────────────┬────────────────────┬──────┘
     │                    │                    │
     ▼                    ▼                    ▼
┌──────────┐      ┌──────────────┐     ┌──────────┐
│   GOAP   │      │   Qwen3-8B    │     │    BT    │
│ (3-5 ns) │      │ (13-21s async)│     │ Fallback │
└──────────┘      └──────────────┘     └──────────┘

Mode State Machine

        ┌──────────────┐
        │     GOAP     │ ◄─────────┐
        │ (Instant AI) │           │
        └───────┬──────┘           │
                │                  │
                │ LLM ready        │ Plan exhausted
                │                  │
        ┌───────▼──────────┐       │
        │   ExecutingLLM   │───────┘
        │  (Step-by-step)  │
        └──────────────────┘
                │
                │ Empty plan
                ▼
        ┌──────────────┐
        │ BehaviorTree │
        │  (Fallback)  │
        └──────────────┘

API Reference

AIArbiter

#![allow(unused)]
fn main() {
pub struct AIArbiter { /* ... */ }

impl AIArbiter {
    /// Create new arbiter in GOAP mode
    pub fn new(
        llm_executor: LlmExecutor,
        goap: Box<dyn Orchestrator>,
        bt: Box<dyn Orchestrator>,
    ) -> Self;
    
    /// Set LLM request cooldown (default: 15s)
    pub fn with_llm_cooldown(self, cooldown: f32) -> Self;
    
    /// Main control loop - call every frame
    pub fn update(&mut self, snap: &WorldSnapshot) -> ActionStep;
    
    /// Get current mode
    pub fn mode(&self) -> AIControlMode;
    
    /// Check if LLM task is active
    pub fn is_llm_active(&self) -> bool;
    
    /// Get performance metrics
    pub fn metrics(&self) -> (
        usize,  // mode_transitions
        usize,  // llm_requests
        usize,  // llm_successes
        usize,  // llm_failures
        usize,  // goap_actions
        usize,  // llm_steps_executed
    );
}
}

AIControlMode

#![allow(unused)]
fn main() {
pub enum AIControlMode {
    GOAP,                              // Fast tactical mode
    ExecutingLLM { step_index: usize }, // Executing LLM plan
    BehaviorTree,                      // Emergency fallback
}
}

Common Patterns

Pattern 1: Basic Agent

#![allow(unused)]
fn main() {
pub struct AIAgent {
    arbiter: AIArbiter,
}

impl AIAgent {
    pub fn update(&mut self, snap: &WorldSnapshot) -> ActionStep {
        self.arbiter.update(snap)
    }
}
}

Pattern 2: Shared LLM Executor

#![allow(unused)]
fn main() {
// Create once, clone for each agent
let base_executor = LlmExecutor::new(llm_orch, runtime);

let agents: Vec<AIAgent> = (0..100)
    .map(|_| AIAgent::new(base_executor.clone()))
    .collect();
}

Pattern 3: Custom Cooldown

#![allow(unused)]
fn main() {
// Aggressive (more LLM requests)
let arbiter = AIArbiter::new(executor, goap, bt)
    .with_llm_cooldown(5.0);

// Passive (fewer LLM requests)
let arbiter = AIArbiter::new(executor, goap, bt)
    .with_llm_cooldown(30.0);
}

Pattern 4: Metrics Monitoring

#![allow(unused)]
fn main() {
let (transitions, requests, successes, failures, goap_actions, llm_steps) = 
    arbiter.metrics();

let success_rate = 100.0 * successes as f64 / requests as f64;
if success_rate < 50.0 {
    warn!("LLM success rate low: {:.1}%", success_rate);
}
}

Pattern 5: Mode-Specific Logic

#![allow(unused)]
fn main() {
match arbiter.mode() {
    AIControlMode::GOAP => {
        ui.show_status("Tactical Mode");
    }
    AIControlMode::ExecutingLLM { step_index } => {
        ui.show_status(&format!("Strategic Step {}", step_index));
        ui.show_indicator("LLM Active");
    }
    AIControlMode::BehaviorTree => {
        ui.show_warning("Fallback Mode");
    }
}
}

Cooldown Configuration

The LLM cooldown controls how frequently the arbiter requests new strategic plans:

CooldownUse Case
5sAggressive - Frequent strategic updates
15sDefault - Balanced performance
30sPassive - Reduce LLM costs
0sImmediate - Testing only
#![allow(unused)]
fn main() {
let arbiter = AIArbiter::new(executor, goap, bt)
    .with_llm_cooldown(15.0);  // Default
}

Troubleshooting

LLM Never Completes

Symptoms: is_llm_active() always true, never transitions to ExecutingLLM

Causes:

  1. Ollama not running
  2. Model not loaded
  3. Network issues

Fix:

# Verify Ollama is running
ollama list

# Test model directly
ollama run qwen3:8b

High Failure Rate

Symptoms: llm_failures > 50% of requests

Causes:

  1. Model quality issues
  2. Bad prompts
  3. Timeout too short

Fix:

#![allow(unused)]
fn main() {
// Increase cooldown to reduce impact
let arbiter = AIArbiter::new(executor, goap, bt)
    .with_llm_cooldown(30.0);
}

Stuck in ExecutingLLM

Symptoms: Same action repeated, step_index doesn’t advance

Causes:

  1. Plan has duplicate steps
  2. Plan too long

Fix: Validate plan length before execution:

#![allow(unused)]
fn main() {
if plan.steps.len() > 50 {
    warn!("LLM plan too long: {} steps", plan.steps.len());
}
}

Running the Demo

# GOAP-only mode
cargo run -p hello_companion --release

# Arbiter mode (GOAP + Qwen3-8B)
cargo run -p hello_companion --release --features llm_orchestrator -- --arbiter

Expected output:

Frame 0: MoveTo { x: 5, y: 5 } (GOAP)
Frame 1: TakeCover { position: Some((3, 2)) } (GOAP)
[INFO] LLM plan ready: 3 steps
Frame 3: MoveTo { x: 4, y: 0 } (ExecutingLLM[step 1])
Frame 4: TakeCover { position: Some((4, 1)) } (ExecutingLLM[step 2])
Frame 5: Attack { target: 1 } (ExecutingLLM[step 3])
[INFO] Plan exhausted, returning to GOAP
Frame 6: MoveTo { x: 5, y: 5 } (GOAP)

See Also

Physics System

The AstraWeave physics system provides comprehensive 3D physics simulation through integration with Rapier, a high-performance physics engine written in Rust.

Overview

The physics system runs at a fixed 60Hz tick rate to ensure deterministic simulation.

Key features:

  • Rigid Body Dynamics - Full 3D rigid body simulation
  • Character Controllers - Player and NPC movement with collision
  • Collision Detection - Broad and narrow phase collision
  • Spatial Queries - Raycasting, shape casting, overlap tests
  • Joints & Constraints - Connect bodies with various joint types
  • Continuous Collision Detection - Prevents tunneling for fast objects

Architecture

graph TB
    subgraph Physics Pipeline
        A[Collect Inputs] --> B[Broad Phase]
        B --> C[Narrow Phase]
        C --> D[Constraint Solver]
        D --> E[Position Integration]
        E --> F[Sync to ECS]
    end
    
    G[ECS World] --> A
    F --> G

Core Components

RigidBody

Represents a physics-simulated body:

#![allow(unused)]
fn main() {
use astraweave_physics::{RigidBody, RigidBodyType};

let body = RigidBody {
    body_type: RigidBodyType::Dynamic,
    mass: 1.0,
    linear_damping: 0.1,
    angular_damping: 0.1,
    gravity_scale: 1.0,
    ..Default::default()
};
}

Body types:

  • Dynamic - Fully simulated, responds to forces
  • Static - Immovable, used for environment
  • Kinematic - Moved by code, pushes dynamic bodies

Collider

Defines collision shapes:

#![allow(unused)]
fn main() {
use astraweave_physics::{Collider, ColliderShape};

let collider = Collider {
    shape: ColliderShape::Box { half_extents: Vec3::new(1.0, 1.0, 1.0) },
    friction: 0.5,
    restitution: 0.3,
    sensor: false,
    ..Default::default()
};
}

Available shapes:

  • Box - Axis-aligned box
  • Sphere - Perfect sphere
  • Capsule - Cylinder with spherical caps
  • Cylinder - Circular cylinder
  • ConvexHull - Convex mesh
  • TriMesh - Triangle mesh (static only)
  • HeightField - Terrain heightmap

CharacterController

Handles player/NPC movement:

#![allow(unused)]
fn main() {
use astraweave_physics::CharacterController;

let controller = CharacterController {
    height: 1.8,
    radius: 0.3,
    step_height: 0.3,
    max_slope: 45.0_f32.to_radians(),
    ..Default::default()
};
}

Spatial Queries

Raycasting

#![allow(unused)]
fn main() {
use astraweave_physics::{RaycastQuery, RaycastHit};

let query = RaycastQuery {
    origin: Vec3::new(0.0, 5.0, 0.0),
    direction: Vec3::NEG_Y,
    max_distance: 100.0,
    filter: CollisionFilter::default(),
};

if let Some(hit) = physics.raycast(&query) {
    println!("Hit entity {:?} at distance {}", hit.entity, hit.distance);
}
}

Shape Casting

#![allow(unused)]
fn main() {
use astraweave_physics::{ShapeCastQuery, ColliderShape};

let query = ShapeCastQuery {
    shape: ColliderShape::Sphere { radius: 0.5 },
    origin: start_pos,
    direction: velocity.normalize(),
    max_distance: velocity.length(),
};

let hits = physics.shape_cast(&query);
}

Overlap Tests

#![allow(unused)]
fn main() {
use astraweave_physics::OverlapQuery;

let query = OverlapQuery {
    shape: ColliderShape::Sphere { radius: 5.0 },
    position: explosion_center,
    filter: CollisionFilter::default(),
};

for entity in physics.overlap(&query) {
    apply_explosion_damage(entity);
}
}

Collision Filtering

Control which objects can collide:

#![allow(unused)]
fn main() {
use astraweave_physics::{CollisionFilter, CollisionGroup};

let player_filter = CollisionFilter {
    membership: CollisionGroup::PLAYER,
    filter: CollisionGroup::WORLD | CollisionGroup::ENEMY | CollisionGroup::PROJECTILE,
};
}

Performance Optimization

Spatial Hash

The physics system uses a spatial hash for broad-phase acceleration:

#![allow(unused)]
fn main() {
use astraweave_physics::SpatialHash;

let spatial = SpatialHash::new(10.0);
spatial.insert(entity, aabb);

let nearby = spatial.query_aabb(&query_aabb);
}

Async Scheduling

For large simulations, physics can run asynchronously:

#![allow(unused)]
fn main() {
use astraweave_physics::AsyncPhysicsScheduler;

let scheduler = AsyncPhysicsScheduler::new(4);
scheduler.step(&mut physics_world, delta_time).await;
}

Integration with ECS

Physics components sync automatically with ECS transforms:

#![allow(unused)]
fn main() {
fn physics_sync_system(
    query: Query<(&mut Transform, &RigidBodyHandle)>,
    physics: Res<PhysicsWorld>,
) {
    for (mut transform, handle) in query.iter_mut() {
        if let Some(body) = physics.get_body(*handle) {
            transform.translation = body.position();
            transform.rotation = body.rotation();
        }
    }
}
}

See Also

Rendering System

AstraWeave’s rendering system is built on top of WGPU, providing a modern, high-performance graphics pipeline with support for physically-based rendering (PBR), advanced lighting techniques, and post-processing effects.

Architecture Overview

The rendering system follows a modular architecture with clear separation of concerns:

graph TD
    A[Render Graph] --> B[Material System]
    A --> C[Lighting System]
    A --> D[Post-Processing]
    B --> E[Texture Streaming]
    C --> F[Clustered Forward]
    D --> G[Bloom/Tonemap]
    E --> H[GPU Upload]
    F --> I[Light Culling]

Key Components

  • Render Graph: Declarative frame composition with automatic resource barriers
  • Material System: PBR materials with metallic-roughness workflow
  • Lighting: Clustered forward rendering supporting thousands of dynamic lights
  • Texture Streaming: Virtual texture system with priority-based loading
  • Post-Processing: Modular effects stack (bloom, tonemap, FXAA, etc.)
The rendering system is designed to scale from integrated graphics to high-end GPUs, with automatic quality adaptation based on detected hardware capabilities.

WGPU-Based Renderer

AstraWeave uses WGPU as its graphics abstraction layer, providing cross-platform support for Vulkan, Metal, DirectX 12, and WebGPU.

Initialization

#![allow(unused)]
fn main() {
use astraweave_render::{RenderContext, RenderConfig};

// Initialize the renderer
let config = RenderConfig {
    width: 1920,
    height: 1080,
    vsync: true,
    msaa_samples: 4,
    ..Default::default()
};

let render_ctx = RenderContext::new(&window, config).await?;
}

Device and Queue Management

The render context manages the WGPU device and queue lifecycle:

#![allow(unused)]
fn main() {
// Access device for resource creation
let device = render_ctx.device();
let queue = render_ctx.queue();

// Create a texture
let texture = device.create_texture(&wgpu::TextureDescriptor {
    label: Some("game_texture"),
    size: wgpu::Extent3d {
        width: 1024,
        height: 1024,
        depth_or_array_layers: 1,
    },
    mip_level_count: 1,
    sample_count: 1,
    dimension: wgpu::TextureDimension::D2,
    format: wgpu::TextureFormat::Rgba8UnormSrgb,
    usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
    view_formats: &[],
});
}

PBR Materials and Lighting

AstraWeave implements a complete PBR pipeline based on the metallic-roughness workflow, closely following theglTF 2.0 specification.

Material Definition

#![allow(unused)]
fn main() {
use astraweave_render::material::{Material, PbrMaterial};

let material = PbrMaterial {
    base_color: [1.0, 1.0, 1.0, 1.0],
    base_color_texture: Some(texture_handle),
    metallic: 0.0,
    roughness: 0.5,
    metallic_roughness_texture: Some(mr_texture),
    normal_texture: Some(normal_map),
    emissive: [0.0, 0.0, 0.0],
    emissive_texture: None,
    occlusion_texture: Some(ao_map),
    alpha_mode: AlphaMode::Opaque,
    double_sided: false,
};

// Register material with renderer
let material_id = render_ctx.register_material(material);
}

Material Properties

PropertyTypeDescription
base_color[f32; 4]RGB color with alpha channel
metallicf32Metallic factor (0 = dielectric, 1 = metal)
roughnessf32Surface roughness (0 = smooth, 1 = rough)
emissive[f32; 3]Emissive color (HDR)
normal_textureOption<Handle>Tangent-space normal map
occlusion_textureOption<Handle>Ambient occlusion map
Use texture packing to reduce memory bandwidth: combine metallic (B channel), roughness (G channel), and occlusion (R channel) into a single texture.

Lighting Model

The PBR shader implements a Cook-Torrance BRDF with:

  • Diffuse: Lambertian diffuse term
  • Specular: GGX/Trowbridge-Reitz normal distribution
  • Fresnel: Schlick’s approximation
  • Geometry: Smith’s shadowing-masking function
#![allow(unused)]
fn main() {
// BRDF calculation (shader pseudo-code)
fn pbr_brdf(
    N: vec3<f32>,  // Normal
    V: vec3<f32>,  // View direction
    L: vec3<f32>,  // Light direction
    F0: vec3<f32>, // Fresnel reflectance at 0°
    roughness: f32,
    metallic: f32,
) -> vec3<f32> {
    let H = normalize(V + L);
    let NdotH = max(dot(N, H), 0.0);
    let NdotV = max(dot(N, V), 0.0);
    let NdotL = max(dot(N, L), 0.0);
    
    // Distribution term (GGX)
    let D = distribution_ggx(NdotH, roughness);
    
    // Geometry term (Smith)
    let G = geometry_smith(NdotV, NdotL, roughness);
    
    // Fresnel term (Schlick)
    let F = fresnel_schlick(max(dot(H, V), 0.0), F0);
    
    // Cook-Torrance BRDF
    let specular = (D * G * F) / (4.0 * NdotV * NdotL + 0.0001);
    
    let kD = (vec3(1.0) - F) * (1.0 - metallic);
    let diffuse = kD * base_color.rgb / PI;
    
    return (diffuse + specular) * radiance * NdotL;
}
}

Clustered Forward Rendering

Clustered forward rendering divides the view frustum into a 3D grid of clusters, assigning lights to clusters for efficient culling.

Cluster Configuration

#![allow(unused)]
fn main() {
use astraweave_render::lighting::{ClusteredLightingConfig, LightCluster};

let cluster_config = ClusteredLightingConfig {
    tile_size_x: 16,
    tile_size_y: 16,
    z_slices: 24,
    max_lights_per_cluster: 256,
};

render_ctx.configure_clustered_lighting(cluster_config);
}

Cluster Grid

graph LR
    A[View Frustum] --> B[XY Tiling]
    B --> C[Z Slicing]
    C --> D[Cluster Grid]
    D --> E[Light Assignment]
    E --> F[Per-Cluster Light Lists]

The frustum is divided into:

  • XY tiles: Screen-space subdivision (typically 16x16 pixels)
  • Z slices: Exponential depth slices for better near-field distribution

Adding Lights

#![allow(unused)]
fn main() {
use astraweave_render::lighting::{PointLight, SpotLight, DirectionalLight};

// Point light
let point_light = PointLight {
    position: [0.0, 5.0, 0.0],
    color: [1.0, 0.9, 0.8],
    intensity: 100.0,
    radius: 10.0,
    falloff: 2.0, // Inverse square falloff
};

render_ctx.add_light(point_light);

// Directional light (sun)
let sun = DirectionalLight {
    direction: [-0.3, -1.0, -0.5],
    color: [1.0, 0.95, 0.9],
    intensity: 5.0,
    cast_shadows: true,
    shadow_cascade_count: 4,
};

render_ctx.set_directional_light(sun);
}

Light Culling

The system performs GPU-based light culling using a compute shader:

#![allow(unused)]
fn main() {
// Compute shader for light culling (WGSL)
@compute @workgroup_size(16, 16, 1)
fn cull_lights(
    @builtin(global_invocation_id) global_id: vec3<u32>,
) {
    let cluster_id = compute_cluster_id(global_id);
    let cluster_aabb = get_cluster_bounds(cluster_id);
    
    var light_count: u32 = 0u;
    for (var i = 0u; i < light_buffer.count; i++) {
        let light = light_buffer.lights[i];
        if (sphere_aabb_intersection(light.position, light.radius, cluster_aabb)) {
            light_indices[cluster_id * MAX_LIGHTS + light_count] = i;
            light_count++;
        }
    }
    
    cluster_light_counts[cluster_id] = light_count;
}
}
Clustered forward rendering requires compute shader support. Fallback to traditional forward rendering is provided for older hardware.

Post-Processing Effects

AstraWeave provides a flexible post-processing stack with composable effects.

Effect Pipeline

#![allow(unused)]
fn main() {
use astraweave_render::post::{PostProcessStack, BloomEffect, TonemapEffect, FxaaEffect};

let mut post_stack = PostProcessStack::new();

// Add bloom
post_stack.add_effect(BloomEffect {
    threshold: 1.0,
    intensity: 0.5,
    blur_passes: 5,
});

// Add tonemapping
post_stack.add_effect(TonemapEffect {
    exposure: 1.0,
    operator: TonemapOperator::AcesFilmic,
});

// Add anti-aliasing
post_stack.add_effect(FxaaEffect {
    quality: FxaaQuality::High,
});

render_ctx.set_post_processing(post_stack);
}

Available Effects

EffectDescriptionPerformance Impact
BloomHDR glow effectMedium
TonemapHDR to LDR mappingLow
FXAAFast approximate anti-aliasingLow
SMAASubpixel morphological anti-aliasingMedium
Depth of FieldBokeh blur based on depthHigh
Motion BlurVelocity-based blurMedium
Color GradingLUT-based color correctionLow

Custom Post-Processing Effects

#![allow(unused)]
fn main() {
use astraweave_render::post::{PostEffect, PostEffectContext};

struct CustomVignetteEffect {
    intensity: f32,
    radius: f32,
}

impl PostEffect for CustomVignetteEffect {
    fn apply(&self, ctx: &mut PostEffectContext) -> Result<()> {
        let shader = ctx.load_shader("vignette.wgsl")?;
        
        ctx.bind_uniform("intensity", &self.intensity);
        ctx.bind_uniform("radius", &self.radius);
        ctx.bind_texture(0, ctx.input_texture());
        
        ctx.dispatch_fullscreen(shader)?;
        
        Ok(())
    }
}
}

Texture Streaming

The texture streaming system manages GPU memory efficiently by loading textures on-demand with priority-based eviction.

Virtual Texture System

#![allow(unused)]
fn main() {
use astraweave_render::texture::{TextureStreamingConfig, VirtualTexture};

let streaming_config = TextureStreamingConfig {
    budget_mb: 512,
    min_mip_level: 0,
    max_mip_level: 12,
    load_bias: 0.0,
    anisotropy: 16,
};

render_ctx.configure_texture_streaming(streaming_config);
}

Loading Textures

#![allow(unused)]
fn main() {
use astraweave_render::texture::TextureLoader;

// Load texture with streaming
let texture = TextureLoader::new()
    .with_streaming(true)
    .with_mip_levels(8)
    .with_compression(TextureCompression::BC7)
    .load("assets/textures/ground_albedo.png")?;

// Set priority (higher = load sooner)
texture.set_priority(1.0);
}

Mip Streaming

The system automatically selects mip levels based on:

  • Distance to camera
  • Screen coverage
  • Available GPU memory
  • User-defined priority
graph TD
    A[Texture Request] --> B{In Budget?}
    B -->|Yes| C[Load Full Resolution]
    B -->|No| D[Calculate Required Mip]
    D --> E[Load Mip Level]
    E --> F{Budget Exceeded?}
    F -->|Yes| G[Evict Low Priority]
    F -->|No| H[Complete]
    G --> H
    C --> H
Pre-generate mip chains offline and compress textures to reduce load times. AstraWeave supports BC1-BC7, ASTC, and ETC2 compression formats.

Performance Optimization

GPU Profiling

#![allow(unused)]
fn main() {
use astraweave_render::profiling::GpuProfiler;

let profiler = GpuProfiler::new(&render_ctx);

profiler.begin_frame();
{
    profiler.begin_scope("Shadow Pass");
    render_shadows(&mut render_ctx);
    profiler.end_scope();
    
    profiler.begin_scope("Main Pass");
    render_main_scene(&mut render_ctx);
    profiler.end_scope();
    
    profiler.begin_scope("Post Processing");
    apply_post_effects(&mut render_ctx);
    profiler.end_scope();
}
profiler.end_frame();

// Print timing report
println!("{}", profiler.report());
}

Instancing

Use GPU instancing for rendering many identical meshes:

#![allow(unused)]
fn main() {
use astraweave_render::mesh::InstancedMesh;

let instances: Vec<InstanceData> = trees
    .iter()
    .map(|tree| InstanceData {
        transform: tree.transform.matrix(),
        color: tree.color,
    })
    .collect();

render_ctx.draw_instanced(tree_mesh, &instances);
}

Occlusion Culling

#![allow(unused)]
fn main() {
use astraweave_render::culling::OcclusionCulling;

let mut occlusion = OcclusionCulling::new(&render_ctx);

// First pass: render occluders
occlusion.render_occluders(&occluder_meshes);

// Second pass: test visibility
let visible_objects = occlusion.test_visibility(&all_objects);

// Third pass: render only visible objects
render_objects(&visible_objects);
}

Code Examples

Complete Rendering Loop

use astraweave_render::{RenderContext, Camera, Scene};
use winit::event_loop::EventLoop;

fn main() -> Result<()> {
    let event_loop = EventLoop::new();
    let window = Window::new(&event_loop)?;
    
    let mut render_ctx = RenderContext::new(&window, Default::default()).await?;
    let mut scene = Scene::new();
    let mut camera = Camera::perspective(60.0, 16.0 / 9.0, 0.1, 1000.0);
    
    event_loop.run(move |event, _, control_flow| {
        match event {
            Event::RedrawRequested(_) => {
                // Update camera
                camera.update(&input_state);
                
                // Begin frame
                let mut frame = render_ctx.begin_frame().unwrap();
                
                // Render scene
                frame.render_scene(&scene, &camera);
                
                // Present
                frame.present();
            }
            _ => {}
        }
    });
}

Dynamic Material Updates

#![allow(unused)]
fn main() {
// Update material properties at runtime
let material = render_ctx.get_material_mut(material_id)?;
material.base_color = [1.0, 0.0, 0.0, 1.0]; // Change to red
material.roughness = 0.8; // Make rougher
render_ctx.mark_material_dirty(material_id);
}

API Reference

For complete API documentation, see:

Audio System

AstraWeave’s audio system provides comprehensive spatial audio, dialogue management, and music/SFX playback capabilities using modern Rust audio libraries (kira/rodio).

Architecture Overview

The audio system is designed for real-time game audio with low latency, spatial positioning, and dynamic mixing.

graph TD
    A[Audio Engine] --> B[Spatial Audio]
    A --> C[Dialogue Runtime]
    A --> D[Music Manager]
    A --> E[SFX Manager]
    B --> F[3D Positioning]
    B --> G[Attenuation]
    C --> H[Voice System]
    D --> I[Adaptive Music]
    E --> J[Sound Pool]

Key Components

  • Audio Engine: Core audio processing and device management
  • Spatial Audio: 3D positional audio with HRTF and distance attenuation
  • Dialogue Runtime: Conversation system with voice line playback
  • Music Manager: Adaptive music with crossfading and layering
  • SFX Manager: Sound effect pooling and priority management
The audio system supports multiple backends (WASAPI, ALSA, CoreAudio) and automatically selects the best available option for the platform.

Audio Engine Initialization

Basic Setup

#![allow(unused)]
fn main() {
use astraweave_audio::{AudioEngine, AudioConfig};

let config = AudioConfig {
    sample_rate: 48000,
    buffer_size: 512,
    num_channels: 2,
    enable_spatial_audio: true,
    max_voices: 64,
    ..Default::default()
};

let mut audio_engine = AudioEngine::new(config)?;
}

Backend Selection

#![allow(unused)]
fn main() {
use astraweave_audio::backend::{AudioBackend, BackendType};

// Explicitly select backend
let backend = AudioBackend::new(BackendType::WASAPI)?;
let audio_engine = AudioEngine::with_backend(backend, config)?;

// Or use automatic selection
let audio_engine = AudioEngine::auto_detect(config)?;
}
For lowest latency on Windows, use WASAPI in exclusive mode. For compatibility, use the automatic backend selection.

Spatial Audio

AstraWeave’s spatial audio system provides realistic 3D sound positioning with distance attenuation, obstruction, and HRTF (Head-Related Transfer Function) support.

3D Audio Listener

#![allow(unused)]
fn main() {
use astraweave_audio::spatial::{AudioListener, ListenerOrientation};

// Create listener (typically attached to camera/player)
let mut listener = AudioListener::new();

// Update listener position and orientation each frame
listener.set_position([player_pos.x, player_pos.y, player_pos.z]);
listener.set_orientation(ListenerOrientation {
    forward: [forward.x, forward.y, forward.z],
    up: [up.x, up.y, up.z],
});

audio_engine.set_listener(listener);
}

Spatial Sound Sources

#![allow(unused)]
fn main() {
use astraweave_audio::spatial::{SpatialSound, AttenuationModel};

// Load and play spatial sound
let sound = audio_engine.load_sound("assets/audio/footstep.ogg")?;

let spatial_sound = SpatialSound::new(sound)
    .with_position([10.0, 0.0, 5.0])
    .with_velocity([0.0, 0.0, 0.0]) // For Doppler effect
    .with_attenuation(AttenuationModel::InverseDistance {
        reference_distance: 1.0,
        max_distance: 50.0,
        rolloff_factor: 1.0,
    })
    .with_cone(90.0, 180.0, 0.5) // Directional sound
    .with_volume(0.8);

let handle = audio_engine.play_spatial(spatial_sound)?;
}

Attenuation Models

AstraWeave supports multiple distance attenuation models:

ModelFormulaUse Case
Linear1 - d/maxSimple, predictable falloff
Inverse Distanceref / (ref + rolloff * (d - ref))Realistic physical attenuation
Exponential(d / ref)^(-rolloff)Dramatic falloff for emphasis
CustomUser-defined curveComplete control
#![allow(unused)]
fn main() {
use astraweave_audio::spatial::AttenuationModel;

// Custom attenuation curve
let custom_attenuation = AttenuationModel::Custom(Box::new(|distance| {
    if distance < 5.0 {
        1.0
    } else if distance < 20.0 {
        1.0 - ((distance - 5.0) / 15.0).powi(2)
    } else {
        0.0
    }
}));
}

Doppler Effect

#![allow(unused)]
fn main() {
// Enable Doppler shift based on velocity
spatial_sound.set_doppler_factor(1.0); // 1.0 = realistic, 0.0 = disabled
spatial_sound.set_velocity([velocity.x, velocity.y, velocity.z]);
}

Obstruction and Occlusion

#![allow(unused)]
fn main() {
use astraweave_audio::spatial::Obstruction;

// Simple obstruction (lowpass filter based on raycast)
if raycast_to_listener_blocked {
    spatial_sound.set_obstruction(Obstruction {
        direct_gain: 0.3,      // Reduce direct sound
        lowpass_cutoff: 1000.0, // Apply lowpass filter
    });
} else {
    spatial_sound.clear_obstruction();
}
}
graph LR
    A[Sound Source] --> B{Line of Sight?}
    B -->|Clear| C[Direct Sound]
    B -->|Blocked| D[Obstructed Sound]
    D --> E[Apply Lowpass]
    D --> F[Reduce Gain]
    E --> G[Audio Output]
    F --> G
    C --> G

Dialogue Runtime

The dialogue runtime manages conversation playback with support for branching, voice lines, and subtitles.

Dialogue Graph

#![allow(unused)]
fn main() {
use astraweave_audio::dialogue::{DialogueGraph, DialogueNode, DialogueChoice};

let mut dialogue = DialogueGraph::new();

// Add dialogue nodes
let node1 = dialogue.add_node(DialogueNode {
    id: "greeting",
    speaker: "NPC_Guard",
    text: "Halt! What's your business here?",
    voice_line: Some("assets/voice/guard_greeting.ogg"),
    duration: 3.0,
});

let node2 = dialogue.add_node(DialogueNode {
    id: "response_friendly",
    speaker: "Player",
    text: "Just passing through, friend.",
    voice_line: Some("assets/voice/player_friendly.ogg"),
    duration: 2.5,
});

let node3 = dialogue.add_node(DialogueNode {
    id: "response_hostile",
    speaker: "Player",
    text: "None of your concern.",
    voice_line: Some("assets/voice/player_hostile.ogg"),
    duration: 2.0,
});

// Add choices
dialogue.add_choice(node1, DialogueChoice {
    text: "Be friendly",
    next_node: node2,
    condition: None,
});

dialogue.add_choice(node1, DialogueChoice {
    text: "Be hostile",
    next_node: node3,
    condition: None,
});
}

Playing Dialogue

#![allow(unused)]
fn main() {
use astraweave_audio::dialogue::DialoguePlayer;

let mut player = DialoguePlayer::new(&audio_engine);

// Start dialogue
player.start_dialogue(dialogue, "greeting")?;

// Update each frame
loop {
    player.update(delta_time)?;
    
    if let Some(current_node) = player.current_node() {
        println!("Speaker: {}", current_node.speaker);
        println!("Text: {}", current_node.text);
        
        // Display choices when node finishes
        if player.is_waiting_for_choice() {
            let choices = player.available_choices();
            for (idx, choice) in choices.iter().enumerate() {
                println!("{}: {}", idx, choice.text);
            }
        }
    }
    
    if player.is_finished() {
        break;
    }
}

// Handle player input
player.select_choice(0)?; // Select first choice
}

Voice System

#![allow(unused)]
fn main() {
use astraweave_audio::voice::{VoiceConfig, VoiceEffect};

// Configure voice processing
let voice_config = VoiceConfig {
    normalize: true,
    compress: true,      // Dynamic range compression
    eq_preset: Some(VoiceEffect::RadioFilter), // Apply effects
    ducking: true,       // Duck music/sfx during dialogue
};

player.set_voice_config(voice_config);
}

Subtitle Synchronization

#![allow(unused)]
fn main() {
use astraweave_audio::dialogue::SubtitleTrack;

// Load subtitle track
let subtitles = SubtitleTrack::load("assets/dialogue/quest1.srt")?;
player.set_subtitles(subtitles);

// Get current subtitle
if let Some(subtitle) = player.current_subtitle() {
    ui.draw_subtitle(subtitle.text, subtitle.speaker);
}
}
Ensure voice line durations match actual audio file lengths. Use the `analyze_duration()` function to automatically detect durations from audio files.

Music Manager

The music manager provides adaptive music with layering, crossfading, and dynamic transitions.

Music Tracks

#![allow(unused)]
fn main() {
use astraweave_audio::music::{MusicManager, MusicTrack, MusicLayer};

let mut music_manager = MusicManager::new(&audio_engine);

// Load music track with layers
let mut combat_music = MusicTrack::new("combat");
combat_music.add_layer(MusicLayer {
    name: "percussion",
    file: "assets/music/combat_percussion.ogg",
    volume: 1.0,
    loop_start: 0.0,
    loop_end: 32.0, // Loop point in seconds
});
combat_music.add_layer(MusicLayer {
    name: "strings",
    file: "assets/music/combat_strings.ogg",
    volume: 0.8,
    loop_start: 0.0,
    loop_end: 32.0,
});
combat_music.add_layer(MusicLayer {
    name: "brass",
    file: "assets/music/combat_brass.ogg",
    volume: 0.6,
    loop_start: 0.0,
    loop_end: 32.0,
});

music_manager.register_track(combat_music);
}

Adaptive Music System

#![allow(unused)]
fn main() {
// Play music with intensity control
music_manager.play("combat")?;

// Dynamically adjust layers based on game state
match combat_intensity {
    CombatIntensity::Low => {
        music_manager.set_layer_volume("combat", "percussion", 1.0);
        music_manager.set_layer_volume("combat", "strings", 0.3);
        music_manager.set_layer_volume("combat", "brass", 0.0);
    }
    CombatIntensity::Medium => {
        music_manager.set_layer_volume("combat", "percussion", 1.0);
        music_manager.set_layer_volume("combat", "strings", 0.8);
        music_manager.set_layer_volume("combat", "brass", 0.4);
    }
    CombatIntensity::High => {
        music_manager.set_layer_volume("combat", "percussion", 1.0);
        music_manager.set_layer_volume("combat", "strings", 1.0);
        music_manager.set_layer_volume("combat", "brass", 1.0);
    }
}
}

Crossfading and Transitions

#![allow(unused)]
fn main() {
use astraweave_audio::music::TransitionType;

// Immediate transition
music_manager.transition_to("exploration", TransitionType::Immediate)?;

// Crossfade over 2 seconds
music_manager.transition_to("exploration", TransitionType::Crossfade(2.0))?;

// Wait for musical bar/beat
music_manager.transition_to("exploration", TransitionType::OnBeat {
    bars: 1,  // Wait for next bar
    fade_duration: 1.0,
})?;

// Sequential (finish current, then play next)
music_manager.transition_to("victory", TransitionType::Sequential)?;
}

Music States

#![allow(unused)]
fn main() {
use astraweave_audio::music::MusicState;

// Create music state machine
let mut music_states = MusicState::new();
music_states.add_state("explore", "exploration_music");
music_states.add_state("combat", "combat_music");
music_states.add_state("boss", "boss_music");

// Define transitions
music_states.add_transition("explore", "combat", TransitionType::Crossfade(1.5));
music_states.add_transition("combat", "explore", TransitionType::OnBeat {
    bars: 2,
    fade_duration: 2.0,
});

// Trigger state changes
music_states.change_state("combat")?;
}
stateDiagram-v2
    [*] --> Exploration
    Exploration --> Combat: Enemy Detected
    Combat --> Exploration: Combat Ended
    Combat --> Boss: Boss Appears
    Boss --> Victory: Boss Defeated
    Victory --> Exploration: Return

SFX Manager

The SFX manager handles sound effect pooling, priority management, and one-shot sounds.

Sound Pool

#![allow(unused)]
fn main() {
use astraweave_audio::sfx::{SfxManager, SoundPool};

let mut sfx_manager = SfxManager::new(&audio_engine);

// Create sound pool for common effects
let footstep_pool = SoundPool::new()
    .add_variant("assets/sfx/footstep_01.ogg")
    .add_variant("assets/sfx/footstep_02.ogg")
    .add_variant("assets/sfx/footstep_03.ogg")
    .add_variant("assets/sfx/footstep_04.ogg")
    .with_random_pitch(0.95, 1.05)
    .with_max_instances(4); // Limit concurrent sounds

sfx_manager.register_pool("footstep", footstep_pool);
}

Playing Sound Effects

#![allow(unused)]
fn main() {
// Play from pool (automatically selects variant)
sfx_manager.play("footstep")?;

// Play with position
sfx_manager.play_at("footstep", player_position)?;

// Play with custom parameters
sfx_manager.play_with("explosion", SfxParams {
    position: Some(explosion_pos),
    volume: 1.0,
    pitch: 1.0,
    priority: SoundPriority::High,
})?;
}

Priority System

#![allow(unused)]
fn main() {
use astraweave_audio::sfx::SoundPriority;

// Define sound priorities
pub enum SoundPriority {
    Critical = 100,  // UI, dialogue
    High = 75,       // Explosions, important events
    Medium = 50,     // Gunfire, abilities
    Low = 25,        // Footsteps, ambient
    VeryLow = 10,    // Background details
}

// Priority-based voice stealing
sfx_manager.set_max_voices(32);
sfx_manager.set_voice_stealing(true); // Steal low priority when full
}

Sound Categories

#![allow(unused)]
fn main() {
use astraweave_audio::sfx::SoundCategory;

// Create categories for volume control
sfx_manager.create_category("master", 1.0);
sfx_manager.create_category("sfx", 0.8);
sfx_manager.create_category("ui", 1.0);
sfx_manager.create_category("ambient", 0.6);

// Assign sounds to categories
sfx_manager.set_category("footstep", "sfx");
sfx_manager.set_category("button_click", "ui");

// Adjust category volumes
sfx_manager.set_category_volume("sfx", 0.5);
}

Audio Effects and Processing

Built-in Effects

#![allow(unused)]
fn main() {
use astraweave_audio::effects::{Effect, Reverb, LowPassFilter, HighPassFilter};

// Add reverb to spatial sound
let reverb = Reverb {
    room_size: 0.7,
    damping: 0.5,
    wet: 0.3,
    dry: 0.7,
};
spatial_sound.add_effect(Effect::Reverb(reverb));

// Lowpass filter for underwater effect
let underwater_filter = LowPassFilter {
    cutoff: 800.0,
    resonance: 1.0,
};
audio_engine.add_global_effect(Effect::LowPass(underwater_filter));
}

Custom DSP

#![allow(unused)]
fn main() {
use astraweave_audio::dsp::{AudioProcessor, AudioBuffer};

struct CustomDistortion {
    gain: f32,
    threshold: f32,
}

impl AudioProcessor for CustomDistortion {
    fn process(&mut self, buffer: &mut AudioBuffer) {
        for sample in buffer.samples_mut() {
            *sample *= self.gain;
            *sample = sample.clamp(-self.threshold, self.threshold);
        }
    }
}

// Apply custom processor
let distortion = CustomDistortion {
    gain: 2.0,
    threshold: 0.8,
};
sfx_manager.add_processor("distortion", Box::new(distortion));
}

Performance Optimization

Voice Limiting

#![allow(unused)]
fn main() {
// Limit total active voices
audio_engine.set_max_voices(64);

// Per-pool limiting
let gunshot_pool = SoundPool::new()
    .add_variant("gunshot.ogg")
    .with_max_instances(8); // Max 8 simultaneous gunshots
}

Streaming vs. Loaded

#![allow(unused)]
fn main() {
// Load small sounds into memory
let ui_click = audio_engine.load_sound_static("click.ogg")?;

// Stream large files (music, ambience)
let music = audio_engine.load_sound_streaming("music.ogg")?;
}

Audio Budget

#![allow(unused)]
fn main() {
use astraweave_audio::budget::AudioBudget;

let budget = AudioBudget {
    max_memory_mb: 256,
    max_streaming_sources: 4,
    max_static_sources: 60,
};

audio_engine.set_budget(budget);
}

Complete Example

Game Audio System

#![allow(unused)]
fn main() {
use astraweave_audio::*;

pub struct GameAudio {
    engine: AudioEngine,
    music: MusicManager,
    sfx: SfxManager,
    dialogue: DialoguePlayer,
}

impl GameAudio {
    pub fn new() -> Result<Self> {
        let mut engine = AudioEngine::new(AudioConfig::default())?;
        let music = MusicManager::new(&engine);
        let sfx = SfxManager::new(&engine);
        let dialogue = DialoguePlayer::new(&engine);
        
        Ok(Self { engine, music, sfx, dialogue })
    }
    
    pub fn update(&mut self, delta_time: f32, listener_pos: Vec3, listener_forward: Vec3) {
        // Update listener
        let mut listener = AudioListener::new();
        listener.set_position([listener_pos.x, listener_pos.y, listener_pos.z]);
        listener.set_orientation(ListenerOrientation {
            forward: [listener_forward.x, listener_forward.y, listener_forward.z],
            up: [0.0, 1.0, 0.0],
        });
        self.engine.set_listener(listener);
        
        // Update managers
        self.music.update(delta_time);
        self.dialogue.update(delta_time).ok();
    }
    
    pub fn play_footstep(&mut self, position: Vec3) {
        self.sfx.play_at("footstep", [position.x, position.y, position.z]).ok();
    }
    
    pub fn start_combat_music(&mut self) {
        self.music.transition_to("combat", TransitionType::Crossfade(1.5)).ok();
    }
}
}

API Reference

For complete API documentation, see:

Navigation System

AstraWeave’s navigation system provides robust pathfinding and agent movement with support for dynamic navmesh generation, A* pathfinding, local steering, and obstacle avoidance.

Architecture Overview

The navigation system consists of multiple layers working together to enable intelligent agent movement.

graph TD
    A[Navigation System] --> B[Navmesh Generation]
    A --> C[Pathfinding]
    A --> D[Steering]
    B --> E[Voxelization]
    B --> F[Region Building]
    C --> G[A* Search]
    D --> H[Obstacle Avoidance]
    D --> I[Local Movement]

Key Components

  • Navmesh Generation: Automatic navigation mesh creation from level geometry
  • Pathfinding: A* algorithm with hierarchical path optimization
  • Steering: Local navigation with obstacle avoidance and crowd simulation
  • Dynamic Updates: Runtime navmesh modification for destructible environments
  • Off-Mesh Links: Manual connections for jumps, ladders, and teleports
The navigation system is based on Recast & Detour algorithms, providing industry-standard navmesh generation and pathfinding.

Basic Navmesh Creation

#![allow(unused)]
fn main() {
use astraweave_nav::{NavmeshBuilder, NavmeshConfig};

let config = NavmeshConfig {
    cell_size: 0.3,           // XZ plane cell size
    cell_height: 0.2,         // Y axis cell size
    agent_height: 2.0,        // Agent height in meters
    agent_radius: 0.6,        // Agent radius in meters
    agent_max_climb: 0.9,     // Max step height
    agent_max_slope: 45.0,    // Max walkable slope in degrees
    region_min_size: 8.0,     // Min region area
    region_merge_size: 20.0,  // Region merge threshold
    edge_max_len: 12.0,       // Max edge length
    edge_max_error: 1.3,      // Edge simplification error
    verts_per_poly: 6,        // Vertices per polygon
    detail_sample_dist: 6.0,  // Detail mesh sample distance
    detail_sample_max_error: 1.0, // Detail mesh error
};

let mut builder = NavmeshBuilder::new(config);
}

Building from Geometry

#![allow(unused)]
fn main() {
use astraweave_nav::geometry::InputGeometry;

// Collect level geometry
let mut geometry = InputGeometry::new();

// Add static meshes
for mesh in level.static_meshes() {
    geometry.add_mesh(mesh.vertices(), mesh.indices());
}

// Mark areas (walkable, non-walkable, water, etc.)
geometry.mark_area(
    bounding_box,
    AreaType::NonWalkable,
);

// Build navmesh
let navmesh = builder.build(&geometry)?;
}
#![allow(unused)]
fn main() {
use astraweave_nav::NavmeshConfig;

// Humanoid character (default)
let human_config = NavmeshConfig::humanoid();

// Small creature (rat, dog)
let small_config = NavmeshConfig::small_creature();

// Large creature (ogre, vehicle)
let large_config = NavmeshConfig::large_creature();

// Flying unit
let flying_config = NavmeshConfig::flying();

// Custom configuration
let custom_config = NavmeshConfig {
    agent_height: 1.5,
    agent_radius: 0.4,
    agent_max_climb: 0.5,
    agent_max_slope: 60.0,
    ..NavmeshConfig::humanoid()
};
}
Use smaller cell sizes for more detailed navmeshes, but be aware of increased memory usage and build times. A cell size of 0.3m works well for humanoid characters.
#![allow(unused)]
fn main() {
use astraweave_nav::NavmeshLayer;

// Create separate navmeshes for different agent types
let mut nav_system = NavigationSystem::new();

// Layer 0: Infantry
nav_system.add_layer(
    NavmeshLayer::new("infantry")
        .with_config(NavmeshConfig::humanoid())
        .with_geometry(&level_geometry)
);

// Layer 1: Vehicles
nav_system.add_layer(
    NavmeshLayer::new("vehicles")
        .with_config(NavmeshConfig::large_creature())
        .with_geometry(&vehicle_geometry)
);

// Layer 2: Flying
nav_system.add_layer(
    NavmeshLayer::new("flying")
        .with_config(NavmeshConfig::flying())
        .with_geometry(&flying_geometry)
);
}

A* Pathfinding

Basic Pathfinding

#![allow(unused)]
fn main() {
use astraweave_nav::{Pathfinder, PathQuery};

let pathfinder = Pathfinder::new(&navmesh);

// Find path from start to end
let query = PathQuery {
    start: [10.0, 0.0, 5.0],
    end: [50.0, 0.0, 30.0],
    extents: [2.0, 4.0, 2.0], // Search extents for nearest poly
    filter: None,
};

let path = pathfinder.find_path(query)?;

// Path is a Vec<[f32; 3]> of waypoints
for waypoint in path.waypoints() {
    println!("Waypoint: {:?}", waypoint);
}
}

Path Smoothing

#![allow(unused)]
fn main() {
use astraweave_nav::PathSmoothingOptions;

let smoothing = PathSmoothingOptions {
    max_iterations: 100,
    max_smooth_distance: 0.5,
};

let smooth_path = pathfinder.smooth_path(&path, smoothing)?;
}

Path Filtering

#![allow(unused)]
fn main() {
use astraweave_nav::filter::{PathFilter, AreaFlags};

// Create filter for specific area types
let mut filter = PathFilter::new();
filter.set_area_cost(AreaType::Road, 1.0);      // Prefer roads
filter.set_area_cost(AreaType::Grass, 2.0);     // Neutral
filter.set_area_cost(AreaType::Water, 10.0);    // Avoid water
filter.set_area_cost(AreaType::Lava, f32::MAX); // Never path through lava

filter.exclude_flags(AreaFlags::DISABLED);

let query = PathQuery {
    start: start_pos,
    end: end_pos,
    extents: [2.0, 4.0, 2.0],
    filter: Some(filter),
};

let path = pathfinder.find_path(query)?;
}

Hierarchical Pathfinding

#![allow(unused)]
fn main() {
use astraweave_nav::hierarchical::HierarchicalPathfinder;

// Create hierarchical graph for long-distance paths
let hpa_pathfinder = HierarchicalPathfinder::new(&navmesh, 32.0); // 32m clusters

// Fast long-distance pathfinding
let long_path = hpa_pathfinder.find_path(query)?;
}
graph TD
    A[Start] --> B[Find Nearest Poly]
    B --> C[A* Search]
    C --> D{Path Found?}
    D -->|Yes| E[Smooth Path]
    D -->|No| F[Partial Path]
    E --> G[String Pulling]
    G --> H[Return Waypoints]
    F --> H

Partial Paths

#![allow(unused)]
fn main() {
// Allow partial paths when destination unreachable
let query = PathQuery {
    start: start_pos,
    end: unreachable_pos,
    extents: [2.0, 4.0, 2.0],
    filter: None,
};

let result = pathfinder.find_path(query)?;

if result.is_partial() {
    println!("Couldn't reach destination, got {} waypoints", result.waypoints().len());
    // Path goes as close as possible to target
} else {
    println!("Complete path found!");
}
}

Agent Movement

#![allow(unused)]
fn main() {
use astraweave_nav::agent::{NavAgent, AgentParams};

// Create navigation agent
let agent_params = AgentParams {
    radius: 0.6,
    height: 2.0,
    max_acceleration: 8.0,
    max_speed: 5.5,
    collision_query_range: 2.0,
    path_optimization_range: 5.0,
    separation_weight: 2.0,
    ..Default::default()
};

let mut agent = NavAgent::new(agent_params);

// Set destination
agent.set_destination([50.0, 0.0, 30.0]);

// Update each frame
let delta_time = 0.016; // 60 FPS
agent.update(delta_time, &pathfinder);

// Get current velocity and position
let velocity = agent.velocity();
let position = agent.position();

// Apply to entity transform
entity.position = position;
entity.rotation = Quat::from_rotation_y(velocity.x.atan2(velocity.z));
}

Agent States

#![allow(unused)]
fn main() {
use astraweave_nav::agent::AgentState;

match agent.state() {
    AgentState::Idle => {
        // Agent has no destination
    }
    AgentState::Moving => {
        // Agent is moving toward destination
    }
    AgentState::Arrived => {
        // Agent reached destination
        agent.stop();
    }
    AgentState::Stuck => {
        // Agent couldn't make progress
        agent.set_destination(fallback_position);
    }
}
}

Speed Modulation

#![allow(unused)]
fn main() {
// Dynamic speed based on terrain
let current_area = navmesh.get_area_at(agent.position());
let speed_multiplier = match current_area {
    AreaType::Road => 1.2,
    AreaType::Grass => 1.0,
    AreaType::Water => 0.5,
    _ => 1.0,
};

agent.set_max_speed(5.5 * speed_multiplier);
}

Obstacle Avoidance

Local Steering

#![allow(unused)]
fn main() {
use astraweave_nav::steering::{SteeringBehavior, AvoidanceParams};

let avoidance = AvoidanceParams {
    horizon_time: 2.5,        // Look-ahead time
    obstacle_margin: 0.3,      // Extra clearance
    max_neighbors: 10,         // Max agents to consider
    neighbor_dist: 10.0,       // Neighbor search radius
    separation_weight: 2.0,    // Separation strength
    alignment_weight: 1.0,     // Velocity alignment
    cohesion_weight: 1.0,      // Group cohesion
};

agent.set_avoidance_params(avoidance);
}

Dynamic Obstacles

#![allow(unused)]
fn main() {
use astraweave_nav::obstacle::DynamicObstacle;

// Add temporary obstacles
let obstacle = DynamicObstacle::cylinder(
    position,
    1.5,  // radius
    2.0,  // height
);

nav_system.add_obstacle(obstacle);

// Agents will automatically avoid the obstacle

// Remove when no longer needed
nav_system.remove_obstacle(obstacle.id());
}

Crowd Simulation

#![allow(unused)]
fn main() {
use astraweave_nav::crowd::{CrowdManager, CrowdAgent};

let mut crowd = CrowdManager::new(&navmesh, 100); // Max 100 agents

// Add agents to crowd
let agent_id = crowd.add_agent(CrowdAgent {
    position: start_pos,
    params: agent_params,
});

// Set target
crowd.set_target(agent_id, end_pos);

// Update all agents efficiently
crowd.update(delta_time);

// Get agent state
let agent = crowd.get_agent(agent_id);
println!("Position: {:?}, Velocity: {:?}", agent.position, agent.velocity);
}
CrowdManager uses spatial hashing and optimized neighbor queries to efficiently simulate hundreds of agents with local avoidance.

Off-mesh links enable navigation across gaps, jumps, ladders, and teleporters.

#![allow(unused)]
fn main() {
use astraweave_nav::offmesh::{OffMeshLink, OffMeshLinkType};

// Jump down
let jump_link = OffMeshLink {
    start: [10.0, 5.0, 0.0],
    end: [15.0, 0.0, 0.0],
    radius: 0.5,
    bidirectional: false, // One-way
    link_type: OffMeshLinkType::Jump,
    cost: 2.0, // Path cost
};

navmesh.add_offmesh_link(jump_link);

// Ladder (bidirectional)
let ladder_link = OffMeshLink {
    start: [20.0, 0.0, 0.0],
    end: [20.0, 5.0, 0.0],
    radius: 0.5,
    bidirectional: true,
    link_type: OffMeshLinkType::Climb,
    cost: 5.0,
};

navmesh.add_offmesh_link(ladder_link);
}
#![allow(unused)]
fn main() {
// Agent detects off-mesh link during pathfinding
if let Some(link) = agent.current_offmesh_link() {
    match link.link_type {
        OffMeshLinkType::Jump => {
            // Play jump animation
            animation.play("jump");
            
            // Arc trajectory
            let t = link.traversal_progress();
            let height = 2.0 * t * (1.0 - t); // Parabolic arc
            agent.position.y += height;
        }
        OffMeshLinkType::Climb => {
            // Play climb animation
            animation.play("climb_ladder");
        }
        OffMeshLinkType::Teleport => {
            // Instant teleport
            agent.position = link.end;
        }
    }
}
}

Dynamic Navmesh Updates

Runtime Modifications

#![allow(unused)]
fn main() {
use astraweave_nav::dynamic::{NavmeshModifier, TileCache};

// Create tile cache for dynamic updates
let mut tile_cache = TileCache::new(&navmesh, config);

// Add obstacle (e.g., fallen tree)
let obstacle_id = tile_cache.add_obstacle(
    position,
    bounding_box,
);

// Rebuild affected tiles
tile_cache.update()?;

// Remove obstacle when destroyed
tile_cache.remove_obstacle(obstacle_id);
tile_cache.update()?;
}

Temporary Blocking

#![allow(unused)]
fn main() {
// Block area temporarily
let blocker_id = nav_system.block_area(
    center,
    radius,
    duration_seconds,
);

// Automatically unblocks after duration
}

Performance Optimization

Async Pathfinding

#![allow(unused)]
fn main() {
use astraweave_nav::async_pathfinding::AsyncPathfinder;

let async_pathfinder = AsyncPathfinder::new(&navmesh);

// Request path asynchronously
let path_future = async_pathfinder.find_path_async(query);

// Continue gameplay, path calculated in background

// Check if ready
if let Some(path) = path_future.try_get() {
    agent.set_path(path);
}
}

Path Caching

#![allow(unused)]
fn main() {
use astraweave_nav::cache::PathCache;

let mut path_cache = PathCache::new(1000); // Cache 1000 paths

// Check cache before pathfinding
let cache_key = PathCache::key(start, end);
if let Some(cached_path) = path_cache.get(&cache_key) {
    return cached_path.clone();
}

// Calculate and cache
let path = pathfinder.find_path(query)?;
path_cache.insert(cache_key, path.clone());
}

LOD for Distant Agents

#![allow(unused)]
fn main() {
// Reduce update frequency for distant agents
let distance_to_player = (agent.position - player.position).length();

let update_interval = if distance_to_player < 20.0 {
    0.0 // Every frame
} else if distance_to_player < 50.0 {
    0.1 // 10 times per second
} else {
    0.5 // 2 times per second
};

if agent.time_since_update() >= update_interval {
    agent.update(delta_time, &pathfinder);
}
}

Complete Example

AI Character with Navigation

use astraweave_nav::*;

pub struct AICharacter {
    agent: NavAgent,
    current_path: Option<Path>,
    destination: Option<Vec3>,
}

impl AICharacter {
    pub fn new(start_position: Vec3) -> Self {
        let params = AgentParams {
            radius: 0.6,
            height: 2.0,
            max_speed: 5.0,
            max_acceleration: 10.0,
            ..Default::default()
        };
        
        let mut agent = NavAgent::new(params);
        agent.set_position([start_position.x, start_position.y, start_position.z]);
        
        Self {
            agent,
            current_path: None,
            destination: None,
        }
    }
    
    pub fn move_to(&mut self, destination: Vec3, pathfinder: &Pathfinder) -> Result<()> {
        let query = PathQuery {
            start: self.agent.position(),
            end: [destination.x, destination.y, destination.z],
            extents: [2.0, 4.0, 2.0],
            filter: None,
        };
        
        let path = pathfinder.find_path(query)?;
        self.current_path = Some(path);
        self.destination = Some(destination);
        self.agent.set_destination([destination.x, destination.y, destination.z]);
        
        Ok(())
    }
    
    pub fn update(&mut self, delta_time: f32, pathfinder: &Pathfinder) {
        self.agent.update(delta_time, pathfinder);
        
        // Check if arrived
        if self.agent.state() == AgentState::Arrived {
            self.current_path = None;
            self.destination = None;
        }
    }
    
    pub fn position(&self) -> Vec3 {
        let pos = self.agent.position();
        Vec3::new(pos[0], pos[1], pos[2])
    }
    
    pub fn velocity(&self) -> Vec3 {
        let vel = self.agent.velocity();
        Vec3::new(vel[0], vel[1], vel[2])
    }
}

// Usage
fn main() -> Result<()> {
    let navmesh = build_navmesh()?;
    let pathfinder = Pathfinder::new(&navmesh);
    
    let mut character = AICharacter::new(Vec3::new(0.0, 0.0, 0.0));
    character.move_to(Vec3::new(50.0, 0.0, 30.0), &pathfinder)?;
    
    loop {
        let delta_time = 0.016;
        character.update(delta_time, &pathfinder);
        
        println!("Position: {:?}", character.position());
        
        std::thread::sleep(std::time::Duration::from_millis(16));
    }
}

Debugging and Visualization

Debug Rendering

#![allow(unused)]
fn main() {
use astraweave_nav::debug::NavmeshDebugRenderer;

let mut debug_renderer = NavmeshDebugRenderer::new();

// Render navmesh
debug_renderer.draw_navmesh(&navmesh, Color::GREEN);

// Render path
if let Some(path) = &agent.current_path {
    debug_renderer.draw_path(path, Color::YELLOW);
}

// Render agent
debug_renderer.draw_agent(&agent, Color::RED);

// Render off-mesh links
debug_renderer.draw_offmesh_links(&navmesh, Color::CYAN);
}

API Reference

For complete API documentation, see:

Input System

AstraWeave’s input system provides comprehensive input handling with action mapping, binding system, controller support, input replay, and cross-platform abstraction.

Architecture Overview

The input system is designed to decouple raw input events from gameplay actions, enabling flexible control schemes and remapping.

graph TD
    A[Raw Input] --> B[Input Devices]
    B --> C[Keyboard]
    B --> D[Mouse]
    B --> E[Gamepad]
    B --> F[Touch]
    C --> G[Input Mapper]
    D --> G
    E --> G
    F --> G
    G --> H[Action System]
    H --> I[Gameplay Actions]
    H --> J[UI Actions]

Key Components

  • Input Devices: Cross-platform abstraction for keyboard, mouse, gamepad, and touch
  • Action Mapping: Map raw inputs to high-level gameplay actions
  • Binding System: Customizable control schemes with conflict detection
  • Input Context: Hierarchical input contexts for different game states
  • Input Replay: Record and playback input sequences for testing and demos
The input system uses winit for window events and gilrs for gamepad support, providing consistent cross-platform input handling.

Input System Initialization

Basic Setup

#![allow(unused)]
fn main() {
use astraweave_input::{InputSystem, InputConfig};

let config = InputConfig {
    deadzone: 0.15,              // Gamepad deadzone
    mouse_sensitivity: 1.0,
    enable_raw_mouse_input: true, // For FPS games
    double_click_time_ms: 300,
    key_repeat_delay_ms: 500,
    key_repeat_rate_ms: 50,
    ..Default::default()
};

let mut input_system = InputSystem::new(config);
}

Processing Window Events

#![allow(unused)]
fn main() {
use winit::event::{Event, WindowEvent};

event_loop.run(move |event, _, control_flow| {
    // Feed events to input system
    input_system.handle_event(&event);
    
    match event {
        Event::WindowEvent { event, .. } => {
            match event {
                WindowEvent::Focused(false) => {
                    input_system.clear_input_state();
                }
                _ => {}
            }
        }
        Event::MainEventsCleared => {
            // Update input system
            input_system.update(delta_time);
            
            // Process actions
            update_game(&input_system);
        }
        _ => {}
    }
});
}

Action Mapping

Defining Actions

#![allow(unused)]
fn main() {
use astraweave_input::action::{Action, ActionType};

#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq)]
pub enum GameAction {
    // Movement
    MoveForward,
    MoveBackward,
    MoveLeft,
    MoveRight,
    Jump,
    Crouch,
    Sprint,
    
    // Camera
    LookHorizontal,
    LookVertical,
    
    // Combat
    Fire,
    AimDownSights,
    Reload,
    SwitchWeapon,
    
    // Interaction
    Interact,
    UseItem,
    
    // UI
    OpenMenu,
    Inventory,
}

impl Action for GameAction {
    fn action_type(&self) -> ActionType {
        match self {
            // Button actions (pressed/released)
            GameAction::Jump | GameAction::Fire | GameAction::Interact => {
                ActionType::Button
            }
            // Axis actions (continuous values)
            GameAction::MoveForward | GameAction::LookHorizontal => {
                ActionType::Axis
            }
        }
    }
}
}

Creating Action Maps

#![allow(unused)]
fn main() {
use astraweave_input::mapping::{ActionMap, InputBinding};
use astraweave_input::device::{Key, MouseButton, GamepadButton, GamepadAxis};

let mut action_map = ActionMap::new("default");

// Keyboard bindings
action_map.bind(GameAction::MoveForward, InputBinding::Key(Key::W));
action_map.bind(GameAction::MoveBackward, InputBinding::Key(Key::S));
action_map.bind(GameAction::MoveLeft, InputBinding::Key(Key::A));
action_map.bind(GameAction::MoveRight, InputBinding::Key(Key::D));
action_map.bind(GameAction::Jump, InputBinding::Key(Key::Space));
action_map.bind(GameAction::Crouch, InputBinding::Key(Key::ControlLeft));
action_map.bind(GameAction::Sprint, InputBinding::Key(Key::ShiftLeft));

// Mouse bindings
action_map.bind(GameAction::Fire, InputBinding::MouseButton(MouseButton::Left));
action_map.bind(GameAction::AimDownSights, InputBinding::MouseButton(MouseButton::Right));
action_map.bind(GameAction::LookHorizontal, InputBinding::MouseAxisX);
action_map.bind(GameAction::LookVertical, InputBinding::MouseAxisY);

// Gamepad bindings
action_map.bind(GameAction::Jump, InputBinding::GamepadButton(GamepadButton::South));
action_map.bind(GameAction::Fire, InputBinding::GamepadButton(GamepadButton::RightTrigger2));
action_map.bind(GameAction::MoveForward, InputBinding::GamepadAxis {
    axis: GamepadAxis::LeftStickY,
    threshold: 0.0,
    inverted: false,
});
action_map.bind(GameAction::LookHorizontal, InputBinding::GamepadAxis {
    axis: GamepadAxis::RightStickX,
    threshold: 0.0,
    inverted: false,
});

// Register action map
input_system.register_action_map(action_map);
}

Multiple Bindings

#![allow(unused)]
fn main() {
// Bind multiple inputs to the same action
action_map.bind(GameAction::MoveForward, InputBinding::Key(Key::W));
action_map.bind(GameAction::MoveForward, InputBinding::Key(Key::ArrowUp));
action_map.bind(GameAction::MoveForward, InputBinding::GamepadAxis {
    axis: GamepadAxis::LeftStickY,
    threshold: 0.2,
    inverted: false,
});
}

Input Modifiers

#![allow(unused)]
fn main() {
use astraweave_input::modifier::{InputModifier, ModifierKey};

// Bind with modifier keys (e.g., Shift+E for fast interaction)
action_map.bind_with_modifiers(
    GameAction::Interact,
    InputBinding::Key(Key::E),
    &[ModifierKey::Shift],
);

// Input modifiers for axis scaling
action_map.bind_with_modifier_fn(
    GameAction::LookHorizontal,
    InputBinding::MouseAxisX,
    |value| value * mouse_sensitivity,
);
}

Querying Input State

Button State

#![allow(unused)]
fn main() {
// Check if action is currently pressed
if input_system.is_action_pressed(GameAction::Jump) {
    player.jump();
}

// Check if action was just pressed this frame
if input_system.is_action_just_pressed(GameAction::Fire) {
    weapon.fire();
}

// Check if action was just released this frame
if input_system.is_action_just_released(GameAction::AimDownSights) {
    player.stop_aiming();
}

// Get how long action has been held
let sprint_duration = input_system.action_duration(GameAction::Sprint);
if sprint_duration > 2.0 {
    player.start_breathing_heavily();
}
}

Axis Values

#![allow(unused)]
fn main() {
// Get axis value (-1.0 to 1.0)
let move_forward = input_system.action_value(GameAction::MoveForward);
let move_right = input_system.action_value(GameAction::MoveRight);

// Create movement vector
let movement = Vec3::new(move_right, 0.0, move_forward).normalize_or_zero();
player.move_direction(movement);

// Get raw axis value (before deadzone)
let raw_look = input_system.raw_action_value(GameAction::LookHorizontal);

// Get axis delta (change since last frame)
let look_delta = input_system.action_delta(GameAction::LookHorizontal);
camera.rotate_yaw(look_delta * sensitivity);
}

Composite Inputs

#![allow(unused)]
fn main() {
use astraweave_input::composite::Vec2Input;

// Create 2D input from multiple actions
let movement_input = Vec2Input::new(
    GameAction::MoveRight,   // X axis
    GameAction::MoveForward, // Y axis
);

let movement = movement_input.get_value(&input_system);
player.move_direction(Vec3::new(movement.x, 0.0, movement.y));
}

Binding System

Runtime Rebinding

#![allow(unused)]
fn main() {
use astraweave_input::rebinding::BindingRecorder;

// Start recording new binding
let mut recorder = BindingRecorder::new();
recorder.start_recording(GameAction::Jump);

// Wait for user input
loop {
    input_system.update(delta_time);
    
    if let Some(binding) = recorder.check_input(&input_system) {
        // User pressed a key/button
        println!("New binding: {:?}", binding);
        
        // Check for conflicts
        if let Some(conflict) = action_map.find_conflict(GameAction::Jump, &binding) {
            println!("Warning: {} already bound to {:?}", conflict, binding);
            
            // Optionally swap or clear conflict
            action_map.unbind(conflict, &binding);
        }
        
        // Apply new binding
        action_map.bind(GameAction::Jump, binding);
        break;
    }
}
}

Saving and Loading Bindings

#![allow(unused)]
fn main() {
use astraweave_input::persistence::BindingProfile;

// Save bindings
let profile = BindingProfile::from_action_map(&action_map);
profile.save_to_file("user_bindings.json")?;

// Load bindings
let profile = BindingProfile::load_from_file("user_bindings.json")?;
let action_map = profile.to_action_map();
input_system.register_action_map(action_map);
}

Preset Binding Schemes

#![allow(unused)]
fn main() {
// Create multiple preset schemes
let keyboard_mouse = ActionMap::keyboard_mouse_preset();
let gamepad = ActionMap::gamepad_preset();
let left_handed = ActionMap::left_handed_preset();

// Switch schemes
input_system.set_active_action_map("keyboard_mouse");
}

Input Contexts

Input contexts allow different parts of the game to handle input differently (e.g., gameplay vs. menu).

Creating Contexts

#![allow(unused)]
fn main() {
use astraweave_input::context::{InputContext, ContextPriority};

// Gameplay context
let mut gameplay_context = InputContext::new("gameplay", ContextPriority::Normal);
gameplay_context.set_action_map(gameplay_action_map);

// UI context (higher priority)
let mut ui_context = InputContext::new("ui", ContextPriority::High);
ui_context.set_action_map(ui_action_map);

// Add contexts to system
input_system.add_context(gameplay_context);
input_system.add_context(ui_context);
}

Context Activation

#![allow(unused)]
fn main() {
// Activate/deactivate contexts
input_system.activate_context("gameplay");
input_system.deactivate_context("ui");

// Push/pop context stack
input_system.push_context("menu"); // Menu now receives input
// ... menu is open ...
input_system.pop_context(); // Back to previous context

// Query which contexts are active
if input_system.is_context_active("gameplay") {
    // Process gameplay input
}
}

Input Consumption

#![allow(unused)]
fn main() {
// Higher priority contexts can consume input
ui_context.set_consumes_input(true); // UI blocks input from reaching gameplay

// Check if input was consumed
if !input_system.was_action_consumed(GameAction::Fire) {
    weapon.fire();
}
}

Controller Support

Gamepad Detection

#![allow(unused)]
fn main() {
use astraweave_input::gamepad::{Gamepad, GamepadEvent};

// List connected gamepads
for gamepad in input_system.gamepads() {
    println!("Gamepad {}: {}", gamepad.id(), gamepad.name());
}

// Handle gamepad events
while let Some(event) = input_system.poll_gamepad_event() {
    match event {
        GamepadEvent::Connected(id) => {
            println!("Gamepad {} connected", id);
        }
        GamepadEvent::Disconnected(id) => {
            println!("Gamepad {} disconnected", id);
        }
        GamepadEvent::ButtonPressed(id, button) => {
            println!("Gamepad {} button {:?} pressed", id, button);
        }
        _ => {}
    }
}
}

Vibration/Haptics

#![allow(unused)]
fn main() {
use astraweave_input::gamepad::{VibrationEffect, VibrationDuration};

// Simple vibration
input_system.vibrate_gamepad(
    gamepad_id,
    0.5, // Weak motor (0.0 - 1.0)
    0.8, // Strong motor (0.0 - 1.0)
    VibrationDuration::Milliseconds(200),
);

// Complex effect
let effect = VibrationEffect::explosion()
    .with_intensity(1.0)
    .with_duration_ms(500);

input_system.play_vibration_effect(gamepad_id, effect);
}

Per-Player Input

#![allow(unused)]
fn main() {
use astraweave_input::player::PlayerInput;

// Assign gamepads to players
let player1_input = PlayerInput::new()
    .with_gamepad(0)
    .with_keyboard_mouse();

let player2_input = PlayerInput::new()
    .with_gamepad(1);

// Query player-specific input
if player1_input.is_action_pressed(&input_system, GameAction::Jump) {
    player1.jump();
}

if player2_input.is_action_pressed(&input_system, GameAction::Jump) {
    player2.jump();
}
}
For split-screen games, use separate PlayerInput instances to isolate input between players.

Input Replay

Input replay enables testing, demo playback, and replay systems.

Recording Input

#![allow(unused)]
fn main() {
use astraweave_input::replay::{InputRecorder, InputRecording};

// Start recording
let mut recorder = InputRecorder::new();
recorder.start();

// Update each frame (automatically records input)
loop {
    input_system.update(delta_time);
    recorder.record_frame(&input_system);
    
    // ... game logic ...
}

// Stop and save
recorder.stop();
let recording = recorder.finalize();
recording.save_to_file("replay.bin")?;
}

Playback

#![allow(unused)]
fn main() {
use astraweave_input::replay::InputPlayer;

// Load recording
let recording = InputRecording::load_from_file("replay.bin")?;
let mut player = InputPlayer::new(recording);

// Playback loop
loop {
    // Apply recorded input to system
    player.update(delta_time, &mut input_system);
    
    // Game state will be driven by recorded input
    update_game(&input_system);
    
    if player.is_finished() {
        break;
    }
}
}

Replay Controls

#![allow(unused)]
fn main() {
// Pause/resume
player.pause();
player.resume();

// Seek
player.seek_to_time(10.0); // Seek to 10 seconds
player.seek_to_frame(600); // Seek to frame 600

// Playback speed
player.set_playback_speed(0.5); // Half speed
player.set_playback_speed(2.0); // Double speed

// Loop
player.set_loop(true);
}

Advanced Features

Input Buffering

#![allow(unused)]
fn main() {
use astraweave_input::buffer::InputBuffer;

// Create input buffer for fighting game-style combo inputs
let mut buffer = InputBuffer::new()
    .with_buffer_duration(0.3) // 300ms window
    .with_max_inputs(8);

// Add inputs to buffer
buffer.record_input(&input_system);

// Check for sequences
if buffer.matches_sequence(&[
    GameAction::MoveForward,
    GameAction::MoveDown,
    GameAction::MoveForward,
    GameAction::Fire,
]) {
    character.perform_special_move("Hadouken");
    buffer.clear();
}
}

Input Prediction (Networking)

#![allow(unused)]
fn main() {
use astraweave_input::prediction::InputPredictor;

// For networked games, predict input during lag
let mut predictor = InputPredictor::new();

// Record recent inputs
predictor.record(&input_system);

// Predict future input (e.g., during packet loss)
let predicted_input = predictor.predict_next_frame();

// Use predicted input until real input arrives
if network.has_input() {
    apply_input(network.get_input());
} else {
    apply_input(predicted_input);
}
}

Touch Input

#![allow(unused)]
fn main() {
use astraweave_input::touch::{TouchEvent, TouchPhase};

// Handle touch events (mobile/tablet)
while let Some(event) = input_system.poll_touch_event() {
    match event.phase {
        TouchPhase::Started => {
            println!("Touch started at {:?}", event.position);
        }
        TouchPhase::Moved => {
            let delta = event.position - event.previous_position;
            camera.rotate(delta);
        }
        TouchPhase::Ended => {
            println!("Touch ended");
        }
        TouchPhase::Cancelled => {
            println!("Touch cancelled");
        }
    }
}

// Multi-touch gestures
if let Some(pinch) = input_system.get_pinch_gesture() {
    camera.zoom(pinch.scale);
}

if let Some(swipe) = input_system.get_swipe_gesture() {
    player.dodge(swipe.direction);
}
}

Complete Example

FPS Input System

#![allow(unused)]
fn main() {
use astraweave_input::*;

pub struct FPSInput {
    input_system: InputSystem,
    gameplay_context: ContextId,
    menu_context: ContextId,
}

impl FPSInput {
    pub fn new() -> Self {
        let mut input_system = InputSystem::new(InputConfig::default());
        
        // Create gameplay action map
        let mut gameplay_map = ActionMap::new("gameplay");
        gameplay_map.bind(GameAction::MoveForward, InputBinding::Key(Key::W));
        gameplay_map.bind(GameAction::MoveBackward, InputBinding::Key(Key::S));
        gameplay_map.bind(GameAction::MoveLeft, InputBinding::Key(Key::A));
        gameplay_map.bind(GameAction::MoveRight, InputBinding::Key(Key::D));
        gameplay_map.bind(GameAction::Jump, InputBinding::Key(Key::Space));
        gameplay_map.bind(GameAction::Fire, InputBinding::MouseButton(MouseButton::Left));
        gameplay_map.bind(GameAction::LookHorizontal, InputBinding::MouseAxisX);
        gameplay_map.bind(GameAction::LookVertical, InputBinding::MouseAxisY);
        
        // Create menu action map
        let mut menu_map = ActionMap::new("menu");
        menu_map.bind(GameAction::OpenMenu, InputBinding::Key(Key::Escape));
        
        // Register contexts
        let gameplay_context = input_system.add_context(
            InputContext::new("gameplay", ContextPriority::Normal)
                .with_action_map(gameplay_map)
        );
        
        let menu_context = input_system.add_context(
            InputContext::new("menu", ContextPriority::High)
                .with_action_map(menu_map)
                .with_consumes_input(true)
        );
        
        // Start with gameplay active
        input_system.activate_context(gameplay_context);
        
        Self {
            input_system,
            gameplay_context,
            menu_context,
        }
    }
    
    pub fn update(&mut self, delta_time: f32, player: &mut Player, camera: &mut Camera) {
        self.input_system.update(delta_time);
        
        // Handle menu toggle
        if self.input_system.is_action_just_pressed(GameAction::OpenMenu) {
            self.toggle_menu();
        }
        
        // Process gameplay input if not in menu
        if self.input_system.is_context_active(self.gameplay_context) {
            self.process_gameplay_input(player, camera);
        }
    }
    
    fn process_gameplay_input(&self, player: &mut Player, camera: &mut Camera) {
        // Movement
        let move_forward = self.input_system.action_value(GameAction::MoveForward);
        let move_right = self.input_system.action_value(GameAction::MoveRight);
        let movement = Vec3::new(move_right, 0.0, move_forward);
        player.set_movement(movement);
        
        // Jump
        if self.input_system.is_action_just_pressed(GameAction::Jump) {
            player.jump();
        }
        
        // Camera look
        let look_x = self.input_system.action_delta(GameAction::LookHorizontal);
        let look_y = self.input_system.action_delta(GameAction::LookVertical);
        camera.rotate(look_x, look_y);
        
        // Fire weapon
        if self.input_system.is_action_pressed(GameAction::Fire) {
            player.fire_weapon();
        }
    }
    
    fn toggle_menu(&mut self) {
        if self.input_system.is_context_active(self.menu_context) {
            self.input_system.deactivate_context(self.menu_context);
            self.input_system.activate_context(self.gameplay_context);
        } else {
            self.input_system.deactivate_context(self.gameplay_context);
            self.input_system.activate_context(self.menu_context);
        }
    }
    
    pub fn handle_event(&mut self, event: &Event<()>) {
        self.input_system.handle_event(event);
    }
}
}

API Reference

For complete API documentation, see:

Networking System

AstraWeave’s networking system provides robust client-server architecture with state synchronization, delta encoding, lag compensation, and prediction for responsive multiplayer gameplay.

Architecture Overview

The networking system is built on a client-server model with authoritative server and client-side prediction.

graph TD
    A[Server] --> B[State Authority]
    A --> C[Physics Simulation]
    A --> D[Game Logic]
    B --> E[Delta Encoding]
    E --> F[Network Transport]
    F --> G[Client 1]
    F --> H[Client 2]
    F --> I[Client N]
    G --> J[Prediction]
    G --> K[Interpolation]
    J --> L[Reconciliation]

Key Components

  • Transport Layer: UDP-based reliable/unreliable messaging with Quinn (QUIC)
  • State Synchronization: Efficient delta compression and entity replication
  • Client Prediction: Responsive input with server reconciliation
  • Lag Compensation: Server-side hit detection with rewind
  • Network Relevancy: Bandwidth optimization through spatial partitioning
AstraWeave uses QUIC (via Quinn) for modern, secure, and multiplexed networking with built-in congestion control and 0-RTT connection establishment.

Server Setup

Creating a Server

#![allow(unused)]
fn main() {
use astraweave_net::{Server, ServerConfig};

let config = ServerConfig {
    bind_address: "0.0.0.0:7777".parse()?,
    max_clients: 64,
    tick_rate: 60,              // Server updates per second
    timeout_seconds: 10.0,
    enable_encryption: true,
    max_bandwidth_kbps: 1024,
    ..Default::default()
};

let mut server = Server::new(config)?;
}

Server Loop

#![allow(unused)]
fn main() {
use astraweave_net::{ServerEvent, ClientId};

loop {
    let delta_time = 1.0 / 60.0; // 60 Hz tick rate
    
    // Process incoming events
    while let Some(event) = server.poll_event() {
        match event {
            ServerEvent::ClientConnected { client_id, address } => {
                println!("Client {} connected from {}", client_id, address);
                spawn_player(client_id);
            }
            ServerEvent::ClientDisconnected { client_id, reason } => {
                println!("Client {} disconnected: {:?}", client_id, reason);
                despawn_player(client_id);
            }
            ServerEvent::MessageReceived { client_id, message } => {
                handle_client_message(client_id, message);
            }
        }
    }
    
    // Update game state
    update_game_state(delta_time);
    
    // Send state updates to clients
    server.broadcast_state_update(&world_state)?;
    
    // Tick network
    server.tick(delta_time)?;
    
    std::thread::sleep(std::time::Duration::from_secs_f32(delta_time));
}
}

Client Setup

Connecting to Server

#![allow(unused)]
fn main() {
use astraweave_net::{Client, ClientConfig};

let config = ClientConfig {
    server_address: "127.0.0.1:7777".parse()?,
    timeout_seconds: 10.0,
    enable_prediction: true,
    interpolation_delay_ms: 100,
    ..Default::default()
};

let mut client = Client::connect(config).await?;
}

Client Loop

#![allow(unused)]
fn main() {
use astraweave_net::ClientEvent;

loop {
    let delta_time = 1.0 / 60.0;
    
    // Process server events
    while let Some(event) = client.poll_event() {
        match event {
            ClientEvent::Connected => {
                println!("Connected to server");
            }
            ClientEvent::Disconnected { reason } => {
                println!("Disconnected: {:?}", reason);
                break;
            }
            ClientEvent::StateUpdate { state } => {
                apply_state_update(state);
            }
            ClientEvent::MessageReceived { message } => {
                handle_server_message(message);
            }
        }
    }
    
    // Get local input
    let input = gather_input();
    
    // Send input to server
    client.send_input(input)?;
    
    // Client-side prediction
    if config.enable_prediction {
        simulate_local_player(input, delta_time);
    }
    
    // Interpolate remote entities
    interpolate_entities(delta_time);
    
    // Tick network
    client.tick(delta_time)?;
    
    std::thread::sleep(std::time::Duration::from_secs_f32(delta_time));
}
}

State Synchronization

Entity Replication

#![allow(unused)]
fn main() {
use astraweave_net::replication::{NetEntity, ReplicationMode};

#[derive(NetEntity)]
pub struct Player {
    #[replicate(mode = ReplicationMode::Always)]
    pub entity_id: u64,
    
    #[replicate(mode = ReplicationMode::OnChange, priority = 10)]
    pub position: Vec3,
    
    #[replicate(mode = ReplicationMode::OnChange, priority = 10)]
    pub rotation: Quat,
    
    #[replicate(mode = ReplicationMode::OnChange, priority = 5)]
    pub velocity: Vec3,
    
    #[replicate(mode = ReplicationMode::OnChange, priority = 2)]
    pub health: f32,
    
    #[replicate(mode = ReplicationMode::OnChange, priority = 1)]
    pub ammo: u32,
    
    // Not replicated
    #[replicate(mode = ReplicationMode::Never)]
    local_animation_state: AnimationState,
}
}

Replication Modes

ModeDescriptionUse Case
AlwaysSend every frameEntity ID, critical state
OnChangeSend when value changesPosition, health
OnChangeThresholdSend when change exceeds thresholdAnalog values
NeverNever replicateClient-only data

World State Updates

#![allow(unused)]
fn main() {
use astraweave_net::state::{WorldState, EntityState};

// Server: Build world state
let mut world_state = WorldState::new();

for player in players.iter() {
    world_state.add_entity(player.entity_id, EntityState {
        position: player.position,
        rotation: player.rotation,
        velocity: player.velocity,
        custom_data: serialize_player_data(player),
    });
}

// Send to all clients
server.broadcast_state_update(&world_state)?;

// Client: Apply state update
fn apply_state_update(state: WorldState) {
    for (entity_id, entity_state) in state.entities() {
        if let Some(entity) = world.get_entity_mut(entity_id) {
            entity.position = entity_state.position;
            entity.rotation = entity_state.rotation;
            entity.velocity = entity_state.velocity;
        }
    }
}
}

Delta Encoding

Delta encoding dramatically reduces bandwidth by only sending changed values.

Automatic Delta Compression

#![allow(unused)]
fn main() {
use astraweave_net::delta::{DeltaEncoder, DeltaDecoder};

// Server: Encode state changes
let mut encoder = DeltaEncoder::new();

// First frame: full state
let full_state = encoder.encode_full(&world_state)?;
server.broadcast(full_state)?;

// Subsequent frames: only deltas
loop {
    let new_state = get_current_world_state();
    let delta = encoder.encode_delta(&new_state)?;
    
    if delta.size_bytes() < full_state.size_bytes() * 0.8 {
        server.broadcast(delta)?;
    } else {
        // Send full state if delta is too large
        server.broadcast(encoder.encode_full(&new_state)?)?;
    }
}

// Client: Decode deltas
let mut decoder = DeltaDecoder::new();

match received_message {
    NetworkMessage::FullState(state) => {
        decoder.apply_full_state(state);
    }
    NetworkMessage::DeltaState(delta) => {
        decoder.apply_delta(delta);
    }
}

let current_state = decoder.get_state();
}

Custom Delta Serialization

#![allow(unused)]
fn main() {
use astraweave_net::serialization::{BitWriter, BitReader};

impl Player {
    pub fn write_delta(&self, previous: &Player, writer: &mut BitWriter) {
        // Position (quantized)
        if self.position != previous.position {
            writer.write_bit(true);
            writer.write_vec3_quantized(self.position, -1000.0, 1000.0, 0.01);
        } else {
            writer.write_bit(false);
        }
        
        // Rotation (quaternion compression)
        if self.rotation != previous.rotation {
            writer.write_bit(true);
            writer.write_quaternion_compressed(self.rotation);
        } else {
            writer.write_bit(false);
        }
        
        // Health (8-bit)
        if self.health != previous.health {
            writer.write_bit(true);
            writer.write_u8((self.health * 255.0) as u8);
        } else {
            writer.write_bit(false);
        }
    }
    
    pub fn read_delta(&mut self, reader: &mut BitReader) {
        if reader.read_bit() {
            self.position = reader.read_vec3_quantized(-1000.0, 1000.0, 0.01);
        }
        if reader.read_bit() {
            self.rotation = reader.read_quaternion_compressed();
        }
        if reader.read_bit() {
            self.health = reader.read_u8() as f32 / 255.0;
        }
    }
}
}
Quantize floating-point values to reduce precision and save bandwidth. For example, position precision of 1cm (0.01m) is sufficient for most games.

Client Prediction

Client-side prediction makes the game feel responsive despite network latency.

Input Prediction

#![allow(unused)]
fn main() {
use astraweave_net::prediction::{PredictionSystem, InputBuffer};

pub struct PredictedPlayer {
    // Authoritative state from server
    server_position: Vec3,
    server_velocity: Vec3,
    server_tick: u64,
    
    // Predicted state
    predicted_position: Vec3,
    predicted_velocity: Vec3,
    
    // Input history
    input_buffer: InputBuffer,
}

impl PredictedPlayer {
    pub fn send_input(&mut self, input: PlayerInput, client: &mut Client) {
        // Store input locally
        self.input_buffer.push(input.clone());
        
        // Send to server
        client.send_input(input)?;
        
        // Predict movement
        self.simulate_movement(input, TICK_DELTA);
    }
    
    pub fn reconcile(&mut self, server_state: PlayerState) {
        self.server_position = server_state.position;
        self.server_velocity = server_state.velocity;
        self.server_tick = server_state.tick;
        
        // Start from server state
        self.predicted_position = server_state.position;
        self.predicted_velocity = server_state.velocity;
        
        // Re-simulate inputs after server tick
        for input in self.input_buffer.after_tick(server_state.tick) {
            self.simulate_movement(input, TICK_DELTA);
        }
        
        // Clear old inputs
        self.input_buffer.clear_before_tick(server_state.tick);
    }
    
    fn simulate_movement(&mut self, input: PlayerInput, delta_time: f32) {
        // Same movement code as server
        let acceleration = input.move_direction * MOVE_SPEED;
        self.predicted_velocity += acceleration * delta_time;
        self.predicted_velocity *= 0.9; // Friction
        self.predicted_position += self.predicted_velocity * delta_time;
    }
}
}

Prediction Error Correction

#![allow(unused)]
fn main() {
// Smooth correction of prediction errors
pub fn correct_prediction_error(&mut self, delta_time: f32) {
    let error = self.server_position - self.predicted_position;
    let error_magnitude = error.length();
    
    if error_magnitude > 0.01 {
        // Smooth correction over time
        let correction_speed = 10.0; // Adjust for smoothness
        let correction = error * correction_speed * delta_time;
        
        self.predicted_position += correction;
    } else {
        // Snap if very close
        self.predicted_position = self.server_position;
    }
}
}
sequenceDiagram
    participant Client
    participant Server
    
    Client->>Client: Input (frame 1)
    Client->>Client: Predict movement
    Client->>Server: Send input (frame 1)
    
    Client->>Client: Input (frame 2)
    Client->>Client: Predict movement
    Client->>Server: Send input (frame 2)
    
    Server->>Server: Process input (frame 1)
    Server->>Client: State update (frame 1)
    
    Client->>Client: Reconcile with server
    Client->>Client: Re-simulate frames 2+

Entity Interpolation

Remote entities are interpolated between snapshots for smooth movement.

Snapshot Interpolation

#![allow(unused)]
fn main() {
use astraweave_net::interpolation::{SnapshotBuffer, InterpolationTarget};

pub struct InterpolatedEntity {
    snapshots: SnapshotBuffer<EntitySnapshot>,
    interpolation_delay: f32, // 100ms
}

#[derive(Clone)]
pub struct EntitySnapshot {
    timestamp: f32,
    position: Vec3,
    rotation: Quat,
    velocity: Vec3,
}

impl InterpolatedEntity {
    pub fn add_snapshot(&mut self, snapshot: EntitySnapshot) {
        self.snapshots.push(snapshot);
    }
    
    pub fn interpolate(&self, current_time: f32) -> InterpolationTarget {
        // Render time is current time minus interpolation delay
        let render_time = current_time - self.interpolation_delay;
        
        // Find bracketing snapshots
        let (before, after) = self.snapshots.get_bracketing(render_time);
        
        // Calculate interpolation factor
        let t = (render_time - before.timestamp) / (after.timestamp - before.timestamp);
        let t = t.clamp(0.0, 1.0);
        
        InterpolationTarget {
            position: before.position.lerp(after.position, t),
            rotation: before.rotation.slerp(after.rotation, t),
            velocity: before.velocity.lerp(after.velocity, t),
        }
    }
}
}

Extrapolation for Missing Snapshots

#![allow(unused)]
fn main() {
pub fn extrapolate(&self, current_time: f32) -> InterpolationTarget {
    let latest = self.snapshots.latest();
    let time_diff = current_time - latest.timestamp;
    
    if time_diff > 0.2 {
        // Too old, don't extrapolate
        return InterpolationTarget {
            position: latest.position,
            rotation: latest.rotation,
            velocity: latest.velocity,
        };
    }
    
    // Linear extrapolation using velocity
    InterpolationTarget {
        position: latest.position + latest.velocity * time_diff,
        rotation: latest.rotation,
        velocity: latest.velocity,
    }
}
}

Lag Compensation

Server-side lag compensation ensures fair hit detection.

Rewinding Game State

#![allow(unused)]
fn main() {
use astraweave_net::lag_compensation::{HistoryBuffer, HistoricalState};

pub struct LagCompensationSystem {
    history: HistoryBuffer<WorldSnapshot>,
    max_rewind_ms: u64,
}

impl LagCompensationSystem {
    pub fn record_snapshot(&mut self, world: &World, timestamp: u64) {
        let snapshot = WorldSnapshot {
            timestamp,
            entities: world.entities().map(|e| e.clone()).collect(),
        };
        
        self.history.push(snapshot);
        self.history.prune_older_than(timestamp - self.max_rewind_ms);
    }
    
    pub fn rewind_and_test(&self, client_latency_ms: u64, raycast: Ray) -> Option<Hit> {
        let rewind_time = current_time() - client_latency_ms;
        let snapshot = self.history.get_at_time(rewind_time)?;
        
        // Test hit detection against historical state
        for entity in &snapshot.entities {
            if let Some(hit) = raycast.intersect(&entity.hitbox) {
                return Some(Hit {
                    entity_id: entity.id,
                    position: hit.point,
                    distance: hit.distance,
                });
            }
        }
        
        None
    }
}
}

Server-Side Hit Validation

#![allow(unused)]
fn main() {
pub fn process_shot(
    &mut self,
    client_id: ClientId,
    shot_data: ShotData,
) -> Result<Option<Hit>> {
    let client = self.clients.get(client_id)?;
    let latency_ms = client.round_trip_time() / 2;
    
    // Rewind to client's view of the world
    let hit = self.lag_compensation.rewind_and_test(
        latency_ms,
        shot_data.raycast,
    );
    
    if let Some(hit) = hit {
        // Validate hit (anti-cheat)
        if validate_shot(&shot_data, &hit) {
            apply_damage(hit.entity_id, shot_data.damage);
            return Ok(Some(hit));
        }
    }
    
    Ok(None)
}
}
Limit maximum rewind time (typically 200-250ms) to prevent abuse and reduce server memory usage.

Network Relevancy

Only send relevant entities to each client to save bandwidth.

Spatial Partitioning

#![allow(unused)]
fn main() {
use astraweave_net::relevancy::{RelevancySystem, SpatialGrid};

pub struct RelevancySystem {
    grid: SpatialGrid,
    relevancy_range: f32,
}

impl RelevancySystem {
    pub fn update_relevancy(&mut self, clients: &[Client], entities: &[Entity]) {
        self.grid.clear();
        
        // Insert entities into spatial grid
        for entity in entities {
            self.grid.insert(entity.id, entity.position);
        }
        
        // Calculate relevant entities for each client
        for client in clients {
            let player_pos = client.player_position();
            let relevant = self.grid.query_radius(player_pos, self.relevancy_range);
            client.set_relevant_entities(relevant);
        }
    }
    
    pub fn should_replicate(&self, client_id: ClientId, entity_id: EntityId) -> bool {
        let client = self.clients.get(client_id);
        client.relevant_entities.contains(&entity_id)
    }
}
}

Priority-Based Updates

#![allow(unused)]
fn main() {
pub struct ReplicationPriority {
    pub distance_factor: f32,
    pub importance: f32,
    pub update_frequency: f32,
}

impl ReplicationPriority {
    pub fn calculate(client_pos: Vec3, entity: &Entity) -> f32 {
        let distance = (entity.position - client_pos).length();
        let distance_priority = 1.0 / (1.0 + distance * 0.1);
        
        let importance_priority = match entity.entity_type {
            EntityType::Player => 1.0,
            EntityType::Projectile => 0.8,
            EntityType::Enemy => 0.6,
            EntityType::Pickup => 0.4,
            EntityType::Decoration => 0.1,
        };
        
        distance_priority * importance_priority
    }
}

// Send high-priority entities every frame, low-priority less often
pub fn send_updates(&mut self, delta_time: f32) {
    for client in &mut self.clients {
        let mut updates = Vec::new();
        
        for entity_id in &client.relevant_entities {
            let entity = self.world.get_entity(*entity_id);
            let priority = ReplicationPriority::calculate(client.position(), entity);
            
            // Update frequency based on priority
            let update_interval = 1.0 / (60.0 * priority);
            
            if entity.time_since_update(client.id) >= update_interval {
                updates.push(entity.create_update());
                entity.mark_updated(client.id);
            }
        }
        
        client.send_updates(updates)?;
    }
}
}

Complete Example

Multiplayer FPS Game

#![allow(unused)]
fn main() {
use astraweave_net::*;

// Server
pub struct GameServer {
    server: Server,
    world: World,
    lag_compensation: LagCompensationSystem,
    relevancy: RelevancySystem,
}

impl GameServer {
    pub fn run(&mut self) {
        loop {
            let delta_time = 1.0 / 60.0;
            
            // Process events
            while let Some(event) = self.server.poll_event() {
                match event {
                    ServerEvent::MessageReceived { client_id, message } => {
                        self.handle_message(client_id, message);
                    }
                    _ => {}
                }
            }
            
            // Update game logic
            self.world.update(delta_time);
            
            // Record for lag compensation
            self.lag_compensation.record_snapshot(&self.world, current_time());
            
            // Update relevancy
            self.relevancy.update_relevancy(&self.server.clients(), &self.world.entities());
            
            // Send state updates
            self.send_state_updates();
            
            self.server.tick(delta_time).unwrap();
            std::thread::sleep(std::time::Duration::from_secs_f32(delta_time));
        }
    }
    
    fn handle_message(&mut self, client_id: ClientId, message: ClientMessage) {
        match message {
            ClientMessage::Input(input) => {
                self.process_input(client_id, input);
            }
            ClientMessage::Shot(shot_data) => {
                if let Some(hit) = self.process_shot(client_id, shot_data) {
                    self.broadcast_hit(hit);
                }
            }
        }
    }
}

// Client
pub struct GameClient {
    client: Client,
    local_player: PredictedPlayer,
    remote_players: HashMap<EntityId, InterpolatedEntity>,
}

impl GameClient {
    pub fn run(&mut self) {
        loop {
            let delta_time = 1.0 / 60.0;
            
            // Process server messages
            while let Some(event) = self.client.poll_event() {
                match event {
                    ClientEvent::StateUpdate { state } => {
                        self.apply_state_update(state);
                    }
                    _ => {}
                }
            }
            
            // Gather input
            let input = self.gather_input();
            self.local_player.send_input(input, &mut self.client);
            
            // Interpolate remote entities
            let current_time = current_time();
            for entity in self.remote_players.values_mut() {
                let target = entity.interpolate(current_time);
                // Apply to visual representation
            }
            
            self.client.tick(delta_time).unwrap();
            std::thread::sleep(std::time::Duration::from_secs_f32(delta_time));
        }
    }
}
}

API Reference

For complete API documentation, see:

Fluids System

Status: Production Ready (A+ Grade)
Coverage: 94.2% (2,404 tests)
Crate: astraweave-fluids

AstraWeave’s fluid simulation system provides production-quality water effects for games, featuring Position-Based Dynamics (PBD) particle simulation, volumetric grids, and comprehensive visual effects.

Overview

Core Systems

SystemDescription
Position-Based Dynamics (PBD)GPU-accelerated particle simulation
Volumetric GridVoxel-based water for building/terrain interaction
Terrain IntegrationAutomatic river, lake, and waterfall detection
SPH KernelsSmoothed Particle Hydrodynamics (poly6, spiky, viscosity)

Visual Effects

EffectDescription
CausticsUnderwater light refraction patterns
God RaysVolumetric light shafts through water
ReflectionsScreen-space and planar water reflections
FoamDynamic whitecaps, wakes, and shore foam
ParticlesWaterfalls, bubbles, debris, and spray

Quick Start

#![allow(unused)]
fn main() {
use astraweave_fluids::{WaterEffectsManager, WaterQualityPreset};

// Create water effects manager with quality preset
let manager = WaterEffectsManager::from_preset(WaterQualityPreset::High)?;

// Game loop
loop {
    // Update water simulation
    manager.update(delta_time, camera_pos, water_height);
    
    // Render water
    manager.render(&render_context);
}
}

Architecture

Module Overview

astraweave-fluids/
├── Core Simulation
│   ├── pcisph_system.rs      # PCISPH pressure solver
│   ├── unified_solver.rs     # Combined solver
│   ├── viscosity.rs          # Viscosity handling
│   ├── boundary.rs           # Domain boundaries
│   └── simd_ops.rs           # SIMD optimizations
├── Visual Effects
│   ├── caustics.rs           # Light caustics
│   ├── god_rays.rs           # Volumetric rays
│   ├── foam.rs               # Foam generation
│   ├── water_reflections.rs  # Reflection system
│   └── underwater.rs         # Underwater effects
├── Terrain Integration
│   ├── terrain_integration.rs
│   ├── volume_grid.rs
│   └── waterfall.rs
└── Editor/Tools
    ├── editor.rs             # Editor integration
    ├── profiling.rs          # Performance stats
    └── serialization.rs      # Save/load

SPH Simulation

Kernel Functions

AstraWeave implements standard SPH kernels:

#![allow(unused)]
fn main() {
// Poly6 kernel for density calculation
fn poly6_kernel(r_sq: f32, h: f32) -> f32 {
    let h2 = h * h;
    if r_sq > h2 { return 0.0; }
    let diff = h2 - r_sq;
    POLY6_FACTOR / h.powi(9) * diff * diff * diff
}

// Spiky kernel for pressure gradient
fn spiky_gradient(r: Vec3, r_len: f32, h: f32) -> Vec3 {
    if r_len > h || r_len < 1e-6 { return Vec3::ZERO; }
    let diff = h - r_len;
    SPIKY_FACTOR / h.powi(6) * diff * diff * (-r / r_len)
}

// Viscosity kernel for velocity diffusion
fn viscosity_laplacian(r_len: f32, h: f32) -> f32 {
    if r_len > h { return 0.0; }
    VISCOSITY_FACTOR / h.powi(3) * (h - r_len)
}
}

Performance

Operation1K Particles10K Particles100K Particles
Density calc5.3 µs53 µs530 µs
Pressure solve12 µs120 µs1.2 ms
SPH kernels171-223 µs
Full step1.8-3.0 ms

Volume Grid

For voxel-based water simulation:

#![allow(unused)]
fn main() {
use astraweave_fluids::{VolumeGrid, FlowDirection};

// Create volume grid
let mut grid = VolumeGrid::new(
    dimensions: (64, 32, 64),  // Grid size
    cell_size: 0.5,            // World units per cell
);

// Fill cells
grid.set_water_level(x, y, z, 1.0);

// Simulate flow
grid.simulate_step(delta_time);

// Query water at position
let water_level = grid.sample_at(world_pos);
}

Water Effects Manager

The WaterEffectsManager coordinates all water visual effects:

#![allow(unused)]
fn main() {
use astraweave_fluids::{
    WaterEffectsManager,
    WaterQualityPreset,
    CausticsConfig,
    GodRaysConfig,
    FoamConfig,
};

// Create with custom configuration
let manager = WaterEffectsManager::new()
    .with_caustics(CausticsConfig {
        intensity: 0.8,
        scale: 2.0,
        speed: 0.5,
    })
    .with_god_rays(GodRaysConfig {
        density: 0.3,
        decay: 0.95,
        samples: 64,
    })
    .with_foam(FoamConfig {
        threshold: 0.7,
        persistence: 2.0,
    });
}

Quality Presets

PresetParticlesGrid ResolutionEffects
Low1,00032³Basic reflections
Medium5,00064³+ Caustics
High10,000128³+ God rays, foam
Ultra25,000256³All effects

Terrain Integration

Automatic water body detection:

#![allow(unused)]
fn main() {
use astraweave_fluids::{TerrainWaterIntegration, WaterBodyType};

let integration = TerrainWaterIntegration::new(&terrain);

// Detect water bodies
let water_bodies = integration.detect_water_bodies();

for body in water_bodies {
    match body.body_type {
        WaterBodyType::Lake { depth, .. } => {
            println!("Lake detected: depth = {}", depth);
        }
        WaterBodyType::River { flow_speed, .. } => {
            println!("River detected: flow = {}", flow_speed);
        }
        WaterBodyType::Waterfall { height, .. } => {
            println!("Waterfall detected: height = {}", height);
        }
    }
}
}

Building Integration

Interactive water elements for building systems:

#![allow(unused)]
fn main() {
use astraweave_fluids::{
    WaterBuildingManager,
    WaterDispenser,
    WaterDrain,
    WaterGate,
    WaterWheel,
};

let mut water_manager = WaterBuildingManager::new(&volume_grid);

// Water source
let dispenser = WaterDispenser::new(position, flow_rate: 5.0);
water_manager.add_dispenser(dispenser);

// Water sink
let drain = WaterDrain::new(position, capacity: 10.0);
water_manager.add_drain(drain);

// Player-controlled gate
let gate = WaterGate::new(position, width: 2.0);
water_manager.add_gate(gate);

// Power generator
let wheel = WaterWheel::new(position, radius: 1.5);
water_manager.add_wheel(wheel);

// Query power generation
let power = water_manager.get_wheel_power(&wheel);
}

Editor Integration

Full editor support with undo/redo:

#![allow(unused)]
fn main() {
use astraweave_fluids::editor::{
    FluidEditorConfig,
    ConfigHistory,
    ValidationIssue,
};

let mut config = FluidEditorConfig::default();
let mut history = ConfigHistory::new();

// Make changes with history tracking
history.push(config.clone());
config.physics.viscosity = 0.5;

// Undo
if let Some(prev) = history.undo() {
    config = prev;
}

// Validate configuration
let issues: Vec<ValidationIssue> = config.validate();
for issue in issues {
    eprintln!("{}: {}", issue.severity, issue.message);
}
}

Serialization

Save and load fluid state:

#![allow(unused)]
fn main() {
use astraweave_fluids::serialization::{FluidSnapshot, save_snapshot, load_snapshot};

// Save current state
let snapshot = FluidSnapshot::capture(&simulation);
save_snapshot(&snapshot, "saves/water_state.bin")?;

// Load state
let loaded = load_snapshot("saves/water_state.bin")?;
simulation.restore(&loaded);
}

Performance Optimization

SIMD Operations

The fluids system uses SIMD for critical operations:

#![allow(unused)]
fn main() {
use astraweave_fluids::simd_ops;

// Batch particle updates (SIMD accelerated)
simd_ops::update_positions_batch(&mut particles, delta_time);
simd_ops::compute_densities_batch(&particles, &neighbors, kernel_h);
}

LOD System

Distance-based quality reduction:

#![allow(unused)]
fn main() {
use astraweave_fluids::lod::{FluidLodConfig, LodLevel};

let lod_config = FluidLodConfig {
    levels: vec![
        LodLevel { distance: 0.0, particle_skip: 1 },   // Full detail
        LodLevel { distance: 50.0, particle_skip: 2 },  // 50% particles
        LodLevel { distance: 100.0, particle_skip: 4 }, // 25% particles
    ],
    cull_distance: 200.0,
};
}

Profiling

Built-in performance monitoring:

#![allow(unused)]
fn main() {
use astraweave_fluids::profiling::FluidProfiler;

let profiler = FluidProfiler::new();

// Wrap simulation step
profiler.begin("simulation_step");
simulation.step(dt);
profiler.end("simulation_step");

// Get statistics
let stats = profiler.get_stats();
println!("Avg step time: {:?}", stats.avg_step_time);
println!("Particle count: {}", stats.active_particles);
}

Benchmarks

From Master Benchmark Report:

BenchmarkResultNotes
Particle operations (1K)5.3 µs100-322 Melem/s
Spatial hashing163 µs - 5.6 ms38-62% improvement
SPH kernels (100K)171-223 µspoly6/spiky/viscosity
Simulation step (1K)1.8-3.0 ms
Multi-step450-500 µs45-57% faster
GPU data prep0.9-2.6 nsSub-nanosecond

Examples

Basic Water Pool

cargo run --example fluids_demo -- --preset pool

Interactive Waterfall

cargo run --example fluids_demo -- --preset waterfall

Building Water System

cargo run --example fluids_demo -- --preset building

See Also

Terrain System

Status: Production Ready
Coverage: 71.5%
Crate: astraweave-terrain

AstraWeave’s terrain system provides procedural terrain generation using noise functions, voxel data, and biome classification with full LOD support and async streaming.

Overview

Core Features

FeatureDescription
Voxel DataDensity-based terrain with material IDs
Marching Cubes256-configuration mesh generation
Biome SystemClimate-based biome distribution
Erosion SimulationHydraulic, thermal, and wind erosion
LOD ManagementDistance-based quality with hysteresis
Async StreamingBackground chunk loading

Performance

BenchmarkResult
World chunk generation15.06 ms
Marching cubes mesh~5 ms per chunk
SIMD heightmap2-3× faster than scalar

Quick Start

#![allow(unused)]
fn main() {
use astraweave_terrain::{
    ChunkManager, TerrainChunk, ChunkId,
    Heightmap, HeightmapConfig,
    Biome, BiomeType,
};

// Create heightmap generator
let heightmap = Heightmap::new(HeightmapConfig {
    seed: 12345,
    octaves: 6,
    frequency: 0.01,
    amplitude: 100.0,
});

// Generate chunk
let chunk_id = ChunkId::new(0, 0);
let chunk = TerrainChunk::generate(&heightmap, chunk_id);

// Get height at world position
let height = heightmap.sample(world_x, world_z);
}

Architecture

Module Overview

astraweave-terrain/
├── Generation
│   ├── noise_gen.rs          # Noise generation
│   ├── noise_simd.rs         # SIMD-optimized noise
│   ├── heightmap.rs          # Heightmap generation
│   └── structures.rs         # Structure placement
├── Voxel System
│   ├── voxel_data.rs         # Voxel grid & chunks
│   ├── chunk.rs              # Chunk management
│   └── terrain_modifier.rs   # Runtime modification
├── Meshing
│   ├── meshing.rs            # Mesh generation
│   ├── marching_cubes_tables.rs  # MC lookup tables
│   └── lod_blending.rs       # LOD mesh morphing
├── Biomes
│   ├── biome.rs              # Biome types
│   ├── biome_blending.rs     # Smooth transitions
│   ├── climate.rs            # Climate simulation
│   └── scatter.rs            # Vegetation placement
├── Erosion
│   ├── erosion.rs            # Basic erosion
│   └── advanced_erosion.rs   # Multi-type erosion
├── Streaming
│   ├── background_loader.rs  # Async loading
│   ├── lod_manager.rs        # LOD control
│   └── streaming_diagnostics.rs  # Debug info
└── Integration
    ├── partition_integration.rs  # World streaming
    ├── texture_splatting.rs      # Material blending
    └── terrain_persistence.rs    # Save/load

Voxel System

Voxel Data Structure

#![allow(unused)]
fn main() {
use astraweave_terrain::{Voxel, Density, MaterialId, VoxelChunk, CHUNK_SIZE};

// Voxel with density and material
let voxel = Voxel {
    density: Density::new(0.7),  // 0.0 = air, 1.0 = solid
    material: MaterialId::STONE,
};

// Create empty chunk
let mut chunk = VoxelChunk::new();

// Set voxel at local position
chunk.set(x, y, z, voxel);

// Get voxel
let v = chunk.get(x, y, z);
}

Chunk Coordinates

#![allow(unused)]
fn main() {
use astraweave_terrain::{ChunkCoord, ChunkId, CHUNK_SIZE};

// World position to chunk
let chunk_coord = ChunkCoord::from_world_pos(world_pos);

// Chunk ID for manager
let chunk_id = ChunkId::new(chunk_coord.x, chunk_coord.z);

// Local position within chunk
let local_pos = chunk_coord.to_local(world_pos);
}

Heightmap Generation

Basic Heightmap

#![allow(unused)]
fn main() {
use astraweave_terrain::{Heightmap, HeightmapConfig};

let config = HeightmapConfig {
    seed: 42,
    octaves: 6,           // Detail levels
    frequency: 0.01,      // Base frequency
    amplitude: 100.0,     // Height range
    persistence: 0.5,     // Octave decay
    lacunarity: 2.0,      // Octave frequency multiplier
};

let heightmap = Heightmap::new(config);

// Sample height at position
let height = heightmap.sample(x, z);

// Get normal at position
let normal = heightmap.normal(x, z);
}

SIMD-Optimized Generation

For bulk generation (2-3× faster):

#![allow(unused)]
fn main() {
use astraweave_terrain::SimdHeightmapGenerator;

let generator = SimdHeightmapGenerator::new(config);

// Generate entire chunk heightmap
let heights: Vec<f32> = generator.generate_chunk(chunk_coord);
}

Biome System

Biome Types

#![allow(unused)]
fn main() {
use astraweave_terrain::{BiomeType, Biome, BiomeConfig};

pub enum BiomeType {
    Ocean,
    Beach,
    Plains,
    Forest,
    Taiga,
    Desert,
    Savanna,
    Jungle,
    Swamp,
    Mountain,
    SnowyMountain,
    Tundra,
}

// Biome configuration
let forest = BiomeConfig {
    biome_type: BiomeType::Forest,
    temperature_range: (0.3, 0.7),
    humidity_range: (0.5, 0.8),
    height_modifier: 1.0,
    vegetation_density: 0.8,
};
}

Climate-Based Distribution

#![allow(unused)]
fn main() {
use astraweave_terrain::{ClimateMap, ClimateConfig};

let climate = ClimateMap::new(ClimateConfig {
    seed: 12345,
    temperature_scale: 0.001,
    humidity_scale: 0.002,
});

// Get biome at position
let biome = climate.get_biome_at(x, z);

// Get climate values
let (temp, humidity) = climate.sample(x, z);
}

Biome Blending

#![allow(unused)]
fn main() {
use astraweave_terrain::{BiomeBlender, BiomeBlendConfig};

let blender = BiomeBlender::new(BiomeBlendConfig {
    blend_distance: 16.0,  // Transition width
    noise_strength: 0.2,   // Edge noise
});

// Get blended biome weights at position
let weights: Vec<BiomeWeight> = blender.get_weights(x, z);

// Apply to terrain materials
for weight in weights {
    material.blend(weight.biome, weight.strength);
}
}

Mesh Generation

Marching Cubes

#![allow(unused)]
fn main() {
use astraweave_terrain::meshing::{ChunkMesh, MeshVertex};

// Generate mesh from voxel chunk
let mesh = ChunkMesh::from_voxels(&chunk, &heightmap);

// Access mesh data
for vertex in mesh.vertices() {
    let pos = vertex.position;
    let normal = vertex.normal;
    let uv = vertex.uv;
}
}

Dual Contouring

For sharper edges:

#![allow(unused)]
fn main() {
use astraweave_terrain::meshing::DualContouring;

let dc = DualContouring::new();
let mesh = dc.generate(&chunk, edge_sharpness: 0.8);
}

LOD Mesh Generation

#![allow(unused)]
fn main() {
use astraweave_terrain::meshing::{LodMeshGenerator, LodConfig};

let generator = LodMeshGenerator::new(LodConfig {
    levels: vec![
        LodLevel { distance: 0.0, subdivisions: 8 },
        LodLevel { distance: 100.0, subdivisions: 4 },
        LodLevel { distance: 300.0, subdivisions: 2 },
        LodLevel { distance: 500.0, subdivisions: 1 },
    ],
});

// Generate LOD mesh for distance
let mesh = generator.generate(&chunk, camera_distance);
}

Erosion Simulation

Hydraulic Erosion

#![allow(unused)]
fn main() {
use astraweave_terrain::{
    AdvancedErosionSimulator,
    HydraulicErosionConfig,
    ErosionPreset,
};

let erosion = AdvancedErosionSimulator::new();

// Apply preset
let config = ErosionPreset::MountainRivers.into_config();
erosion.simulate_hydraulic(&mut heightmap, config, iterations: 50000);

// Or custom config
let custom = HydraulicErosionConfig {
    rain_amount: 0.01,
    erosion_rate: 0.3,
    deposition_rate: 0.3,
    evaporation_rate: 0.02,
    min_slope: 0.01,
};
}

Thermal Erosion

#![allow(unused)]
fn main() {
use astraweave_terrain::ThermalErosionConfig;

let thermal_config = ThermalErosionConfig {
    talus_angle: 0.5,  // Max stable slope
    erosion_rate: 0.1,
};

erosion.simulate_thermal(&mut heightmap, thermal_config, iterations: 1000);
}

Wind Erosion

#![allow(unused)]
fn main() {
use astraweave_terrain::WindErosionConfig;

let wind_config = WindErosionConfig {
    wind_direction: Vec3::new(1.0, 0.0, 0.5).normalize(),
    wind_strength: 0.5,
    particle_count: 10000,
};

erosion.simulate_wind(&mut heightmap, wind_config, iterations: 5000);
}

Async Streaming

Background Loader

#![allow(unused)]
fn main() {
use astraweave_terrain::{
    BackgroundChunkLoader,
    StreamingConfig,
    StreamingStats,
};

let loader = BackgroundChunkLoader::new(StreamingConfig {
    load_radius: 8,           // Chunks to keep loaded
    unload_radius: 12,        // Distance to unload
    max_concurrent_loads: 4,  // Parallel chunk loading
    priority_distance: 2,     // High-priority radius
});

// Update from camera position
loader.update(camera_pos);

// Poll for completed chunks
while let Some(chunk) = loader.poll_completed() {
    chunk_manager.insert(chunk);
}

// Get statistics
let stats: StreamingStats = loader.stats();
println!("Chunks loaded: {}", stats.chunks_loaded);
println!("Load queue: {}", stats.queue_length);
}

LOD Manager

#![allow(unused)]
fn main() {
use astraweave_terrain::{LodManager, LodConfig, LodStats};

let lod_manager = LodManager::new(LodConfig {
    hysteresis: 0.1,  // Prevent LOD flickering
    update_interval: 0.5,  // Seconds between LOD updates
});

// Update LOD states
lod_manager.update(camera_pos, delta_time);

// Get current LOD for chunk
let lod_level = lod_manager.get_lod(chunk_id);
}

Streaming Diagnostics

#![allow(unused)]
fn main() {
use astraweave_terrain::StreamingDiagnostics;

let diagnostics = StreamingDiagnostics::new();

// Record frame data
diagnostics.record_frame(frame_time, chunks_loaded, chunks_visible);

// Detect hitches
if let Some(hitch) = diagnostics.detect_hitch() {
    warn!("Streaming hitch: {:?}", hitch);
}

// Generate report
let report = diagnostics.generate_report();
}

Runtime Modification

Terrain Modifier

#![allow(unused)]
fn main() {
use astraweave_terrain::{TerrainModifier, VoxelOp, VoxelOpType};

let mut modifier = TerrainModifier::new(&mut chunk_manager);

// Dig sphere
modifier.apply(VoxelOp {
    op_type: VoxelOpType::Subtract,
    center: world_pos,
    radius: 5.0,
    material: None,
});

// Add material
modifier.apply(VoxelOp {
    op_type: VoxelOpType::Add,
    center: world_pos,
    radius: 3.0,
    material: Some(MaterialId::STONE),
});

// Smooth area
modifier.apply(VoxelOp {
    op_type: VoxelOpType::Smooth,
    center: world_pos,
    radius: 4.0,
    material: None,
});

// Batch operations for efficiency
modifier.begin_batch();
for op in operations {
    modifier.apply(op);
}
modifier.end_batch();  // Regenerates affected meshes once
}

Texture Splatting

Material Blending

#![allow(unused)]
fn main() {
use astraweave_terrain::{
    SplatMapGenerator,
    SplatConfig,
    SplatRule,
    TerrainMaterial,
};

let generator = SplatMapGenerator::new(SplatConfig {
    resolution: 512,
    rules: vec![
        SplatRule::height_based(TerrainMaterial::Grass, 0.0, 50.0),
        SplatRule::height_based(TerrainMaterial::Rock, 50.0, 100.0),
        SplatRule::height_based(TerrainMaterial::Snow, 100.0, 200.0),
        SplatRule::slope_based(TerrainMaterial::Cliff, 0.7, 1.0),
    ],
});

let splat_map = generator.generate(&heightmap);
}

Triplanar Mapping

#![allow(unused)]
fn main() {
use astraweave_terrain::TriplanarWeights;

// Calculate triplanar weights from normal
let weights = TriplanarWeights::from_normal(vertex_normal);

// Sample texture with triplanar projection
let color = 
    sample_xz(uv_xz) * weights.y +
    sample_xy(uv_xy) * weights.z +
    sample_zy(uv_zy) * weights.x;
}

Persistence

Save/Load Terrain

#![allow(unused)]
fn main() {
use astraweave_terrain::terrain_persistence::{save_terrain, load_terrain};

// Save modified terrain
save_terrain(&chunk_manager, "saves/terrain.bin")?;

// Load terrain
let loaded = load_terrain("saves/terrain.bin")?;
chunk_manager.restore(&loaded);
}

AI Integration

Terrain Solver

For AI-driven terrain queries:

#![allow(unused)]
fn main() {
use astraweave_terrain::{TerrainSolver, ResolvedLocation};

let solver = TerrainSolver::new(&chunk_manager);

// Find valid location for AI agent
let result: ResolvedLocation = solver.find_valid_location(
    search_center,
    radius: 10.0,
    constraints: &[
        Constraint::MinHeight(5.0),
        Constraint::MaxSlope(0.3),
        Constraint::RequireBiome(BiomeType::Forest),
    ],
)?;

println!("Found location: {:?}", result.position);
}

Blueprint Zone System

The Blueprint Zone system enables polygon-based spatial control over vegetation generation and heightmap injection, bridging .blend scene imports with the terrain scatter pipeline.

Overview

Editor Canvas → BlueprintZone → ZoneScatterGenerator → ZoneGenerationResult
     ↓               ↓                ↓                      ↓
Polygon drawing   ZoneRegistry    Replica/Inspired      placements + patches
                  (save/load)      mode dispatch         ↓
                                                    apply_heightmap_patches()
                                                         ↓
                                                    TerrainChunk updates

Zone Data Model

#![allow(unused)]
fn main() {
use astraweave_terrain::blueprint_zone::*;

// Define a zone with polygon vertices
let zone = BlueprintZone {
    id: ZoneId(1),
    name: "Forest Clearing".into(),
    vertices: vec![[0.0, 0.0], [100.0, 0.0], [100.0, 100.0], [0.0, 100.0]],
    source: ZoneSource::BlendScene {
        pack_path: "assets/pine_forest.biomepack".into(),
        placement_mode: PlacementMode::Replica,
    },
    priority: 0,
    enabled: true,
};

// ZoneRegistry — CRUD, spatial queries, persistence
let mut registry = ZoneRegistry::new();
registry.add_zone(zone);
let zones = registry.zones_containing_point(50.0, 50.0);
registry.save(&Path::new("zones.json"))?;
let loaded = ZoneRegistry::load(&Path::new("zones.json"))?;
}

Placement Modes

ModeBehavior
Replica1:1 reproduction — fixed positions from .blend scene, scaled by AdaptiveScaleParams
InspiredProcedural scatter using ScatterConfig derived from BiomePack, respects zone polygon
BiomePresetPure biome-driven scatter (Grassland, Forest, Desert, etc.) without .blend data

Zone-Scoped Generation

#![allow(unused)]
fn main() {
use astraweave_terrain::zone_scatter::*;

let gen = ZoneScatterGenerator::new(256.0, 128); // chunk_size, heightmap_resolution
let result = gen.generate_zone_scatter(&zone, &biome_pack)?;

// result.placements — Vec<VegetationInstance> (position, rotation, scale, model_path)
// result.heightmap_patches — Vec<HeightmapPatch> (per-chunk height modifications)

// Apply heightmap patches to terrain chunks
let results = vec![result];
apply_heightmap_patches(&mut chunk_map, &results);
}

Adaptive Scaling

When zone area differs from source scene footprint, density and scale adjust automatically:

#![allow(unused)]
fn main() {
let params = AdaptiveScaleParams::compute(reference_area, zone_area);
// density_multiplier = sqrt(zone_area / reference_area)
// scale_multiplier   = (zone_area / reference_area)^0.25
// position_scale     = sqrt(zone_area / reference_area)
}

Boundary Blending

apply_boundary_blending() uses smoothstep falloff at zone edges:

  • Vegetation: density fades via BlendMask::sample(x, z) → 0.0–1.0
  • Heightmap: height delta scaled by mask value at each sample point
  • Manual: BrushMode::ZoneBlend for blend-weight painting in-editor

Heightmap Rasterization

Terrain meshes from .blend decomposition are rasterized into heightmaps:

#![allow(unused)]
fn main() {
use astraweave_blend::heightmap_raster::*;

let heightmap = rasterize_terrain_meshes(&terrain_meshes, resolution)?;
let height = heightmap.sample_bilinear(u, v); // Normalized [0,1] coords
let area = heightmap.footprint_area();         // World-space area in m²
}

Editor Integration

The BlueprintPanel provides a 2D canvas for polygon zone editing:

  • Tools: Select, DrawPolygon, MoveVertex, DeleteZone
  • Zone inspector with name, source selection, placement mode toggle
  • Undo/redo via command stack
  • Save/Load zones as .zones.json

The BlueprintOverlay projects zone polygons as wireframe outlines into the 3D viewport, integrated with the physics renderer debug line pass.

Test Coverage

Test SuiteTests
blueprint_zone unit tests24
zone_scatter unit + integration27
heightmap_raster unit + integration21
biome_pack extended9
blueprint_panel unit tests17
blueprint_overlay unit tests7
blend_scanner unit tests8
Total113+

See Also

Blueprint Zone Editor

Status: Production Ready
Tests: 125+
Crates: astraweave-terrain, astraweave-blend, aw_editor

The Blueprint Zone system provides polygon-based spatial control for vegetation generation and heightmap injection. It bridges .blend scene imports with AstraWeave’s terrain scatter pipeline, enabling both exact 1:1 reproduction (Replica mode) and procedural variation (Inspired mode) of imported environments.

Overview

Pipeline

.blend Scene → Decomposition → BiomePack → BlueprintZone → ZoneScatterGenerator
                    ↓                           ↓                    ↓
              heightmap_raster           ZoneRegistry          VegetationInstance
              (terrain mesh →            (polygon CRUD,        + HeightmapPatch
               heightmap + fixed          spatial queries,          ↓
               placements)                JSON persistence)   apply_heightmap_patches()
                                                                    ↓
                                                              TerrainChunk updates

Key Features

FeatureDescription
Polygon ZonesArbitrary polygons define generation boundaries
Replica Mode1:1 fixed placement from .blend scene data
Inspired ModeProcedural scatter using extracted ScatterConfig
Biome PresetsPure biome-driven scatter without .blend data
Heightmap InjectionRasterized terrain meshes applied as height patches
Boundary BlendingSmoothstep falloff prevents hard cutoffs at zone edges
Adaptive ScalingAuto-adjusts density/scale when zone ≠ source area
Undo/RedoFull command stack in blueprint panel
3D OverlayWireframe zone projection in viewport

Quick Start

#![allow(unused)]
fn main() {
use astraweave_terrain::blueprint_zone::*;
use astraweave_terrain::zone_scatter::*;
use std::path::Path;

// 1. Define a zone
let zone = BlueprintZone {
    id: ZoneId(1),
    name: "Pine Forest".into(),
    vertices: vec![
        [0.0, 0.0], [200.0, 0.0],
        [200.0, 150.0], [0.0, 150.0],
    ],
    source: ZoneSource::BlendScene {
        pack_path: "assets/pine_forest.biomepack".into(),
        placement_mode: PlacementMode::Replica,
    },
    priority: 0,
    enabled: true,
};

// 2. Register it
let mut registry = ZoneRegistry::new();
registry.add_zone(zone.clone());

// 3. Generate scatter
let gen = ZoneScatterGenerator::new(256.0, 128);
let result = gen.generate_zone_scatter(&zone, &biome_pack)?;

println!("{} placements", result.placement_count());
println!("{} height modifications", result.modified_height_count());

// 4. Apply heightmap patches to terrain
let mut chunks = HashMap::new();
// ... populate with TerrainChunks ...
apply_heightmap_patches(&mut chunks, &[result]);

// 5. Persist zones
registry.save(Path::new("assets/zones.json"))?;
}

Zone Data Model

BlueprintZone

A BlueprintZone represents a polygon region with a vegetation/terrain source:

#![allow(unused)]
fn main() {
pub struct BlueprintZone {
    pub id: ZoneId,                    // Unique identifier
    pub name: String,                  // Display name
    pub vertices: Vec<[f32; 2]>,       // Polygon vertices (XZ plane)
    pub source: ZoneSource,            // What to generate
    pub priority: u32,                 // Overlap resolution (higher wins)
    pub enabled: bool,                 // Toggle generation
}
}

ZoneSource

#![allow(unused)]
fn main() {
pub enum ZoneSource {
    BiomePreset(BiomeType),            // Built-in biome scatter
    BlendScene {
        pack_path: String,             // Path to .biomepack
        placement_mode: PlacementMode, // Replica or Inspired
    },
}
}

PlacementMode

ModeDescription
ReplicaExact positions from .blend scene, scaled by AdaptiveScaleParams
InspiredProcedural scatter using extracted density/distribution rules

ZoneRegistry

#![allow(unused)]
fn main() {
let mut registry = ZoneRegistry::new();

// CRUD operations
registry.add_zone(zone);
registry.remove_zone(zone_id);
let zone = registry.get_zone(zone_id);
let zone_mut = registry.get_zone_mut(zone_id);

// Spatial queries
let zones = registry.zones_containing_point(x, z);
let overlaps = registry.zones_overlapping_rect(min_x, min_z, max_x, max_z);

// Persistence
registry.save(Path::new("zones.json"))?;
let loaded = ZoneRegistry::load(Path::new("zones.json"))?;
}

Zone-Scoped Generation

ZoneScatterGenerator

#![allow(unused)]
fn main() {
use astraweave_terrain::zone_scatter::*;

// Create generator with chunk size and heightmap resolution
let gen = ZoneScatterGenerator::new(256.0, 128);

// Generate for a single zone
let result: ZoneGenerationResult = gen.generate_zone_scatter(&zone, &biome_pack)?;
}

ZoneGenerationResult

#![allow(unused)]
fn main() {
pub struct ZoneGenerationResult {
    pub placements: Vec<VegetationInstance>,     // What to place
    pub heightmap_patches: Vec<HeightmapPatch>,  // Height modifications
}

// VegetationInstance fields:
// - position: Vec3
// - rotation: f32
// - scale: f32
// - vegetation_type: String
// - model_path: String
// - terrain_normal: Vec3
}

Multi-Zone Generation

#![allow(unused)]
fn main() {
// Generate scatter for all zones with overlap priority resolution
let results = generate_multi_zone_scatter(&zones, &gen, &biome_packs)?;

// Apply all heightmap patches at once
apply_heightmap_patches(&mut chunk_map, &results);
}

Adaptive Scaling

When a zone’s area differs from the source scene’s footprint, scaling parameters adjust automatically:

#![allow(unused)]
fn main() {
let params = AdaptiveScaleParams::compute(reference_area, zone_area);
}
ParameterFormulaEffect
density_multiplier$\sqrt{\frac{\text{zone}}{\text{ref}}}$Adjusts placement density
scale_multiplier$\left(\frac{\text{zone}}{\text{ref}}\right)^{0.25}$Adjusts object scale
position_scale$\sqrt{\frac{\text{zone}}{\text{ref}}}$Adjusts position spread

Example: A zone 4× larger than the source scene would have:

  • density_multiplier = 2.0 (double the objects)
  • scale_multiplier ≈ 1.41 (slightly larger objects)
  • position_scale = 2.0 (spread positions wider)

Boundary Blending

Smoothstep Falloff

Zone edges use smoothstep interpolation to prevent hard vegetation/height cutoffs:

#![allow(unused)]
fn main() {
// BlendMask provides per-point blending weights
let mask = BlendMask::new(resolution, world_bounds);
let weight = mask.sample(x, z); // 0.0 at edge → 1.0 at center
}

Manual Painting

The editor provides BrushMode::ZoneBlend for manually painting blend weights at zone boundaries, giving artists fine control over transition regions.


Heightmap Rasterization

Terrain meshes extracted from .blend files are rasterized into heightmaps:

#![allow(unused)]
fn main() {
use astraweave_blend::heightmap_raster::*;

let heightmap = rasterize_terrain_meshes(&terrain_meshes, 128)?;

// Query rasterized data
let height = heightmap.sample_bilinear(0.5, 0.5); // Normalized UV coords
let area = heightmap.footprint_area();              // m² world-space

// Fixed placements (exact object positions from scene)
let placements: Vec<FixedPlacement> = extract_fixed_placements(&scene_objects);
}

The rasterizer uses ray-triangle intersection with:

  • Seam averaging for multi-tile terrain boundaries
  • Hole filling via neighbor interpolation for missing samples

Editor Integration

Blueprint Panel

The BlueprintPanel (panel type: Blueprint) provides:

  • 2D Canvas: Pan/zoom view for polygon zone drawing
  • Tools: Select, DrawPolygon, MoveVertex, DeleteZone
  • Zone Inspector: Name, source selection (biome preset or blend scene), placement mode toggle
  • Undo/Redo: Full BlueprintCommand stack with Ctrl+Z/Ctrl+Shift+Z
  • Persistence: Save/Load zones as .zones.json files

Viewport Overlay

BlueprintOverlay projects zone polygons as colored wireframe outlines in the 3D viewport. Zone boundaries are rendered alongside component gizmos and brush cursors in the physics renderer debug line pass.

Asset Browser

The BlendAssetScanner adds .blend file discovery to the asset browser with:

  • Automatic directory scanning for .blend files
  • Decomposition status detection (checking for manifest.json)
  • Quick actions: Import Blend Scene, Use as Zone Source

System Wiring

The editor’s update loop dispatches BlueprintAction events:

ActionHandlerEffect
GenerateZonehandle_generate_zone()Runs ZoneScatterGenerator for one zone
GenerateAllIterates all zonesGenerates scatter for every enabled zone
ClearGenerationClears resultsRemoves generated placements
SaveZoneshandle_save_zones()Saves ZoneRegistry to JSON
LoadZoneshandle_load_zones()Loads registry + syncs panel state

After each action, sync_zone_overlay() pushes updated zone data to the viewport.


Test Coverage

SuiteCountScope
blueprint_zone unit24Zone model, registry, polygon math, persistence
zone_scatter unit16Generation modes, blending, heightmap patches
zone_scatter_e2e integration11Full pipeline, multi-zone, chunk spanning
heightmap_raster unit11Rasterization, terrain bounds, seam stitching
heightmap_raster_e2e integration10Multi-tile, bilinear sampling, edge cases
biome_pack unit9BiomePack extension fields, detection
blueprint_panel unit17Canvas, tools, undo/redo, actions
blueprint_overlay unit7Wireframe generation, color mapping
blend_scanner unit8Directory scanning, status detection
Total113+

See Also

Cinematics System

Status: Production Ready
Crate: astraweave-cinematics
Coverage: ~85%

The cinematics system provides a timeline-based sequencer for creating cutscenes, scripted events, and cinematic sequences with synchronized camera, animation, audio, and visual effects tracks.

Core Types

TypeDescription
TimePrecise time representation (seconds/milliseconds)
TrackTrack variant (Camera, Animation, Audio, Fx)
CameraKeyCamera keyframe with position, rotation, FOV
TimelineCollection of tracks with duration
SequencerPlayback engine with seek and step
SequencerEventEvents emitted during playback

Quick Start

#![allow(unused)]
fn main() {
use astraweave_cinematics::{Time, Timeline, Track, CameraKey, Sequencer};

// Create a 10-second timeline
let mut timeline = Timeline::new("intro_cutscene", 10.0);

// Add camera keyframes
let keyframes = vec![
    CameraKey::new(Time::zero(), [0.0, 5.0, -10.0], [0.0, 0.0, 0.0], 60.0),
    CameraKey::new(Time::from_secs(5.0), [10.0, 8.0, -5.0], [0.0, 45.0, 0.0], 55.0),
    CameraKey::new(Time::from_secs(10.0), [0.0, 3.0, 0.0], [0.0, 90.0, 0.0], 70.0),
];
timeline.add_camera_track(keyframes);

// Add audio
timeline.add_audio_track("music/intro.ogg", Time::zero(), 0.8);

// Add animation trigger
timeline.add_track(Track::animation(1, "character_wave", Time::from_secs(2.0)));

// Add FX
timeline.add_track(Track::fx("explosion", Time::from_secs(8.0), serde_json::json!({
    "scale": 2.0,
    "particles": 500
})));

// Playback
let mut sequencer = Sequencer::new();
let dt = 1.0 / 60.0; // 60 FPS

loop {
    match sequencer.step(dt, &timeline) {
        Ok(events) => {
            for event in events {
                handle_event(event);
            }
        }
        Err(_) => break, // End of timeline
    }
}
}

Architecture

Timeline Structure

Timeline "intro_cutscene" (10.0s)
├── Track::Camera
│   ├── Keyframe @ 0.0s (pos, rot, fov)
│   ├── Keyframe @ 5.0s
│   └── Keyframe @ 10.0s
├── Track::Audio "music/intro.ogg" @ 0.0s
├── Track::Animation target=1 "character_wave" @ 2.0s
└── Track::Fx "explosion" @ 8.0s

Playback Flow

┌───────────────────────────────────────────────────┐
│                    Sequencer                       │
│  ┌─────────┐    ┌───────────┐    ┌─────────────┐  │
│  │  seek() │ →  │  step(dt) │ →  │   events    │  │
│  └─────────┘    └───────────┘    └─────────────┘  │
│                        │                           │
│                        ▼                           │
│  ┌─────────────────────────────────────────────┐  │
│  │ For each track, emit events where           │  │
│  │ start_time > previous_t && start_time <= t  │  │
│  └─────────────────────────────────────────────┘  │
└───────────────────────────────────────────────────┘

Time API

The Time type provides precision time handling:

#![allow(unused)]
fn main() {
use astraweave_cinematics::Time;

// Construction
let t1 = Time::zero();                  // 0.0s
let t2 = Time::from_secs(5.0);          // 5.0s
let t3 = Time::from_millis(1500.0);     // 1.5s

// Access
println!("Seconds: {}", t2.as_secs());   // 5.0
println!("Millis: {}", t2.as_millis());  // 5000.0

// Arithmetic
let sum = t2 + t3;                       // 6.5s
let diff = t2 - t3;                      // 3.5s
let added = t2.add_secs(2.5);            // 7.5s

// Utilities
let clamped = t2.clamp(Time::zero(), Time::from_secs(4.0)); // 4.0s
let lerped = Time::zero().lerp(Time::from_secs(10.0), 0.3); // 3.0s

// Display
println!("{}", Time::from_secs(2.5));    // "2.50s"
println!("{}", Time::from_millis(500.0)); // "500ms"
}

Track Types

Camera Track

Controls camera position, rotation, and field of view:

#![allow(unused)]
fn main() {
use astraweave_cinematics::{Track, CameraKey, Time};

let track = Track::camera(vec![
    CameraKey {
        t: Time::zero(),
        pos: [0.0, 5.0, -10.0],
        rot: [0.0, 0.0, 0.0],      // Euler angles (degrees)
        fov: 60.0,                  // Field of view
    },
    CameraKey {
        t: Time::from_secs(5.0),
        pos: [10.0, 8.0, -5.0],
        rot: [0.0, 45.0, 0.0],
        fov: 55.0,
    },
]);

assert!(track.is_camera());
assert_eq!(track.keyframe_count(), Some(2));
}

Animation Track

Triggers character/object animations:

#![allow(unused)]
fn main() {
let track = Track::animation(
    42,                        // Target entity ID
    "run_forward",             // Animation clip name
    Time::from_secs(1.5),      // Start time
);

assert!(track.is_animation());
assert_eq!(track.start_time(), Some(Time::from_secs(1.5)));
}

Audio Track

Plays sound effects or music:

#![allow(unused)]
fn main() {
let track = Track::audio(
    "sfx/explosion.wav",       // Audio clip path
    Time::from_secs(3.0),      // Start time
    0.8,                       // Volume (0.0 - 1.0)
);

assert!(track.is_audio());
}

FX Track

Triggers visual effects with parameters:

#![allow(unused)]
fn main() {
use serde_json::json;

let track = Track::fx(
    "particle_burst",          // Effect name
    Time::from_secs(2.0),      // Start time
    json!({
        "count": 100,
        "color": [1.0, 0.5, 0.0],
        "lifetime": 2.0
    }),
);

assert!(track.is_fx());
}

Timeline API

Creating Timelines

#![allow(unused)]
fn main() {
use astraweave_cinematics::Timeline;

// Named timeline with duration
let mut timeline = Timeline::new("boss_intro", 30.0);

// Empty timeline
let empty = Timeline::empty();
assert!(empty.is_empty());
}

Building Timelines

#![allow(unused)]
fn main() {
let mut timeline = Timeline::new("action_sequence", 15.0);

// Add tracks
timeline.add_camera_track(camera_keyframes);
timeline.add_audio_track("music/action.ogg", Time::zero(), 1.0);
timeline.add_track(Track::animation(1, "attack", Time::from_secs(5.0)));

// Track counts
println!("Camera tracks: {}", timeline.camera_track_count());
println!("Audio tracks: {}", timeline.audio_track_count());
println!("Animation tracks: {}", timeline.animation_track_count());
println!("FX tracks: {}", timeline.fx_track_count());
println!("Total tracks: {}", timeline.track_count());
println!("Total keyframes: {}", timeline.total_keyframes());
}

Timeline Properties

#![allow(unused)]
fn main() {
let timeline = Timeline::new("example", 10.0);

println!("Name: {}", timeline.name);
println!("Duration: {}s", timeline.duration_secs());
println!("Empty: {}", timeline.is_empty());

// Display
println!("{}", timeline); // Timeline("example", duration=10.00s, 0 tracks)
}

Sequencer API

Basic Playback

#![allow(unused)]
fn main() {
use astraweave_cinematics::Sequencer;

let mut seq = Sequencer::new();
let dt = 1.0 / 60.0;  // 60 FPS

// Step through timeline
loop {
    match seq.step(dt, &timeline) {
        Ok(events) => {
            for event in events {
                process_event(event);
            }
        }
        Err(SeqError::Range(_)) => {
            println!("Timeline complete");
            break;
        }
    }
}
}

Seeking

#![allow(unused)]
fn main() {
let mut seq = Sequencer::new();

// Jump to specific time
seq.seek(Time::from_secs(5.0));

// Current time
println!("Current: {}", seq.t);
}

Event Handling

#![allow(unused)]
fn main() {
use astraweave_cinematics::SequencerEvent;

fn handle_event(event: SequencerEvent) {
    match event {
        SequencerEvent::CameraKey(key) => {
            camera.set_position(key.pos);
            camera.set_rotation(key.rot);
            camera.set_fov(key.fov);
        }
        SequencerEvent::AnimStart { target, clip } => {
            if let Some(entity) = world.get_entity(target) {
                animator.play(entity, &clip);
            }
        }
        SequencerEvent::AudioPlay { clip, volume } => {
            audio.play(&clip, volume);
        }
        SequencerEvent::FxTrigger { name, params } => {
            fx_system.trigger(&name, params);
        }
    }
}
}

Serialization

Timelines are fully serializable with Serde:

#![allow(unused)]
fn main() {
use astraweave_cinematics::Timeline;

// Save to JSON
let json = serde_json::to_string_pretty(&timeline)?;
std::fs::write("cutscene.json", &json)?;

// Load from JSON
let loaded: Timeline = serde_json::from_str(&json)?;

// Save to RON (common in game dev)
let ron = ron::to_string(&timeline)?;
}

Example JSON:

{
  "name": "intro",
  "duration": 10.0,
  "tracks": [
    {
      "Camera": {
        "keyframes": [
          { "t": 0.0, "pos": [0, 5, -10], "rot": [0, 0, 0], "fov": 60 },
          { "t": 5.0, "pos": [10, 8, -5], "rot": [0, 45, 0], "fov": 55 }
        ]
      }
    },
    {
      "Audio": {
        "clip": "music/intro.ogg",
        "start": 0.0,
        "volume": 0.8
      }
    }
  ]
}

Integration Examples

With ECS

#![allow(unused)]
fn main() {
use astraweave_ecs::World;
use astraweave_cinematics::{Sequencer, Timeline};

fn cinematics_system(world: &mut World, timeline: &Timeline, seq: &mut Sequencer, dt: f32) {
    if let Ok(events) = seq.step(dt, timeline) {
        for event in events {
            match event {
                SequencerEvent::CameraKey(key) => {
                    // Update camera component
                    if let Some(camera) = world.query_mut::<CameraComponent>().next() {
                        camera.position = key.pos.into();
                        camera.rotation = key.rot.into();
                        camera.fov = key.fov;
                    }
                }
                SequencerEvent::AnimStart { target, clip } => {
                    // Trigger animation on entity
                    if let Some(anim) = world.get_component_mut::<Animator>(target) {
                        anim.play(&clip);
                    }
                }
                // ... handle other events
            }
        }
    }
}
}

With Game State

#![allow(unused)]
fn main() {
enum GameState {
    Playing,
    Cutscene { timeline: Timeline, sequencer: Sequencer },
}

impl GameState {
    fn update(&mut self, dt: f32) -> Option<SequencerEvent> {
        match self {
            GameState::Cutscene { timeline, sequencer } => {
                match sequencer.step(dt, timeline) {
                    Ok(events) => {
                        for event in events {
                            // Handle events
                        }
                        None
                    }
                    Err(_) => {
                        // Cutscene complete, return to gameplay
                        *self = GameState::Playing;
                        None
                    }
                }
            }
            _ => None,
        }
    }
}
}

Performance

OperationLatencyNotes
Time creation~1 nsZero-cost abstraction
step() per track~50 nsLinear scan of events
Event emission~100 nsAllocation per event
Full timeline step~500 nsTypical 10-track timeline

Optimization Tips

  1. Pre-sort tracks by start time for faster event lookup
  2. Reuse sequencers instead of creating new ones
  3. Batch event handling when multiple events fire simultaneously
  4. Use seek() sparingly - stepping is more efficient

Common Patterns

Looping Cutscenes

#![allow(unused)]
fn main() {
let mut seq = Sequencer::new();

loop {
    match seq.step(dt, &timeline) {
        Ok(events) => handle_events(events),
        Err(_) => {
            // Loop back to start
            seq.seek(Time::zero());
        }
    }
}
}

Skippable Cutscenes

#![allow(unused)]
fn main() {
fn update_cutscene(seq: &mut Sequencer, timeline: &Timeline, dt: f32, skip_pressed: bool) {
    if skip_pressed {
        // Jump to end
        seq.seek(timeline.duration);
        return;
    }
    
    if let Ok(events) = seq.step(dt, timeline) {
        handle_events(events);
    }
}
}

Branching Cutscenes

#![allow(unused)]
fn main() {
struct BranchingCutscene {
    timelines: HashMap<String, Timeline>,
    current: String,
    sequencer: Sequencer,
}

impl BranchingCutscene {
    fn switch_to(&mut self, name: &str) {
        self.current = name.to_string();
        self.sequencer.seek(Time::zero());
    }
    
    fn update(&mut self, dt: f32) -> Vec<SequencerEvent> {
        let timeline = &self.timelines[&self.current];
        self.sequencer.step(dt, timeline).unwrap_or_default()
    }
}
}

See Also

Building Your First Game

This guide walks you through building a complete game with AstraWeave, from project setup to a playable demo featuring AI companions, physics, and navigation.

Before starting, ensure you have:
- Rust 1.75+ installed
- AstraWeave cloned and building successfully
- Ollama running with a compatible model (see [Installation Guide](../getting-started/installation.md))

Project Overview

We’ll build “Companion Quest” - a small adventure where an AI companion helps the player navigate a dungeon, solve puzzles, and defeat enemies. This showcases:

  • AI companion with LLM-powered dialogue
  • Physics-based puzzles
  • Navigation and pathfinding
  • Combat with adaptive enemies
  • Save/load functionality

Step 1: Project Setup

Create a New Example

Create a new directory in examples/:

mkdir -p examples/companion_quest/src

Create examples/companion_quest/Cargo.toml:

[package]
name = "companion_quest"
version = "0.1.0"
edition = "2021"

[dependencies]
astraweave-core = { path = "../../astraweave-core" }
astraweave-ecs = { path = "../../astraweave-ecs" }
astraweave-ai = { path = "../../astraweave-ai" }
astraweave-physics = { path = "../../astraweave-physics" }
astraweave-nav = { path = "../../astraweave-nav" }
astraweave-render = { path = "../../astraweave-render" }
astraweave-audio = { path = "../../astraweave-audio" }
astraweave-input = { path = "../../astraweave-input" }
astraweave-llm = { path = "../../astraweave-llm" }
astraweave-gameplay = { path = "../../astraweave-gameplay" }
tokio = { version = "1.0", features = ["rt-multi-thread", "macros"] }

Main Entry Point

Create examples/companion_quest/src/main.rs:

use astraweave_core::prelude::*;
use astraweave_ecs::prelude::*;
use astraweave_ai::prelude::*;
use astraweave_physics::prelude::*;
use astraweave_nav::prelude::*;

fn main() {
    let mut world = World::new();
    
    setup_world(&mut world);
    setup_player(&mut world);
    setup_companion(&mut world);
    setup_dungeon(&mut world);
    
    run_game_loop(&mut world);
}

fn setup_world(world: &mut World) {
    world.insert_resource(PhysicsConfig::default());
    world.insert_resource(NavigationConfig::default());
    world.insert_resource(AiConfig {
        tick_budget_ms: 8,
        max_concurrent_plans: 4,
        ..Default::default()
    });
}

Step 2: Creating the Player

The player entity needs transform, physics, and input components:

#![allow(unused)]
fn main() {
fn setup_player(world: &mut World) {
    let player = world.spawn((
        Transform::from_xyz(0.0, 1.0, 0.0),
        RigidBody::Dynamic,
        Collider::capsule(0.5, 1.8),
        Player {
            health: 100.0,
            max_health: 100.0,
            stamina: 100.0,
        },
        InputReceiver::default(),
        CharacterController {
            speed: 5.0,
            jump_force: 8.0,
            ..Default::default()
        },
    ));
    
    world.insert_resource(PlayerEntity(player));
}

#[derive(Component)]
struct Player {
    health: f32,
    max_health: f32,
    stamina: f32,
}
}

Step 3: Creating the AI Companion

The companion uses AstraWeave’s AI-native architecture:

#![allow(unused)]
fn main() {
fn setup_companion(world: &mut World) {
    let companion = world.spawn((
        Transform::from_xyz(2.0, 1.0, 0.0),
        RigidBody::Dynamic,
        Collider::capsule(0.4, 1.6),
        
        AiAgent {
            personality: "helpful and curious".into(),
            knowledge_base: vec![
                "I am Spark, a magical companion".into(),
                "I can help solve puzzles and fight enemies".into(),
            ],
        },
        
        PerceptionRadius(15.0),
        
        NavAgent {
            speed: 4.0,
            acceleration: 10.0,
            avoidance_radius: 0.5,
        },
        
        Companion {
            following: true,
            follow_distance: 3.0,
        },
        
        DialogueCapable::default(),
    ));
    
    world.insert_resource(CompanionEntity(companion));
}

#[derive(Component)]
struct Companion {
    following: bool,
    follow_distance: f32,
}
}

Step 4: Building the Dungeon

Create a simple dungeon with rooms and corridors:

#![allow(unused)]
fn main() {
fn setup_dungeon(world: &mut World) {
    let floor = world.spawn((
        Transform::from_xyz(0.0, 0.0, 0.0),
        RigidBody::Static,
        Collider::cuboid(50.0, 0.5, 50.0),
        Floor,
    ));
    
    spawn_walls(world);
    spawn_doors(world);
    spawn_puzzles(world);
    spawn_enemies(world);
    
    generate_navmesh(world);
}

fn generate_navmesh(world: &mut World) {
    let navmesh = NavMeshBuilder::new()
        .cell_size(0.3)
        .cell_height(0.2)
        .agent_radius(0.5)
        .agent_height(1.8)
        .max_slope(45.0)
        .build_from_world(world);
    
    world.insert_resource(navmesh);
}
}

Step 5: The Game Loop

AstraWeave uses a fixed-tick simulation for determinism:

#![allow(unused)]
fn main() {
fn run_game_loop(world: &mut World) {
    let mut scheduler = Scheduler::new();
    
    scheduler.add_system(input_system);
    scheduler.add_system(player_movement_system);
    scheduler.add_system(ai_perception_system);
    scheduler.add_system(ai_planning_system);
    scheduler.add_system(companion_follow_system);
    scheduler.add_system(navigation_system);
    scheduler.add_system(physics_system);
    scheduler.add_system(combat_system);
    scheduler.add_system(dialogue_system);
    scheduler.add_system(render_system);
    
    let tick_rate = Duration::from_secs_f64(1.0 / 60.0);
    let mut accumulator = Duration::ZERO;
    let mut last_time = Instant::now();
    
    loop {
        let now = Instant::now();
        let delta = now - last_time;
        last_time = now;
        accumulator += delta;
        
        while accumulator >= tick_rate {
            scheduler.run(world);
            accumulator -= tick_rate;
        }
        
        if should_quit(world) {
            break;
        }
    }
}
}

Step 6: AI Companion Behavior

The companion uses perception and planning:

#![allow(unused)]
fn main() {
fn companion_follow_system(
    player_query: Query<&Transform, With<Player>>,
    mut companion_query: Query<(&mut NavAgent, &Companion, &Transform)>,
    navmesh: Res<NavMesh>,
) {
    let player_pos = player_query.single().translation;
    
    for (mut nav_agent, companion, transform) in companion_query.iter_mut() {
        if !companion.following {
            continue;
        }
        
        let distance = transform.translation.distance(player_pos);
        
        if distance > companion.follow_distance {
            let target = player_pos - (player_pos - transform.translation)
                .normalize() * companion.follow_distance;
            
            if let Some(path) = navmesh.find_path(transform.translation, target) {
                nav_agent.set_path(path);
            }
        }
    }
}
}

Step 7: Dialogue Integration

Enable LLM-powered dialogue with the companion:

#![allow(unused)]
fn main() {
fn dialogue_system(
    mut dialogue_events: EventReader<DialogueEvent>,
    mut llm_client: ResMut<LlmClient>,
    companion_query: Query<&AiAgent, With<Companion>>,
    mut dialogue_responses: EventWriter<DialogueResponse>,
) {
    for event in dialogue_events.read() {
        let agent = companion_query.single();
        
        let prompt = format!(
            "You are {}. The player says: '{}'. Respond in character.",
            agent.personality,
            event.message
        );
        
        match llm_client.generate_blocking(&prompt) {
            Ok(response) => {
                dialogue_responses.send(DialogueResponse {
                    speaker: "Spark".into(),
                    text: response,
                });
            }
            Err(e) => {
                dialogue_responses.send(DialogueResponse {
                    speaker: "Spark".into(),
                    text: "Hmm, I'm not sure what to say...".into(),
                });
            }
        }
    }
}
}

Step 8: Combat System

Add combat with tool validation:

#![allow(unused)]
fn main() {
fn combat_system(
    mut combat_events: EventReader<CombatEvent>,
    mut health_query: Query<&mut Health>,
    tool_validator: Res<ToolValidator>,
) {
    for event in combat_events.read() {
        let validation = tool_validator.validate(&ToolCall {
            tool: "attack".into(),
            params: json!({
                "attacker": event.attacker,
                "target": event.target,
                "damage": event.damage,
            }),
        });
        
        match validation {
            ToolResult::Success => {
                if let Ok(mut health) = health_query.get_mut(event.target) {
                    health.current -= event.damage;
                }
            }
            ToolResult::Blocked(reason) => {
                println!("Attack blocked: {}", reason);
            }
        }
    }
}
}

Step 9: Running Your Game

Build and run:

cargo run -p companion_quest --release

Expected Output

[INFO] AstraWeave v0.1.0 starting...
[INFO] Physics initialized: Rapier 0.17
[INFO] Navigation: NavMesh built (2,450 polygons)
[INFO] AI: Connected to Ollama (hermes2-pro)
[INFO] Companion "Spark" spawned at (2.0, 1.0, 0.0)
[INFO] Game loop started at 60 Hz

Complete Example

See the full working example at examples/companion_quest/ or explore these related examples:

ExampleDescription
hello_companionMinimal AI companion demo
adaptive_bossMulti-phase adaptive boss fight
quest_dialogue_demoDialogue and quest system
combat_physics_demoPhysics-based combat
unified_showcaseFull engine demonstration

Next Steps

Enable release mode (`--release`) for LLM inference. Debug builds can be 10-50x slower for AI operations.

AI Companions

AI companions are a signature feature of AstraWeave. Unlike scripted NPCs in traditional engines, companions in AstraWeave use LLM-powered planning to make intelligent decisions while respecting game rules through tool validation.

Companions cannot cheat - they perceive the world, plan actions, and execute through the same validated systems as players.

Companion Architecture

graph LR
    subgraph Companion
        Perception[Perception]
        Memory[Memory]
        Personality[Personality]
        Planning[LLM Planning]
        Actions[Action Queue]
    end
    
    World[World State] --> Perception
    Perception --> Memory
    Memory --> Planning
    Personality --> Planning
    Planning --> Actions
    Actions --> World

Creating a Companion

Basic Companion Setup

#![allow(unused)]
fn main() {
use astraweave_ai::prelude::*;
use astraweave_ecs::prelude::*;

fn spawn_companion(world: &mut World, name: &str, personality: &str) -> Entity {
    world.spawn((
        Name::new(name),
        Transform::from_xyz(0.0, 0.0, 0.0),
        
        AiAgent::new()
            .with_personality(personality)
            .with_perception_radius(20.0)
            .with_memory_capacity(100),
        
        CompanionBehavior {
            follow_distance: 3.0,
            engage_distance: 10.0,
            flee_health_threshold: 0.2,
        },
        
        AvailableTools::new(vec![
            Tool::move_to(),
            Tool::follow(),
            Tool::attack(),
            Tool::defend(),
            Tool::use_item(),
            Tool::speak(),
            Tool::investigate(),
        ]),
        
        PerceptionState::default(),
        NavAgent::default(),
        DialogueCapable::default(),
        Health::new(100.0),
    ))
}
}

Personality Configuration

Companions have distinct personalities that influence their decisions:

#![allow(unused)]
fn main() {
let warrior_companion = AiAgent::new()
    .with_personality("brave and protective warrior who prioritizes the player's safety")
    .with_traits(vec![
        Trait::Brave,
        Trait::Protective,
        Trait::DirectCommunicator,
    ])
    .with_knowledge(vec![
        "I am a seasoned warrior".into(),
        "I protect my allies at all costs".into(),
        "I prefer melee combat".into(),
    ]);

let scholar_companion = AiAgent::new()
    .with_personality("curious scholar who loves discovering secrets and solving puzzles")
    .with_traits(vec![
        Trait::Curious,
        Trait::Analytical,
        Trait::Cautious,
    ])
    .with_knowledge(vec![
        "I study ancient texts and artifacts".into(),
        "I prefer avoiding combat when possible".into(),
        "I excel at puzzle-solving".into(),
    ]);
}

Companion Behaviors

Following the Player

#![allow(unused)]
fn main() {
fn companion_follow_system(
    player: Query<&Transform, With<Player>>,
    mut companions: Query<(&mut NavAgent, &CompanionBehavior, &Transform), With<AiAgent>>,
    navmesh: Res<NavMesh>,
) {
    let player_pos = player.single().translation;
    
    for (mut nav, behavior, transform) in companions.iter_mut() {
        let distance = transform.translation.distance(player_pos);
        
        if distance > behavior.follow_distance {
            let target = calculate_follow_position(player_pos, transform.translation, behavior.follow_distance);
            
            if let Some(path) = navmesh.find_path(transform.translation, target) {
                nav.set_path(path);
            }
        } else {
            nav.stop();
        }
    }
}
}

Combat Assistance

#![allow(unused)]
fn main() {
fn companion_combat_system(
    mut companions: Query<(&AiAgent, &mut ActionQueue, &Transform, &Health)>,
    enemies: Query<(Entity, &Transform, &Health), With<Enemy>>,
    player: Query<&Transform, With<Player>>,
) {
    let player_pos = player.single().translation;
    
    for (agent, mut actions, companion_pos, health) in companions.iter_mut() {
        if health.percentage() < agent.flee_threshold {
            actions.push(Action::Flee);
            continue;
        }
        
        let nearest_enemy = enemies.iter()
            .filter(|(_, t, h)| h.current > 0.0)
            .min_by_key(|(_, t, _)| {
                (t.translation.distance(player_pos) * 100.0) as i32
            });
        
        if let Some((enemy_entity, enemy_pos, _)) = nearest_enemy {
            let distance = companion_pos.translation.distance(enemy_pos.translation);
            
            if distance < agent.engage_distance {
                actions.push(Action::Attack(enemy_entity));
            } else {
                actions.push(Action::MoveTo(enemy_pos.translation));
            }
        }
    }
}
}

Dialogue Interaction

#![allow(unused)]
fn main() {
fn companion_dialogue_system(
    mut events: EventReader<DialogueRequest>,
    mut companions: Query<(&AiAgent, &PerceptionState, &mut DialogueState)>,
    llm: Res<LlmClient>,
    mut responses: EventWriter<DialogueResponse>,
) {
    for request in events.read() {
        if let Ok((agent, perception, mut dialogue)) = companions.get_mut(request.target) {
            let context = build_dialogue_context(agent, perception, &dialogue.history);
            
            let prompt = format!(
                "{}\n\nThe player says: '{}'\n\nRespond in character:",
                context,
                request.message
            );
            
            match llm.generate(&prompt) {
                Ok(response) => {
                    dialogue.history.push(DialogueEntry {
                        speaker: "Player".into(),
                        text: request.message.clone(),
                    });
                    dialogue.history.push(DialogueEntry {
                        speaker: agent.name.clone(),
                        text: response.clone(),
                    });
                    
                    responses.send(DialogueResponse {
                        speaker: agent.name.clone(),
                        text: response,
                    });
                }
                Err(_) => {
                    responses.send(DialogueResponse {
                        speaker: agent.name.clone(),
                        text: "Hmm, let me think about that...".into(),
                    });
                }
            }
        }
    }
}
}

Memory System

Companions remember past events and use them in decision-making:

#![allow(unused)]
fn main() {
#[derive(Component)]
struct CompanionMemory {
    short_term: VecDeque<MemoryEntry>,
    long_term: Vec<MemoryEntry>,
    capacity: usize,
}

impl CompanionMemory {
    fn remember(&mut self, event: MemoryEntry) {
        self.short_term.push_back(event);
        
        if self.short_term.len() > self.capacity {
            if let Some(old) = self.short_term.pop_front() {
                if old.importance >= 0.7 {
                    self.long_term.push(old);
                }
            }
        }
    }
    
    fn recall(&self, query: &str, limit: usize) -> Vec<&MemoryEntry> {
        self.long_term.iter()
            .chain(self.short_term.iter())
            .filter(|m| m.matches(query))
            .take(limit)
            .collect()
    }
}

#[derive(Clone)]
struct MemoryEntry {
    timestamp: GameTime,
    event_type: EventType,
    description: String,
    importance: f32,
    entities: Vec<Entity>,
    location: Vec3,
}
}

Companion Commands

Players can issue commands to companions:

#![allow(unused)]
fn main() {
#[derive(Event)]
enum CompanionCommand {
    Follow,
    Stay,
    Attack(Entity),
    Investigate(Vec3),
    UseAbility(AbilityId),
    ToggleAggressive,
}

fn handle_companion_commands(
    mut commands: EventReader<CompanionCommand>,
    mut companions: Query<(&mut CompanionBehavior, &mut ActionQueue)>,
) {
    for command in commands.read() {
        for (mut behavior, mut actions) in companions.iter_mut() {
            match command {
                CompanionCommand::Follow => {
                    behavior.mode = CompanionMode::Follow;
                    actions.clear();
                }
                CompanionCommand::Stay => {
                    behavior.mode = CompanionMode::Stay;
                    actions.clear();
                }
                CompanionCommand::Attack(target) => {
                    actions.push(Action::Attack(*target));
                }
                CompanionCommand::Investigate(pos) => {
                    actions.push(Action::MoveTo(*pos));
                    actions.push(Action::Investigate);
                }
                CompanionCommand::UseAbility(ability) => {
                    actions.push(Action::UseAbility(*ability));
                }
                CompanionCommand::ToggleAggressive => {
                    behavior.aggressive = !behavior.aggressive;
                }
            }
        }
    }
}
}

Configuration

Companion Presets

#![allow(unused)]
fn main() {
pub fn create_healer_companion(world: &mut World) -> Entity {
    spawn_companion(world, "Luna", "compassionate healer who prioritizes keeping allies alive")
        .with(AvailableTools::new(vec![
            Tool::heal(),
            Tool::cure_status(),
            Tool::buff(),
            Tool::follow(),
            Tool::flee(),
        ]))
        .with(CompanionBehavior {
            follow_distance: 5.0,
            engage_distance: 15.0,
            flee_health_threshold: 0.3,
            mode: CompanionMode::Support,
        })
}

pub fn create_tank_companion(world: &mut World) -> Entity {
    spawn_companion(world, "Grimjaw", "fearless guardian who draws enemy attention")
        .with(AvailableTools::new(vec![
            Tool::taunt(),
            Tool::block(),
            Tool::attack(),
            Tool::charge(),
            Tool::protect_ally(),
        ]))
        .with(CompanionBehavior {
            follow_distance: 2.0,
            engage_distance: 8.0,
            flee_health_threshold: 0.1,
            mode: CompanionMode::Aggressive,
        })
}
}

Best Practices

- Limit perception radius for better performance
- Use plan caching to reduce LLM calls
- Batch companion updates in the scheduler
- Don't give companions tools they shouldn't have
- Always define fallback behaviors for LLM timeouts
- Test personality prompts for consistent behavior

See Also

Adaptive Bosses

Adaptive bosses in AstraWeave learn from player behavior and adjust their strategies in real-time. Unlike scripted boss encounters, these AI-driven enemies create unique, memorable fights that evolve with each attempt.

Bosses use the same AI architecture as companions - perception, planning, and validated tools - but with combat-focused behaviors and learning capabilities.

Boss Architecture

graph TB
    subgraph "Boss AI"
        Perception[Player Analysis]
        Memory[Pattern Memory]
        Strategy[Strategy Selection]
        Phase[Phase Management]
        Actions[Action Execution]
    end
    
    Player[Player Behavior] --> Perception
    Perception --> Memory
    Memory --> Strategy
    Strategy --> Phase
    Phase --> Actions
    Actions --> Player

Creating an Adaptive Boss

Basic Boss Setup

#![allow(unused)]
fn main() {
use astraweave_ai::prelude::*;
use astraweave_gameplay::boss::*;

fn spawn_adaptive_boss(world: &mut World) -> Entity {
    world.spawn((
        Name::new("The Hollow Knight"),
        Transform::from_xyz(0.0, 2.0, 0.0),
        
        BossAi::new()
            .with_phases(3)
            .with_adaptation_rate(0.3)
            .with_pattern_memory(50),
        
        BossHealth {
            current: 10000.0,
            max: 10000.0,
            phase_thresholds: vec![0.7, 0.4, 0.15],
        },
        
        AvailableTools::new(vec![
            Tool::melee_combo(),
            Tool::ranged_attack(),
            Tool::area_attack(),
            Tool::summon_minions(),
            Tool::teleport(),
            Tool::enrage(),
        ]),
        
        PlayerAnalyzer::default(),
        StrategySelector::default(),
        
        RigidBody::Dynamic,
        Collider::capsule(1.0, 4.0),
        NavAgent::default(),
    ))
}
}

Phase Configuration

#![allow(unused)]
fn main() {
#[derive(Component)]
struct BossPhaseConfig {
    phases: Vec<BossPhase>,
    current_phase: usize,
}

struct BossPhase {
    name: String,
    health_threshold: f32,
    available_attacks: Vec<AttackPattern>,
    behavior_modifiers: BehaviorModifiers,
    transition_animation: Option<AnimationId>,
}

let phase_config = BossPhaseConfig {
    phases: vec![
        BossPhase {
            name: "Cautious".into(),
            health_threshold: 1.0,
            available_attacks: vec![
                AttackPattern::SingleSlash,
                AttackPattern::Thrust,
                AttackPattern::Sidestep,
            ],
            behavior_modifiers: BehaviorModifiers {
                aggression: 0.3,
                defense: 0.7,
                patience: 0.8,
            },
            transition_animation: None,
        },
        BossPhase {
            name: "Aggressive".into(),
            health_threshold: 0.6,
            available_attacks: vec![
                AttackPattern::TripleCombo,
                AttackPattern::SpinAttack,
                AttackPattern::LeapSlam,
                AttackPattern::Thrust,
            ],
            behavior_modifiers: BehaviorModifiers {
                aggression: 0.7,
                defense: 0.3,
                patience: 0.4,
            },
            transition_animation: Some(AnimationId::Enrage),
        },
        BossPhase {
            name: "Desperate".into(),
            health_threshold: 0.25,
            available_attacks: vec![
                AttackPattern::FuryCombo,
                AttackPattern::ShadowClones,
                AttackPattern::AreaDenial,
                AttackPattern::GrabAttack,
            ],
            behavior_modifiers: BehaviorModifiers {
                aggression: 1.0,
                defense: 0.1,
                patience: 0.1,
            },
            transition_animation: Some(AnimationId::Transform),
        },
    ],
    current_phase: 0,
};
}

Player Analysis

Bosses track player behavior to counter their strategies:

#![allow(unused)]
fn main() {
#[derive(Component, Default)]
struct PlayerAnalyzer {
    dodge_pattern: DodgePattern,
    attack_timing: AttackTiming,
    positioning_preference: PositioningStyle,
    heal_threshold: f32,
    aggression_level: f32,
    pattern_history: VecDeque<PlayerAction>,
}

#[derive(Default)]
struct DodgePattern {
    left_count: u32,
    right_count: u32,
    back_count: u32,
    roll_timing: Vec<f32>,
}

fn analyze_player_system(
    mut analyzers: Query<&mut PlayerAnalyzer, With<BossAi>>,
    player_actions: EventReader<PlayerActionEvent>,
) {
    for action in player_actions.read() {
        for mut analyzer in analyzers.iter_mut() {
            analyzer.pattern_history.push_back(action.clone());
            
            if analyzer.pattern_history.len() > 100 {
                analyzer.pattern_history.pop_front();
            }
            
            match action {
                PlayerActionEvent::Dodge(direction) => {
                    match direction {
                        Direction::Left => analyzer.dodge_pattern.left_count += 1,
                        Direction::Right => analyzer.dodge_pattern.right_count += 1,
                        Direction::Back => analyzer.dodge_pattern.back_count += 1,
                        _ => {}
                    }
                }
                PlayerActionEvent::Attack(timing) => {
                    analyzer.attack_timing.record(*timing);
                }
                PlayerActionEvent::Heal => {
                    analyzer.heal_threshold = calculate_heal_threshold(&analyzer);
                }
                _ => {}
            }
        }
    }
}
}

Strategy Adaptation

#![allow(unused)]
fn main() {
#[derive(Component)]
struct StrategySelector {
    current_strategy: BossStrategy,
    strategy_effectiveness: HashMap<BossStrategy, f32>,
    adaptation_cooldown: Timer,
}

#[derive(Clone, Copy, Hash, Eq, PartialEq)]
enum BossStrategy {
    Aggressive,
    Defensive,
    Counter,
    Pressure,
    Bait,
    Mixed,
}

fn adapt_strategy_system(
    mut bosses: Query<(&PlayerAnalyzer, &mut StrategySelector, &BossHealth)>,
    time: Res<Time>,
) {
    for (analyzer, mut selector, health) in bosses.iter_mut() {
        selector.adaptation_cooldown.tick(time.delta());
        
        if selector.adaptation_cooldown.finished() {
            let new_strategy = select_counter_strategy(analyzer);
            
            if new_strategy != selector.current_strategy {
                let old_effectiveness = selector.strategy_effectiveness
                    .get(&selector.current_strategy)
                    .copied()
                    .unwrap_or(0.5);
                
                selector.strategy_effectiveness
                    .insert(selector.current_strategy, old_effectiveness * 0.9);
                
                selector.current_strategy = new_strategy;
                selector.adaptation_cooldown.reset();
            }
        }
    }
}

fn select_counter_strategy(analyzer: &PlayerAnalyzer) -> BossStrategy {
    if analyzer.aggression_level > 0.7 {
        BossStrategy::Counter
    } else if analyzer.dodge_pattern.is_predictable() {
        BossStrategy::Bait
    } else if analyzer.heal_threshold > 0.5 {
        BossStrategy::Pressure
    } else {
        BossStrategy::Mixed
    }
}
}

Attack Patterns

#![allow(unused)]
fn main() {
fn boss_attack_system(
    mut bosses: Query<(
        &BossAi,
        &StrategySelector,
        &PlayerAnalyzer,
        &Transform,
        &mut ActionQueue,
    )>,
    player: Query<&Transform, With<Player>>,
) {
    let player_pos = player.single().translation;
    
    for (boss, strategy, analyzer, transform, mut actions) in bosses.iter_mut() {
        let distance = transform.translation.distance(player_pos);
        
        let attack = match strategy.current_strategy {
            BossStrategy::Aggressive => {
                select_aggressive_attack(distance, boss.current_phase)
            }
            BossStrategy::Counter => {
                if analyzer.is_player_attacking() {
                    Some(AttackPattern::Parry)
                } else {
                    Some(AttackPattern::Wait)
                }
            }
            BossStrategy::Bait => {
                let predicted_dodge = analyzer.predict_dodge_direction();
                Some(AttackPattern::DelayedStrike(predicted_dodge.opposite()))
            }
            BossStrategy::Pressure => {
                Some(AttackPattern::RelentlessCombo)
            }
            _ => {
                select_random_attack(boss.current_phase)
            }
        };
        
        if let Some(pattern) = attack {
            actions.push(Action::ExecutePattern(pattern));
        }
    }
}
}

Learning Between Attempts

Bosses can remember strategies across player deaths:

#![allow(unused)]
fn main() {
#[derive(Resource)]
struct BossMemory {
    player_deaths: u32,
    successful_attacks: HashMap<AttackPattern, u32>,
    failed_attacks: HashMap<AttackPattern, u32>,
    player_weaknesses: Vec<Weakness>,
}

impl BossMemory {
    fn record_attack_result(&mut self, pattern: AttackPattern, hit: bool) {
        if hit {
            *self.successful_attacks.entry(pattern).or_default() += 1;
        } else {
            *self.failed_attacks.entry(pattern).or_default() += 1;
        }
    }
    
    fn get_attack_priority(&self, pattern: AttackPattern) -> f32 {
        let successes = self.successful_attacks.get(&pattern).copied().unwrap_or(0) as f32;
        let failures = self.failed_attacks.get(&pattern).copied().unwrap_or(0) as f32;
        
        if successes + failures < 3.0 {
            return 0.5;
        }
        
        successes / (successes + failures)
    }
}
}

Example: The Hollow Knight

A complete adaptive boss implementation:

#![allow(unused)]
fn main() {
pub fn spawn_hollow_knight(world: &mut World) -> Entity {
    let boss = world.spawn((
        Name::new("The Hollow Knight"),
        Transform::from_xyz(0.0, 2.0, -20.0),
        
        BossAi::new()
            .with_phases(4)
            .with_adaptation_rate(0.25),
        
        BossHealth::new(15000.0)
            .with_phases(vec![0.75, 0.5, 0.25]),
        
        PlayerAnalyzer::default(),
        StrategySelector::new(BossStrategy::Defensive),
        
        AttackPatterns::new(vec![
            ("slash", AttackPattern::Slash { damage: 80.0, range: 3.0 }),
            ("thrust", AttackPattern::Thrust { damage: 100.0, range: 5.0 }),
            ("spin", AttackPattern::Spin { damage: 60.0, radius: 4.0 }),
            ("leap", AttackPattern::LeapSlam { damage: 120.0, radius: 6.0 }),
            ("shadow", AttackPattern::ShadowDash { damage: 50.0, distance: 10.0 }),
        ]),
        
        RigidBody::Dynamic,
        Collider::capsule(1.2, 3.5),
        NavAgent::default(),
    ));
    
    world.send_event(BossSpawnedEvent { boss });
    boss
}
}

Best Practices

- Start with simpler patterns, add complexity in later phases
- Ensure all attacks are telegraphed and fair
- Test adaptation rates - too fast feels unfair, too slow feels scripted
- Provide recovery windows between attack chains
- Adaptation should challenge, not frustrate
- Preserve attack patterns that players can learn
- Don't punish the same mistake indefinitely
- Allow multiple valid strategies

See Also

Crafting & Combat Systems

AstraWeave provides a unified crafting and combat framework that integrates with the AI systems for dynamic, adaptive gameplay. This guide covers implementing both systems with physics integration, AI-driven balancing, and procedural content generation.

Architecture Overview

graph TB
    subgraph Combat["Combat System"]
        CS[Combat State] --> DM[Damage Model]
        DM --> HR[Hit Resolution]
        HR --> EF[Effects Pipeline]
        EF --> FB[Feedback System]
    end
    
    subgraph Crafting["Crafting System"]
        IV[Inventory] --> RC[Recipe Check]
        RC --> CR[Craft Resolution]
        CR --> QR[Quality Roll]
        QR --> IT[Item Creation]
    end
    
    subgraph Integration["AI Integration"]
        AI[AI Director] --> CB[Combat Balancing]
        AI --> CG[Craft Guidance]
        CB --> CS
        CG --> RC
    end
    
    Physics[Physics Engine] --> HR
    Physics --> EF

Combat System

Combat Components

Define the core components for combat entities:

#![allow(unused)]
fn main() {
use astraweave_ecs::prelude::*;
use astraweave_physics::prelude::*;

#[derive(Component, Debug, Clone)]
pub struct CombatStats {
    pub health: f32,
    pub max_health: f32,
    pub stamina: f32,
    pub max_stamina: f32,
    pub poise: f32,
    pub max_poise: f32,
}

impl Default for CombatStats {
    fn default() -> Self {
        Self {
            health: 100.0,
            max_health: 100.0,
            stamina: 100.0,
            max_stamina: 100.0,
            poise: 50.0,
            max_poise: 50.0,
        }
    }
}

#[derive(Component, Debug, Clone)]
pub struct DamageResistances {
    pub physical: f32,
    pub fire: f32,
    pub ice: f32,
    pub lightning: f32,
    pub poison: f32,
}

#[derive(Component, Debug, Clone)]
pub struct WeaponStats {
    pub base_damage: f32,
    pub damage_type: DamageType,
    pub attack_speed: f32,
    pub reach: f32,
    pub stamina_cost: f32,
    pub poise_damage: f32,
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum DamageType {
    Physical,
    Fire,
    Ice,
    Lightning,
    Poison,
}

#[derive(Component, Debug)]
pub struct CombatState {
    pub stance: CombatStance,
    pub combo_count: u32,
    pub combo_timer: f32,
    pub stagger_timer: f32,
    pub invincibility_frames: f32,
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum CombatStance {
    Neutral,
    Attacking,
    Blocking,
    Dodging,
    Staggered,
    Recovering,
}
}

Attack System

Implement the attack resolution with physics-based hit detection:

#![allow(unused)]
fn main() {
use astraweave_physics::collision::*;

#[derive(Debug, Clone)]
pub struct AttackEvent {
    pub attacker: Entity,
    pub weapon: Entity,
    pub attack_type: AttackType,
    pub direction: Vec3,
}

#[derive(Debug, Clone, Copy)]
pub enum AttackType {
    Light,
    Heavy,
    Special,
    Charged { charge_time: f32 },
}

pub fn attack_system(
    mut commands: Commands,
    mut attack_events: EventReader<AttackEvent>,
    mut combat_query: Query<(&mut CombatState, &mut CombatStats, &Transform)>,
    weapon_query: Query<&WeaponStats>,
    physics: Res<PhysicsWorld>,
) {
    for event in attack_events.iter() {
        let Ok((mut state, mut stats, transform)) = combat_query.get_mut(event.attacker) else {
            continue;
        };
        
        if stats.stamina < get_stamina_cost(&event.attack_type) {
            continue;
        }
        
        let Ok(weapon) = weapon_query.get(event.weapon) else {
            continue;
        };
        
        stats.stamina -= weapon.stamina_cost * get_attack_multiplier(&event.attack_type);
        state.stance = CombatStance::Attacking;
        
        let hitbox = create_attack_hitbox(transform, weapon, &event.attack_type);
        
        commands.spawn((
            hitbox,
            AttackHitbox {
                owner: event.attacker,
                damage: calculate_attack_damage(weapon, &event.attack_type, state.combo_count),
                damage_type: weapon.damage_type,
                poise_damage: weapon.poise_damage,
                knockback: event.direction * 5.0,
            },
            Lifetime { remaining: 0.2 },
        ));
        
        state.combo_count = (state.combo_count + 1).min(5);
        state.combo_timer = 1.5;
    }
}

fn calculate_attack_damage(weapon: &WeaponStats, attack_type: &AttackType, combo: u32) -> f32 {
    let base = weapon.base_damage;
    let type_mult = match attack_type {
        AttackType::Light => 1.0,
        AttackType::Heavy => 1.8,
        AttackType::Special => 2.5,
        AttackType::Charged { charge_time } => 1.0 + (charge_time * 0.5).min(2.0),
    };
    let combo_mult = 1.0 + (combo as f32 * 0.1);
    
    base * type_mult * combo_mult
}

fn create_attack_hitbox(
    transform: &Transform,
    weapon: &WeaponStats,
    attack_type: &AttackType,
) -> Collider {
    let size = match attack_type {
        AttackType::Light => Vec3::new(weapon.reach * 0.8, 1.0, weapon.reach * 0.8),
        AttackType::Heavy => Vec3::new(weapon.reach * 1.2, 1.5, weapon.reach * 1.2),
        AttackType::Special => Vec3::new(weapon.reach * 2.0, 2.0, weapon.reach * 2.0),
        AttackType::Charged { .. } => Vec3::new(weapon.reach * 1.5, 1.2, weapon.reach * 1.5),
    };
    
    Collider::cuboid(size.x, size.y, size.z)
        .with_offset(transform.forward() * (weapon.reach * 0.5))
}
}

Damage Resolution

Process hits and apply damage with resistance calculations:

#![allow(unused)]
fn main() {
#[derive(Component)]
pub struct AttackHitbox {
    pub owner: Entity,
    pub damage: f32,
    pub damage_type: DamageType,
    pub poise_damage: f32,
    pub knockback: Vec3,
}

pub fn damage_resolution_system(
    mut commands: Commands,
    hitbox_query: Query<(Entity, &AttackHitbox, &CollidingEntities)>,
    mut target_query: Query<(
        &mut CombatStats,
        &mut CombatState,
        &DamageResistances,
        &mut Transform,
    )>,
    mut damage_events: EventWriter<DamageEvent>,
) {
    for (hitbox_entity, hitbox, colliding) in hitbox_query.iter() {
        for &target in colliding.iter() {
            if target == hitbox.owner {
                continue;
            }
            
            let Ok((mut stats, mut state, resistances, mut transform)) = 
                target_query.get_mut(target) else {
                continue;
            };
            
            if state.invincibility_frames > 0.0 {
                continue;
            }
            
            let resistance = get_resistance(resistances, hitbox.damage_type);
            let final_damage = hitbox.damage * (1.0 - resistance);
            
            stats.health = (stats.health - final_damage).max(0.0);
            stats.poise -= hitbox.poise_damage;
            
            if stats.poise <= 0.0 {
                state.stance = CombatStance::Staggered;
                state.stagger_timer = 1.5;
                stats.poise = stats.max_poise * 0.5;
            }
            
            transform.translation += hitbox.knockback * 0.1;
            
            damage_events.send(DamageEvent {
                target,
                attacker: hitbox.owner,
                damage: final_damage,
                damage_type: hitbox.damage_type,
                was_critical: false,
            });
            
            state.invincibility_frames = 0.15;
        }
        
        commands.entity(hitbox_entity).despawn();
    }
}

fn get_resistance(resistances: &DamageResistances, damage_type: DamageType) -> f32 {
    match damage_type {
        DamageType::Physical => resistances.physical,
        DamageType::Fire => resistances.fire,
        DamageType::Ice => resistances.ice,
        DamageType::Lightning => resistances.lightning,
        DamageType::Poison => resistances.poison,
    }.clamp(0.0, 0.9)
}
}

Combat AI Integration

Leverage the AI system for dynamic combat behavior:

#![allow(unused)]
fn main() {
use astraweave_ai::prelude::*;

#[derive(Component)]
pub struct CombatAI {
    pub aggression: f32,
    pub patience: f32,
    pub preferred_range: f32,
    pub combo_tendency: f32,
}

pub fn ai_combat_decision_system(
    mut ai_query: Query<(
        Entity,
        &CombatAI,
        &CombatStats,
        &CombatState,
        &Transform,
        &mut AiPlanner,
    )>,
    target_query: Query<(&CombatStats, &CombatState, &Transform), Without<CombatAI>>,
    mut attack_events: EventWriter<AttackEvent>,
) {
    for (entity, ai, stats, state, transform, mut planner) in ai_query.iter_mut() {
        if state.stance != CombatStance::Neutral {
            continue;
        }
        
        let Some(target) = find_nearest_target(transform, &target_query) else {
            continue;
        };
        
        let (target_stats, target_state, target_transform) = target_query.get(target).unwrap();
        let distance = transform.translation.distance(target_transform.translation);
        let direction = (target_transform.translation - transform.translation).normalize();
        
        let decision = evaluate_combat_options(
            ai,
            stats,
            target_stats,
            target_state,
            distance,
        );
        
        match decision {
            CombatDecision::Attack(attack_type) => {
                attack_events.send(AttackEvent {
                    attacker: entity,
                    weapon: entity,
                    attack_type,
                    direction,
                });
            }
            CombatDecision::Reposition(target_distance) => {
                let goal_pos = target_transform.translation - direction * target_distance;
                planner.set_goal(AiGoal::MoveTo { position: goal_pos });
            }
            CombatDecision::Defend => {
                planner.set_goal(AiGoal::Custom("block".into()));
            }
            CombatDecision::Wait => {}
        }
    }
}

#[derive(Debug)]
enum CombatDecision {
    Attack(AttackType),
    Reposition(f32),
    Defend,
    Wait,
}

fn evaluate_combat_options(
    ai: &CombatAI,
    stats: &CombatStats,
    target_stats: &CombatStats,
    target_state: &CombatState,
    distance: f32,
) -> CombatDecision {
    let health_ratio = stats.health / stats.max_health;
    let stamina_ratio = stats.stamina / stats.max_stamina;
    let target_health_ratio = target_stats.health / target_stats.max_health;
    
    if target_state.stance == CombatStance::Staggered && distance < 3.0 {
        return CombatDecision::Attack(AttackType::Heavy);
    }
    
    if health_ratio < 0.3 && ai.patience > 0.5 {
        return CombatDecision::Reposition(ai.preferred_range * 1.5);
    }
    
    if distance > ai.preferred_range * 1.2 {
        return CombatDecision::Reposition(ai.preferred_range);
    }
    
    if target_state.stance == CombatStance::Attacking && ai.patience > 0.7 {
        return CombatDecision::Defend;
    }
    
    if stamina_ratio > 0.3 && distance < ai.preferred_range {
        let attack_type = if ai.aggression > 0.7 && stamina_ratio > 0.6 {
            AttackType::Heavy
        } else {
            AttackType::Light
        };
        return CombatDecision::Attack(attack_type);
    }
    
    CombatDecision::Wait
}
}

Crafting System

Inventory and Items

Define the item and inventory structures:

#![allow(unused)]
fn main() {
#[derive(Component, Debug, Clone)]
pub struct Item {
    pub id: ItemId,
    pub name: String,
    pub item_type: ItemType,
    pub rarity: ItemRarity,
    pub stack_size: u32,
    pub max_stack: u32,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ItemId(pub u64);

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ItemType {
    Material,
    Consumable,
    Weapon,
    Armor,
    Accessory,
    Tool,
    QuestItem,
}

#[derive(Debug, Clone, Copy, PartialEq, Ord, PartialOrd, Eq)]
pub enum ItemRarity {
    Common,
    Uncommon,
    Rare,
    Epic,
    Legendary,
}

#[derive(Component, Debug)]
pub struct Inventory {
    pub slots: Vec<Option<InventorySlot>>,
    pub max_slots: usize,
    pub weight_limit: f32,
    pub current_weight: f32,
}

#[derive(Debug, Clone)]
pub struct InventorySlot {
    pub item: Item,
    pub quantity: u32,
}

impl Inventory {
    pub fn new(max_slots: usize, weight_limit: f32) -> Self {
        Self {
            slots: vec![None; max_slots],
            max_slots,
            weight_limit,
            current_weight: 0.0,
        }
    }
    
    pub fn add_item(&mut self, item: Item, quantity: u32) -> Result<(), InventoryError> {
        for slot in self.slots.iter_mut() {
            if let Some(ref mut existing) = slot {
                if existing.item.id == item.id && existing.quantity < item.max_stack {
                    let space = item.max_stack - existing.quantity;
                    let add = quantity.min(space);
                    existing.quantity += add;
                    if add == quantity {
                        return Ok(());
                    }
                }
            }
        }
        
        for slot in self.slots.iter_mut() {
            if slot.is_none() {
                *slot = Some(InventorySlot {
                    item: item.clone(),
                    quantity: quantity.min(item.max_stack),
                });
                return Ok(());
            }
        }
        
        Err(InventoryError::Full)
    }
    
    pub fn remove_item(&mut self, item_id: ItemId, quantity: u32) -> Result<u32, InventoryError> {
        let mut remaining = quantity;
        
        for slot in self.slots.iter_mut() {
            if remaining == 0 {
                break;
            }
            
            if let Some(ref mut existing) = slot {
                if existing.item.id == item_id {
                    let remove = remaining.min(existing.quantity);
                    existing.quantity -= remove;
                    remaining -= remove;
                    
                    if existing.quantity == 0 {
                        *slot = None;
                    }
                }
            }
        }
        
        if remaining > 0 {
            Err(InventoryError::InsufficientItems)
        } else {
            Ok(quantity)
        }
    }
    
    pub fn count_item(&self, item_id: ItemId) -> u32 {
        self.slots
            .iter()
            .filter_map(|s| s.as_ref())
            .filter(|s| s.item.id == item_id)
            .map(|s| s.quantity)
            .sum()
    }
}

#[derive(Debug)]
pub enum InventoryError {
    Full,
    InsufficientItems,
    WeightExceeded,
}
}

Recipe System

Define recipes and crafting requirements:

#![allow(unused)]
fn main() {
#[derive(Debug, Clone)]
pub struct Recipe {
    pub id: RecipeId,
    pub name: String,
    pub ingredients: Vec<RecipeIngredient>,
    pub outputs: Vec<RecipeOutput>,
    pub crafting_time: f32,
    pub required_station: Option<CraftingStation>,
    pub required_skill: Option<(SkillType, u32)>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct RecipeId(pub u64);

#[derive(Debug, Clone)]
pub struct RecipeIngredient {
    pub item_id: ItemId,
    pub quantity: u32,
    pub consumed: bool,
}

#[derive(Debug, Clone)]
pub struct RecipeOutput {
    pub item_id: ItemId,
    pub base_quantity: u32,
    pub quality_scaling: bool,
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum CraftingStation {
    Workbench,
    Forge,
    Alchemy,
    Enchanting,
    Cooking,
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum SkillType {
    Smithing,
    Alchemy,
    Enchanting,
    Cooking,
    Woodworking,
}

#[derive(Resource)]
pub struct RecipeRegistry {
    recipes: HashMap<RecipeId, Recipe>,
    by_station: HashMap<Option<CraftingStation>, Vec<RecipeId>>,
    by_output: HashMap<ItemId, Vec<RecipeId>>,
}

impl RecipeRegistry {
    pub fn new() -> Self {
        Self {
            recipes: HashMap::new(),
            by_station: HashMap::new(),
            by_output: HashMap::new(),
        }
    }
    
    pub fn register(&mut self, recipe: Recipe) {
        let id = recipe.id;
        
        self.by_station
            .entry(recipe.required_station)
            .or_default()
            .push(id);
        
        for output in &recipe.outputs {
            self.by_output
                .entry(output.item_id)
                .or_default()
                .push(id);
        }
        
        self.recipes.insert(id, recipe);
    }
    
    pub fn get(&self, id: RecipeId) -> Option<&Recipe> {
        self.recipes.get(&id)
    }
    
    pub fn available_at_station(&self, station: Option<CraftingStation>) -> &[RecipeId] {
        self.by_station.get(&station).map(|v| v.as_slice()).unwrap_or(&[])
    }
    
    pub fn recipes_for_item(&self, item_id: ItemId) -> &[RecipeId] {
        self.by_output.get(&item_id).map(|v| v.as_slice()).unwrap_or(&[])
    }
}
}

Crafting Execution

Process crafting with quality calculations:

#![allow(unused)]
fn main() {
#[derive(Debug)]
pub struct CraftRequest {
    pub crafter: Entity,
    pub recipe_id: RecipeId,
    pub station: Option<Entity>,
}

#[derive(Debug)]
pub struct CraftResult {
    pub success: bool,
    pub items: Vec<(ItemId, u32, ItemRarity)>,
    pub experience: u32,
}

pub fn crafting_system(
    mut craft_requests: EventReader<CraftRequest>,
    mut craft_results: EventWriter<CraftResult>,
    registry: Res<RecipeRegistry>,
    mut crafter_query: Query<(&mut Inventory, Option<&CrafterSkills>)>,
    station_query: Query<&CraftingStationComponent>,
    item_registry: Res<ItemRegistry>,
) {
    for request in craft_requests.iter() {
        let Some(recipe) = registry.get(request.recipe_id) else {
            continue;
        };
        
        let Ok((mut inventory, skills)) = crafter_query.get_mut(request.crafter) else {
            continue;
        };
        
        if let Some(station_entity) = request.station {
            if let Ok(station) = station_query.get(station_entity) {
                if Some(station.station_type) != recipe.required_station {
                    continue;
                }
            }
        } else if recipe.required_station.is_some() {
            continue;
        }
        
        if !can_craft(&inventory, recipe) {
            continue;
        }
        
        if let Some((skill_type, level)) = recipe.required_skill {
            let crafter_level = skills
                .map(|s| s.get_level(skill_type))
                .unwrap_or(0);
            if crafter_level < level {
                continue;
            }
        }
        
        for ingredient in &recipe.ingredients {
            if ingredient.consumed {
                let _ = inventory.remove_item(ingredient.item_id, ingredient.quantity);
            }
        }
        
        let quality = calculate_craft_quality(skills, recipe);
        let rarity = quality_to_rarity(quality);
        
        let mut crafted_items = Vec::new();
        for output in &recipe.outputs {
            let quantity = if output.quality_scaling {
                (output.base_quantity as f32 * (1.0 + quality * 0.5)) as u32
            } else {
                output.base_quantity
            };
            
            if let Some(base_item) = item_registry.get(output.item_id) {
                let mut item = base_item.clone();
                item.rarity = rarity;
                let _ = inventory.add_item(item, quantity);
                crafted_items.push((output.item_id, quantity, rarity));
            }
        }
        
        craft_results.send(CraftResult {
            success: true,
            items: crafted_items,
            experience: calculate_craft_xp(recipe, quality),
        });
    }
}

fn can_craft(inventory: &Inventory, recipe: &Recipe) -> bool {
    recipe.ingredients.iter().all(|ing| {
        inventory.count_item(ing.item_id) >= ing.quantity
    })
}

fn calculate_craft_quality(skills: Option<&CrafterSkills>, recipe: &Recipe) -> f32 {
    let base_quality = 0.5;
    
    let skill_bonus = if let (Some(skills), Some((skill_type, required))) = (skills, recipe.required_skill) {
        let level = skills.get_level(skill_type) as f32;
        let excess = (level - required as f32).max(0.0);
        excess * 0.05
    } else {
        0.0
    };
    
    let random_factor = rand::random::<f32>() * 0.2;
    
    (base_quality + skill_bonus + random_factor).clamp(0.0, 1.0)
}

fn quality_to_rarity(quality: f32) -> ItemRarity {
    match quality {
        q if q >= 0.95 => ItemRarity::Legendary,
        q if q >= 0.8 => ItemRarity::Epic,
        q if q >= 0.6 => ItemRarity::Rare,
        q if q >= 0.4 => ItemRarity::Uncommon,
        _ => ItemRarity::Common,
    }
}
}

AI-Assisted Crafting

Use AI to suggest recipes and optimize crafting:

#![allow(unused)]
fn main() {
use astraweave_llm::prelude::*;

pub struct CraftingAdvisor {
    llm: LlmClient,
}

impl CraftingAdvisor {
    pub async fn suggest_recipes(
        &self,
        inventory: &Inventory,
        goal: &str,
        registry: &RecipeRegistry,
    ) -> Vec<RecipeSuggestion> {
        let available_items: Vec<_> = inventory.slots
            .iter()
            .filter_map(|s| s.as_ref())
            .map(|s| format!("{} x{}", s.item.name, s.quantity))
            .collect();
        
        let prompt = format!(
            r#"Given these materials: {}
            
Player goal: {}

Suggest the best crafting recipes to achieve this goal.
Return as JSON array of recipe suggestions."#,
            available_items.join(", "),
            goal
        );
        
        let response = self.llm.complete(&prompt).await;
        parse_suggestions(&response, registry)
    }
    
    pub async fn optimize_craft_order(
        &self,
        target_item: ItemId,
        inventory: &Inventory,
        registry: &RecipeRegistry,
    ) -> Vec<RecipeId> {
        let mut order = Vec::new();
        let mut simulated_inventory = inventory.clone();
        
        self.build_craft_tree(target_item, &mut simulated_inventory, registry, &mut order);
        
        order
    }
    
    fn build_craft_tree(
        &self,
        target: ItemId,
        inventory: &mut Inventory,
        registry: &RecipeRegistry,
        order: &mut Vec<RecipeId>,
    ) {
        let recipes = registry.recipes_for_item(target);
        
        for &recipe_id in recipes {
            if let Some(recipe) = registry.get(recipe_id) {
                let mut can_craft = true;
                
                for ingredient in &recipe.ingredients {
                    let have = inventory.count_item(ingredient.item_id);
                    if have < ingredient.quantity {
                        self.build_craft_tree(
                            ingredient.item_id,
                            inventory,
                            registry,
                            order,
                        );
                        
                        if inventory.count_item(ingredient.item_id) < ingredient.quantity {
                            can_craft = false;
                            break;
                        }
                    }
                }
                
                if can_craft {
                    order.push(recipe_id);
                    return;
                }
            }
        }
    }
}

#[derive(Debug)]
pub struct RecipeSuggestion {
    pub recipe_id: RecipeId,
    pub reason: String,
    pub priority: u32,
}
}

Combat-Crafting Integration

Weapon Modification

Allow runtime weapon customization:

#![allow(unused)]
fn main() {
#[derive(Component, Debug)]
pub struct ModifiableWeapon {
    pub base: WeaponStats,
    pub modifications: Vec<WeaponMod>,
    pub max_mods: usize,
}

#[derive(Debug, Clone)]
pub struct WeaponMod {
    pub mod_type: WeaponModType,
    pub value: f32,
    pub item_source: ItemId,
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum WeaponModType {
    DamageBonus,
    ElementalDamage(DamageType),
    AttackSpeed,
    CriticalChance,
    CriticalDamage,
    LifeSteal,
    PoiseDamage,
}

impl ModifiableWeapon {
    pub fn calculate_stats(&self) -> WeaponStats {
        let mut stats = self.base.clone();
        
        for modification in &self.modifications {
            match modification.mod_type {
                WeaponModType::DamageBonus => {
                    stats.base_damage *= 1.0 + modification.value;
                }
                WeaponModType::AttackSpeed => {
                    stats.attack_speed *= 1.0 + modification.value;
                }
                WeaponModType::PoiseDamage => {
                    stats.poise_damage *= 1.0 + modification.value;
                }
                _ => {}
            }
        }
        
        stats
    }
    
    pub fn add_mod(&mut self, modification: WeaponMod) -> Result<(), &'static str> {
        if self.modifications.len() >= self.max_mods {
            return Err("Maximum modifications reached");
        }
        
        let same_type_count = self.modifications
            .iter()
            .filter(|m| std::mem::discriminant(&m.mod_type) == std::mem::discriminant(&modification.mod_type))
            .count();
        
        if same_type_count >= 2 {
            return Err("Cannot stack more than 2 of the same mod type");
        }
        
        self.modifications.push(modification);
        Ok(())
    }
}

pub fn apply_weapon_mods_system(
    mut weapons: Query<(&ModifiableWeapon, &mut WeaponStats), Changed<ModifiableWeapon>>,
) {
    for (modifiable, mut stats) in weapons.iter_mut() {
        *stats = modifiable.calculate_stats();
    }
}
}

Combat Loot System

Generate loot based on combat performance:

#![allow(unused)]
fn main() {
#[derive(Component)]
pub struct LootTable {
    pub entries: Vec<LootEntry>,
    pub guaranteed: Vec<ItemId>,
}

#[derive(Debug, Clone)]
pub struct LootEntry {
    pub item_id: ItemId,
    pub weight: f32,
    pub min_quantity: u32,
    pub max_quantity: u32,
    pub min_rarity: ItemRarity,
}

pub fn combat_loot_system(
    mut death_events: EventReader<EntityDeathEvent>,
    loot_query: Query<&LootTable>,
    combat_log: Res<CombatLog>,
    mut loot_events: EventWriter<LootDropEvent>,
) {
    for event in death_events.iter() {
        let Ok(loot_table) = loot_query.get(event.entity) else {
            continue;
        };
        
        let performance = combat_log.get_performance(event.killer, event.entity);
        let luck_bonus = calculate_luck_bonus(&performance);
        
        let mut drops = Vec::new();
        
        for item_id in &loot_table.guaranteed {
            drops.push((*item_id, 1, ItemRarity::Common));
        }
        
        let roll_count = 1 + (luck_bonus * 2.0) as usize;
        for _ in 0..roll_count {
            if let Some(entry) = roll_loot(loot_table, luck_bonus) {
                let quantity = rand::thread_rng()
                    .gen_range(entry.min_quantity..=entry.max_quantity);
                let rarity = roll_rarity(entry.min_rarity, luck_bonus);
                drops.push((entry.item_id, quantity, rarity));
            }
        }
        
        loot_events.send(LootDropEvent {
            position: event.position,
            drops,
        });
    }
}

fn calculate_luck_bonus(performance: &CombatPerformance) -> f32 {
    let mut bonus = 0.0;
    
    if performance.no_damage_taken {
        bonus += 0.3;
    }
    
    if performance.time_to_kill < 30.0 {
        bonus += 0.2;
    }
    
    if performance.parries > 0 {
        bonus += performance.parries as f32 * 0.05;
    }
    
    bonus.min(1.0)
}
}

Best Practices

1. **Balance Stamina Economy**: Ensure attacks have meaningful stamina costs to create decision points
2. **Poise Windows**: Use stagger states to create openings for both players and AI
3. **Readable Attacks**: Give clear visual tells before powerful attacks
4. **Combo Depth**: Reward skill with combo multipliers but keep basic attacks viable
- **Damage Sponges**: Avoid enemies that just have more health; vary resistances and mechanics
- **Inventory Overflow**: Always handle full inventory gracefully
- **Recipe Bloat**: Curate recipes to avoid overwhelming players
- **Mod Stacking**: Prevent multiplicative mod abuse

Dialogue Systems

AstraWeave’s dialogue system combines traditional branching dialogue with LLM-powered dynamic conversations, enabling NPCs to engage in contextual, personality-driven interactions while maintaining narrative coherence.

Architecture Overview

graph TB
    subgraph Input["Player Input"]
        PI[Player Choice] --> DM
        TI[Text Input] --> NLP[NLP Parser]
        NLP --> DM
    end
    
    subgraph DialogueManager["Dialogue Manager"]
        DM[Dialogue Controller] --> SC[Script Check]
        SC -->|Scripted| SN[Script Node]
        SC -->|Dynamic| LLM[LLM Handler]
        
        SN --> VAL[Validator]
        LLM --> VAL
        VAL --> OUT[Output]
    end
    
    subgraph Context["Context System"]
        MEM[Memory] --> DM
        REL[Relationships] --> DM
        QS[Quest State] --> DM
        WS[World State] --> DM
    end
    
    OUT --> UI[Dialogue UI]
    OUT --> AUD[Voice/Audio]
    OUT --> EVT[Event Triggers]

Core Components

Dialogue Node System

Define structured dialogue with branching paths:

#![allow(unused)]
fn main() {
use astraweave_ecs::prelude::*;
use std::collections::HashMap;

#[derive(Debug, Clone)]
pub struct DialogueTree {
    pub id: DialogueId,
    pub nodes: HashMap<NodeId, DialogueNode>,
    pub entry_node: NodeId,
    pub metadata: DialogueMetadata,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct DialogueId(pub u64);

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct NodeId(pub u32);

#[derive(Debug, Clone)]
pub struct DialogueNode {
    pub id: NodeId,
    pub speaker: SpeakerId,
    pub content: DialogueContent,
    pub responses: Vec<DialogueResponse>,
    pub conditions: Vec<DialogueCondition>,
    pub effects: Vec<DialogueEffect>,
}

#[derive(Debug, Clone)]
pub enum DialogueContent {
    Text(String),
    Localized { key: String, fallback: String },
    Dynamic { prompt_template: String },
    Voice { text: String, audio_id: String },
}

#[derive(Debug, Clone)]
pub struct DialogueResponse {
    pub text: String,
    pub next_node: Option<NodeId>,
    pub conditions: Vec<DialogueCondition>,
    pub effects: Vec<DialogueEffect>,
    pub tone: ResponseTone,
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ResponseTone {
    Neutral,
    Friendly,
    Hostile,
    Curious,
    Sarcastic,
    Romantic,
}

#[derive(Debug, Clone)]
pub enum DialogueCondition {
    QuestState { quest_id: u64, state: String },
    RelationshipMin { npc_id: u64, value: i32 },
    HasItem { item_id: u64, quantity: u32 },
    PlayerStat { stat: String, min: i32 },
    Flag { name: String, value: bool },
    TimeOfDay { start: u32, end: u32 },
    Custom { script: String },
}

#[derive(Debug, Clone)]
pub enum DialogueEffect {
    SetFlag { name: String, value: bool },
    ModifyRelationship { npc_id: u64, delta: i32 },
    GiveItem { item_id: u64, quantity: u32 },
    TakeItem { item_id: u64, quantity: u32 },
    StartQuest { quest_id: u64 },
    AdvanceQuest { quest_id: u64, stage: String },
    TriggerEvent { event_name: String },
    PlayAnimation { animation: String },
}
}

Dialogue Controller

Manage dialogue state and flow:

#![allow(unused)]
fn main() {
#[derive(Component)]
pub struct DialogueController {
    pub active_dialogue: Option<ActiveDialogue>,
    pub history: Vec<DialogueHistoryEntry>,
    pub context: DialogueContext,
}

#[derive(Debug)]
pub struct ActiveDialogue {
    pub tree: DialogueTree,
    pub current_node: NodeId,
    pub participants: Vec<Entity>,
    pub started_at: f64,
}

#[derive(Debug, Clone)]
pub struct DialogueHistoryEntry {
    pub node_id: NodeId,
    pub speaker: SpeakerId,
    pub text: String,
    pub timestamp: f64,
    pub response_chosen: Option<usize>,
}

#[derive(Debug, Default)]
pub struct DialogueContext {
    pub player_name: String,
    pub location: String,
    pub time_of_day: String,
    pub recent_events: Vec<String>,
    pub custom_vars: HashMap<String, String>,
}

impl DialogueController {
    pub fn start_dialogue(&mut self, tree: DialogueTree, participants: Vec<Entity>, time: f64) {
        self.active_dialogue = Some(ActiveDialogue {
            current_node: tree.entry_node,
            tree,
            participants,
            started_at: time,
        });
    }
    
    pub fn get_current_node(&self) -> Option<&DialogueNode> {
        self.active_dialogue.as_ref().and_then(|active| {
            active.tree.nodes.get(&active.current_node)
        })
    }
    
    pub fn select_response(&mut self, index: usize, time: f64) -> Option<Vec<DialogueEffect>> {
        let active = self.active_dialogue.as_mut()?;
        let node = active.tree.nodes.get(&active.current_node)?;
        let response = node.responses.get(index)?;
        
        self.history.push(DialogueHistoryEntry {
            node_id: active.current_node,
            speaker: node.speaker,
            text: match &node.content {
                DialogueContent::Text(t) => t.clone(),
                DialogueContent::Localized { fallback, .. } => fallback.clone(),
                DialogueContent::Dynamic { .. } => "[Dynamic]".into(),
                DialogueContent::Voice { text, .. } => text.clone(),
            },
            timestamp: time,
            response_chosen: Some(index),
        });
        
        if let Some(next) = response.next_node {
            active.current_node = next;
            Some(response.effects.clone())
        } else {
            let effects = response.effects.clone();
            self.active_dialogue = None;
            Some(effects)
        }
    }
    
    pub fn end_dialogue(&mut self) {
        self.active_dialogue = None;
    }
}
}

Dialogue System

Process dialogue logic each frame:

#![allow(unused)]
fn main() {
pub fn dialogue_system(
    mut dialogue_query: Query<&mut DialogueController>,
    mut effect_events: EventWriter<DialogueEffectEvent>,
    conditions: Res<ConditionEvaluator>,
    time: Res<Time>,
) {
    for mut controller in dialogue_query.iter_mut() {
        let Some(active) = &controller.active_dialogue else {
            continue;
        };
        
        let Some(node) = active.tree.nodes.get(&active.current_node) else {
            controller.end_dialogue();
            continue;
        };
        
        if !conditions.evaluate_all(&node.conditions) {
            if let Some(fallback) = find_fallback_node(&active.tree, &conditions) {
                if let Some(active) = controller.active_dialogue.as_mut() {
                    active.current_node = fallback;
                }
            } else {
                controller.end_dialogue();
            }
            continue;
        }
        
        for effect in &node.effects {
            effect_events.send(DialogueEffectEvent {
                effect: effect.clone(),
                timestamp: time.elapsed_seconds_f64(),
            });
        }
    }
}

pub fn dialogue_effect_system(
    mut effect_events: EventReader<DialogueEffectEvent>,
    mut quest_events: EventWriter<QuestEvent>,
    mut inventory_events: EventWriter<InventoryEvent>,
    mut relationship_events: EventWriter<RelationshipEvent>,
    mut flags: ResMut<GameFlags>,
) {
    for event in effect_events.iter() {
        match &event.effect {
            DialogueEffect::SetFlag { name, value } => {
                flags.set(name.clone(), *value);
            }
            DialogueEffect::ModifyRelationship { npc_id, delta } => {
                relationship_events.send(RelationshipEvent::Modify {
                    npc_id: *npc_id,
                    delta: *delta,
                });
            }
            DialogueEffect::GiveItem { item_id, quantity } => {
                inventory_events.send(InventoryEvent::Add {
                    item_id: *item_id,
                    quantity: *quantity,
                });
            }
            DialogueEffect::StartQuest { quest_id } => {
                quest_events.send(QuestEvent::Start { quest_id: *quest_id });
            }
            DialogueEffect::AdvanceQuest { quest_id, stage } => {
                quest_events.send(QuestEvent::Advance {
                    quest_id: *quest_id,
                    stage: stage.clone(),
                });
            }
            _ => {}
        }
    }
}
}

LLM-Powered Dialogue

Dynamic Conversation Handler

Enable free-form AI conversations:

#![allow(unused)]
fn main() {
use astraweave_llm::prelude::*;

#[derive(Component)]
pub struct DynamicDialogue {
    pub npc_persona: NpcPersona,
    pub conversation_history: Vec<ConversationTurn>,
    pub memory_context: Vec<String>,
    pub allowed_topics: Vec<String>,
    pub forbidden_topics: Vec<String>,
    pub max_history: usize,
}

#[derive(Debug, Clone)]
pub struct NpcPersona {
    pub name: String,
    pub personality: String,
    pub background: String,
    pub speaking_style: String,
    pub knowledge: Vec<String>,
    pub goals: Vec<String>,
    pub relationships: HashMap<String, String>,
}

#[derive(Debug, Clone)]
pub struct ConversationTurn {
    pub role: ConversationRole,
    pub content: String,
    pub timestamp: f64,
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ConversationRole {
    Player,
    Npc,
    System,
}

impl DynamicDialogue {
    pub fn new(persona: NpcPersona) -> Self {
        Self {
            npc_persona: persona,
            conversation_history: Vec::new(),
            memory_context: Vec::new(),
            allowed_topics: Vec::new(),
            forbidden_topics: Vec::new(),
            max_history: 20,
        }
    }
    
    pub fn add_turn(&mut self, role: ConversationRole, content: String, time: f64) {
        self.conversation_history.push(ConversationTurn {
            role,
            content,
            timestamp: time,
        });
        
        if self.conversation_history.len() > self.max_history {
            self.conversation_history.remove(0);
        }
    }
    
    pub fn build_prompt(&self, player_input: &str, context: &DialogueContext) -> String {
        let mut prompt = format!(
            r#"You are {}, an NPC in a fantasy game.

Personality: {}
Background: {}
Speaking style: {}

Current situation:
- Location: {}
- Time: {}
- Recent events: {}

Knowledge you have:
{}

Your goals:
{}

"#,
            self.npc_persona.name,
            self.npc_persona.personality,
            self.npc_persona.background,
            self.npc_persona.speaking_style,
            context.location,
            context.time_of_day,
            context.recent_events.join(", "),
            self.npc_persona.knowledge.join("\n- "),
            self.npc_persona.goals.join("\n- "),
        );
        
        if !self.memory_context.is_empty() {
            prompt.push_str("Relevant memories:\n");
            for memory in &self.memory_context {
                prompt.push_str(&format!("- {}\n", memory));
            }
            prompt.push('\n');
        }
        
        prompt.push_str("Conversation so far:\n");
        for turn in &self.conversation_history {
            let speaker = match turn.role {
                ConversationRole::Player => &context.player_name,
                ConversationRole::Npc => &self.npc_persona.name,
                ConversationRole::System => "[System]",
            };
            prompt.push_str(&format!("{}: {}\n", speaker, turn.content));
        }
        
        prompt.push_str(&format!("\n{}: {}\n", context.player_name, player_input));
        prompt.push_str(&format!("\nRespond as {}. Stay in character. Keep response under 100 words.", 
            self.npc_persona.name));
        
        prompt
    }
}
}

LLM Dialogue System

Process dynamic dialogue requests:

#![allow(unused)]
fn main() {
pub struct LlmDialogueSystem {
    llm: LlmClient,
    validator: DialogueValidator,
}

impl LlmDialogueSystem {
    pub async fn generate_response(
        &self,
        dialogue: &mut DynamicDialogue,
        player_input: &str,
        context: &DialogueContext,
    ) -> Result<String, DialogueError> {
        dialogue.add_turn(
            ConversationRole::Player, 
            player_input.to_string(), 
            0.0
        );
        
        let prompt = dialogue.build_prompt(player_input, context);
        
        let response = self.llm.complete(&prompt).await
            .map_err(|e| DialogueError::LlmError(e.to_string()))?;
        
        let validated = self.validator.validate_response(
            &response,
            &dialogue.forbidden_topics,
            &dialogue.npc_persona,
        )?;
        
        dialogue.add_turn(ConversationRole::Npc, validated.clone(), 0.0);
        
        Ok(validated)
    }
}

pub struct DialogueValidator {
    blocked_patterns: Vec<regex::Regex>,
}

impl DialogueValidator {
    pub fn validate_response(
        &self,
        response: &str,
        forbidden_topics: &[String],
        persona: &NpcPersona,
    ) -> Result<String, DialogueError> {
        if response.len() > 500 {
            return Err(DialogueError::ResponseTooLong);
        }
        
        for pattern in &self.blocked_patterns {
            if pattern.is_match(response) {
                return Err(DialogueError::BlockedContent);
            }
        }
        
        for topic in forbidden_topics {
            if response.to_lowercase().contains(&topic.to_lowercase()) {
                return Err(DialogueError::ForbiddenTopic(topic.clone()));
            }
        }
        
        Ok(response.trim().to_string())
    }
}

#[derive(Debug)]
pub enum DialogueError {
    LlmError(String),
    ResponseTooLong,
    BlockedContent,
    ForbiddenTopic(String),
    ValidationFailed,
}
}

Tool-Based Dialogue Actions

Allow NPCs to take actions during conversation:

#![allow(unused)]
fn main() {
use astraweave_ai::tools::*;

pub fn create_dialogue_tools() -> Vec<AiTool> {
    vec![
        AiTool {
            name: "give_item".into(),
            description: "Give an item to the player as a gift or quest reward".into(),
            parameters: json!({
                "type": "object",
                "properties": {
                    "item_name": { "type": "string" },
                    "quantity": { "type": "integer", "minimum": 1 },
                    "reason": { "type": "string" }
                },
                "required": ["item_name", "quantity"]
            }),
            handler: Box::new(|params| {
                let item = params["item_name"].as_str().unwrap();
                let qty = params["quantity"].as_i64().unwrap() as u32;
                ToolResult::Success(json!({
                    "action": "give_item",
                    "item": item,
                    "quantity": qty
                }))
            }),
        },
        AiTool {
            name: "share_rumor".into(),
            description: "Share a rumor or piece of information with the player".into(),
            parameters: json!({
                "type": "object",
                "properties": {
                    "rumor_id": { "type": "string" },
                    "importance": { "type": "string", "enum": ["minor", "major", "critical"] }
                },
                "required": ["rumor_id"]
            }),
            handler: Box::new(|params| {
                let rumor = params["rumor_id"].as_str().unwrap();
                ToolResult::Success(json!({
                    "action": "share_rumor",
                    "rumor": rumor,
                    "unlocked": true
                }))
            }),
        },
        AiTool {
            name: "offer_quest".into(),
            description: "Offer a quest to the player".into(),
            parameters: json!({
                "type": "object",
                "properties": {
                    "quest_id": { "type": "string" },
                    "urgency": { "type": "string", "enum": ["low", "medium", "high"] }
                },
                "required": ["quest_id"]
            }),
            handler: Box::new(|params| {
                let quest = params["quest_id"].as_str().unwrap();
                ToolResult::Success(json!({
                    "action": "offer_quest",
                    "quest": quest
                }))
            }),
        },
        AiTool {
            name: "express_emotion".into(),
            description: "Show an emotional reaction through animation".into(),
            parameters: json!({
                "type": "object",
                "properties": {
                    "emotion": { 
                        "type": "string", 
                        "enum": ["happy", "sad", "angry", "surprised", "fearful", "disgusted"]
                    },
                    "intensity": { "type": "number", "minimum": 0, "maximum": 1 }
                },
                "required": ["emotion"]
            }),
            handler: Box::new(|params| {
                let emotion = params["emotion"].as_str().unwrap();
                let intensity = params.get("intensity")
                    .and_then(|v| v.as_f64())
                    .unwrap_or(0.5);
                ToolResult::Success(json!({
                    "action": "express_emotion",
                    "emotion": emotion,
                    "intensity": intensity
                }))
            }),
        },
    ]
}
}

Relationship System

Relationship Tracking

Track player-NPC relationships:

#![allow(unused)]
fn main() {
#[derive(Component)]
pub struct RelationshipTracker {
    pub relationships: HashMap<Entity, Relationship>,
}

#[derive(Debug, Clone)]
pub struct Relationship {
    pub affinity: i32,
    pub trust: i32,
    pub fear: i32,
    pub respect: i32,
    pub romantic_interest: i32,
    pub interaction_count: u32,
    pub last_interaction: f64,
    pub memories: Vec<RelationshipMemory>,
}

#[derive(Debug, Clone)]
pub struct RelationshipMemory {
    pub event: String,
    pub impact: i32,
    pub timestamp: f64,
}

impl Relationship {
    pub fn new() -> Self {
        Self {
            affinity: 0,
            trust: 0,
            fear: 0,
            respect: 0,
            romantic_interest: 0,
            interaction_count: 0,
            last_interaction: 0.0,
            memories: Vec::new(),
        }
    }
    
    pub fn get_disposition(&self) -> Disposition {
        let total = self.affinity + self.trust + self.respect - self.fear;
        match total {
            t if t >= 50 => Disposition::Friendly,
            t if t >= 20 => Disposition::Warm,
            t if t >= -20 => Disposition::Neutral,
            t if t >= -50 => Disposition::Cool,
            _ => Disposition::Hostile,
        }
    }
    
    pub fn modify(&mut self, event: &str, changes: RelationshipChanges, time: f64) {
        self.affinity = (self.affinity + changes.affinity).clamp(-100, 100);
        self.trust = (self.trust + changes.trust).clamp(-100, 100);
        self.fear = (self.fear + changes.fear).clamp(0, 100);
        self.respect = (self.respect + changes.respect).clamp(-100, 100);
        self.romantic_interest = (self.romantic_interest + changes.romantic).clamp(0, 100);
        
        self.interaction_count += 1;
        self.last_interaction = time;
        
        let impact = changes.affinity + changes.trust + changes.respect;
        if impact.abs() >= 5 {
            self.memories.push(RelationshipMemory {
                event: event.to_string(),
                impact,
                timestamp: time,
            });
            
            if self.memories.len() > 20 {
                self.memories.sort_by(|a, b| b.impact.abs().cmp(&a.impact.abs()));
                self.memories.truncate(10);
            }
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Disposition {
    Hostile,
    Cool,
    Neutral,
    Warm,
    Friendly,
}

#[derive(Debug, Clone, Default)]
pub struct RelationshipChanges {
    pub affinity: i32,
    pub trust: i32,
    pub fear: i32,
    pub respect: i32,
    pub romantic: i32,
}
}

Relationship-Aware Dialogue

Modify dialogue based on relationship:

#![allow(unused)]
fn main() {
pub fn relationship_dialogue_modifier(
    dialogue: &mut DynamicDialogue,
    relationship: &Relationship,
) {
    let disposition = relationship.get_disposition();
    
    let style_modifier = match disposition {
        Disposition::Hostile => "Be cold and dismissive. Keep responses short. Show distrust.",
        Disposition::Cool => "Be polite but distant. Don't volunteer extra information.",
        Disposition::Neutral => "Be professional and helpful when asked.",
        Disposition::Warm => "Be friendly and open. Share more freely.",
        Disposition::Friendly => "Be warm and trusting. Treat them as a close friend.",
    };
    
    dialogue.npc_persona.speaking_style.push_str(&format!("\n{}", style_modifier));
    
    for memory in &relationship.memories {
        dialogue.memory_context.push(format!(
            "{} (impact: {:+})",
            memory.event,
            memory.impact
        ));
    }
}
}

Voice Integration

Text-to-Speech Support

Generate voice output for dialogue:

#![allow(unused)]
fn main() {
use astraweave_audio::prelude::*;

#[derive(Component)]
pub struct VoiceDialogue {
    pub voice_id: String,
    pub pitch: f32,
    pub speed: f32,
    pub volume: f32,
}

pub struct VoiceSynthesizer {
    tts_endpoint: String,
}

impl VoiceSynthesizer {
    pub async fn synthesize(
        &self,
        text: &str,
        voice: &VoiceDialogue,
    ) -> Result<AudioBuffer, VoiceError> {
        let request = VoiceSynthRequest {
            text: text.to_string(),
            voice_id: voice.voice_id.clone(),
            pitch: voice.pitch,
            speed: voice.speed,
        };
        
        let response = reqwest::Client::new()
            .post(&self.tts_endpoint)
            .json(&request)
            .send()
            .await?
            .bytes()
            .await?;
        
        Ok(AudioBuffer::from_bytes(&response))
    }
}

pub fn dialogue_voice_system(
    dialogue_query: Query<(&DialogueController, &VoiceDialogue)>,
    synthesizer: Res<VoiceSynthesizer>,
    mut audio_events: EventWriter<PlayAudioEvent>,
    runtime: Res<TokioRuntime>,
) {
    for (controller, voice) in dialogue_query.iter() {
        if let Some(node) = controller.get_current_node() {
            if let DialogueContent::Voice { text, audio_id } = &node.content {
                if let Some(cached) = try_get_cached_audio(audio_id) {
                    audio_events.send(PlayAudioEvent {
                        buffer: cached,
                        volume: voice.volume,
                        spatial: false,
                    });
                } else {
                    let text = text.clone();
                    let voice = voice.clone();
                    runtime.spawn(async move {
                        if let Ok(buffer) = synthesizer.synthesize(&text, &voice).await {
                            cache_audio(audio_id, buffer);
                        }
                    });
                }
            }
        }
    }
}
}

Dialogue UI

Dialogue Display Component

#![allow(unused)]
fn main() {
use astraweave_ui::prelude::*;

#[derive(Component)]
pub struct DialogueUI {
    pub visible: bool,
    pub speaker_name: String,
    pub current_text: String,
    pub responses: Vec<String>,
    pub selected_response: usize,
    pub text_progress: f32,
    pub chars_per_second: f32,
}

impl DialogueUI {
    pub fn show(&mut self, speaker: &str, text: &str, responses: Vec<String>) {
        self.visible = true;
        self.speaker_name = speaker.to_string();
        self.current_text = text.to_string();
        self.responses = responses;
        self.selected_response = 0;
        self.text_progress = 0.0;
    }
    
    pub fn get_visible_text(&self) -> &str {
        let char_count = (self.current_text.len() as f32 * self.text_progress) as usize;
        &self.current_text[..char_count.min(self.current_text.len())]
    }
    
    pub fn is_text_complete(&self) -> bool {
        self.text_progress >= 1.0
    }
    
    pub fn skip_to_end(&mut self) {
        self.text_progress = 1.0;
    }
}

pub fn dialogue_ui_system(
    mut ui_query: Query<&mut DialogueUI>,
    dialogue_query: Query<&DialogueController>,
    input: Res<Input>,
    time: Res<Time>,
    npc_query: Query<&NpcInfo>,
) {
    for (mut ui, controller) in ui_query.iter_mut().zip(dialogue_query.iter()) {
        if let Some(node) = controller.get_current_node() {
            let speaker_name = npc_query
                .get(node.speaker.0)
                .map(|n| n.name.clone())
                .unwrap_or_else(|_| "Unknown".to_string());
            
            let text = match &node.content {
                DialogueContent::Text(t) => t.clone(),
                DialogueContent::Localized { fallback, .. } => fallback.clone(),
                _ => String::new(),
            };
            
            let responses: Vec<String> = node.responses
                .iter()
                .map(|r| r.text.clone())
                .collect();
            
            if !ui.visible || ui.current_text != text {
                ui.show(&speaker_name, &text, responses);
            }
            
            if !ui.is_text_complete() {
                ui.text_progress += time.delta_seconds() * ui.chars_per_second 
                    / ui.current_text.len() as f32;
                ui.text_progress = ui.text_progress.min(1.0);
            }
            
            if input.just_pressed(KeyCode::Space) || input.just_pressed(MouseButton::Left) {
                if !ui.is_text_complete() {
                    ui.skip_to_end();
                }
            }
            
            if ui.is_text_complete() {
                if input.just_pressed(KeyCode::Up) {
                    ui.selected_response = ui.selected_response.saturating_sub(1);
                }
                if input.just_pressed(KeyCode::Down) {
                    ui.selected_response = (ui.selected_response + 1).min(ui.responses.len() - 1);
                }
            }
        } else {
            ui.visible = false;
        }
    }
}
}

Best Practices

1. **Blend Scripted and Dynamic**: Use scripted dialogue for critical story beats, dynamic for flavor
2. **Maintain Consistency**: Ensure LLM responses align with established NPC personality
3. **Respect Player Time**: Keep responses concise; players can ask for details
4. **Branch Meaningfully**: Only offer choices that have real consequences
- **Context Window Overflow**: Trim conversation history to prevent LLM context issues
- **Hallucination**: Validate LLM outputs against game state to prevent impossible claims
- **Tone Whiplash**: Ensure relationship state affects dialogue tone gradually
- **Dead Ends**: Always provide a way to exit or continue dialogue

Procedural Content Generation

AstraWeave’s PCG system combines traditional algorithmic generation with AI-driven content creation, enabling infinite variety while maintaining coherent, designer-guided output.

Architecture Overview

graph TB
    subgraph Input["Generation Input"]
        SEED[Seed/Parameters] --> GEN
        RULES[Design Rules] --> GEN
        CONTEXT[World Context] --> GEN
    end
    
    subgraph Generator["PCG Pipeline"]
        GEN[Generator Core] --> ALGO[Algorithmic Pass]
        ALGO --> AI[AI Enhancement]
        AI --> VAL[Validation]
        VAL --> POST[Post-Processing]
    end
    
    subgraph Output["Generated Content"]
        POST --> TERRAIN[Terrain]
        POST --> DUNGEONS[Dungeons]
        POST --> ITEMS[Items]
        POST --> QUESTS[Quests]
        POST --> NPCS[NPCs]
    end
    
    VAL -->|Reject| GEN

Core PCG Framework

Generator Trait

Define the common interface for all generators:

#![allow(unused)]
fn main() {
use astraweave_ecs::prelude::*;
use std::hash::{Hash, Hasher};

pub trait Generator<T> {
    fn generate(&self, params: &GenerationParams) -> GenerationResult<T>;
    fn validate(&self, output: &T, params: &GenerationParams) -> ValidationResult;
    fn seed_from_hash<H: Hash>(&self, hashable: H) -> u64;
}

#[derive(Debug, Clone)]
pub struct GenerationParams {
    pub seed: u64,
    pub difficulty: f32,
    pub density: f32,
    pub theme: String,
    pub constraints: Vec<GenerationConstraint>,
    pub context: GenerationContext,
}

#[derive(Debug, Clone)]
pub struct GenerationContext {
    pub world_position: Vec3,
    pub biome: String,
    pub player_level: u32,
    pub story_stage: String,
    pub nearby_content: Vec<String>,
}

#[derive(Debug, Clone)]
pub enum GenerationConstraint {
    MinSize(f32),
    MaxSize(f32),
    MustContain(String),
    MustNotContain(String),
    ConnectTo(Vec3),
    StyleMatch(String),
    Custom { name: String, value: String },
}

pub type GenerationResult<T> = Result<T, GenerationError>;

#[derive(Debug)]
pub enum GenerationError {
    InvalidParams(String),
    ConstraintViolation(String),
    MaxIterationsReached,
    ValidationFailed(String),
}

#[derive(Debug)]
pub enum ValidationResult {
    Valid,
    Invalid(String),
    NeedsAdjustment(Vec<String>),
}
}

Seeded Random Generator

Deterministic random number generation:

#![allow(unused)]
fn main() {
use rand::{Rng, SeedableRng};
use rand_chacha::ChaCha8Rng;

pub struct SeededRng {
    rng: ChaCha8Rng,
    initial_seed: u64,
}

impl SeededRng {
    pub fn new(seed: u64) -> Self {
        Self {
            rng: ChaCha8Rng::seed_from_u64(seed),
            initial_seed: seed,
        }
    }
    
    pub fn from_position(x: i32, y: i32, z: i32, world_seed: u64) -> Self {
        let position_hash = ((x as u64) << 42) ^ ((y as u64) << 21) ^ (z as u64);
        Self::new(world_seed ^ position_hash)
    }
    
    pub fn next_float(&mut self) -> f32 {
        self.rng.gen()
    }
    
    pub fn next_range(&mut self, min: i32, max: i32) -> i32 {
        self.rng.gen_range(min..=max)
    }
    
    pub fn next_float_range(&mut self, min: f32, max: f32) -> f32 {
        self.rng.gen_range(min..=max)
    }
    
    pub fn choose<T: Clone>(&mut self, options: &[T]) -> Option<T> {
        if options.is_empty() {
            None
        } else {
            let index = self.rng.gen_range(0..options.len());
            Some(options[index].clone())
        }
    }
    
    pub fn weighted_choose<T: Clone>(&mut self, options: &[(T, f32)]) -> Option<T> {
        let total_weight: f32 = options.iter().map(|(_, w)| w).sum();
        let mut roll = self.next_float() * total_weight;
        
        for (item, weight) in options {
            roll -= weight;
            if roll <= 0.0 {
                return Some(item.clone());
            }
        }
        
        options.last().map(|(item, _)| item.clone())
    }
    
    pub fn fork(&mut self) -> Self {
        Self::new(self.rng.gen())
    }
}
}

Terrain Generation

Heightmap Generator

Generate terrain heightmaps with noise:

#![allow(unused)]
fn main() {
use noise::{NoiseFn, Perlin, Fbm, MultiFractal};

pub struct TerrainGenerator {
    base_noise: Fbm<Perlin>,
    detail_noise: Perlin,
    erosion_passes: u32,
}

impl TerrainGenerator {
    pub fn new(seed: u64) -> Self {
        let mut fbm = Fbm::new(seed as u32);
        fbm.octaves = 6;
        fbm.frequency = 0.005;
        fbm.lacunarity = 2.0;
        fbm.persistence = 0.5;
        
        Self {
            base_noise: fbm,
            detail_noise: Perlin::new(seed as u32 + 1),
            erosion_passes: 50,
        }
    }
    
    pub fn generate_chunk(&self, chunk_x: i32, chunk_z: i32, size: usize) -> Heightmap {
        let mut heights = vec![vec![0.0f32; size]; size];
        
        for z in 0..size {
            for x in 0..size {
                let world_x = (chunk_x * size as i32 + x as i32) as f64;
                let world_z = (chunk_z * size as i32 + z as i32) as f64;
                
                let base = self.base_noise.get([world_x, world_z]) as f32;
                
                let detail = self.detail_noise.get([world_x * 0.1, world_z * 0.1]) as f32 * 0.1;
                
                heights[z][x] = (base + detail + 1.0) * 0.5 * 256.0;
            }
        }
        
        self.apply_erosion(&mut heights);
        
        Heightmap {
            data: heights,
            chunk_x,
            chunk_z,
            size,
        }
    }
    
    fn apply_erosion(&self, heights: &mut Vec<Vec<f32>>) {
        let size = heights.len();
        let mut water = vec![vec![0.0f32; size]; size];
        let mut sediment = vec![vec![0.0f32; size]; size];
        
        for _ in 0..self.erosion_passes {
            for z in 1..size - 1 {
                for x in 1..size - 1 {
                    let current = heights[z][x];
                    
                    let neighbors = [
                        (heights[z - 1][x], 0, -1),
                        (heights[z + 1][x], 0, 1),
                        (heights[z][x - 1], -1, 0),
                        (heights[z][x + 1], 1, 0),
                    ];
                    
                    if let Some((lowest, dx, dz)) = neighbors
                        .iter()
                        .filter(|(h, _, _)| *h < current)
                        .min_by(|a, b| a.0.partial_cmp(&b.0).unwrap())
                    {
                        let diff = current - lowest;
                        let transfer = diff * 0.1;
                        
                        heights[z][x] -= transfer * 0.5;
                        heights[(z as i32 + dz) as usize][(x as i32 + dx) as usize] += transfer * 0.3;
                    }
                }
            }
        }
    }
}

#[derive(Debug)]
pub struct Heightmap {
    pub data: Vec<Vec<f32>>,
    pub chunk_x: i32,
    pub chunk_z: i32,
    pub size: usize,
}

impl Heightmap {
    pub fn get_height(&self, x: usize, z: usize) -> f32 {
        self.data.get(z).and_then(|row| row.get(x)).copied().unwrap_or(0.0)
    }
    
    pub fn get_normal(&self, x: usize, z: usize) -> Vec3 {
        let h_l = self.get_height(x.saturating_sub(1), z);
        let h_r = self.get_height((x + 1).min(self.size - 1), z);
        let h_d = self.get_height(x, z.saturating_sub(1));
        let h_u = self.get_height(x, (z + 1).min(self.size - 1));
        
        Vec3::new(h_l - h_r, 2.0, h_d - h_u).normalize()
    }
}
}

Biome Assignment

Assign biomes based on terrain properties:

#![allow(unused)]
fn main() {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Biome {
    Ocean,
    Beach,
    Plains,
    Forest,
    Desert,
    Tundra,
    Mountains,
    Swamp,
    Jungle,
    Volcanic,
}

pub struct BiomeGenerator {
    temperature_noise: Perlin,
    moisture_noise: Perlin,
}

impl BiomeGenerator {
    pub fn new(seed: u64) -> Self {
        Self {
            temperature_noise: Perlin::new(seed as u32 + 100),
            moisture_noise: Perlin::new(seed as u32 + 200),
        }
    }
    
    pub fn get_biome(&self, x: f64, z: f64, height: f32) -> Biome {
        if height < 10.0 {
            return Biome::Ocean;
        }
        if height < 15.0 {
            return Biome::Beach;
        }
        if height > 200.0 {
            return Biome::Mountains;
        }
        
        let temp = (self.temperature_noise.get([x * 0.001, z * 0.001]) + 1.0) * 0.5;
        let moisture = (self.moisture_noise.get([x * 0.001, z * 0.001]) + 1.0) * 0.5;
        
        let temp = temp as f32 - (height - 50.0) * 0.002;
        
        match (temp, moisture as f32) {
            (t, _) if t < 0.2 => Biome::Tundra,
            (t, m) if t > 0.8 && m < 0.3 => Biome::Desert,
            (t, m) if t > 0.7 && m > 0.7 => Biome::Jungle,
            (_, m) if m > 0.8 => Biome::Swamp,
            (_, m) if m > 0.5 => Biome::Forest,
            _ => Biome::Plains,
        }
    }
    
    pub fn get_biome_properties(&self, biome: Biome) -> BiomeProperties {
        match biome {
            Biome::Ocean => BiomeProperties {
                tree_density: 0.0,
                grass_density: 0.0,
                rock_density: 0.1,
                enemy_level_mod: 0.8,
                ambient_sound: "ocean".into(),
            },
            Biome::Forest => BiomeProperties {
                tree_density: 0.7,
                grass_density: 0.8,
                rock_density: 0.2,
                enemy_level_mod: 1.0,
                ambient_sound: "forest".into(),
            },
            Biome::Desert => BiomeProperties {
                tree_density: 0.02,
                grass_density: 0.05,
                rock_density: 0.4,
                enemy_level_mod: 1.2,
                ambient_sound: "desert".into(),
            },
            Biome::Mountains => BiomeProperties {
                tree_density: 0.1,
                grass_density: 0.2,
                rock_density: 0.8,
                enemy_level_mod: 1.5,
                ambient_sound: "wind".into(),
            },
            _ => BiomeProperties::default(),
        }
    }
}

#[derive(Debug, Clone)]
pub struct BiomeProperties {
    pub tree_density: f32,
    pub grass_density: f32,
    pub rock_density: f32,
    pub enemy_level_mod: f32,
    pub ambient_sound: String,
}
}

Dungeon Generation

Room-Based Dungeon Generator

Generate dungeons using room placement:

#![allow(unused)]
fn main() {
#[derive(Debug, Clone)]
pub struct DungeonLayout {
    pub rooms: Vec<Room>,
    pub corridors: Vec<Corridor>,
    pub entry_room: usize,
    pub boss_room: usize,
    pub width: i32,
    pub height: i32,
}

#[derive(Debug, Clone)]
pub struct Room {
    pub id: usize,
    pub bounds: Rect,
    pub room_type: RoomType,
    pub connections: Vec<usize>,
    pub content: RoomContent,
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum RoomType {
    Entry,
    Normal,
    Treasure,
    Boss,
    Secret,
    Shop,
    Shrine,
}

#[derive(Debug, Clone)]
pub struct RoomContent {
    pub enemies: Vec<EnemySpawn>,
    pub items: Vec<ItemSpawn>,
    pub props: Vec<PropSpawn>,
    pub triggers: Vec<TriggerSpawn>,
}

#[derive(Debug, Clone)]
pub struct Corridor {
    pub from_room: usize,
    pub to_room: usize,
    pub path: Vec<(i32, i32)>,
    pub width: i32,
}

pub struct DungeonGenerator {
    min_rooms: usize,
    max_rooms: usize,
    room_size_range: (i32, i32),
    room_templates: Vec<RoomTemplate>,
}

impl DungeonGenerator {
    pub fn generate(&self, params: &GenerationParams) -> GenerationResult<DungeonLayout> {
        let mut rng = SeededRng::new(params.seed);
        let room_count = rng.next_range(self.min_rooms as i32, self.max_rooms as i32) as usize;
        
        let mut rooms = Vec::new();
        let mut attempts = 0;
        let max_attempts = room_count * 100;
        
        while rooms.len() < room_count && attempts < max_attempts {
            let width = rng.next_range(self.room_size_range.0, self.room_size_range.1);
            let height = rng.next_range(self.room_size_range.0, self.room_size_range.1);
            let x = rng.next_range(0, 100 - width);
            let y = rng.next_range(0, 100 - height);
            
            let bounds = Rect { x, y, width, height };
            
            if !rooms.iter().any(|r: &Room| r.bounds.intersects_padded(&bounds, 2)) {
                let room_type = if rooms.is_empty() {
                    RoomType::Entry
                } else {
                    self.choose_room_type(&mut rng, rooms.len(), room_count)
                };
                
                rooms.push(Room {
                    id: rooms.len(),
                    bounds,
                    room_type,
                    connections: Vec::new(),
                    content: RoomContent::default(),
                });
            }
            
            attempts += 1;
        }
        
        if rooms.len() < self.min_rooms {
            return Err(GenerationError::MaxIterationsReached);
        }
        
        let corridors = self.connect_rooms(&mut rooms, &mut rng);
        
        let boss_room = rooms.iter()
            .enumerate()
            .filter(|(_, r)| r.room_type != RoomType::Entry)
            .max_by_key(|(_, r)| self.distance_from_entry(&rooms, r.id))
            .map(|(i, _)| i)
            .unwrap_or(rooms.len() - 1);
        
        rooms[boss_room].room_type = RoomType::Boss;
        
        for room in &mut rooms {
            room.content = self.generate_room_content(room, params, &mut rng);
        }
        
        Ok(DungeonLayout {
            rooms,
            corridors,
            entry_room: 0,
            boss_room,
            width: 100,
            height: 100,
        })
    }
    
    fn connect_rooms(&self, rooms: &mut [Room], rng: &mut SeededRng) -> Vec<Corridor> {
        let mut corridors = Vec::new();
        let mut connected = vec![false; rooms.len()];
        connected[0] = true;
        
        while connected.iter().any(|&c| !c) {
            let mut best_pair = None;
            let mut best_dist = f32::MAX;
            
            for (i, room_a) in rooms.iter().enumerate() {
                if !connected[i] {
                    continue;
                }
                
                for (j, room_b) in rooms.iter().enumerate() {
                    if connected[j] {
                        continue;
                    }
                    
                    let dist = room_a.bounds.center_distance(&room_b.bounds);
                    if dist < best_dist {
                        best_dist = dist;
                        best_pair = Some((i, j));
                    }
                }
            }
            
            if let Some((from, to)) = best_pair {
                let path = self.create_corridor_path(
                    rooms[from].bounds.center(),
                    rooms[to].bounds.center(),
                    rng,
                );
                
                rooms[from].connections.push(to);
                rooms[to].connections.push(from);
                connected[to] = true;
                
                corridors.push(Corridor {
                    from_room: from,
                    to_room: to,
                    path,
                    width: 2,
                });
            } else {
                break;
            }
        }
        
        corridors
    }
    
    fn create_corridor_path(
        &self,
        start: (i32, i32),
        end: (i32, i32),
        rng: &mut SeededRng,
    ) -> Vec<(i32, i32)> {
        let mut path = Vec::new();
        let (mut x, mut y) = start;
        
        if rng.next_float() > 0.5 {
            while x != end.0 {
                path.push((x, y));
                x += (end.0 - x).signum();
            }
            while y != end.1 {
                path.push((x, y));
                y += (end.1 - y).signum();
            }
        } else {
            while y != end.1 {
                path.push((x, y));
                y += (end.1 - y).signum();
            }
            while x != end.0 {
                path.push((x, y));
                x += (end.0 - x).signum();
            }
        }
        
        path.push(end);
        path
    }
    
    fn choose_room_type(&self, rng: &mut SeededRng, current: usize, total: usize) -> RoomType {
        let options = [
            (RoomType::Normal, 10.0),
            (RoomType::Treasure, 2.0),
            (RoomType::Secret, 1.0),
            (RoomType::Shop, 1.5),
            (RoomType::Shrine, 1.0),
        ];
        
        rng.weighted_choose(&options).unwrap_or(RoomType::Normal)
    }
    
    fn generate_room_content(
        &self,
        room: &Room,
        params: &GenerationParams,
        rng: &mut SeededRng,
    ) -> RoomContent {
        let mut content = RoomContent::default();
        
        match room.room_type {
            RoomType::Entry => {}
            RoomType::Normal => {
                let enemy_count = rng.next_range(1, 4);
                for _ in 0..enemy_count {
                    content.enemies.push(EnemySpawn {
                        position: room.bounds.random_point(rng),
                        enemy_type: "skeleton".into(),
                        level: params.context.player_level,
                    });
                }
            }
            RoomType::Treasure => {
                content.items.push(ItemSpawn {
                    position: room.bounds.center(),
                    loot_table: "treasure_chest".into(),
                });
            }
            RoomType::Boss => {
                content.enemies.push(EnemySpawn {
                    position: room.bounds.center(),
                    enemy_type: "boss".into(),
                    level: params.context.player_level + 5,
                });
            }
            _ => {}
        }
        
        content
    }
}
}

Item Generation

Procedural Item Generator

Generate items with random properties:

#![allow(unused)]
fn main() {
#[derive(Debug, Clone)]
pub struct GeneratedItem {
    pub base_type: ItemBaseType,
    pub name: String,
    pub rarity: ItemRarity,
    pub level: u32,
    pub stats: ItemStats,
    pub affixes: Vec<ItemAffix>,
    pub special_ability: Option<SpecialAbility>,
}

#[derive(Debug, Clone)]
pub struct ItemStats {
    pub damage: Option<(f32, f32)>,
    pub armor: Option<f32>,
    pub durability: f32,
    pub weight: f32,
}

#[derive(Debug, Clone)]
pub struct ItemAffix {
    pub affix_type: AffixType,
    pub tier: u32,
    pub value: f32,
}

#[derive(Debug, Clone, Copy)]
pub enum AffixType {
    Strength,
    Agility,
    Intelligence,
    Vitality,
    FireDamage,
    IceDamage,
    LightningDamage,
    LifeSteal,
    CriticalChance,
    CriticalDamage,
    AttackSpeed,
    MovementSpeed,
}

pub struct ItemGenerator {
    name_generator: NameGenerator,
    affix_pool: Vec<AffixTemplate>,
}

impl ItemGenerator {
    pub fn generate(&self, params: &ItemGenParams, rng: &mut SeededRng) -> GeneratedItem {
        let rarity = self.roll_rarity(params.base_rarity_chance, rng);
        let affix_count = self.affix_count_for_rarity(rarity);
        
        let base = &params.base_type;
        let mut stats = self.roll_base_stats(base, params.level, rng);
        
        let mut affixes = Vec::new();
        for _ in 0..affix_count {
            if let Some(affix) = self.roll_affix(params.level, &affixes, rng) {
                affixes.push(affix);
            }
        }
        
        for affix in &affixes {
            self.apply_affix_to_stats(&mut stats, affix);
        }
        
        let special = if rarity >= ItemRarity::Epic && rng.next_float() > 0.5 {
            Some(self.generate_special_ability(params.level, rng))
        } else {
            None
        };
        
        let name = self.generate_name(base, &affixes, rarity, rng);
        
        GeneratedItem {
            base_type: base.clone(),
            name,
            rarity,
            level: params.level,
            stats,
            affixes,
            special_ability: special,
        }
    }
    
    fn roll_rarity(&self, base_chance: f32, rng: &mut SeededRng) -> ItemRarity {
        let roll = rng.next_float() * base_chance;
        match roll {
            r if r > 0.99 => ItemRarity::Legendary,
            r if r > 0.95 => ItemRarity::Epic,
            r if r > 0.85 => ItemRarity::Rare,
            r if r > 0.65 => ItemRarity::Uncommon,
            _ => ItemRarity::Common,
        }
    }
    
    fn affix_count_for_rarity(&self, rarity: ItemRarity) -> usize {
        match rarity {
            ItemRarity::Common => 0,
            ItemRarity::Uncommon => 1,
            ItemRarity::Rare => 2,
            ItemRarity::Epic => 3,
            ItemRarity::Legendary => 4,
        }
    }
    
    fn roll_affix(
        &self,
        level: u32,
        existing: &[ItemAffix],
        rng: &mut SeededRng,
    ) -> Option<ItemAffix> {
        let available: Vec<_> = self.affix_pool
            .iter()
            .filter(|a| a.min_level <= level)
            .filter(|a| !existing.iter().any(|e| e.affix_type == a.affix_type))
            .collect();
        
        if available.is_empty() {
            return None;
        }
        
        let template = rng.choose(&available)?;
        let tier = rng.next_range(1, template.max_tier as i32) as u32;
        let value = template.base_value * (1.0 + tier as f32 * 0.25);
        
        Some(ItemAffix {
            affix_type: template.affix_type,
            tier,
            value,
        })
    }
    
    fn generate_name(
        &self,
        base: &ItemBaseType,
        affixes: &[ItemAffix],
        rarity: ItemRarity,
        rng: &mut SeededRng,
    ) -> String {
        if rarity >= ItemRarity::Legendary {
            return self.name_generator.generate_legendary_name(base, rng);
        }
        
        let prefix = affixes.first().map(|a| self.affix_to_prefix(a));
        let suffix = affixes.get(1).map(|a| self.affix_to_suffix(a));
        
        match (prefix, suffix) {
            (Some(p), Some(s)) => format!("{} {} {}", p, base.display_name(), s),
            (Some(p), None) => format!("{} {}", p, base.display_name()),
            (None, Some(s)) => format!("{} {}", base.display_name(), s),
            (None, None) => base.display_name().to_string(),
        }
    }
}
}

AI-Enhanced Generation

LLM-Powered Content Creation

Use AI for narrative content:

#![allow(unused)]
fn main() {
use astraweave_llm::prelude::*;

pub struct AiContentGenerator {
    llm: LlmClient,
    templates: ContentTemplates,
}

impl AiContentGenerator {
    pub async fn generate_quest(
        &self,
        context: &QuestGenContext,
    ) -> Result<GeneratedQuest, GenerationError> {
        let prompt = format!(
            r#"Generate a side quest for a fantasy RPG.

Setting: {}
Player level: {}
Available NPCs: {}
Recent events: {}
Theme: {}

Generate a quest with:
1. Title (catchy, thematic)
2. Description (2-3 sentences)
3. Objectives (3-5 steps)
4. Rewards (appropriate for level)
5. Optional twist or complication

Return as JSON."#,
            context.location,
            context.player_level,
            context.available_npcs.join(", "),
            context.recent_events.join(", "),
            context.theme
        );
        
        let response = self.llm.complete(&prompt).await
            .map_err(|e| GenerationError::InvalidParams(e.to_string()))?;
        
        self.parse_quest_response(&response)
    }
    
    pub async fn generate_npc_backstory(
        &self,
        npc: &NpcGenContext,
    ) -> Result<NpcBackstory, GenerationError> {
        let prompt = format!(
            r#"Create a backstory for an NPC in a fantasy game.

Name: {}
Role: {}
Location: {}
Personality traits: {}

Generate:
1. Background (2-3 sentences)
2. Current motivation
3. Secret or hidden aspect
4. Connection to the world
5. Speech mannerism

Keep responses concise and game-appropriate."#,
            npc.name,
            npc.role,
            npc.location,
            npc.personality_traits.join(", ")
        );
        
        let response = self.llm.complete(&prompt).await
            .map_err(|e| GenerationError::InvalidParams(e.to_string()))?;
        
        self.parse_npc_response(&response)
    }
    
    pub async fn generate_location_description(
        &self,
        location: &LocationGenContext,
    ) -> Result<LocationDescription, GenerationError> {
        let prompt = format!(
            r#"Describe a location for a fantasy game.

Type: {}
Biome: {}
Key features: {}
Mood: {}

Provide:
1. Name (evocative)
2. Short description (1 sentence for UI)
3. Full description (2-3 sentences for exploration)
4. Notable elements (3-5 items)
5. Ambient sounds suggestion"#,
            location.location_type,
            location.biome,
            location.features.join(", "),
            location.mood
        );
        
        let response = self.llm.complete(&prompt).await
            .map_err(|e| GenerationError::InvalidParams(e.to_string()))?;
        
        self.parse_location_response(&response)
    }
}

#[derive(Debug)]
pub struct GeneratedQuest {
    pub title: String,
    pub description: String,
    pub objectives: Vec<QuestObjective>,
    pub rewards: Vec<QuestReward>,
    pub twist: Option<String>,
}

#[derive(Debug)]
pub struct NpcBackstory {
    pub background: String,
    pub motivation: String,
    pub secret: String,
    pub world_connection: String,
    pub speech_pattern: String,
}
}

Validation and Constraints

Content Validator

Ensure generated content meets requirements:

#![allow(unused)]
fn main() {
pub struct ContentValidator {
    rules: Vec<ValidationRule>,
}

pub enum ValidationRule {
    MinRooms(usize),
    MaxDeadEnds(usize),
    RequireRoomType(RoomType),
    MaxDifficulty(f32),
    Connectivity,
    NoOverlap,
    Custom(Box<dyn Fn(&DungeonLayout) -> ValidationResult>),
}

impl ContentValidator {
    pub fn validate_dungeon(&self, dungeon: &DungeonLayout) -> ValidationResult {
        for rule in &self.rules {
            match rule {
                ValidationRule::MinRooms(min) => {
                    if dungeon.rooms.len() < *min {
                        return ValidationResult::Invalid(
                            format!("Too few rooms: {} < {}", dungeon.rooms.len(), min)
                        );
                    }
                }
                ValidationRule::MaxDeadEnds(max) => {
                    let dead_ends = dungeon.rooms
                        .iter()
                        .filter(|r| r.connections.len() <= 1)
                        .count();
                    if dead_ends > *max {
                        return ValidationResult::Invalid(
                            format!("Too many dead ends: {} > {}", dead_ends, max)
                        );
                    }
                }
                ValidationRule::RequireRoomType(room_type) => {
                    if !dungeon.rooms.iter().any(|r| r.room_type == *room_type) {
                        return ValidationResult::Invalid(
                            format!("Missing required room type: {:?}", room_type)
                        );
                    }
                }
                ValidationRule::Connectivity => {
                    if !self.check_connectivity(dungeon) {
                        return ValidationResult::Invalid("Dungeon is not fully connected".into());
                    }
                }
                _ => {}
            }
        }
        
        ValidationResult::Valid
    }
    
    fn check_connectivity(&self, dungeon: &DungeonLayout) -> bool {
        if dungeon.rooms.is_empty() {
            return true;
        }
        
        let mut visited = vec![false; dungeon.rooms.len()];
        let mut stack = vec![0usize];
        
        while let Some(current) = stack.pop() {
            if visited[current] {
                continue;
            }
            visited[current] = true;
            
            for &connected in &dungeon.rooms[current].connections {
                if !visited[connected] {
                    stack.push(connected);
                }
            }
        }
        
        visited.iter().all(|&v| v)
    }
}
}

Best Practices

1. **Seed Everything**: Use deterministic RNG for reproducible generation
2. **Validate Early**: Reject invalid content before expensive operations
3. **Layer Generation**: Start coarse, refine with detail passes
4. **Cache Results**: Store expensive generation results for reuse
- **Infinite Loops**: Always limit generation attempts
- **Memory Bloat**: Stream large content; don't hold everything in memory
- **Sameness**: Vary parameters enough to feel unique
- **Performance**: Profile generation; move to background threads if slow

Save & Load Systems

Crates: astraweave-persistence-ecs, astraweave-core
Status: Production Ready

AstraWeave provides deterministic save/load with full ECS state serialization, player profiles, and versioned migrations.

Quick Start

#![allow(unused)]
fn main() {
use astraweave_persistence_ecs::{SaveSystem, LoadSystem, SaveConfig};
use astraweave_ecs::World;

let config = SaveConfig {
    path: "saves/",
    compression: true,
    encryption: false,
    ..Default::default()
};

let save_system = SaveSystem::new(config);

// Save world state
save_system.save(&world, "quicksave")?;

// Load world state
let loaded_world = save_system.load("quicksave")?;
}

Save System Architecture

Save File Structure
├── Header (version, timestamp, checksum)
├── World State
│   ├── Entities (sparse set serialization)
│   ├── Components (per-archetype)
│   └── Resources (global state)
├── Player Data
│   ├── Inventory
│   ├── Progress
│   └── Settings
└── Metadata
    ├── Screenshot (optional)
    └── Play time

ECS Serialization

Component Registration

Components must be registered for serialization:

#![allow(unused)]
fn main() {
use astraweave_ecs::{Component, Serialize, Deserialize};

#[derive(Component, Serialize, Deserialize)]
struct Health {
    current: f32,
    max: f32,
}

#[derive(Component, Serialize, Deserialize)]
struct Position {
    x: f32,
    y: f32,
    z: f32,
}

// Register with world
app.register_serializable::<Health>();
app.register_serializable::<Position>();
}

Selective Serialization

Mark components to skip during save:

#![allow(unused)]
fn main() {
#[derive(Component)]
#[component(skip_save)]
struct RenderCache {
    // Regenerated at load time
}

#[derive(Component, Serialize, Deserialize)]
#[component(save_priority = "high")]
struct PlayerData {
    // Saved first, loaded first
}
}

Save Slots

Slot Management

#![allow(unused)]
fn main() {
use astraweave_persistence_ecs::{SaveSlot, SlotManager};

let mut slots = SlotManager::new("saves/", 10); // 10 slots

// Get available slots
let available = slots.list()?;
for slot in &available {
    println!("Slot {}: {} ({})", slot.index, slot.name, slot.timestamp);
}

// Save to slot
slots.save_to_slot(3, &world, "My Save")?;

// Load from slot
let world = slots.load_from_slot(3)?;

// Delete slot
slots.delete_slot(3)?;
}

Auto-Save

#![allow(unused)]
fn main() {
use astraweave_persistence_ecs::AutoSave;

let mut autosave = AutoSave::new(Duration::from_secs(300)); // 5 minutes

// In game loop
if autosave.should_save() {
    slots.save_to_slot(0, &world, "Autosave")?;
    autosave.mark_saved();
}
}

Player Profiles

Profile Management

#![allow(unused)]
fn main() {
use astraweave_persistence_ecs::{PlayerProfile, ProfileManager};

let mut profiles = ProfileManager::new("profiles/")?;

// Create profile
let profile = PlayerProfile::new("Player1");
profiles.create(profile)?;

// Load profile
let profile = profiles.load("Player1")?;

// Update settings
profile.settings.volume = 0.8;
profile.settings.difficulty = Difficulty::Hard;
profiles.save(&profile)?;
}

Profile Contents

#![allow(unused)]
fn main() {
pub struct PlayerProfile {
    pub name: String,
    pub created: DateTime,
    pub play_time: Duration,
    pub settings: PlayerSettings,
    pub achievements: Vec<Achievement>,
    pub statistics: PlayerStats,
}

pub struct PlayerSettings {
    pub volume: f32,
    pub music_volume: f32,
    pub sfx_volume: f32,
    pub voice_volume: f32,
    pub difficulty: Difficulty,
    pub controls: ControlBindings,
    pub graphics: GraphicsSettings,
}
}

Versioning & Migration

Version Handling

#![allow(unused)]
fn main() {
use astraweave_persistence_ecs::{SaveVersion, Migration};

// Current save version
const SAVE_VERSION: SaveVersion = SaveVersion::new(1, 2, 0);

// Load with migration
let world = save_system.load_with_migrations(
    "old_save",
    &[
        Migration::new(
            SaveVersion::new(1, 0, 0),
            SaveVersion::new(1, 1, 0),
            migrate_1_0_to_1_1,
        ),
        Migration::new(
            SaveVersion::new(1, 1, 0),
            SaveVersion::new(1, 2, 0),
            migrate_1_1_to_1_2,
        ),
    ],
)?;

fn migrate_1_0_to_1_1(data: &mut SaveData) -> Result<()> {
    // Add new health.shield field
    for entity in data.entities_with::<Health>() {
        let health: &mut Health = data.get_mut(entity)?;
        health.shield = 0.0; // New field
    }
    Ok(())
}
}

Corruption Recovery

Backup System

#![allow(unused)]
fn main() {
use astraweave_persistence_ecs::BackupConfig;

let config = SaveConfig {
    backup: BackupConfig {
        enabled: true,
        max_backups: 3,
        backup_on_save: true,
    },
    ..Default::default()
};

// Backups created automatically
// saves/quicksave.sav
// saves/quicksave.sav.bak1
// saves/quicksave.sav.bak2
// saves/quicksave.sav.bak3
}

Integrity Verification

#![allow(unused)]
fn main() {
use astraweave_persistence_ecs::verify_save;

match verify_save("saves/quicksave.sav") {
    Ok(info) => {
        println!("Valid save: v{}", info.version);
    }
    Err(SaveError::Corrupted) => {
        // Try backup
        if let Ok(backup) = find_valid_backup("quicksave") {
            restore_from_backup(backup)?;
        }
    }
    Err(SaveError::VersionMismatch(v)) => {
        println!("Save from future version: {}", v);
    }
}
}

Deterministic Replay

AstraWeave’s deterministic ECS enables save/load that reproduces exact game states:

#![allow(unused)]
fn main() {
use astraweave_core::replay::{ReplayRecorder, ReplayPlayer};

// Record session
let mut recorder = ReplayRecorder::new();
recorder.start(&world);

// During gameplay
recorder.record_frame(&world, &inputs);

// Save replay
recorder.save("replay.bin")?;

// Play back (bit-identical to original)
let mut player = ReplayPlayer::load("replay.bin")?;
while let Some(frame) = player.next_frame() {
    world.apply_inputs(frame.inputs);
    world.step();
    
    assert_eq!(world.checksum(), frame.checksum);
}
}

Performance

OperationLatencyNotes
Quick save (1K entities)~50 msWith compression
Quick load (1K entities)~30 msWith decompression
Profile save~5 msSettings only
Checksum verify~2 msPer save file

Optimization Tips

  1. Async saves - Don’t block gameplay
  2. Incremental saves - Only save changed data
  3. Compression - 60-80% size reduction
  4. Memory-mapped loading - Faster large worlds
#![allow(unused)]
fn main() {
// Async save
let handle = save_system.save_async(&world, "quicksave");

// Continue gameplay...

// Wait for completion
handle.await?;
}

See Also

Scripting with Rhai

Crates: astraweave-scripting, astraweave-author
Status: Production Ready (Sandboxed)

AstraWeave provides secure Rhai scripting for modding, behavior authoring, and runtime customization with full sandboxing.

Quick Start

// scripts/my_behavior.rhai

// Called when entity spawns
fn on_spawn(entity) {
    log("Entity spawned: " + entity.id);
    entity.set_health(100);
    entity.play_animation("idle");
}

// Called every frame
fn on_update(entity, dt) {
    let player = find_nearest("Player");
    
    if distance(entity.position, player.position) < 10.0 {
        entity.look_at(player);
        entity.move_towards(player, dt * 5.0);
    }
}

// Called when taking damage
fn on_damage(entity, amount, source) {
    if entity.health - amount <= 0 {
        entity.play_animation("death");
        spawn_particles("blood", entity.position, 20);
    }
}

Script Engine Setup

#![allow(unused)]
fn main() {
use astraweave_scripting::{ScriptEngine, ScriptConfig};

let config = ScriptConfig {
    max_operations: 100_000,    // Operation limit per call
    timeout_ms: 10,             // Max execution time
    memory_limit_kb: 1024,      // Memory limit
    allow_network: false,       // Network access
    allow_filesystem: false,    // File access
};

let mut engine = ScriptEngine::new(config);

// Register scripts
engine.load_script("behaviors/enemy.rhai")?;
engine.load_script("behaviors/companion.rhai")?;

// Execute
engine.call("on_spawn", (entity,))?;
}

Available APIs

Entity API

// Position & Movement
entity.position              // Get Vec3
entity.set_position(x, y, z) // Set position
entity.move_towards(target, speed)
entity.look_at(target)
entity.rotate(yaw, pitch, roll)

// Health & Combat
entity.health               // Current health
entity.max_health           // Maximum health
entity.set_health(amount)
entity.damage(amount)
entity.heal(amount)
entity.is_dead()

// Animation
entity.play_animation(name)
entity.stop_animation()
entity.is_animating()

// Components
entity.has_component("Physics")
entity.get_tag("faction")
entity.set_tag("faction", "enemy")

World API

// Entity queries
let player = find_nearest("Player");
let enemies = find_all("Enemy");
let nearby = find_in_radius(position, radius);

// Spawning
let entity = spawn("enemy_type", position);
destroy(entity);

// Raycasting
let hit = raycast(origin, direction, max_distance);
if hit.success {
    log("Hit: " + hit.entity.name + " at " + hit.point);
}

// Time
let time = game_time();
let dt = delta_time();

Math API

// Vectors
let v = vec3(1.0, 2.0, 3.0);
let dist = distance(a, b);
let dir = normalize(v);
let dot = dot_product(a, b);
let cross = cross_product(a, b);

// Interpolation
let result = lerp(a, b, t);
let smooth = smoothstep(a, b, t);

// Random
let r = random();           // 0.0 - 1.0
let ri = random_range(1, 10); // 1 - 10
let choice = random_choice(array);

Audio API

// Sound playback
play_sound("sfx/explosion.ogg");
play_sound_at("sfx/growl.ogg", position);
play_music("music/combat.ogg", fade_in_seconds);
stop_music(fade_out_seconds);
set_volume("sfx", 0.8);

Particle API

spawn_particles("fire", position, count);
spawn_particles_at_entity("smoke", entity, count);
stop_particles(particle_system);

Behavior Components

Attaching Scripts to Entities

#![allow(unused)]
fn main() {
use astraweave_scripting::ScriptComponent;

commands.spawn((
    Transform::default(),
    Health::new(100),
    ScriptComponent::new("behaviors/enemy.rhai"),
));
}

Callbacks

CallbackWhen CalledParameters
on_spawnEntity created(entity)
on_updateEvery frame(entity, dt)
on_destroyEntity removed(entity)
on_collisionPhysics collision(entity, other, contact)
on_triggerTrigger overlap(entity, other, entered)
on_damageTaking damage(entity, amount, source)
on_deathHealth reaches 0(entity, killer)
on_interactPlayer interaction(entity, player)

Security Sandboxing

Operation Limits

// This will be terminated after 100,000 operations
fn infinite_loop(entity) {
    while true {
        // Will hit operation limit
    }
}

Memory Limits

fn memory_hog(entity) {
    let huge_array = [];
    for i in 0..1000000 {
        huge_array.push(i);  // Will hit memory limit
    }
}

Blocked Operations

Scripts cannot:

  • Access filesystem
  • Open network connections
  • Call system commands
  • Modify engine internals
  • Access other scripts’ data

Hot Reloading

Scripts can be modified at runtime:

#![allow(unused)]
fn main() {
// Enable hot reload
engine.enable_hot_reload(true);

// In game loop
engine.check_for_changes()?; // Reloads modified scripts
}

Development workflow:

  1. Run game
  2. Edit .rhai files
  3. Changes apply immediately
  4. No restart needed

Custom Functions

Exposing Rust to Scripts

#![allow(unused)]
fn main() {
use astraweave_scripting::ScriptEngine;

engine.register_fn("custom_ability", |entity: Entity, power: f64| {
    // Rust implementation
    let damage = power * 2.0;
    apply_area_damage(entity.position(), damage, 5.0);
});
}

Using in Scripts

fn on_special_attack(entity) {
    custom_ability(entity, 50.0);
    play_sound("sfx/special.ogg");
    spawn_particles("explosion", entity.position, 100);
}

State Machines

Defining States

// AI state machine
let state = "idle";

fn on_update(entity, dt) {
    switch state {
        "idle" => {
            if see_player() {
                state = "chase";
            }
        }
        "chase" => {
            let player = find_nearest("Player");
            entity.move_towards(player, dt * 8.0);
            
            if distance(entity.position, player.position) < 2.0 {
                state = "attack";
            }
        }
        "attack" => {
            entity.attack();
            state = "cooldown";
            set_timer("attack_cooldown", 1.0);
        }
        "cooldown" => {
            if timer_expired("attack_cooldown") {
                state = "chase";
            }
        }
    }
}

Debugging

Logging

log("Debug message");
log_warn("Warning message");
log_error("Error message");

Console Commands

#![allow(unused)]
fn main() {
// Register console command
engine.register_command("spawn_enemy", |args| {
    let count = args.get(0).unwrap_or(1);
    for _ in 0..count {
        spawn("enemy", random_position());
    }
});
}

Script Inspector

The editor provides:

  • Breakpoints
  • Variable inspection
  • Call stack
  • Performance profiling

Performance

OperationLatencyNotes
Script call~5 µsPer function call
Entity API~100 nsProperty access
Script load~1 msParse + compile
Hot reload~2 msPer file

Optimization Tips

  1. Cache lookups - Don’t find entities every frame
  2. Limit scope - Smaller scripts are faster
  3. Use events - Don’t poll in on_update
  4. Batch operations - Group similar calls
// Bad: Finding player every frame
fn on_update(entity, dt) {
    let player = find_nearest("Player"); // Expensive!
}

// Good: Cache reference
let cached_player = null;

fn on_spawn(entity) {
    cached_player = find_nearest("Player");
}

fn on_update(entity, dt) {
    if cached_player != null {
        entity.look_at(cached_player);
    }
}

See Also

Working Examples

AstraWeave includes over 20 examples demonstrating different aspects of the engine. This page focuses on the working examples that you can build and run to learn the engine.

Note: AstraWeave is under active development. Some examples have compilation issues due to API evolution. This page focuses on examples that are confirmed to work.

Core AI Examples

These examples demonstrate the AI-native architecture:

Hello Companion ✅

Location: examples/hello_companion
Status: ✅ Working (expected panic)

The simplest example of AI perception, planning, and validation.

cargo run -p hello_companion --release

What it demonstrates:

  • AI perception system capturing world state
  • LLM-based planning generating intents
  • Tool validation system (demonstrates failure case)
  • Fixed-tick simulation loop

Expected behavior: Shows AI plan generation, then panics with “LosBlocked” error. This demonstrates that the AI cannot perform invalid actions.

→ Detailed walkthrough

Adaptive Boss ✅

Location: examples/adaptive_boss
Status: ✅ Working

Multi-phase boss with Director AI that adapts tactics based on player behavior.

cargo run -p adaptive_boss --release

What it demonstrates:

  • BossDirector for dynamic encounter planning
  • Budget-constrained AI decisions
  • Telegraph system for attack warnings
  • Phase-based combat behavior

→ Detailed walkthrough

Companion Profile ✅

Location: examples/companion_profile
Status: ✅ Working

Demonstrates persistent AI profiles that learn and adapt.

cargo run -p companion_profile --release

What it demonstrates:

  • AI profile serialization/deserialization
  • Learning from player interactions
  • Personality trait adjustment
  • Long-term memory systems

Core Engine Examples

These examples showcase fundamental engine systems:

Fluids Demo ✅

Location: examples/fluids_demo
Status: ✅ Working

Interactive fluid simulation with PCISPH physics and multiple scenarios.

cargo run -p fluids_demo --release

What it demonstrates:

  • Real-time particle-based fluid simulation
  • Multiple scenarios (laboratory, ocean, waterfall, splash)
  • Interactive particle spawning (click to add water)
  • LOD optimization for performance scaling
  • egui debug panel with live parameters

→ Detailed walkthrough

Unified Showcase ✅

Location: examples/unified_showcase
Status: ✅ Working

Comprehensive rendering demo with shadows, terrain, GLTF models, and skybox.

cargo run -p unified_showcase --release

What it demonstrates:

  • Shadow mapping with 2048×2048 depth textures
  • GLTF model loading with materials
  • Procedural terrain with multi-texture blending
  • HDR skybox rendering
  • 4× MSAA antialiasing

→ Detailed walkthrough

Physics Demo 3D ✅

Location: examples/physics_demo3d
Status: ✅ Working

Demonstrates the Rapier3D physics integration with character controllers, destructibles, and environmental forces.

cargo run -p physics_demo3d --release

What it demonstrates:

  • 3D physics simulation with Rapier3D
  • Character controller with slope handling
  • Destructible objects that shatter
  • Water buoyancy and wind forces
  • Collision layer filtering

→ Detailed walkthrough

Location: examples/navmesh_demo
Status: ✅ Working

Shows navigation mesh baking and A* pathfinding.

cargo run -p navmesh_demo --release

What it demonstrates:

  • NavMesh baking from triangle geometry
  • Slope-based walkability filtering
  • A* pathfinding with path visualization
  • Agent radius margin calculation

→ Detailed walkthrough

Audio Spatial Demo ✅

Location: examples/audio_spatial_demo
Status: ✅ Working

Spatial audio system with 3D positioning and music crossfading.

cargo run -p audio_spatial_demo --release

What it demonstrates:

  • 3D positional audio (left/center/right beeps)
  • Music playback with crossfading
  • Listener tracking tied to camera
  • Volume bus control (master/music/SFX)

→ Detailed walkthrough

Networking Examples

These examples show multiplayer and IPC capabilities:

IPC Loopback ✅

Location: examples/ipc_loopback
Status: ✅ Should work

Demonstrates inter-process communication for AI models.

cargo run -p ipc_loopback --release

What it demonstrates:

  • Local/cloud AI model switching
  • Process isolation for AI
  • IPC message passing
  • AI model hot-swapping

Coop Server/Client ✅

Location: examples/coop_server, examples/coop_client
Status: ✅ Working

Basic multiplayer client-server architecture.

# Terminal 1
cargo run -p coop_server --release

# Terminal 2  
cargo run -p coop_client --release

What it demonstrates:

  • Server-authoritative validation
  • Intent-based networking
  • AI agent synchronization
  • Anti-cheat through determinism

Tool and Planning Examples

These examples focus on AI planning and tool usage:

LLM Tool Call ✅

Location: examples/llm_toolcall
Status: ✅ Working

Direct demonstration of LLM tool calling.

cargo run -p llm_toolcall --release

What it demonstrates:

  • LLM integration
  • Tool definition and usage
  • Structured AI responses
  • Planning validation

Phase Director ✅

Location: examples/phase_director
Status: ✅ Working

Complex AI director managing multiple phases.

cargo run -p phase_director --release

What it demonstrates:

  • Multi-phase AI behavior
  • Director pattern implementation
  • State machine management
  • Complex AI coordination

Development Examples

These examples help with engine development:

Debug Overlay ❌

Location: examples/debug_overlay
Status: ❌ Has compilation issues (egui API)

Debug UI overlay for development.

Known issues: egui API mismatches with current version.

Persona Loader ✅

Location: examples/persona_loader
Status: ✅ Working

Loading and managing AI personas from files.

cargo run -p persona_loader --release

What it demonstrates:

  • AI persona definition files
  • Dynamic persona loading
  • Personality trait configuration
  • Behavioral parameter tuning

Known Compilation Issues

Some examples have known issues due to API evolution:

Graphics Examples ❌

  • visual_3d: winit API mismatches
  • ui_controls_demo: egui API compatibility issues
  • debug_overlay: egui API changes

Authoring Examples ❌

  • rhai_authoring: Depends on broken astraweave-author crate
  • Issues with rhai sync/send traits

Complex Demos ❌

  • npc_town_demo: Multiple API mismatches
  • weaving_playground: Dependency issues
  • cutscene_render_demo: Graphics API issues

Testing Examples

To verify your installation is working:

Minimal Test Sequence

# 1. Build core components
cargo build -p astraweave-core -p astraweave-ai -p hello_companion

# 2. Run the basic example
cargo run -p hello_companion --release

# 3. Run unit tests
cargo test -p astraweave-input

Debugging Build Issues

If examples fail to compile:

  1. Check Rust version: rustc --version should match rust-toolchain.toml
  2. Update dependencies: cargo update
  3. Clean build: cargo clean && cargo build
  4. Check system dependencies: Ensure graphics and audio libraries are installed

Reporting Issues

If you find compilation issues with examples marked as working:

  1. Check your platform and Rust version
  2. Ensure all system dependencies are installed
  3. Try a clean build
  4. Report the issue with full error output

Building Your Own Examples

When creating new examples:

Minimal Example Structure

// examples/my_example/src/main.rs
use astraweave_core::*;
use astraweave_ai::*;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Initialize the engine
    let mut world = World::new();
    
    // Add your systems
    world.add_system(my_custom_system);
    
    // Run the simulation
    world.run()?;
    
    Ok(())
}

Cargo.toml Template

[package]
name = "my_example"
version.workspace = true
edition.workspace = true

[dependencies]
astraweave-core = { path = "../../astraweave-core" }
astraweave-ai = { path = "../../astraweave-ai" }
anyhow.workspace = true

Next Steps


The working examples are your best introduction to AstraWeave’s capabilities. Start with hello_companion and work your way up to more complex scenarios.

Hello Companion Walkthrough

The hello_companion example is the perfect introduction to AstraWeave’s AI-native architecture. This walkthrough explains every step of what happens when you run this example and why it’s designed this way.

Running the Example

cargo run -p hello_companion --release

Expected Output

[INFO] Initializing AstraWeave Engine...
[INFO] Creating world with ECS...
[INFO] Spawning AI companion entity
[INFO] Starting simulation loop at 60Hz
[INFO] Tick 1: Capturing perception snapshot
[INFO] AI perception: 1 entities visible, 0 audio events
[INFO] Sending perception to AI planning layer
[INFO] AI generated plan: MoveTo { target: Vec3(10.0, 0.0, 5.0), urgency: 0.7 }
[INFO] Validating movement tool usage...
[ERROR] Tool validation failed: LosBlocked - No clear line of sight to target
thread 'main' panicked at examples/hello_companion/src/main.rs:42:5:
called `Result::unwrap()` on an `Err` value: ToolValidationError(LosBlocked)

This panic is intentional! It demonstrates AstraWeave’s core principle: AI agents cannot perform actions that violate the game world’s constraints.

Code Walkthrough

Let’s examine the source code to understand each step:

1. Engine Initialization

// examples/hello_companion/src/main.rs
use astraweave_core::*;
use astraweave_ai::*;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Initialize logging
    env_logger::init();
    
    // Create the ECS world
    let mut world = World::new();
    
    // Configure the engine systems
    world.add_plugin(CorePlugin)
         .add_plugin(AIPlugin)
         .add_plugin(PhysicsPlugin);

What’s happening:

  • Sets up the Entity-Component-System (ECS) world
  • Registers core systems for AI, physics, and simulation
  • Configures 60Hz fixed-tick simulation

2. Spawning the AI Companion

#![allow(unused)]
fn main() {
    // Spawn an AI companion entity
    let companion = world.spawn()
        .insert(Position(Vec3::new(0.0, 0.0, 0.0)))
        .insert(AIAgent {
            perception_range: 10.0,
            planning_interval: Duration::from_millis(500),
            ai_model: AIModel::Local("companion-7b".to_string()),
        })
        .insert(MovementCapability {
            max_speed: 5.0,
            acceleration: 2.0,
        })
        .id();
    
    info!("Spawned AI companion with ID: {:?}", companion);
}

What’s happening:

  • Creates a new entity in the ECS world
  • Adds position component (where the companion is)
  • Adds AI agent component (makes it intelligent)
  • Adds movement capability (what it can do)

3. The Simulation Loop

#![allow(unused)]
fn main() {
    // Run simulation for a few ticks
    for tick in 0..5 {
        info!("Starting tick {}", tick);
        
        // This is where the AI magic happens
        world.step(Duration::from_nanos(16_666_667)); // 1/60 second
        
        // Small delay so we can see the output
        std::thread::sleep(Duration::from_millis(100));
    }
}

What’s happening:

  • Runs exactly 5 simulation ticks
  • Each tick advances the world by exactly 1/60th of a second
  • The deterministic timing ensures consistent behavior

4. The AI Pipeline (Inside world.step())

During each world.step() call, several systems run in sequence:

A. Perception System

#![allow(unused)]
fn main() {
fn ai_perception_system(
    mut query: Query<(&Position, &AIAgent, &mut PerceptionState)>,
    world_query: Query<&Position>,
) {
    for (pos, agent, mut perception) in query.iter_mut() {
        // Gather what the AI can see
        let mut visible_entities = Vec::new();
        
        for other_pos in world_query.iter() {
            let distance = pos.0.distance(other_pos.0);
            if distance <= agent.perception_range {
                visible_entities.push(EntityData {
                    position: other_pos.0,
                    distance,
                    entity_type: "unknown".to_string(),
                });
            }
        }
        
        // Create perception snapshot
        perception.last_snapshot = Some(PerceptionSnapshot {
            timestamp: world.current_tick(),
            agent_id: entity,
            visible_entities,
            audio_events: vec![], // None in this simple example
            world_state: WorldState::default(),
        });
    }
}
}

B. AI Planning System

#![allow(unused)]
fn main() {
fn ai_planning_system(
    mut query: Query<(&AIAgent, &PerceptionState, &mut PlanningState)>,
    ai_service: Res<AIService>,
) {
    for (agent, perception, mut planning) in query.iter_mut() {
        if let Some(snapshot) = &perception.last_snapshot {
            // Send to AI model for planning
            let plan_request = PlanningRequest {
                perception: snapshot.clone(),
                agent_profile: agent.clone(),
                available_tools: vec!["MovementTool", "InteractionTool"],
            };
            
            // This is where the LLM generates a plan
            let plan = ai_service.generate_plan(plan_request)?;
            
            info!("AI generated plan: {:?}", plan.intent);
            planning.current_plan = Some(plan);
        }
    }
}
}

C. Tool Validation System

#![allow(unused)]
fn main() {
fn tool_validation_system(
    mut query: Query<(&PlanningState, &mut ActionState)>,
    tool_registry: Res<ToolRegistry>,
    world: &World,
) {
    for (planning, mut action) in query.iter_mut() {
        if let Some(plan) = &planning.current_plan {
            for tool_usage in &plan.tools {
                // This is where the validation happens
                let validation_result = tool_registry
                    .get_tool(&tool_usage.tool_name)
                    .unwrap()
                    .validate(world, tool_usage);
                
                match validation_result {
                    ValidationResult::Valid => {
                        info!("Tool validation passed: {}", tool_usage.tool_name);
                        action.pending_actions.push(tool_usage.clone());
                    }
                    ValidationResult::Blocked(reason) => {
                        error!("Tool validation failed: {:?}", reason);
                        // This causes the panic in hello_companion
                        return Err(ToolValidationError::from(reason));
                    }
                }
            }
        }
    }
}
}

Why Does It Panic?

The panic occurs because the AI tries to move to a position but there’s no clear line of sight. Here’s what happens:

1. AI Perception

  • The companion perceives its current position (0, 0, 0)
  • It detects no obstacles in its perception range

2. AI Planning

  • The AI decides it wants to move to position (10, 0, 5)
  • This seems reasonable based on its limited perception

3. Tool Validation

  • The MovementTool.validate() method checks line of sight
  • There’s an invisible obstacle blocking the path
  • Validation fails with LosBlocked error

4. Engine Authority

  • The engine refuses to execute the invalid action
  • This maintains world integrity and prevents AI cheating

Key Learning Points

1. AI Cannot Cheat

The AI doesn’t have perfect information about the world. It can only act based on what it perceives, and all actions must be validated by the engine.

2. Deterministic Behavior

Run the example multiple times - you’ll get the same result every time. This determinism is crucial for:

  • Reliable testing
  • Networking (same simulation on all clients)
  • Debugging AI behavior

3. Tool-Based Architecture

The AI doesn’t directly move entities or change the world. It can only request actions through validated tools:

  • MovementTool (for movement)
  • InteractionTool (for object interaction)
  • CombatTool (for attacks)
  • CommunicationTool (for dialogue)

4. Perception vs Reality

The AI’s perception is limited and may not match reality. This creates interesting emergent behavior as AI agents must:

  • Explore to gather information
  • Make decisions with incomplete data
  • Adapt when actions fail

Modifying the Example

Make It Not Panic

To see successful AI behavior, modify the world setup:

#![allow(unused)]
fn main() {
// Remove the obstacle that blocks line of sight
world.remove_obstacle(Vec3::new(5.0, 0.0, 2.5));

// Or give the AI perfect perception
ai_agent.perception_range = f32::INFINITY;
}

Add More Interesting Behavior

#![allow(unused)]
fn main() {
// Add a target for the AI to find
world.spawn()
    .insert(Position(Vec3::new(15.0, 0.0, 0.0)))
    .insert(InteractableItem {
        item_type: "treasure_chest".to_string(),
        value: 100,
    });

// The AI will now try to navigate to and interact with the chest
}

Enable Logging for More Detail

RUST_LOG=debug cargo run -p hello_companion --release

This shows detailed information about:

  • ECS system execution order
  • AI model input/output
  • Tool validation steps
  • World state changes

Architectural Insights

Fixed-Tick Simulation

The 60Hz fixed timestep ensures:

  • Physics determinism
  • Consistent AI decision making
  • Reliable networking
  • Predictable performance

ECS Benefits

The Entity-Component-System architecture provides:

  • Cache-friendly performance
  • Clear separation of concerns
  • Easy parallel system execution
  • Modular, testable code

AI Validation Pipeline

The perception → planning → validation → execution pipeline ensures:

  • No AI cheating
  • Consistent game rules
  • Emergent behavior from constraints
  • Easy debugging and testing

Next Steps

After understanding hello_companion:

  1. Explore More Examples: Try Adaptive Boss for complex AI
  2. Learn Architecture: Read AI-Native Design
  3. Build Your Own: Follow Building Your First Game
  4. Dive Deeper: Study Core AI Systems

Common Questions

Q: Why does it panic instead of just logging the error?

A: The panic demonstrates that validation failures are serious. In a real game, you’d handle this gracefully, but the example uses panic to make the validation concept crystal clear.

Q: Can I make the AI smarter to avoid this error?

A: Yes! You can:

  • Improve the AI’s perception system
  • Give it better pathfinding tools
  • Add obstacle detection to its planning
  • Implement learning from failed actions

Q: Is this really how a game AI should work?

A: For AI-native games, yes! This approach:

  • Prevents AI cheating
  • Creates emergent behavior
  • Works in multiplayer
  • Enables complex AI interactions

The “failed action = learning opportunity” approach leads to much more interesting AI behavior than scripted sequences.


The hello_companion example may be simple, but it demonstrates the fundamental principles that enable AstraWeave’s AI-native gameplay. Every complex AI behavior in the engine builds on these same validation patterns.

Adaptive Boss Walkthrough

The adaptive_boss example demonstrates AstraWeave’s Director System - an AI-driven game master that orchestrates boss encounters with dynamic phase transitions, telegraphed attacks, and budget-constrained tactical decisions.

Running the Example

cargo run -p adaptive_boss --release

For the full Veilweaver boss experience (requires veilweaver_slice feature):

cargo run -p adaptive_boss --release --features veilweaver_slice

What It Demonstrates

  • BossDirector: AI that plans boss encounter actions
  • Phase-based combat: Boss behavior changes based on situation
  • Director budget: Limited resources for spawns, traps, and terrain edits
  • Telegraph system: Visual/audio warnings before powerful attacks
  • OathboundWardenDirector: Full Veilweaver boss with Fate-weaving mechanics

Expected Output

Warden phase: Stalking
Telegraphs: ["Thread gathering at position (5, 3)"]
Director plan: {
  "spawn": [{"enemy": "shade", "pos": [4, 1]}],
  "traps": [{"type": "thread_snare", "pos": [6, 2]}],
  "terrain": []
}
Remaining budget: traps=1, terrain_edits=2, spawns=1

Code Walkthrough

1. Arena Setup

#![allow(unused)]
fn main() {
let mut w = World::new();

// Create the combat arena: player, companion, and boss
let player = w.spawn("Player", IVec2 { x: 2, y: 2 }, Team { id: 0 }, 100, 0);
let comp = w.spawn("Comp", IVec2 { x: 3, y: 2 }, Team { id: 1 }, 80, 30);
let boss = w.spawn("Boss", IVec2 { x: 14, y: 2 }, Team { id: 2 }, 400, 0);
}

The world contains three entities:

  • Player (HP: 100): The human-controlled character
  • Companion (HP: 80, Ammo: 30): AI ally
  • Boss (HP: 400): The adversary with high HP

2. World Snapshot

#![allow(unused)]
fn main() {
let snap = WorldSnapshot {
    t: w.t,
    player: PlayerState { hp: 100, pos: w.pos_of(player).unwrap(), ... },
    me: CompanionState { ammo: 30, morale: 0.8, pos: w.pos_of(comp).unwrap(), ... },
    enemies: vec![EnemyState { id: boss, hp: 400, cover: "high", ... }],
    pois: vec![],
    obstacles: vec![],
    objective: Some("defeat_boss".into()),
};
}

The Director receives a perception snapshot containing:

  • Current game time
  • Player state (health, position, stance)
  • Companion state (ammo, morale, cooldowns)
  • Enemy states (the boss from its perspective)

3. Director Budget

#![allow(unused)]
fn main() {
let mut budget = DirectorBudget {
    traps: 2,      // Can place 2 traps
    terrain_edits: 2, // Can modify terrain twice
    spawns: 2,     // Can summon 2 minions
};
}

The budget prevents the Director from overwhelming the player. Each action consumes budget:

  • Spawns: Summoning minions costs 1 spawn point
  • Traps: Placing hazards costs 1 trap point
  • Terrain edits: Blocking paths or creating cover costs 1 edit point

4. Director Planning (Basic)

#![allow(unused)]
fn main() {
#[cfg(not(feature = "veilweaver_slice"))]
{
    let director = BossDirector;
    let plan = director.plan(&snap, &budget);
    apply_director_plan(&mut w, &mut budget, &plan, &mut log);
}
}

The basic BossDirector analyzes the world state and generates a plan within budget.

5. Veilweaver Director (Advanced)

#![allow(unused)]
fn main() {
#[cfg(feature = "veilweaver_slice")]
{
    let mut director = OathboundWardenDirector::new();
    let directive = director.step(&snap, &budget);
    
    println!("Warden phase: {:?}", directive.phase);
    if !directive.telegraphs.is_empty() {
        println!("Telegraphs: {:?}", directive.telegraphs);
    }
    
    apply_director_plan(&mut w, &mut budget, &directive.plan, &mut log);
}
}

The Oathbound Warden has distinct phases:

  • Stalking: Observing, placing traps, gathering threads
  • Weaving: Manipulating fate threads, buffing/debuffing
  • Severing: Aggressive attacks, breaking player connections
  • Unraveling: Desperate phase when low HP

Director System Architecture

┌────────────────────────────────────────────────────────┐
│                    BossDirector                        │
├────────────────────────────────────────────────────────┤
│  Input: WorldSnapshot + DirectorBudget                 │
│                                                        │
│  ┌──────────┐   ┌──────────┐   ┌──────────────────┐   │
│  │ Analyze  │ → │ Plan     │ → │ Apply within     │   │
│  │ Threat   │   │ Response │   │ Budget           │   │
│  └──────────┘   └──────────┘   └──────────────────┘   │
│                                                        │
│  Output: DirectorPlan { spawns, traps, terrain }       │
└────────────────────────────────────────────────────────┘

Telegraph System

Telegraphs provide fair warning to players before powerful attacks:

#![allow(unused)]
fn main() {
if !directive.telegraphs.is_empty() {
    for telegraph in &directive.telegraphs {
        // Display visual/audio warning
        // e.g., "Thread gathering at position (5, 3)"
    }
}
}

Telegraph types:

  • Thread gathering: Fate-weaving attack incoming
  • Ground tremor: Area attack warning
  • Shadow forming: Minion spawn location
  • Pattern shift: Phase transition imminent

Key Concepts

Budget-Constrained AI

Unlike traditional boss AI that can spam abilities, the Director operates under resource constraints. This creates:

  • Strategic decisions: Trade-offs between aggression and defense
  • Fair encounters: Players can anticipate resource depletion
  • Dynamic difficulty: Budget recharges over time

Phase-Based Behavior

The boss doesn’t just cycle through attacks. Phases emerge from:

  • HP thresholds: Different behavior at 75%, 50%, 25% HP
  • Player behavior: Adapts to aggressive vs defensive playstyles
  • Companion effectiveness: Responds to ally threat level

Troubleshooting

Missing veilweaver_slice feature

The advanced Warden Director requires the Veilweaver game data:

cargo run -p adaptive_boss --release --features veilweaver_slice

Empty plan output

If the Director returns an empty plan, the budget may already be exhausted or the boss doesn’t see any targets.

Source Location

  • Example: examples/adaptive_boss/src/main.rs
  • Director: astraweave-director/src/lib.rs
  • Warden: astraweave-director/src/oathbound_warden.rs (feature-gated)

Physics Demo Walkthrough

The physics_demo3d example showcases AstraWeave’s Rapier3D-based physics system with character controllers, destructible objects, water buoyancy, wind forces, and real-time rendering.

Running the Example

cargo run -p physics_demo3d --release

What It Demonstrates

  • Character controller: Physics-driven movement with climb detection
  • Destructible objects: Boxes that break under force
  • Water simulation: Buoyancy and drag in water volumes
  • Wind forces: Environmental wind affecting dynamic objects
  • Collision layers: Filtering which objects collide
  • Real-time rendering: wgpu-based visualization

Controls

Camera

KeyAction
W/A/S/DMove camera
SpaceCamera up
ShiftCamera down
Right-click + dragLook around
EscExit

Character

KeyAction
I/J/K/LMove character (forward/left/back/right)
CAttempt climb (hold near climbable surface)

Physics

KeyAction
FSpawn dynamic box at (0, 4, 0)
NSpawn destructible box
MBreak last destructible
BSpawn ragdoll placeholder
TToggle wind on/off
GToggle water volume on/off

Expected Behavior

When you run the demo:

  1. Initial scene: Ground plane, wall, character (green), water volume, destructible box
  2. Press J/L: Character slides left/right with physics
  3. Press F: Box drops and bounces
  4. Press T: Wind pushes boxes in the wind direction
  5. Press G: Water volume toggles, affecting buoyancy
  6. Press M: Destructible box shatters into fragments

Code Walkthrough

1. Physics World Setup

#![allow(unused)]
fn main() {
let mut phys = PhysicsWorld::new(vec3(0.0, -9.81, 0.0));
let _ground = phys.create_ground_plane(vec3(100.0, 0.0, 100.0), 1.0);
}
  • Creates a physics world with Earth gravity (9.81 m/s²)
  • Adds a ground plane at Y=0

2. Static Geometry

#![allow(unused)]
fn main() {
let _wall = phys.add_static_trimesh(
    &[
        vec3(5.0, 0.0, 0.0),
        vec3(5.0, 3.0, 0.0),
        // ... vertices
    ],
    &[[0, 1, 2], [3, 2, 1]],  // Triangle indices
    Layers::CHARACTER | Layers::DEFAULT,
);
}

Static meshes (walls, floors) don’t move but participate in collisions. The Layers flags control which objects can collide.

3. Character Controller

#![allow(unused)]
fn main() {
let char_id = phys.add_character(
    vec3(-2.0, 1.0, 0.0),  // Start position
    vec3(0.4, 0.9, 0.4),   // Capsule half-extents
);
}

The character controller:

  • Handles walking on slopes (up to 50° by default)
  • Provides ground detection and step climbing
  • Prevents tunneling through walls

4. Character Movement

#![allow(unused)]
fn main() {
// In the game loop:
let desired = vec3(self.move_dir.x, 0.0, self.move_dir.z);
self.phys.control_character(self.char_id, desired, dt, self.climb_try);
}

The controller converts desired velocity into physics-respecting movement.

5. Destructible Objects

#![allow(unused)]
fn main() {
let id = phys.add_destructible_box(
    vec3(-1.0, 1.0, 2.0),   // Position
    vec3(0.4, 0.4, 0.4),    // Half-size
    3.0,                     // Mass (kg)
    50.0,                    // Break force threshold
    12.0,                    // Fragment mass
);
}

When break force is exceeded:

#![allow(unused)]
fn main() {
self.phys.break_destructible(id);  // Shatters into fragments
}

6. Water Volume

#![allow(unused)]
fn main() {
phys.add_water_aabb(
    vec3(-2.0, 0.0, -2.0),  // Min corner
    vec3(2.0, 1.2, 2.0),    // Max corner
    1000.0,                  // Water density (kg/m³)
    0.8,                     // Drag coefficient
);
}

Objects inside the volume experience:

  • Buoyancy: Upward force proportional to submerged volume
  • Drag: Velocity-dependent resistance

7. Wind Forces

#![allow(unused)]
fn main() {
self.phys.set_wind(
    vec3(1.0, 0.0, 0.2).normalize(),  // Direction
    8.0,                               // Strength
);
}

Wind applies continuous force to dynamic bodies.

8. Rendering Loop

#![allow(unused)]
fn main() {
fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) {
    // Update physics
    self.phys.step();
    
    // Sync render instances from physics transforms
    self.instances.clear();
    for (handle, _body) in self.phys.bodies.iter() {
        if let Some(m) = self.phys.body_transform(id) {
            self.instances.push(Instance { transform: m, ... });
        }
    }
    
    // Render
    renderer.update_instances(&self.instances);
    renderer.render()?;
}
}

Physics Architecture

┌─────────────────────────────────────────────────────────┐
│                    PhysicsWorld                         │
├─────────────────────────────────────────────────────────┤
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────────┐ │
│  │ Rigid Bodies│  │ Colliders   │  │ Constraints     │ │
│  │ (dynamic)   │  │ (shapes)    │  │ (joints)        │ │
│  └─────────────┘  └─────────────┘  └─────────────────┘ │
│                                                         │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────────┐ │
│  │ Character   │  │ Water       │  │ Wind/Forces     │ │
│  │ Controller  │  │ Volumes     │  │                 │ │
│  └─────────────┘  └─────────────┘  └─────────────────┘ │
│                                                         │
│  Backend: Rapier3D 0.22                                 │
└─────────────────────────────────────────────────────────┘

Collision Layers

AstraWeave uses bitflag layers to control collisions:

#![allow(unused)]
fn main() {
pub struct Layers: u32 {
    const DEFAULT   = 0b0001;  // General objects
    const CHARACTER = 0b0010;  // Player/NPC capsules
    const PROJECTILE= 0b0100;  // Bullets, arrows
    const TRIGGER   = 0b1000;  // Non-solid triggers
}
}

Objects only collide if their layer masks overlap.

Performance Notes

The physics demo runs at 60 FPS with:

  • ~50 dynamic bodies
  • Character controller
  • Water volume intersection tests
  • Wind force application

For larger simulations, consider:

  • Reducing simulation substeps
  • Using simpler collision shapes
  • Spatial partitioning for many bodies

Troubleshooting

Character falls through floor

Ensure the ground plane is created before spawning the character.

Objects don’t collide

Check that collision layers overlap between the objects.

Low framerate

Reduce the number of dynamic bodies or increase the physics timestep.

Source Location

  • Example: examples/physics_demo3d/src/main.rs (301 lines)
  • Physics: astraweave-physics/src/lib.rs
  • Character Controller: astraweave-physics/src/character_controller.rs

Navmesh Demo Walkthrough

The navmesh_demo example demonstrates AstraWeave’s navigation mesh system - baking walkable surfaces from geometry and finding optimal paths with slope constraints.

Running the Example

cargo run -p navmesh_demo --release

What It Demonstrates

  • NavMesh baking: Converting triangle soup into walkable navigation data
  • Slope filtering: Excluding steep surfaces from navigation
  • A pathfinding*: Finding optimal routes across the mesh
  • Path visualization: Rendering waypoints in 3D space

Controls

KeyAction
W/A/S/DMove camera
SpaceCamera up
ShiftCamera down
Right-click + dragLook around
EscExit

Expected Behavior

When you run the demo:

  1. Yellow spheres: Triangle centers of the navigation mesh
  2. Green spheres: Path waypoints from start to goal
  3. Scene geometry: Flat area + ramp + elevated plateau

The path flows from bottom-left (-3.5, 0, -3.5) up the ramp to the plateau (11.5, 0.8, 0).

Code Walkthrough

1. Define Walkable Geometry

#![allow(unused)]
fn main() {
let tris = vec![
    // Main floor (two triangles forming a quad)
    tri(vec3(-4.0, 0.0, -4.0), vec3(4.0, 0.0, -4.0), vec3(4.0, 0.0, 4.0)),
    tri(vec3(-4.0, 0.0, -4.0), vec3(4.0, 0.0, 4.0), vec3(-4.0, 0.0, 4.0)),
    
    // Ramp going up (two triangles)
    tri(vec3(4.0, 0.0, -1.0), vec3(8.0, 0.8, -1.0), vec3(8.0, 0.8, 1.0)),
    tri(vec3(4.0, 0.0, -1.0), vec3(8.0, 0.8, 1.0), vec3(4.0, 0.0, 1.0)),
    
    // Elevated plateau (two triangles)
    tri(vec3(8.0, 0.8, -1.0), vec3(12.0, 0.8, -1.0), vec3(12.0, 0.8, 1.0)),
    tri(vec3(8.0, 0.8, -1.0), vec3(12.0, 0.8, 1.0), vec3(8.0, 0.8, 1.0)),
];
}

The geometry describes:

  • Floor: 8×8 meter flat area at Y=0
  • Ramp: Inclined surface from Y=0 to Y=0.8
  • Plateau: Elevated area at Y=0.8

2. Bake the NavMesh

#![allow(unused)]
fn main() {
let nav = NavMesh::bake(
    &tris,  // Triangle geometry
    0.4,    // Agent radius (meters)
    50.0,   // Max walkable slope (degrees)
);
}

The baking process:

  1. Voxelizes triangles into a 3D grid
  2. Filters steep surfaces (>50° rejected)
  3. Shrinks walkable area by agent radius (0.4m)
  4. Generates navigation polygon data

3. Find a Path

#![allow(unused)]
fn main() {
let start = vec3(-3.5, 0.0, -3.5);  // Bottom-left of floor
let goal = vec3(11.5, 0.8, 0.0);     // Center of plateau

let path = nav.find_path(start, goal);
}

The find_path function:

  1. Projects start/goal onto nearest navmesh triangle
  2. Runs A* across triangle adjacency graph
  3. Returns waypoint list (Vec<Vec3>)

4. Visualize Results

#![allow(unused)]
fn main() {
// Show triangle centers (yellow)
for t in &nav.tris {
    instances.push(Instance::from_pos_scale_color(
        t.center + vec3(0.0, 0.05, 0.0),  // Slightly above surface
        vec3(0.1, 0.1, 0.1),              // Small sphere
        [0.7, 0.7, 0.3, 1.0],             // Yellow
    ));
}

// Show path waypoints (green)
for p in &path {
    instances.push(Instance::from_pos_scale_color(
        *p + vec3(0.0, 0.08, 0.0),
        vec3(0.12, 0.12, 0.12),
        [0.2, 1.0, 0.4, 1.0],             // Green
    ));
}
}
┌─────────────────────────────────────────────────────────┐
│                      NavMesh                            │
├─────────────────────────────────────────────────────────┤
│  Input: Vec<Triangle>                                   │
│                                                         │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐  │
│  │ Voxelization │→ │ Slope Filter │→ │ Region Build │  │
│  └──────────────┘  └──────────────┘  └──────────────┘  │
│                                                         │
│  Output: tris: Vec<NavTri>, adjacency: HashMap          │
├─────────────────────────────────────────────────────────┤
│  find_path(start, goal) → Vec\<Vec3\>                   │
│  ├─ Project points to mesh                              │
│  ├─ A* search on triangle graph                         │
│  └─ String-pull path smoothing                          │
└─────────────────────────────────────────────────────────┘

Slope Filtering

The max_slope parameter (50° in this example) determines walkability:

Surface AngleWalkable? (50° max)
0° (flat)✅ Yes
30° (gentle)✅ Yes
50° (steep)✅ Yes (boundary)
60° (cliff)❌ No
90° (wall)❌ No

Steep surfaces are excluded from the navigation graph.

Agent Radius

The agent_radius (0.4m) shrinks walkable areas:

Before shrinking:        After shrinking:
┌────────────────┐      ┌────────────────┐
│                │      │   ┌────────┐   │
│  Walkable      │  →   │   │Walkable│   │
│                │      │   └────────┘   │
└────────────────┘      └────────────────┘
                           ↑ 0.4m margin

This prevents agents from clipping through walls.

Key Types

Triangle

#![allow(unused)]
fn main() {
pub struct Triangle {
    pub a: Vec3,  // First vertex
    pub b: Vec3,  // Second vertex
    pub c: Vec3,  // Third vertex
}
}
#![allow(unused)]
fn main() {
pub struct NavTri {
    pub center: Vec3,           // Triangle centroid
    pub normal: Vec3,           // Surface normal
    pub neighbors: Vec<usize>,  // Adjacent triangle indices
}
}

Performance Notes

NavMesh baking is an offline process:

  • Bake time: ~10ms for simple geometry, seconds for complex levels
  • Runtime pathfinding: <1ms for typical paths
  • Memory: ~100 bytes per navigation triangle

For large worlds, consider:

  • Pre-baking navmeshes at build time
  • Hierarchical navigation (coarse + fine meshes)
  • Path caching for frequently-used routes

Troubleshooting

Path returns empty

  • Check that start/goal are near the navmesh surface
  • Verify the mesh is connected (no isolated islands)

Agent walks through walls

  • Increase agent_radius parameter
  • Ensure wall geometry is included in navmesh input

Steep ramps not walkable

  • Increase max_slope angle (up to 89°)

Source Location

  • Example: examples/navmesh_demo/src/main.rs (196 lines)
  • NavMesh: astraweave-nav/src/navmesh.rs
  • Pathfinding: astraweave-nav/src/pathfinder.rs

Audio Spatial Demo Walkthrough

The audio_spatial_demo example demonstrates AstraWeave’s 3D spatial audio system - positional sound effects, music playback with crossfading, and listener tracking tied to the camera.

Running the Example

cargo run -p audio_spatial_demo --release

Note: This demo requires audio files in assets/audio/. If files are missing, you’ll see error messages but the demo will still run for visualization.

What It Demonstrates

  • 3D positional audio: Sounds panning based on listener position
  • Procedural beeps: Generated tones at specified frequencies
  • Music playback: Looped background music
  • Crossfading: Smooth transitions between music tracks
  • Listener tracking: Audio follows camera position/orientation

Controls

KeyAction
W/A/S/DMove camera (and listener)
SpaceCamera up
ShiftCamera down
Right-click + dragLook around
1Play beep at center (880 Hz)
2Play beep at left (-3, 1, 0) at 660 Hz
3Play beep at right (+3, 1, 0) at 440 Hz
MSwitch music track (crossfade)
EscExit

Expected Behavior

  1. Launch: Background music plays (if assets/audio/bgm.ogg exists)
  2. Press 1: High-pitched beep plays from center
  3. Press 2: Medium-pitched beep plays from left (pans left in headphones)
  4. Press 3: Low-pitched beep plays from right (pans right)
  5. Move camera with W/A/S/D: Audio panning shifts as listener moves
  6. Press M: Music crossfades to alternate track

Code Walkthrough

1. Audio Engine Setup

#![allow(unused)]
fn main() {
let mut audio = AudioEngine::new()?;
audio.set_master_volume(1.0);

// Try to play looped background music
let _ = audio.play_music(
    MusicTrack {
        path: "assets/audio/bgm.ogg".into(),
        looped: true,
    },
    1.0,  // Volume
);
}

The AudioEngine:

  • Initializes the audio device (rodio backend)
  • Manages multiple simultaneous sounds
  • Controls master/music/SFX volume buses

2. 3D Positional Beeps

#![allow(unused)]
fn main() {
// Center beep (880 Hz, 0.25s duration, 0.5 volume)
self.audio.play_sfx_3d_beep(
    100,                    // Emitter ID
    vec3(0.0, 1.0, 0.0),   // Position (center)
    880.0,                  // Frequency (Hz)
    0.25,                   // Duration (seconds)
    0.5,                    // Volume
);

// Left beep (660 Hz)
self.audio.play_sfx_3d_beep(
    101,
    vec3(-3.0, 1.0, 0.0),  // Left of center
    660.0,
    0.25,
    0.5,
);

// Right beep (440 Hz)
self.audio.play_sfx_3d_beep(
    102,
    vec3(3.0, 1.0, 0.0),   // Right of center
    440.0,
    0.25,
    0.5,
);
}

The emitter ID allows tracking/stopping specific sounds.

3. Music Crossfading

#![allow(unused)]
fn main() {
self.audio.play_music(
    MusicTrack {
        path: "assets/audio/bgm_alt.ogg".into(),
        looped: true,
    },
    1.25,  // Crossfade duration (seconds)
);
}

When switching tracks:

  1. Current music fades out over 1.25 seconds
  2. New music fades in simultaneously
  3. No audio gap or jarring transition

4. Listener Updates

#![allow(unused)]
fn main() {
// Calculate forward direction from camera yaw/pitch
let forward = glam::Quat::from_euler(
    glam::EulerRot::YXZ,
    self.camera.yaw,
    self.camera.pitch,
    0.0,
) * vec3(0.0, 0.0, -1.0);

// Update audio listener pose
self.audio.update_listener(ListenerPose {
    position: self.camera.position,
    forward,
    up: vec3(0.0, 1.0, 0.0),
});

// Advance audio processing
self.audio.tick(dt);
}

The listener pose determines:

  • Position: Where sounds are heard from
  • Forward: Which direction is “front”
  • Up: Which direction is “up” (for proper panning)

Audio Architecture

┌─────────────────────────────────────────────────────────┐
│                    AudioEngine                          │
├─────────────────────────────────────────────────────────┤
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────────┐ │
│  │ Music Bus   │  │ SFX Bus     │  │ Voice Bus       │ │
│  │ (looped)    │  │ (one-shot)  │  │ (dialogue)      │ │
│  └─────────────┘  └─────────────┘  └─────────────────┘ │
│                        ↓                                │
│              ┌─────────────────┐                        │
│              │ Spatial Mixer   │ ← ListenerPose         │
│              │ (3D panning)    │                        │
│              └─────────────────┘                        │
│                        ↓                                │
│              ┌─────────────────┐                        │
│              │ Master Volume   │                        │
│              └─────────────────┘                        │
│                        ↓                                │
│              ┌─────────────────┐                        │
│              │ rodio Output    │                        │
│              └─────────────────┘                        │
│                                                         │
│  Backend: rodio 0.17                                    │
└─────────────────────────────────────────────────────────┘

ListenerPose

#![allow(unused)]
fn main() {
pub struct ListenerPose {
    pub position: Vec3,  // World-space position
    pub forward: Vec3,   // Look direction (normalized)
    pub up: Vec3,        // Up vector (typically Y-up)
}
}

The audio system uses this to compute:

  • Attenuation: Sounds get quieter with distance
  • Panning: Left/right based on angle to listener
  • Doppler (optional): Pitch shift for moving sources

Volume Buses

#![allow(unused)]
fn main() {
// Individual bus control
audio.set_master_volume(1.0);   // Overall volume
audio.set_music_volume(0.8);    // Background music
audio.set_sfx_volume(1.0);      // Sound effects
audio.set_voice_volume(1.0);    // Dialogue/narration
}

Supported Formats

AstraWeave audio supports:

  • OGG Vorbis (recommended for music)
  • WAV (uncompressed, good for short SFX)
  • MP3 (widely compatible)
  • FLAC (lossless, large files)

Performance Notes

  • Simultaneous sounds: Up to 64 by default
  • Streaming: Large files (>1MB) stream from disk
  • CPU usage: <1% for typical game audio
  • Memory: ~10KB per short sound, streaming for long tracks

Troubleshooting

No audio output

  • Check system audio settings
  • Verify audio device is connected
  • Try different audio files

Crackling/popping

  • Increase audio buffer size
  • Reduce simultaneous sound count
  • Check for CPU throttling

Sounds don’t pan correctly

  • Verify update_listener() is called each frame
  • Check that forward/up vectors are normalized
  • Ensure sound positions are in world space

Missing audio files

The demo will print errors if assets/audio/bgm.ogg is missing. Create the directory and add audio files, or the demo will run silently.

Source Location

  • Example: examples/audio_spatial_demo/src/main.rs (215 lines)
  • Audio Engine: astraweave-audio/src/engine.rs
  • Spatial Mixer: astraweave-audio/src/spatial.rs

Fluids Demo

Interactive demonstration of AstraWeave’s PCISPH-based fluid simulation system with real-time rendering, multiple scenarios, and egui-powered controls.

cargo run -p fluids_demo --release

Overview

The fluids demo showcases:

  • Real-time particle-based fluid simulation using PCISPH solver
  • Multiple simulation scenarios (laboratory, ocean, waterfall, splash)
  • Interactive controls for spawning particles and adjusting parameters
  • Advanced rendering with caustics, foam, and refraction effects
  • LOD optimization for performance scaling

Quick Start

# Run the demo
cargo run -p fluids_demo --release

# Controls:
# WASD      - Move camera
# Mouse     - Rotate camera
# LMB       - Spawn particles at cursor
# RMB       - Apply force to particles
# Tab       - Switch scenario
# F3        - Toggle debug panel

Architecture

graph TD
    A[FluidSystem] --> B[PCISPH Solver]
    A --> C[FluidRenderer]
    A --> D[ScenarioManager]
    
    B --> E[Particle Grid]
    B --> F[Pressure Solve]
    B --> G[Viscosity]
    
    C --> H[GPU Rendering]
    C --> I[Caustics]
    C --> J[Foam Effects]
    
    D --> K[Laboratory]
    D --> L[Ocean]
    D --> M[Waterfall]
    D --> N[Splash]

Scenarios

Laboratory Scenario

Controlled environment for testing fluid behavior:

  • Bounded container simulation
  • Particle spawning controls
  • Parameter visualization
  • Ideal for learning system behavior

Ocean Scenario

Large-scale ocean wave simulation:

  • Skybox environment (HDR-based)
  • Wave dynamics
  • Foam generation
  • Infinite horizon effect

Waterfall Scenario

Vertical fluid flow demonstration:

  • Gravity-driven particle flow
  • Splash effects at base
  • Continuous particle recycling

Splash Scenario

High-energy impact simulation:

  • Droplet spawning
  • Surface tension effects
  • Spray particle generation

Configuration

Fluid System Parameters

The demo exposes all major fluid parameters through the debug panel:

#![allow(unused)]
fn main() {
// From the demo - actual parameter configuration
fluid_system.smoothing_radius = 0.5;    // SPH kernel radius
fluid_system.target_density = 1.0;       // Rest density
fluid_system.pressure_multiplier = 100.0; // Stiffness
fluid_system.viscosity = 0.01;           // Fluid thickness
fluid_system.gravity = -9.81;            // Gravity strength
fluid_system.cell_size = 1.2;            // Spatial hash cell size
fluid_system.grid_width = 64;            // Grid dimensions
fluid_system.grid_height = 64;
fluid_system.grid_depth = 64;
}

Quality Presets

The demo includes 4 quality levels (selectable in UI):

PresetParticlesCell SizeTarget FPS
Low5,0002.060+
Medium10,0001.560
High20,0001.245-60
Ultra50,0001.030-45

Interactive Features

Particle Spawning

Left-click in the viewport to spawn particles:

#![allow(unused)]
fn main() {
// Demo implementation - ray-plane intersection for spawn position
fn spawn_particles_at_cursor(&mut self) {
    let (origin, dir) = self.screen_to_world_ray(
        self.mouse_pos[0], 
        self.mouse_pos[1]
    );
    
    // Intersect with Y=5 plane (fluid center height)
    if let Some(hit_pos) = self.ray_plane_intersection(origin, dir, 5.0) {
        let positions: Vec<[f32; 3]> = (0..count)
            .map(|i| {
                let angle = (i as f32 / count as f32) * TAU;
                let radius = (i as f32 * 0.1).sin() * 0.5;
                [hit_pos.x + angle.cos() * radius, 
                 hit_pos.y + 0.5, 
                 hit_pos.z + angle.sin() * radius]
            })
            .collect();
        
        self.fluid_system.spawn_particles(&queue, &positions, &velocities, colors);
    }
}
}

Force Application

Right-click and drag to apply forces:

  • Drag direction determines force vector
  • Force magnitude scales with drag distance
  • Affects all particles in a configurable radius

Debug Panel

Press F3 to toggle the debug panel (egui-based):

  • Real-time particle count
  • FPS / frame time graph
  • Simulation step timing
  • LOD level indicator
  • Parameter sliders

LOD System

The demo uses FluidLodManager and FluidOptimizationController for dynamic quality scaling:

#![allow(unused)]
fn main() {
// LOD configuration from demo
let lod_config = FluidLodConfig {
    target_fps: 60.0,
    min_particles: 1000,
    max_particles: 50000,
    lod_levels: vec![
        LodLevel { distance: 10.0, particle_scale: 1.0 },
        LodLevel { distance: 25.0, particle_scale: 0.5 },
        LodLevel { distance: 50.0, particle_scale: 0.25 },
    ],
};

// Controller maintains target FPS
optimization_controller.update(last_frame_time_ms);
let suggested_count = optimization_controller.suggested_particle_count();
}

Rendering Pipeline

GPU-Accelerated Rendering

The demo uses wgpu 25 for all rendering:

#![allow(unused)]
fn main() {
// Render pass structure
let fluid_renderer = FluidRenderer::new(&device, width, height, format);

// Each frame:
// 1. Render skybox to scene texture (if ocean scenario)
// 2. Render fluid particles with refraction
// 3. Apply caustics and foam effects
// 4. Composite to swapchain
// 5. Render egui overlay
}

Shader Features

The demo includes multiple WGSL shaders:

  • fluid.wgsl - Main particle rendering
  • ocean.wgsl - Ocean surface simulation
  • skybox.wgsl - HDR environment mapping
  • glass.wgsl - Refraction effects

Performance Tips

Achieving 60 FPS

  1. Reduce particle count - Start with Medium preset
  2. Increase cell size - Fewer neighbor lookups
  3. Use LOD - Enable optimization controller
  4. Disable foam - Reduces particle overhead

Profiling

Enable Tracy profiling:

cargo run -p fluids_demo --release --features profiling

Key spans to watch:

  • fluid_system.step() - Physics simulation
  • fluid_renderer.render() - GPU draw calls
  • egui.render() - UI overhead

Dependencies

[dependencies]
astraweave-physics = { path = "../../astraweave-physics" }
astraweave-fluids = { path = "../../astraweave-fluids" }
winit = { workspace = true }
wgpu = { workspace = true }
glam = { workspace = true }
egui = { workspace = true }
egui-wgpu = { workspace = true }
egui-winit = { workspace = true }

Troubleshooting

Low FPS

Symptom: Frame rate drops below 30 FPS

Solutions:

  1. Switch to Low quality preset
  2. Reduce spawn_burst_size in debug panel
  3. Close other GPU-intensive applications
  4. Check GPU driver is up to date

No Particles Visible

Symptom: Scene renders but no fluid particles

Solutions:

  1. Check scenario is initialized (Tab to switch)
  2. Click LMB to spawn particles manually
  3. Verify camera position (reset with R key)

Skybox Not Loading

Symptom: Ocean scenario shows black sky

Cause: Missing HDR file at assets/hdri/polyhaven/kloppenheim_02_puresky_2k.hdr

Solution: Download from Poly Haven or disable skybox feature

wgpu Adapter Errors

Symptom: “No suitable adapter found”

Solutions:

  1. Update GPU drivers
  2. Try different backend: WGPU_BACKEND=vulkan cargo run -p fluids_demo
  3. Ensure system meets minimum requirements (DX12 or Vulkan 1.1 capable GPU)

Unified Showcase

A comprehensive rendering demonstration combining shadows, terrain, GLTF models, and skybox rendering in a self-contained wgpu application.

cargo run -p unified_showcase --release

Overview

The unified showcase demonstrates:

  • Shadow mapping with 2048×2048 depth textures
  • GLTF model loading with materials and transforms
  • Procedural terrain with multi-texture blending
  • HDR skybox rendering
  • 4× MSAA antialiasing
  • First-person camera controls

This is a standalone renderer example that doesn’t depend on engine render crates—ideal for learning wgpu patterns.

Quick Start

# Run the showcase
cargo run -p unified_showcase --release

# Controls:
# WASD      - Move camera
# Mouse     - Look around (click to capture)
# Escape    - Release mouse
# Space     - Move up
# Shift     - Move down

Features

Shadow Mapping

Real-time shadow casting with PCF (Percentage Closer Filtering):

#![allow(unused)]
fn main() {
// Shadow texture configuration
let shadow_texture = device.create_texture(&wgpu::TextureDescriptor {
    label: Some("Shadow Texture"),
    size: wgpu::Extent3d {
        width: 2048,
        height: 2048,
        depth_or_array_layers: 1,
    },
    format: wgpu::TextureFormat::Depth32Float,
    usage: TextureUsages::RENDER_ATTACHMENT | TextureUsages::TEXTURE_BINDING,
    ...
});

// Comparison sampler for PCF
let shadow_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
    compare: Some(wgpu::CompareFunction::Less),
    mag_filter: wgpu::FilterMode::Linear,
    min_filter: wgpu::FilterMode::Linear,
    ...
});
}

GLTF Model Loading

The demo includes a custom GLTF loader for scene objects:

#![allow(unused)]
fn main() {
// Load and render GLTF models
mod gltf_loader;

// Material indices for different model parts
pine_bark_mat: usize,
pine_leaves_mat: usize,
tower_wood_mat: usize,
tower_stone_mat: usize,
}

Supported GLTF features:

  • Mesh geometry (positions, normals, UVs)
  • Base color textures
  • Transform hierarchies
  • Multiple materials per model

Terrain Rendering

Procedural terrain with height-based texture blending:

  • Grass texture - Low elevation areas
  • Rock texture - Steep slopes
  • Snow texture - High elevation peaks
  • Normal mapping - Surface detail
  • Triplanar projection - Seamless texturing on steep surfaces
// terrain.wgsl - height-based texture blending
fn blend_terrain_textures(height: f32, slope: f32) -> vec4<f32> {
    let grass_weight = smoothstep(0.0, 0.3, 1.0 - slope);
    let rock_weight = smoothstep(0.2, 0.5, slope);
    let snow_weight = smoothstep(0.7, 1.0, height);
    
    return grass * grass_weight + rock * rock_weight + snow * snow_weight;
}

Skybox

HDR-based environment rendering:

#![allow(unused)]
fn main() {
// Skybox pipeline - renders at depth 1.0 (far plane)
let sky_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
    depth_stencil: Some(wgpu::DepthStencilState {
        depth_write_enabled: false,
        depth_compare: wgpu::CompareFunction::LessEqual,
        ...
    }),
    ...
});
}

MSAA

4× multisampling for smooth edges:

#![allow(unused)]
fn main() {
multisample: wgpu::MultisampleState {
    count: 4,
    mask: !0,
    alpha_to_coverage_enabled: false,
}
}

Architecture

graph TD
    A[ShowcaseApp] --> B[Render Pipelines]
    A --> C[Scene Data]
    A --> D[Camera System]
    
    B --> E[Main Pipeline]
    B --> F[Sky Pipeline]
    B --> G[Terrain Pipeline]
    B --> H[Shadow Pipeline]
    
    C --> I[Meshes]
    C --> J[Materials]
    C --> K[Scene Objects]
    
    E --> L[MSAA Texture]
    L --> M[Resolve to Swapchain]

Render Pass Order

  1. Shadow Pass - Render scene from light perspective
  2. Skybox Pass - Render environment (no depth write)
  3. Terrain Pass - Render ground with shadows
  4. Object Pass - Render GLTF models with shadows
  5. MSAA Resolve - Resolve 4× samples to swapchain

Pipeline Details

Main Render Pipeline

#![allow(unused)]
fn main() {
let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
    layout: Some(&render_pipeline_layout),
    vertex: wgpu::VertexState {
        module: &shader,
        entry_point: Some("vs_main"),
        buffers: &[Vertex::desc()],
    },
    fragment: Some(wgpu::FragmentState {
        module: &shader,
        entry_point: Some("fs_main"),
        targets: &[Some(wgpu::ColorTargetState {
            format: config.format,
            blend: Some(wgpu::BlendState::ALPHA_BLENDING),
            write_mask: wgpu::ColorWrites::ALL,
        })],
    }),
    primitive: wgpu::PrimitiveState {
        topology: wgpu::PrimitiveTopology::TriangleList,
        front_face: wgpu::FrontFace::Ccw,
        cull_mode: Some(wgpu::Face::Back),
    },
    depth_stencil: Some(wgpu::DepthStencilState {
        format: wgpu::TextureFormat::Depth32Float,
        depth_write_enabled: true,
        depth_compare: wgpu::CompareFunction::Less,
    }),
    multisample: wgpu::MultisampleState { count: 4, ... },
});
}

Bind Group Layouts

The showcase uses 4 main bind group layouts:

SlotNameContents
0CameraView-projection matrix, camera position
1LightLight view-projection, position, color, shadow map
2MaterialAlbedo texture, sampler
3ModelPer-object transform matrix

Vertex Format

#![allow(unused)]
fn main() {
#[repr(C)]
struct Vertex {
    position: [f32; 3],
    normal: [f32; 3],
    uv: [f32; 2],
    color: [f32; 4],
    tangent: [f32; 4],
}
}

Shaders

Main Shader (shader_v2.wgsl)

@vertex
fn vs_main(in: VertexInput) -> VertexOutput {
    var out: VertexOutput;
    let world_pos = model.model * vec4<f32>(in.position, 1.0);
    out.clip_position = camera.view_proj * world_pos;
    out.world_position = world_pos.xyz;
    out.world_normal = normalize((model.model * vec4<f32>(in.normal, 0.0)).xyz);
    out.uv = in.uv;
    out.shadow_position = light.view_proj * world_pos;
    return out;
}

@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
    let shadow = calculate_shadow(in.shadow_position);
    let albedo = textureSample(t_diffuse, s_diffuse, in.uv);
    let lighting = calculate_lighting(in.world_normal, light.position, shadow);
    return vec4<f32>(albedo.rgb * lighting, albedo.a);
}

Shadow Shader

Minimal shader for depth-only rendering:

@vertex
fn vs_main(in: VertexInput) -> @builtin(position) vec4<f32> {
    return light.view_proj * model.model * vec4<f32>(in.position, 1.0);
}

Dependencies

[dependencies]
winit = { workspace = true }
wgpu = { workspace = true }
glam = { workspace = true }
pollster = { workspace = true }
bytemuck = { workspace = true }
image = { version = "0.25", features = ["png", "jpeg", "hdr", "webp"] }
gltf = { version = "1.4", features = ["utils", "KHR_materials_unlit"] }

Note: This example is self-contained and doesn’t use astraweave-render crates. It’s designed as a clean reference implementation.

Asset Requirements

The demo expects assets in the following locations:

assets/
├── models/
│   ├── pine_tree.glb
│   └── watch_tower.glb
├── textures/
│   ├── grass_albedo.png
│   ├── rock_albedo.png
│   └── snow_albedo.png
└── hdri/
    └── polyhaven/
        └── kloppenheim_02_puresky_2k.hdr

Performance

Typical performance on mid-range hardware:

MetricValue
Shadow Resolution2048×2048
MSAA
Draw Calls10-50 (scene dependent)
Frame Time8-12 ms (60-80+ FPS)
VRAM~200-500 MB

Optimization Opportunities

For production use, consider:

  1. Cascaded Shadow Maps - Multiple shadow cascades for large scenes
  2. Instanced Rendering - Batch similar objects
  3. Frustum Culling - Skip off-screen objects
  4. LOD System - Distance-based mesh simplification

Learning Value

This example teaches:

  1. wgpu Pipeline Creation - Complete setup from device to swapchain
  2. Bind Group Management - Proper resource binding patterns
  3. Shadow Mapping - Light-space rendering and PCF sampling
  4. GLTF Integration - Loading and rendering real 3D assets
  5. Multi-Pass Rendering - Coordinating multiple render passes
  6. MSAA Setup - Multisampling with resolve

Troubleshooting

Black Screen

Symptom: Window opens but shows only black

Solutions:

  1. Check GPU supports wgpu backends (Vulkan/DX12/Metal)
  2. Update graphics drivers
  3. Enable logging: RUST_LOG=info cargo run -p unified_showcase

Missing Textures

Symptom: Models render as solid colors

Cause: Asset files not found

Solution: Ensure assets/ directory contains required textures

Slow Performance

Symptom: FPS below 30

Solutions:

  1. Reduce shadow map resolution (edit source)
  2. Disable MSAA (change count to 1)
  3. Close other GPU applications
  4. Try different wgpu backend: WGPU_BACKEND=dx12

GLTF Load Errors

Symptom: Panic on startup with GLTF error

Solutions:

  1. Verify GLTF files exist in assets/models/
  2. Check GLTF files are valid (test in Blender)
  3. Ensure GLTF uses supported features only

Troubleshooting Examples

This guide helps resolve common issues when running AstraWeave examples.

Working Examples

The following examples are verified to build and run:

ExampleCommandNotes
hello_companioncargo run -p hello_companion --releaseExpected panic (demonstrates tool validation)
fluids_democargo run -p fluids_demo --releaseInteractive PCISPH fluid simulation
unified_showcasecargo run -p unified_showcase --releasewgpu rendering with shadows, terrain
physics_demo3dcargo run -p physics_demo3d --releaseRapier3D physics with character controller
navmesh_democargo run -p navmesh_demo --releaseNavMesh baking and A* pathfinding
audio_spatial_democargo run -p audio_spatial_demo --release3D spatial audio
adaptive_bosscargo run -p adaptive_boss --releaseDirector AI for boss encounters
ipc_loopbackcargo run -p ipc_loopback --releaseIPC communication test

Known Compilation Issues

Some examples have compilation issues due to API evolution:

ExampleIssueStatus
debug_overlayegui 0.32 API changesNeeds update
ui_controls_demoegui/winit version mismatchesNeeds update
visual_3dwinit 0.30 migrationNeeds update
rhai_authoringRhai sync/send traitsNeeds refactor
npc_town_demoMultiple API mismatchesNeeds update

Build Aliases

Use these workspace aliases for safer builds:

cargo build-core
cargo check-all
cargo clippy-all

Common Errors

“No suitable graphics adapter”

thread 'main' panicked at 'No suitable graphics adapter found'

Solution: Update graphics drivers or use software rendering:

export WGPU_BACKEND=gl
cargo run -p hello_companion

“AI model not found”

Error: Could not load AI model

Solution: Examples use mock AI by default. This panic is expected when no LLM is configured.

Vulkan Not Found (Linux)

sudo apt-get install mesa-vulkan-drivers vulkan-tools

Missing Build Tools (Windows)

Install Visual Studio with C++ build tools.

Getting Help

Benchmark Dashboard

Performance

Performance Benchmarks

All benchmarks executed via Criterion.rs statistical benchmarking. Results sourced from the Master Benchmark Report v5.55 (January 2026). Every measurement can be reproduced with documented commands.

~1,500 benchmarks across 76 sections Criterion.rs + Real Ollama validation Production Ready (Grade A+)
85% p99 headroom at 1,000 entities @ 60 FPS
15.0µs ECS entity spawn (100), 70% under target
286ns GOAP planning (full), 97% under target
9.8ns GOAP cache hit, 99% under target
1.27ms p50 frame time at 1,000 entities
2.42ms p99 frame time at 1,000 entities
Executive summary

Key validation results — January 2026

Engine has 85% headroom at p99 for 1,000 entities at 60 FPS. All core systems operate well under their per-frame budgets.

SystemBenchmarkTargetActualMargin
ECSEntity spawn (100)<50µs15.0µs70% under
ECSEntity spawn (1000)<500µs106.7µs79% under
AIGOAP planning (full)<10µs286ns97% under
AIGOAP planning (cache)<1µs9.8ns99% under
Framep50 @ 1k entities<16.67ms1.27ms92% under
Framep99 @ 1k entities<16.67ms2.42ms85% under
Best performers

Operations achieving sub-nanosecond and sub-10ns latency.

These benchmarks represent AstraWeave's highest-performing operations, many achieving sub-nanosecond latency.

Sub-nanosecond

Operations under 1 ns

OperationLatencyThroughput
Multi-Agent Per-Agent12-20 ps50-83 trillion/sec
Nav Sliver Triangles99-104 ps10 billion/sec
Multi-Agent Per-Plan0.29-0.31 ns3.2-3.4 billion/sec
Pan Mode Switching418 ps
State Transitions0.49-0.51 ns
Emotion Blending0.55 ns
Multi-Agent Feedback0.73-0.76 ns1.3 billion/sec
MSAA Resize 720p582-645 ps
UI Settings Nav696 ps
Clear Frame0.72 ns
Weather Attenuation730-783 ps22.8 billion/frame
Room Overlap Check571-629 ps
Frustum AABB Inside889-915 ps
GPU Budget Check890 ps-1.05 ns17 billion/frame
Sub-10 nanosecond

Operations under 10 ns

OperationLatencyNotes
SparseSet Lookup (1k)1.56 ns37× faster than BTreeMap
SIMD Movement1.73 ns2.26× faster than naive
Quat Multiply1.34 nsglam SIMD-optimized
Quat Slerp2.10 nsRotation interpolation
Context Switching2.38 ns7M switches/frame
GOAP (no enemies)3.46-3.56 nsIdle detection FREE
Component Deserialize3.50 nsPostcard ECS
Physics Stage3.63 ns7,580× vs perception
RAG Engine Creation4.61 nsZero-cost abstraction
Mat4 Multiply4.28 nsglam SIMD matrix
GOAP (close)4.68-5.11 nsTactical decision
GOAP (far)7.04-7.86 nsStrategic decision
SparseSet Insert9.9 ns13× faster than BTreeMap
Core systems

Engine subsystem benchmarks

Each subsystem is benchmarked independently with Criterion.rs. Results include budget analysis against the 16.67ms frame budget.

ECS

Entity-Component-System

BenchmarkResultBudget
Spawn empty (10k)645µsExcellent
Spawn + Position (10k)5.6msProduction
Despawn empty (10k)287µsFixed
Despawn + comp (10k)2.5ms68% faster
Iteration (10k)273µsExcellent
Archetype trans (10k)5.6msWithin budget
AI

Planning and orchestration

BenchmarkResultNotes
GOAP (cache hit)9.8 ns99% under
GOAP (cache miss)286 ns97% under
GOAP next (idle)3.5 ns4.7B ops/frame
GOAP next (close)5.1 ns3.5B ops/frame
GOAP next (far)7.9 ns2.4B ops/frame
Multi-agent (10)1.34-1.39µs66-68% faster
Arbiter GOAP101.7 ns982× faster
Arbiter LLM575 ns86× faster
Mode transition221.9 ns45× faster
Physics

Simulation and collision

BenchmarkResultNotes
Character move43.8-52.0 ns12-26% faster
Rigid body lookup14.8-15.4 ns10× vs character
Raycast (empty)26.3-31.5 ns8-23% faster
Rigid body batch47µsExcellent
Spatial hash99.96% fewerGrid optimization
Fluids

SPH simulation (A+ grade)

BenchmarkResultNotes
Particles (1K-10K)5.3-110µs100-322 Melem/s
Spatial hashing163µs-5.6ms38-62% improved
SPH kernels (100K)171-223µspoly6/spiky
Density/pressure3.5-10.5ms
Sim step (1K)1.8-3.0ms
Multi-step450-500µs45-57% faster
GPU data prep0.9-2.6 nsSub-nanosecond
Rendering

wgpu pipeline benchmarks

CategoryBenchmarkResult
CullingAABB inside889-915 ps
CullingContains point951 ps-1.01 ns
MSAAMode check795-842 ps
MSAAResize 720p582-645 ps
CameraView matrix4.42-5.36 ns
CameraToggle mode1.72-2.29 ns
InstancingSavings calc1.43-1.52 ns
WeatherParticle1.95-2.04 ns
WeatherAttenuation730-783 ps
Animation and navigation

Interpolation and pathfinding

BenchmarkResultNotes
vec3_lerp1.69-1.83 ns57% faster
quat_to_rotation1.63-1.73 ns36% faster
Tween update22.1 ns
Spring update14.2 ns1.6× vs tween
Sliver triangles99-104 psSub-nanosecond
Impossible paths3.7-24.9µsFast-fail
Maze stress1.6-108µs
Pathfind short7.5µsExcellent
Frame budget analysis

Target: 60 FPS = 16.67ms per frame

Budget breakdown at 1,000 entities shows the engine using only 14.5% of the available frame time at p99, leaving 85% headroom for gameplay logic and rendering.

Budget breakdown (1,000 entities)
SystemTimeBudget %Status
ECS Core85 µs0.51%
AI (500 agents)471 µs2.83%
Physics (100 bodies)47 µs0.28%
Core loop (5k)529 µs3.17%
p50 Total1.27 ms7.6%
p99 Total2.42 ms14.5%
Headroom14.25 ms85%
Scalability projections
Entity Countp99 EstimateFeasibility
1,0002.42 ms✅ 85% headroom
5,000~8-10 ms✅ 40-50% headroom
10,000~15-18 ms⚠️ Near budget
20,000+>30 ms❌ Requires 30 FPS
60 FPS capacity

ECS time at scale

Entity CountECS TimeBudget Used
1,000~85µs0.51%
5,000~529µs3.17%
10,000~1ms~6%
Reproduce locally

Running benchmarks

Every benchmark can be reproduced with a single command. Criterion.rs provides confidence intervals and statistical rigor.

Full suite

# Run all Criterion benchmarks
cargo bench --workspace

# Run with odyssey automation (captures logs)
./scripts/benchmark_odyssey.ps1 \
  -OutDir benchmark_results/$(Get-Date -Format 'yyyy-MM-dd')

Per-crate benchmarks

# ECS benchmarks
cargo bench -p astraweave-ecs

# AI benchmarks
cargo bench -p astraweave-ai

# Physics benchmarks
cargo bench -p astraweave-physics

# Render benchmarks
cargo bench -p astraweave-render

Generating HTML reports

# Open Criterion HTML report
cargo bench -p astraweave-ecs -- --save-baseline main
# Reports at: target/criterion/*/report/index.html
Philosophy

Benchmarks as verification artifacts, not marketing numbers.

Reproducibility Every claimed measurement has a command that reproduces it.
Raw logs All runs capture raw output for auditing.
Statistical rigor Criterion.rs provides confidence intervals.
Adversarial testing 22 adversarial benchmark sections stress edge cases.
Real hardware No synthetic workloads — real game scenarios.
Further reading

See also

Benchmark Methodology

This document describes how AstraWeave performance measurements are collected, validated, and reported.

Measurement Philosophy

“Prove it, don’t hype it.”

Every performance claim in AstraWeave documentation:

  • Has a command that reproduces it
  • Captures raw logs for auditing
  • Uses statistical analysis for reliability
  • Is validated against real workloads, not synthetic benchmarks

Tools & Infrastructure

Criterion.rs

All microbenchmarks use Criterion.rs for statistical rigor.

Why Criterion:

  • Statistical analysis with confidence intervals
  • Outlier detection and filtering
  • Baseline comparison (catch regressions)
  • HTML report generation

Location: target/criterion/**/base/estimates.json

Odyssey Runner

For full-suite benchmarking, use the automation script:

./scripts/benchmark_odyssey.ps1 -OutDir benchmark_results/$(Get-Date -Format 'yyyy-MM-dd')

Outputs:

  • environment.txt - OS/CPU/RAM, rustc/cargo version, git SHA
  • packages_with_benches.txt - Inventory of benchmarked crates
  • run_order.txt - Execution order
  • bench_<package>.log - Raw benchmark output per crate
  • run_results.json - Success/fail status

Statistical Practices

Confidence Intervals

Criterion provides 95% confidence intervals for all measurements. We report:

  • Point estimate: The measured mean
  • Lower bound: 95% CI lower
  • Upper bound: 95% CI upper

Example: 1.34 ns [1.33 ns, 1.35 ns] means the true mean is 95% likely within that range.

Warm-Up & Iterations

Default Criterion settings:

  • Warm-up: 3 seconds (eliminates cold-start artifacts)
  • Measurement: 5 seconds minimum
  • Sample size: 100 samples minimum

Outlier Handling

Criterion automatically detects and reports outliers:

  • Mild outliers: 1.5× IQR
  • Severe outliers: 3× IQR

Outliers are flagged in reports but included in analysis (not discarded).


Benchmark Categories

1. Microbenchmarks

Single-operation measurements (e.g., “how long does vec3_lerp take?”).

Location: crates/*/benches/*.rs

Example:

#![allow(unused)]
fn main() {
fn bench_vec3_lerp(c: &mut Criterion) {
    let a = Vec3::new(0.0, 0.0, 0.0);
    let b = Vec3::new(1.0, 1.0, 1.0);
    c.bench_function("vec3_lerp", |bencher| {
        bencher.iter(|| a.lerp(b, 0.5))
    });
}
}

2. Adversarial Benchmarks

Stress tests for edge cases and worst-case scenarios.

Categories (22 sections):

  • Gameplay adversarial (massive damage, rapid hits)
  • Input adversarial (input storms, frame clear)
  • Math adversarial (IEEE-754 edge cases: infinity, NaN, denormals)
  • Navigation adversarial (sliver triangles, impossible paths)
  • Security adversarial (script sandboxing, anti-cheat)
  • And 17 more…

Purpose: Ensure production stability under extreme conditions.

3. Integration Benchmarks

End-to-end measurements of complete systems.

Example: “Full game loop with 5,000 entities”

#![allow(unused)]
fn main() {
fn bench_full_game_loop(c: &mut Criterion) {
    let mut world = setup_world_with_entities(5000);
    c.bench_function("full_game_loop/5000_entities", |bencher| {
        bencher.iter(|| world.tick(1.0 / 60.0))
    });
}
}

4. Scalability Benchmarks

Measure performance across varying input sizes.

Example: Entity spawn at 10, 100, 1000, 10000 entities.

#![allow(unused)]
fn main() {
fn bench_entity_spawn(c: &mut Criterion) {
    let mut group = c.benchmark_group("entity_spawn");
    for size in [10, 100, 1000, 10000] {
        group.bench_with_input(
            BenchmarkId::from_parameter(size),
            &size,
            |b, &size| b.iter(|| spawn_entities(size))
        );
    }
    group.finish();
}
}

Environment Standardization

Hardware Requirements

Benchmark machines should document:

  • CPU: Model, cores, clock speed
  • RAM: Size, speed
  • OS: Windows/Linux/macOS version
  • Rust: rustc --version
  • Profile: Always --release

Isolation Practices

For reliable measurements:

  1. Close unnecessary applications
  2. Disable turbo boost (optional, for consistency)
  3. Run multiple times to verify reproducibility
  4. Use cargo bench -- --noplot to skip HTML generation (faster)

Reporting Standards

Master Benchmark Report

All benchmark results are consolidated in:

  • docs/masters/MASTER_BENCHMARK_REPORT.md

Update triggers:

  • Any benchmark changes >10%
  • New benchmarks added
  • Performance regressions discovered

Version Tracking

Each report version documents:

  • Version number (e.g., v5.55)
  • Date of measurement
  • Key changes since last version
  • Critical fixes applied

Regression Detection

Baseline Comparison

# Save current as baseline
cargo bench -p astraweave-ecs -- --save-baseline main

# Compare against baseline
cargo bench -p astraweave-ecs -- --baseline main

CI Integration

GitHub Actions workflow (benchmark.yml) runs benchmarks on:

  • Pull requests (compare against main)
  • Nightly builds (detect gradual regressions)

Alert Thresholds

ChangeAction
< 5%Normal variance, no action
5-10%Flag for review
10-20%Investigate root cause
> 20%Block merge, fix required

Coverage Methodology

Test coverage is measured using cargo-llvm-cov:

# Generate coverage report
cargo llvm-cov --workspace --html

# View report
open target/llvm-cov/html/index.html

Coverage by Tier

TierCratesTargetActual
Tier 1 (Critical)ecs, core, ai, render80%75.3%
Tier 2 (Important)physics, nav, gameplay75%72.6%
Tier 3 (Supporting)audio, scene, terrain70%71.8%
Tier 4 (Specialized)fluids, llm, prompts65%71.5%

Per-Crate Coverage (verified January 2026)

CrateCoverageStatus
astraweave-ecs83.2%
astraweave-core79.1%
astraweave-ai71.3%
astraweave-render67.4%
astraweave-physics76.8%
astraweave-fluids94.2%✅ A+
astraweave-nav72.1%
astraweave-gameplay68.9%
astraweave-terrain71.5%
astraweave-audio69.2%
astraweave-scene74.6%
astraweave-llm58.3%⚠️ Beta

Reproducing Results

Quick Verification

# Verify ECS benchmarks match documentation
cargo bench -p astraweave-ecs -- entity_spawn/empty/10000

# Expected: ~645µs (±10%)

Full Reproduction

  1. Clone repository at documented commit
  2. Run ./scripts/benchmark_odyssey.ps1
  3. Compare benchmark_results/*/ against documented values
  4. Variance > 20% indicates environment difference

See Also

Optimization Guide

This guide covers performance optimization techniques for AstraWeave games.

General Principles

Measure First

Never optimize without profiling.

Before optimizing:

  1. Run benchmarks to establish baseline
  2. Profile with Tracy or cargo flamegraph
  3. Identify actual bottlenecks
  4. Optimize the measured slow path

Amdahl’s Law

Only parallelize work that benefits from it:

  • Sequential overhead limits speedup
  • 0.15-22.4% of work is parallelizable in typical games
  • Maximum theoretical speedup: ~1.24× in many cases

ECS Optimization

Batching Over Scattering

#![allow(unused)]
fn main() {
// ❌ Slow: Scattered access
for entity in entities {
    let pos = world.get_mut::<Position>(entity);
    pos.x += 1.0;
}

// ✅ Fast: Batch access (3-5× faster)
let mut positions: Vec<_> = world.query::<&mut Position>().iter_mut().collect();
for pos in &mut positions {
    pos.x += 1.0;
}
}

Why: Archetype lookup is O(log n) per access. Batching amortizes this cost.

Component Layout

  • Keep frequently-accessed components small
  • Group components that are accessed together
  • Use u32 over u64 when possible (cache efficiency)

Entity Spawning

#![allow(unused)]
fn main() {
// ❌ Slow: One-by-one spawning
for _ in 0..1000 {
    world.spawn((Position::default(), Velocity::default()));
}

// ✅ Fast: Batch spawning
world.spawn_batch((0..1000).map(|_| (Position::default(), Velocity::default())));
}

AI Optimization

GOAP Caching

GOAP cache hit: 9.8 ns vs cache miss: 286 ns (29× faster)

#![allow(unused)]
fn main() {
// Enable GOAP caching
let planner = GoapPlanner::new()
    .with_cache_size(1000)  // Cache 1000 plans
    .with_cache_ttl(Duration::from_secs(5));
}

Agent Update Staggering

#![allow(unused)]
fn main() {
// Update 1/10th of agents per frame (10-frame rotation)
for (i, agent) in agents.iter_mut().enumerate() {
    if i % 10 == frame_count % 10 {
        agent.update(&snapshot);
    }
}
}

AIArbiter Cooldown

#![allow(unused)]
fn main() {
// Reduce LLM request frequency for better performance
let arbiter = AIArbiter::new(executor, goap, bt)
    .with_llm_cooldown(15.0);  // 15 seconds between requests
}

Physics Optimization

Spatial Hashing

AstraWeave’s spatial hash reduces collision checks by 99.96%:

#![allow(unused)]
fn main() {
// Automatic with default physics setup
let physics = PhysicsWorld::new()
    .with_spatial_hash(cell_size: 2.0);  // Tune cell size to object density
}

Tuning: Cell size should be ~2× average object radius.

Rigid Body Batching

#![allow(unused)]
fn main() {
// Batch physics updates (47µs for 100 bodies)
physics.step_batch(&mut bodies, dt);
}

Sleep Optimization

#![allow(unused)]
fn main() {
// Enable sleeping for static objects
body.enable_sleeping(linear_threshold: 0.01, angular_threshold: 0.01);
}

Rendering Optimization

Frustum Culling

Frustum check: 889-915 ps (essentially free)

#![allow(unused)]
fn main() {
// Automatic in renderer
renderer.enable_frustum_culling(true);
}

LOD Selection

#![allow(unused)]
fn main() {
// Configure LOD distances
mesh.set_lod_distances(&[10.0, 50.0, 100.0, 500.0]);
}

Instanced Rendering

Instance overhead: 1.43-1.52 ns per calculation

#![allow(unused)]
fn main() {
// Batch identical meshes
let instances = entities
    .filter(|e| e.mesh_id == mesh_id)
    .map(|e| e.transform)
    .collect();
renderer.draw_instanced(mesh, &instances);
}

Memory Optimization

SparseSet vs BTreeMap

OperationSparseSetBTreeMapWinner
Lookup (1000)1.56 ns59 µs37× SparseSet
Insert (1000)9.9 ns129 ns13× SparseSet

AstraWeave uses SparseSet for entity storage—architecture decision validated.

Component Pooling

#![allow(unused)]
fn main() {
// Reuse allocations
let mut pool = ComponentPool::<Particle>::new(10000);
for _ in 0..10000 {
    let particle = pool.allocate();
    // Use particle...
    pool.deallocate(particle);
}
}

Profiling Tools

Tracy Integration

#![allow(unused)]
fn main() {
// Add Tracy spans to hot paths
#[cfg(feature = "profiling")]
astraweave_profiling::span!("AI::Update");
}

Criterion Benchmarks

# Profile specific benchmark
cargo bench -p astraweave-ecs -- entity_spawn/empty/10000 --profile-time 10

# Generate flamegraph
cargo flamegraph -- --bench entity_bench

Frame Timing

#![allow(unused)]
fn main() {
// Log frame timing
let start = Instant::now();
// ... game loop ...
let frame_time = start.elapsed();
if frame_time > Duration::from_millis(16) {
    warn!("Frame budget exceeded: {:?}", frame_time);
}
}

Common Pitfalls

1. Over-Parallelization

Problem: Adding Rayon to small workloads

Why: Thread pool overhead (~50-100 µs) exceeds benefit for <5 ms work

Solution: Only parallelize workloads >5 ms

2. Allocations in Hot Paths

Problem: Vec::new() in per-frame code

Solution: Pre-allocate and reuse

#![allow(unused)]
fn main() {
// ❌ Bad: Allocates every frame
fn update(&mut self) {
    let temp = Vec::new();  // Allocation!
    // ...
}

// ✅ Good: Reuse allocation
fn update(&mut self) {
    self.temp_buffer.clear();  // No allocation
    // ...
}
}

3. Debug vs Release

Problem: Testing performance in debug mode

Why: Debug can be 10-100× slower

Solution: Always benchmark with --release

cargo bench -p astraweave-ecs  # Automatically uses release

See Also

Performance Budgets

This document defines frame time allocations for AstraWeave games targeting 60 FPS.

Target Specification

MetricValueNotes
Target FPS60Standard for action games
Frame Budget16.67 ms1000ms ÷ 60
Headroom Target20%3.33 ms reserved for spikes
Available Budget13.34 ms80% of frame

Budget Allocation

SystemBudget (ms)PercentageNotes
Game Logic2.012%ECS, AI, gameplay
Physics2.515%Simulation, collision
Rendering6.036%Draw calls, GPU sync
Audio0.53%Spatial audio, mixing
Networking1.06%Sync, prediction
UI0.53%HUD, menus
I/O0.53%Streaming, saves
Headroom3.6722%Spike absorption
Total16.67100%

Visual Breakdown

Frame Budget: 16.67ms
├── Game Logic ████░░░░░░░░░░░░░░░░ 2.0ms (12%)
├── Physics    █████░░░░░░░░░░░░░░░ 2.5ms (15%)
├── Rendering  ████████████░░░░░░░░ 6.0ms (36%)
├── Audio      █░░░░░░░░░░░░░░░░░░░ 0.5ms (3%)
├── Networking ██░░░░░░░░░░░░░░░░░░ 1.0ms (6%)
├── UI         █░░░░░░░░░░░░░░░░░░░ 0.5ms (3%)
├── I/O        █░░░░░░░░░░░░░░░░░░░ 0.5ms (3%)
└── Headroom   ███████░░░░░░░░░░░░░ 3.67ms (22%)

Game Logic Budget (2.0 ms)

Sub-Allocation

ComponentBudget (µs)Notes
ECS core (1k entities)850.51% of frame
AI updates (500 agents)471GOAP + arbiter
Gameplay systems500Combat, quests, dialogue
Script execution300Rhai scripts
Event processing200Input, triggers
Total1,55622% headroom within budget

Scaling Guidelines

Entity CountECS BudgetAI AgentsFeasibility
1,00085 µs500✅ Comfortable
5,000529 µs1,000✅ Within budget
10,0001 ms2,000⚠️ Near limit
20,000+>2 ms❌ Consider 30 FPS

Physics Budget (2.5 ms)

Sub-Allocation

ComponentBudget (µs)Notes
Rigid body simulation500100 bodies
Character controllers200Player + NPCs
Collision detection800Spatial hash accelerated
Raycasts300AI, weapons, cameras
Triggers/sensors200Zone detection
Total2,00020% headroom

Collision Budget

With spatial hashing (99.96% check reduction):

ObjectsNaiveWith HashSavings
1004,950 checks2 checks99.96%
1,000499,500 checks~20 checks99.996%
10,00049.9M checks~200 checks99.9996%

Rendering Budget (6.0 ms)

Sub-Allocation

StageBudget (ms)Notes
Culling0.5Frustum, occlusion
Shadow passes1.0CSM, spot shadows
G-Buffer1.5Deferred geometry
Lighting1.0Clustered, IBL
Post-processing1.0Bloom, SSAO, TAA
UI overlay0.5HUD, debug
GPU sync0.5Fence waiting
Total6.0

Draw Call Budget

CategoryCallsNotes
Opaque geometry500With instancing
Transparent100Sorted, no batching
Shadows200Per cascade
UI50Batched
Total850Target maximum

Audio Budget (0.5 ms)

Sub-Allocation

ComponentBudget (µs)Notes
Voice mixing2004-bus mixer
Spatial positioning1503D audio
DSP effects100Reverb, filters
Stream decode50Music, ambient
Total500

Monitoring Budget Compliance

Runtime Checks

#![allow(unused)]
fn main() {
use std::time::Instant;

struct FrameBudget {
    logic_budget: Duration,
    physics_budget: Duration,
    render_budget: Duration,
}

impl FrameBudget {
    fn check(&self, phase: &str, elapsed: Duration, budget: Duration) {
        if elapsed > budget {
            warn!(
                "{} exceeded budget: {:?} > {:?} ({}%)",
                phase,
                elapsed,
                budget,
                (elapsed.as_micros() * 100 / budget.as_micros())
            );
        }
    }
}
}

Tracy Zones

#![allow(unused)]
fn main() {
#[cfg(feature = "profiling")]
{
    astraweave_profiling::zone!("GameLogic");
    // ... game logic ...
}
}

30 FPS Budget (Alternative)

For complex simulations or lower-end hardware:

MetricValue
Frame Budget33.33 ms
Headroom6.67 ms (20%)
Available26.66 ms

When to use 30 FPS:

  • Open worlds with >20k entities
  • Complex physics simulations
  • Lower-end hardware targets
  • Cinematic experiences

Budget Violations

Severity Levels

ViolationDurationResponse
Minor1-2 framesLog, continue
Moderate3-10 framesReduce quality
Severe>10 framesEmergency LOD

Automatic Quality Scaling

#![allow(unused)]
fn main() {
// Reduce quality on budget overrun
if frame_time > Duration::from_millis(18) {
    quality.reduce_shadow_resolution();
}
if frame_time > Duration::from_millis(20) {
    quality.reduce_draw_distance();
}
if frame_time > Duration::from_millis(25) {
    quality.disable_post_processing();
}
}

See Also

Veilweaver: Threads of Eternity

Reference Implementation for AstraWeave AI-Native Game Engine

Veilweaver is a complete AI-native Action RPG that serves as AstraWeave’s flagship reference implementation. It demonstrates the full capabilities of the engine in a production game context.

Overview

Veilweaver showcases:

  • AI Companions with persistent memory and adaptive behavior
  • Adaptive Boss Battles that learn from player strategies
  • Dynamic World Events driven by AI directors
  • Emergent Narrative through AI-orchestrated storytelling
  • Procedural Content integrated with hand-crafted design

Current Status

Veilweaver is developed in a separate repository and is currently in active development.

The Veilweaver documentation is being migrated to this documentation site. For the latest information:

  • GitHub Repository: Games-VEILWEAVER
  • Design Documents: Located in docs/archive/projects/veilweaver/

Key Features Demonstrated

AI Companion System

Veilweaver’s companions use AstraWeave’s full AI stack:

  • Perception Bus: Companions observe the game world in real-time
  • GOAP Planning: Goal-oriented action planning for complex behaviors
  • Memory System: Companions remember interactions and adapt
  • LLM Integration: Natural language dialogue and reasoning

Weaving System

The signature gameplay mechanic demonstrates:

  • Tool Sandbox: AI-validated player abilities
  • Deterministic Simulation: Consistent weaving effects
  • Physics Integration: Weave-affected environments

World Design

Three biome zones showcase terrain systems:

  1. Loomspire Sanctum - Tutorial and hub area
  2. Echo Grove - Forest exploration zone
  3. Fractured Cliffs - Vertical traversal challenges

Getting Started with Veilweaver

# Clone the Veilweaver repository
git clone https://github.com/lazyxeon/Games-VEILWEAVER.git

# Build and run
cd Games-VEILWEAVER
cargo run --release

Additional Documentation

For detailed Veilweaver documentation, see the Games-VEILWEAVER repository:

  • Game mechanics guide
  • AI integration deep-dive
  • World design principles
  • Asset creation workflow
  • Modding support guide

See Also

Contributing to AstraWeave

Thank you for your interest in contributing to AstraWeave! This guide will help you get started with contributing to the project.

Table of Contents

Code of Conduct

We are committed to providing a welcoming and inclusive environment for all contributors. Please be respectful and constructive in all interactions.

Getting Started

Before you start contributing, please:

  1. Read the Building from Source guide
  2. Familiarize yourself with the Code Style Guide
  3. Review the Testing Guide
  4. Check existing Issues and Pull Requests

How to Contribute

Reporting Bugs

When reporting bugs, please include:

  • Clear title: Describe the issue concisely
  • Environment: OS, Rust version, GPU model
  • Steps to reproduce: Detailed steps to trigger the bug
  • Expected behavior: What should happen
  • Actual behavior: What actually happens
  • Logs: Relevant error messages or stack traces
**Environment:**
- OS: Windows 11
- Rust: 1.75.0
- GPU: NVIDIA RTX 4080

**Steps to Reproduce:**
1. Create a new companion with perception
2. Add emotion system
3. Run the simulation

**Expected:** Companion should respond to stimuli
**Actual:** Panic in emotion processing thread

**Logs:**
\`\`\`
thread 'emotion' panicked at 'index out of bounds'
\`\`\`

Suggesting Features

For feature requests, please:

  • Check if the feature already exists or is planned
  • Describe the use case and problem it solves
  • Provide examples of how it would work
  • Consider implementation complexity and alternatives

Submitting Pull Requests

  1. Fork the repository and create a new branch:

    git checkout -b feature/my-awesome-feature
    
  2. Make your changes following our coding standards

  3. Write or update tests to cover your changes

  4. Run the full test suite:

    cargo test --all-features
    cargo clippy --all-targets --all-features
    cargo fmt --check
    
  5. Update documentation if needed

  6. Commit with clear messages:

    git commit -m "feat(ai): Add emotion blending system"
    
  7. Push and create a Pull Request:

    git push origin feature/my-awesome-feature
    
Keep PRs focused on a single feature or fix. Large PRs are harder to review and merge.

Development Setup

Prerequisites

  • Rust 1.75.0 or later
  • Git
  • A supported GPU with Vulkan/DirectX 12/Metal

Initial Setup

# Clone the repository
git clone https://github.com/verdentlabs/astraweave.git
cd astraweave

# Build the project
cargo build

# Run tests
cargo test

# Run examples
cargo run --example basic_companion

Development Workflow

# Create a new branch
git checkout -b feature/my-feature

# Make changes and test frequently
cargo test

# Run clippy for linting
cargo clippy --all-targets --all-features

# Format code
cargo fmt

# Commit changes
git commit -m "feat: Add feature description"

Coding Standards

General Principles

  • Clarity over cleverness: Write code that is easy to understand
  • Performance matters: AstraWeave is a high-performance engine
  • Safety first: Prefer safe abstractions over unsafe code
  • Documentation: Document all public APIs

Rust Conventions

#![allow(unused)]
fn main() {
// Good: Clear naming and documentation
/// Calculates emotion intensity based on stimuli strength.
///
/// # Arguments
/// * `stimulus` - The input stimulus value (0.0 to 1.0)
/// * `sensitivity` - Sensitivity multiplier (default: 1.0)
///
/// # Returns
/// Emotion intensity clamped to [0.0, 1.0]
pub fn calculate_emotion_intensity(stimulus: f32, sensitivity: f32) -> f32 {
    (stimulus * sensitivity).clamp(0.0, 1.0)
}

// Bad: Unclear naming and no documentation
pub fn calc(s: f32, m: f32) -> f32 {
    (s * m).min(1.0).max(0.0)
}
}

Naming Conventions

  • Types: PascalCase (e.g., EmotionSystem, CompanionState)
  • Functions: snake_case (e.g., process_emotion, update_behavior)
  • Constants: SCREAMING_SNAKE_CASE (e.g., MAX_COMPANIONS, DEFAULT_TICK_RATE)
  • Modules: snake_case (e.g., emotion_system, companion_ai)

Error Handling

#![allow(unused)]
fn main() {
// Good: Use Result for recoverable errors
pub fn load_companion_config(path: &Path) -> Result<CompanionConfig, ConfigError> {
    let contents = std::fs::read_to_string(path)
        .map_err(|e| ConfigError::FileRead(path.to_path_buf(), e))?;
    
    serde_json::from_str(&contents)
        .map_err(|e| ConfigError::Parse(e))
}

// Good: Use panic only for programming errors
pub fn get_emotion(&self, index: usize) -> &Emotion {
    assert!(index < self.emotions.len(), "Emotion index out of bounds");
    &self.emotions[index]
}
}

Testing Requirements

All contributions must include appropriate tests:

Unit Tests

#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_emotion_intensity_clamping() {
        assert_eq!(calculate_emotion_intensity(0.5, 1.0), 0.5);
        assert_eq!(calculate_emotion_intensity(1.5, 1.0), 1.0);
        assert_eq!(calculate_emotion_intensity(-0.5, 1.0), 0.0);
    }

    #[test]
    fn test_emotion_blending() {
        let joy = Emotion::new("joy", 0.8);
        let calm = Emotion::new("calm", 0.6);
        let blended = blend_emotions(&joy, &calm, 0.5);
        assert_eq!(blended.intensity, 0.7);
    }
}
}

Integration Tests

Place integration tests in tests/ directory:

#![allow(unused)]
fn main() {
// tests/companion_integration.rs
use astraweave_ai::*;

#[test]
fn test_companion_lifecycle() {
    let mut companion = CompanionBuilder::new()
        .with_perception()
        .with_emotion()
        .build();
    
    companion.update(0.016);
    assert!(companion.is_active());
}
}

Benchmarks

For performance-critical code, add benchmarks:

#![allow(unused)]
fn main() {
use criterion::{criterion_group, criterion_main, Criterion, black_box};

fn benchmark_emotion_processing(c: &mut Criterion) {
    c.bench_function("process_100_emotions", |b| {
        let mut system = EmotionSystem::new();
        b.iter(|| {
            for _ in 0..100 {
                system.process(black_box(0.016));
            }
        });
    });
}

criterion_group!(benches, benchmark_emotion_processing);
criterion_main!(benches);
}

Coverage Requirements

  • New features should have >80% code coverage
  • Bug fixes must include a regression test
  • Run coverage locally:
    cargo tarpaulin --all-features --workspace --timeout 300
    
PRs with insufficient test coverage will not be merged.

Documentation Requirements

Public API Documentation

All public items must have documentation:

#![allow(unused)]
fn main() {
/// Represents an AI companion with emotion and behavior systems.
///
/// A companion can perceive its environment, process emotions, and
/// exhibit behaviors based on its internal state.
///
/// # Examples
///
/// ```
/// use astraweave_ai::Companion;
///
/// let companion = Companion::new("Buddy");
/// companion.update(0.016);
/// ```
pub struct Companion {
    // ...
}
}

Module Documentation

Add module-level documentation in lib.rs or mod.rs:

#![allow(unused)]
fn main() {
//! # Emotion System
//!
//! This module provides the emotion processing system for AI companions.
//! Emotions are processed based on stimuli and decay over time.
//!
//! ## Example
//!
//! ```
//! use astraweave_ai::emotion::EmotionSystem;
//!
//! let mut system = EmotionSystem::new();
//! system.add_emotion("joy", 0.8);
//! system.update(0.016);
//! ```
}

Changelog Updates

Update CHANGELOG.md for significant changes:

## [Unreleased]

### Added
- Emotion blending system for smoother transitions
- New `CompanionBuilder` for easier companion creation

### Changed
- Improved perception system performance by 30%

### Fixed
- Fixed emotion decay rate calculation

Code Review Process

What Reviewers Look For

  1. Correctness: Does the code work as intended?
  2. Tests: Are there adequate tests?
  3. Documentation: Is the code well-documented?
  4. Style: Does it follow our coding standards?
  5. Performance: Are there any performance concerns?
  6. Safety: Are there any unsafe patterns?

Review Timeline

  • Initial review: Within 48 hours
  • Follow-up reviews: Within 24 hours
  • Merging: After approval from at least one maintainer

Addressing Feedback

Respond to all review comments, even if just to acknowledge them.
  • Make requested changes in new commits
  • Don’t force-push during review (makes tracking changes hard)
  • Mark conversations as resolved when addressed
  • Ask questions if feedback is unclear

Approval Process

  1. All CI checks must pass
  2. At least one maintainer approval required
  3. No unresolved review comments
  4. Up-to-date with the main branch

Community

Getting Help

Stay Updated

  • Watch the repository for updates
  • Follow our blog
  • Subscribe to release notifications

Thank you for contributing to AstraWeave! Your efforts help make AI-native gaming better for everyone.

Building from Source

This guide covers how to build AstraWeave from source on all supported platforms.

Table of Contents

Prerequisites

Required Software

Rust Toolchain

AstraWeave requires Rust 1.75.0 or later.

# Install Rust via rustup
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# Or on Windows, download from: https://rustup.rs/

# Verify installation
rustc --version
cargo --version
Keep Rust updated for the latest performance improvements and bug fixes:
\`\`\`bash
rustup update
\`\`\`

Git

# Linux (Debian/Ubuntu)
sudo apt install git

# macOS
brew install git

# Windows
# Download from https://git-scm.com/

GPU Requirements

AstraWeave requires a modern GPU with support for:

  • Vulkan 1.2+ (Linux, Windows)
  • DirectX 12 (Windows)
  • Metal (macOS)

Supported GPUs:

  • NVIDIA: GTX 1060 or newer
  • AMD: RX 580 or newer
  • Intel: Arc series or newer integrated graphics
Older GPUs may work but are not officially supported and may have reduced performance.

Platform-Specific Instructions

Windows

System Dependencies

  1. Visual Studio Build Tools (required for linking):

    • Download Visual Studio 2022 Build Tools
    • Select “Desktop development with C++” workload
    • Minimum components:
      • MSVC v143 build tools
      • Windows 10/11 SDK
  2. Vulkan SDK (optional, for Vulkan backend):

    • Download from LunarG
    • Install with default options

Building on Windows

# Clone the repository
git clone https://github.com/verdentlabs/astraweave.git
cd astraweave

# Build (release mode recommended)
cargo build --release

# Run tests
cargo test --release

# Run an example
cargo run --release --example basic_companion
Windows Defender may flag the first build as suspicious. This is normal for newly compiled executables.

Windows-Specific Issues

Long Path Support: Enable long paths if you encounter path length errors:

# Run as Administrator
New-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem" `
  -Name "LongPathsEnabled" -Value 1 -PropertyType DWORD -Force

Linux

System Dependencies

Debian/Ubuntu:

sudo apt update
sudo apt install -y \
  build-essential \
  pkg-config \
  libx11-dev \
  libxcursor-dev \
  libxrandr-dev \
  libxi-dev \
  libasound2-dev \
  libudev-dev \
  vulkan-tools \
  libvulkan-dev

Fedora/RHEL:

sudo dnf install -y \
  gcc gcc-c++ \
  pkg-config \
  libX11-devel \
  libXcursor-devel \
  libXrandr-devel \
  libXi-devel \
  alsa-lib-devel \
  systemd-devel \
  vulkan-tools \
  vulkan-loader-devel

Arch Linux:

sudo pacman -S --needed \
  base-devel \
  libx11 \
  libxcursor \
  libxrandr \
  libxi \
  alsa-lib \
  vulkan-tools \
  vulkan-headers

Building on Linux

# Clone the repository
git clone https://github.com/verdentlabs/astraweave.git
cd astraweave

# Build
cargo build --release

# Run tests
cargo test --release

# Run an example
cargo run --release --example basic_companion

GPU Drivers

NVIDIA:

# Ubuntu
sudo ubuntu-drivers autoinstall

# Or manually install latest driver
sudo apt install nvidia-driver-535

AMD:

# Mesa drivers (usually pre-installed)
sudo apt install mesa-vulkan-drivers

Intel:

sudo apt install mesa-vulkan-drivers intel-media-va-driver

Verify Vulkan support:

vulkaninfo | grep "deviceName"

macOS

System Dependencies

  1. Xcode Command Line Tools:

    xcode-select --install
    
  2. Homebrew (recommended):

    /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
    

Building on macOS

# Clone the repository
git clone https://github.com/verdentlabs/astraweave.git
cd astraweave

# Build
cargo build --release

# Run tests
cargo test --release

# Run an example
cargo run --release --example basic_companion
macOS uses Metal as the graphics backend. Ensure your system is macOS 10.15+ with Metal-compatible GPU.

Apple Silicon (M1/M2/M3)

AstraWeave fully supports Apple Silicon:

# Verify you're using the ARM toolchain
rustc --version --verbose | grep host
# Should show: aarch64-apple-darwin

# Build optimized for Apple Silicon
RUSTFLAGS="-C target-cpu=native" cargo build --release

Build Configurations

Release vs Debug

Debug build (fast compilation, slow runtime):

cargo build
# Binary: target/debug/

Release build (slow compilation, fast runtime):

cargo build --release
# Binary: target/release/
Always use release builds for performance testing and gameplay. Debug builds can be 10-100x slower.

Feature Flags

AstraWeave uses Cargo features for optional functionality:

# Build with all features
cargo build --all-features

# Build with specific features
cargo build --features "ai,physics,fluids"

# Build without default features
cargo build --no-default-features --features "minimal"

Available features:

  • ai - AI companion systems (default)
  • physics - Physics simulation
  • fluids - Fluid dynamics
  • editor - Level editor tools
  • networking - Multiplayer support
  • serialization - Save/load support

Optimization Profiles

Custom profiles are defined in Cargo.toml:

[profile.release]
opt-level = 3
lto = "thin"
codegen-units = 1

[profile.dev]
opt-level = 1

[profile.bench]
inherits = "release"
lto = "fat"

Build with specific profile:

cargo build --profile bench

Cross-Compilation

Targeting Different Platforms

# Install target
rustup target add x86_64-pc-windows-gnu

# Build for target
cargo build --target x86_64-pc-windows-gnu --release

Common targets:

  • x86_64-pc-windows-msvc - Windows (MSVC)
  • x86_64-unknown-linux-gnu - Linux
  • x86_64-apple-darwin - macOS (Intel)
  • aarch64-apple-darwin - macOS (Apple Silicon)

Troubleshooting

Common Build Errors

“linker not found”

Windows: Install Visual Studio Build Tools

Linux:

sudo apt install build-essential

macOS:

xcode-select --install

“could not find native library ‘vulkan’”

Windows: Install Vulkan SDK

Linux:

sudo apt install libvulkan-dev

macOS: Not needed (uses Metal)

Out of Memory During Build

Reduce parallel compilation:

cargo build --release -j 2

Or increase swap space on Linux:

sudo fallocate -l 8G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile

“failed to run custom build command”

Clean the build cache:

cargo clean
cargo build --release

Performance Issues

Slow Compilation

Use the faster linker:

Linux (lld):

sudo apt install lld
export RUSTFLAGS="-C link-arg=-fuse-ld=lld"
cargo build --release

macOS (zld):

brew install michaeleisel/zld/zld
export RUSTFLAGS="-C link-arg=-fuse-ld=/usr/local/bin/zld"
cargo build --release

Windows (lld-link, included with MSVC):

$env:RUSTFLAGS="-C link-arg=/DEBUG:NONE"
cargo build --release

Enable sccache for cached builds:

cargo install sccache
export RUSTC_WRAPPER=sccache

Runtime Performance

Verify release mode:

# Should show /release/ in path
cargo run --release -- --version

Check GPU usage:

# Windows
nvidia-smi

# Linux
watch -n 1 nvidia-smi

Debugging Build Issues

Enable verbose output:

cargo build --release --verbose

Check dependencies:

cargo tree

Update dependencies:

cargo update

Next Steps

After successfully building AstraWeave:

  1. Run the test suite: See Testing Guide
  2. Try the examples: cargo run --release --example <name>
  3. Read the tutorials: Getting Started
  4. Join the community: Discord
Congratulations! You've successfully built AstraWeave from source. You're ready to start developing AI-native games.

Testing Guide

This guide covers testing practices for AstraWeave, including unit tests, integration tests, benchmarks, and coverage analysis.

Table of Contents

Running Tests

Quick Start

# Run all tests
cargo test

# Run tests in release mode (faster for compute-heavy tests)
cargo test --release

# Run tests for a specific package
cargo test -p astraweave-ai

# Run tests with all features enabled
cargo test --all-features

Verbose Output

# Show println! output from tests
cargo test -- --nocapture

# Show one test per line
cargo test -- --test-threads=1 --nocapture

# Run specific test
cargo test test_emotion_processing -- --exact

Filtering Tests

# Run tests matching a pattern
cargo test emotion

# Run only unit tests (exclude integration tests)
cargo test --lib

# Run only integration tests
cargo test --test '*'

# Run only documentation tests
cargo test --doc
Use `cargo test --help` and `cargo test -- --help` to see all available options.

Writing Unit Tests

Basic Unit Tests

Place unit tests in the same file as the code they test:

#![allow(unused)]
fn main() {
pub fn calculate_emotion_decay(initial: f32, decay_rate: f32, dt: f32) -> f32 {
    initial * (1.0 - decay_rate * dt).max(0.0)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_emotion_decay_basic() {
        let result = calculate_emotion_decay(1.0, 0.5, 0.1);
        assert_eq!(result, 0.95);
    }

    #[test]
    fn test_emotion_decay_never_negative() {
        let result = calculate_emotion_decay(1.0, 2.0, 1.0);
        assert_eq!(result, 0.0);
    }
}
}

Testing with Floating Point

Use approximate comparisons for floating-point values:

#![allow(unused)]
fn main() {
#[test]
fn test_emotion_blending() {
    let result = blend_emotions(0.8, 0.6, 0.5);
    assert!((result - 0.7).abs() < 1e-6, "Expected ~0.7, got {}", result);
}

// Or use the approx crate
use approx::assert_relative_eq;

#[test]
fn test_emotion_blending_approx() {
    let result = blend_emotions(0.8, 0.6, 0.5);
    assert_relative_eq!(result, 0.7, epsilon = 1e-6);
}
}

Testing Errors

Test error conditions explicitly:

#![allow(unused)]
fn main() {
#[test]
fn test_invalid_companion_id() {
    let world = CompanionWorld::new();
    let result = world.get_companion(CompanionId(999));
    assert!(result.is_err());
    assert!(matches!(result, Err(CompanionError::NotFound(_))));
}

#[test]
#[should_panic(expected = "index out of bounds")]
fn test_panic_on_invalid_index() {
    let emotions = vec![Emotion::new("joy", 0.8)];
    let _ = emotions[10]; // Should panic
}
}

Testing with Resources

Use setup and teardown for resource management:

#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
    use super::*;
    use std::fs;

    fn setup_test_config() -> (CompanionConfig, PathBuf) {
        let temp_dir = std::env::temp_dir().join("astraweave_test");
        fs::create_dir_all(&temp_dir).unwrap();
        
        let config_path = temp_dir.join("config.json");
        let config = CompanionConfig::default();
        
        (config, config_path)
    }

    fn teardown_test_config(path: &Path) {
        let _ = fs::remove_file(path);
    }

    #[test]
    fn test_config_save_load() {
        let (config, path) = setup_test_config();
        
        config.save(&path).unwrap();
        let loaded = CompanionConfig::load(&path).unwrap();
        
        assert_eq!(config, loaded);
        
        teardown_test_config(&path);
    }
}
}

Parameterized Tests

Test multiple cases efficiently:

#![allow(unused)]
fn main() {
#[test]
fn test_emotion_clamping() {
    let test_cases = vec![
        (0.5, 0.5),   // Normal case
        (1.5, 1.0),   // Above max
        (-0.5, 0.0),  // Below min
        (0.0, 0.0),   // Edge case: zero
        (1.0, 1.0),   // Edge case: max
    ];

    for (input, expected) in test_cases {
        let result = clamp_emotion(input);
        assert_eq!(result, expected, "Failed for input {}", input);
    }
}
}

Testing Async Code

For async functions, use tokio::test or similar:

#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
    use super::*;
    use tokio::test;

    #[tokio::test]
    async fn test_async_companion_update() {
        let mut companion = Companion::new("TestBot");
        companion.update_async(0.016).await;
        assert!(companion.is_initialized());
    }

    #[tokio::test]
    async fn test_concurrent_updates() {
        let companion = Arc::new(Mutex::new(Companion::new("Concurrent")));
        
        let handles: Vec<_> = (0..10)
            .map(|_| {
                let c = Arc::clone(&companion);
                tokio::spawn(async move {
                    c.lock().await.update_async(0.016).await;
                })
            })
            .collect();

        for handle in handles {
            handle.await.unwrap();
        }
    }
}
}

Integration Tests

Integration Test Structure

Integration tests live in the tests/ directory:

astraweave-ai/
├── src/
│   └── lib.rs
├── tests/
│   ├── companion_lifecycle.rs
│   ├── emotion_integration.rs
│   └── perception_behavior.rs
└── Cargo.toml

Example Integration Test

#![allow(unused)]
fn main() {
// tests/companion_lifecycle.rs
use astraweave_ai::*;

#[test]
fn test_complete_companion_lifecycle() {
    // Creation
    let mut companion = CompanionBuilder::new()
        .with_name("IntegrationBot")
        .with_perception()
        .with_emotion()
        .with_behavior()
        .build();

    assert!(!companion.is_initialized());

    // Initialization
    companion.initialize();
    assert!(companion.is_initialized());

    // Update cycle
    for _ in 0..100 {
        companion.update(0.016);
    }

    // Verify state
    assert!(companion.get_emotion("joy").is_some());
    assert!(companion.perception_active());

    // Shutdown
    companion.shutdown();
    assert!(!companion.is_active());
}

#[test]
fn test_companion_persistence() {
    let temp_path = std::env::temp_dir().join("companion_save.json");

    // Create and save
    let original = CompanionBuilder::new()
        .with_name("Persistent")
        .build();
    original.save(&temp_path).unwrap();

    // Load and verify
    let loaded = Companion::load(&temp_path).unwrap();
    assert_eq!(original.name(), loaded.name());

    std::fs::remove_file(&temp_path).unwrap();
}
}

Integration Test Helpers

Create test utilities in tests/common/mod.rs:

#![allow(unused)]
fn main() {
// tests/common/mod.rs
use astraweave_ai::*;

pub fn create_test_companion() -> Companion {
    CompanionBuilder::new()
        .with_name("TestCompanion")
        .with_perception()
        .with_emotion()
        .build()
}

pub fn create_test_world() -> CompanionWorld {
    let mut world = CompanionWorld::new();
    for i in 0..5 {
        world.add_companion(create_test_companion());
    }
    world
}

pub fn assert_emotion_in_range(emotion: &Emotion, min: f32, max: f32) {
    assert!(
        emotion.intensity >= min && emotion.intensity <= max,
        "Emotion {} out of range [{}, {}]",
        emotion.intensity,
        min,
        max
    );
}
}
#![allow(unused)]
fn main() {
// tests/world_simulation.rs
mod common;

use common::*;

#[test]
fn test_world_simulation() {
    let mut world = create_test_world();
    
    for _ in 0..100 {
        world.update(0.016);
    }

    assert_eq!(world.companion_count(), 5);
}
}

Benchmarks

Setting Up Benchmarks

Benchmarks use the criterion crate. Add to Cargo.toml:

[dev-dependencies]
criterion = "0.5"

[[bench]]
name = "emotion_benchmarks"
harness = false

Writing Benchmarks

#![allow(unused)]
fn main() {
// benches/emotion_benchmarks.rs
use criterion::{black_box, criterion_group, criterion_main, Criterion, BenchmarkId};
use astraweave_ai::*;

fn benchmark_emotion_update(c: &mut Criterion) {
    let mut system = EmotionSystem::new();
    
    c.bench_function("emotion_update_single", |b| {
        b.iter(|| {
            system.update(black_box(0.016));
        });
    });
}

fn benchmark_emotion_blending(c: &mut Criterion) {
    let mut group = c.benchmark_group("emotion_blending");
    
    for count in [10, 100, 1000] {
        group.bench_with_input(
            BenchmarkId::from_parameter(count),
            &count,
            |b, &count| {
                let emotions: Vec<_> = (0..count)
                    .map(|i| Emotion::new(&format!("emotion_{}", i), 0.5))
                    .collect();
                
                b.iter(|| {
                    blend_emotion_array(black_box(&emotions))
                });
            },
        );
    }
    
    group.finish();
}

fn benchmark_companion_update(c: &mut Criterion) {
    let mut companion = CompanionBuilder::new()
        .with_perception()
        .with_emotion()
        .with_behavior()
        .build();
    
    c.bench_function("companion_full_update", |b| {
        b.iter(|| {
            companion.update(black_box(0.016));
        });
    });
}

criterion_group!(
    benches,
    benchmark_emotion_update,
    benchmark_emotion_blending,
    benchmark_companion_update
);
criterion_main!(benches);
}

Running Benchmarks

# Run all benchmarks
cargo bench

# Run specific benchmark
cargo bench emotion_update

# Save baseline for comparison
cargo bench -- --save-baseline main

# Compare against baseline
cargo bench -- --baseline main

# Generate detailed reports
cargo bench -- --verbose
Benchmark results are saved in `target/criterion/`. You can view HTML reports by opening `target/criterion/report/index.html`.

Benchmark Best Practices

#![allow(unused)]
fn main() {
// Good: Use black_box to prevent optimization
use std::hint::black_box;

c.bench_function("optimized_safe", |b| {
    b.iter(|| {
        expensive_function(black_box(input))
    });
});

// Good: Separate setup from measurement
c.bench_function("with_setup", |b| {
    let data = create_large_dataset();
    b.iter(|| {
        process_dataset(black_box(&data))
    });
});

// Good: Use iter_batched for setup per iteration
use criterion::BatchSize;

c.bench_function("setup_per_iter", |b| {
    b.iter_batched(
        || create_test_data(),
        |data| process_data(data),
        BatchSize::SmallInput,
    );
});
}

Code Coverage

Installing Coverage Tools

# Install tarpaulin (Linux only)
cargo install cargo-tarpaulin

# Or use llvm-cov (cross-platform)
cargo install cargo-llvm-cov

Running Coverage Analysis

Using tarpaulin (Linux):

# Generate coverage report
cargo tarpaulin --all-features --workspace --timeout 300 --out Html

# View report
firefox tarpaulin-report.html

Using llvm-cov (cross-platform):

# Install llvm-tools
rustup component add llvm-tools-preview

# Generate coverage
cargo llvm-cov --all-features --workspace --html

# View report
open target/llvm-cov/html/index.html

Coverage in CI/CD

# .github/workflows/coverage.yml
name: Coverage

on: [push, pull_request]

jobs:
  coverage:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Install Rust
        uses: actions-rs/toolchain@v1
        with:
          toolchain: stable
          override: true
      
      - name: Install tarpaulin
        run: cargo install cargo-tarpaulin
      
      - name: Generate coverage
        run: cargo tarpaulin --all-features --workspace --timeout 300 --out Xml
      
      - name: Upload to codecov
        uses: codecov/codecov-action@v3
        with:
          files: cobertura.xml

Coverage Goals

  • Overall: Aim for >80% code coverage
  • Critical paths: AI logic, physics should be >90%
  • Edge cases: All error paths should be tested
  • Public APIs: 100% coverage of public interfaces
High coverage doesn't guarantee quality tests. Focus on meaningful test cases, not just hitting lines.

Testing Best Practices

Test Organization

#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
    use super::*;

    // Group related tests
    mod emotion_tests {
        use super::*;

        #[test]
        fn test_creation() { /* ... */ }

        #[test]
        fn test_update() { /* ... */ }
    }

    mod perception_tests {
        use super::*;

        #[test]
        fn test_stimulus_detection() { /* ... */ }
    }
}
}

Clear Test Names

#![allow(unused)]
fn main() {
// Good: Descriptive names
#[test]
fn test_emotion_decay_clamps_to_zero() { }

#[test]
fn test_companion_initialization_fails_without_required_components() { }

// Bad: Unclear names
#[test]
fn test1() { }

#[test]
fn test_emotion() { }
}

Arrange-Act-Assert Pattern

#![allow(unused)]
fn main() {
#[test]
fn test_companion_emotion_response() {
    // Arrange
    let mut companion = create_test_companion();
    let stimulus = Stimulus::new(StimulusType::Positive, 0.8);

    // Act
    companion.process_stimulus(stimulus);
    companion.update(0.016);

    // Assert
    let joy = companion.get_emotion("joy").unwrap();
    assert!(joy.intensity > 0.5);
}
}

Test Independence

#![allow(unused)]
fn main() {
// Good: Each test is independent
#[test]
fn test_a() {
    let mut system = EmotionSystem::new();
    system.add_emotion("joy", 0.5);
    assert_eq!(system.emotion_count(), 1);
}

#[test]
fn test_b() {
    let mut system = EmotionSystem::new(); // Fresh instance
    system.add_emotion("calm", 0.8);
    assert_eq!(system.emotion_count(), 1);
}
}

Documentation in Tests

#![allow(unused)]
fn main() {
/// Tests that emotion decay never produces negative values,
/// even with extreme decay rates or large time deltas.
#[test]
fn test_emotion_decay_bounds() {
    let test_cases = vec![
        (1.0, 10.0, 1.0),  // Extreme decay
        (0.5, 1.0, 100.0), // Large time delta
    ];

    for (initial, rate, dt) in test_cases {
        let result = calculate_emotion_decay(initial, rate, dt);
        assert!(result >= 0.0, "Decay produced negative value: {}", result);
    }
}
}
Well-tested code is reliable code. Invest time in comprehensive tests to catch bugs early and enable confident refactoring.

Code Style Guide

This guide defines the coding standards and style conventions for AstraWeave. Consistent code style improves readability and maintainability.

Table of Contents

Rust Conventions

General Principles

  1. Idiomatic Rust: Write code that follows Rust idioms and patterns
  2. Safety First: Prefer safe abstractions; document unsafe code thoroughly
  3. Zero-Cost Abstractions: Don’t sacrifice performance for convenience
  4. Explicit over Implicit: Make behavior clear and predictable

Code Layout

#![allow(unused)]
fn main() {
// Good: Clear structure with proper spacing
pub struct Companion {
    id: CompanionId,
    name: String,
    emotion_system: EmotionSystem,
    perception: PerceptionSystem,
}

impl Companion {
    pub fn new(name: impl Into<String>) -> Self {
        Self {
            id: CompanionId::generate(),
            name: name.into(),
            emotion_system: EmotionSystem::default(),
            perception: PerceptionSystem::default(),
        }
    }

    pub fn update(&mut self, dt: f32) {
        self.perception.update(dt);
        self.emotion_system.update(dt);
    }
}

// Bad: Cramped, hard to read
pub struct Companion{id:CompanionId,name:String}
impl Companion{pub fn new(n:String)->Self{Self{id:CompanionId::generate(),name:n}}}
}

Line Length

  • Maximum line length: 100 characters
  • Break long lines at logical points
#![allow(unused)]
fn main() {
// Good: Broken at logical points
let companion = CompanionBuilder::new()
    .with_name("Alice")
    .with_perception()
    .with_emotion()
    .with_behavior()
    .build();

// Bad: Too long
let companion = CompanionBuilder::new().with_name("Alice").with_perception().with_emotion().with_behavior().build();
}

Imports

Group and sort imports:

#![allow(unused)]
fn main() {
// 1. Standard library
use std::collections::HashMap;
use std::sync::Arc;

// 2. External crates (alphabetically)
use bevy::prelude::*;
use serde::{Deserialize, Serialize};

// 3. Internal crates
use astraweave_ai::*;
use astraweave_physics::*;

// 4. Local modules
use crate::emotion::*;
use crate::perception::Stimulus;

// 5. Type aliases and trait imports
use super::CompanionId;
}
Use `cargo fmt` to automatically organize imports.

Naming Conventions

Types

PascalCase for types, structs, enums, and traits:

#![allow(unused)]
fn main() {
// Good
pub struct CompanionState { }
pub enum EmotionType { }
pub trait BehaviorController { }

// Bad
pub struct companion_state { }
pub enum emotion_type { }
pub trait behavior_controller { }
}

Functions and Methods

snake_case for functions and methods:

#![allow(unused)]
fn main() {
// Good
pub fn calculate_emotion_intensity(stimulus: f32) -> f32 { }
pub fn process_visual_stimulus(&mut self, data: &[u8]) { }

// Bad
pub fn CalculateEmotionIntensity(stimulus: f32) -> f32 { }
pub fn ProcessVisualStimulus(&mut self, data: &[u8]) { }
}

Variables

snake_case for variables and parameters:

#![allow(unused)]
fn main() {
// Good
let companion_id = CompanionId::new();
let emotion_decay_rate = 0.95;

// Bad
let CompanionID = CompanionId::new();
let EmotionDecayRate = 0.95;
}

Constants

SCREAMING_SNAKE_CASE for constants:

#![allow(unused)]
fn main() {
// Good
pub const MAX_COMPANIONS: usize = 1000;
pub const DEFAULT_TICK_RATE: f32 = 60.0;
const PI: f32 = std::f32::consts::PI;

// Bad
pub const max_companions: usize = 1000;
pub const DefaultTickRate: f32 = 60.0;
}

Modules

snake_case for module names:

#![allow(unused)]
fn main() {
// Good
mod emotion_system;
mod companion_ai;
mod perception_processing;

// Bad
mod EmotionSystem;
mod CompanionAI;
mod PerceptionProcessing;
}

Acronyms

Treat acronyms as words in PascalCase:

#![allow(unused)]
fn main() {
// Good
struct HttpClient;
struct XmlParser;
struct AiCompanion;

// Bad (but acceptable for well-known 2-letter acronyms)
struct AICompanion; // Acceptable
struct XMLParser;   // Avoid

// Definitely bad
struct HTTPClient;
}

Code Organization

Module Structure

Organize code logically by functionality:

astraweave-ai/
├── src/
│   ├── lib.rs              // Public API exports
│   ├── companion/
│   │   ├── mod.rs          // Module root
│   │   ├── builder.rs      // CompanionBuilder
│   │   ├── state.rs        // CompanionState
│   │   └── lifecycle.rs    // Lifecycle management
│   ├── emotion/
│   │   ├── mod.rs
│   │   ├── system.rs       // EmotionSystem
│   │   ├── types.rs        // Emotion types
│   │   └── blending.rs     // Emotion blending
│   └── perception/
│       ├── mod.rs
│       ├── visual.rs       // Visual perception
│       └── auditory.rs     // Auditory perception

File Organization

Structure within a file:

#![allow(unused)]
fn main() {
// 1. Module documentation
//! # Emotion System
//!
//! This module implements the emotion processing system.

// 2. Imports (grouped as shown earlier)
use std::collections::HashMap;
use bevy::prelude::*;

// 3. Type definitions
pub struct EmotionSystem {
    emotions: HashMap<String, Emotion>,
}

// 4. Implementation blocks
impl EmotionSystem {
    pub fn new() -> Self {
        Self {
            emotions: HashMap::new(),
        }
    }
}

// 5. Trait implementations
impl Default for EmotionSystem {
    fn default() -> Self {
        Self::new()
    }
}

// 6. Tests
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_emotion_system_creation() {
        let system = EmotionSystem::new();
        assert!(system.emotions.is_empty());
    }
}
}

Public vs Private

Be intentional about visibility:

#![allow(unused)]
fn main() {
// Good: Clear public API
pub struct Companion {
    // Public fields only when necessary
    pub id: CompanionId,
    
    // Private implementation details
    emotion_system: EmotionSystem,
    internal_state: State,
}

impl Companion {
    // Public constructor
    pub fn new(name: String) -> Self { }
    
    // Public methods for API
    pub fn update(&mut self, dt: f32) { }
    
    // Private helper methods
    fn process_internal_state(&mut self) { }
}

// Bad: Everything public
pub struct Companion {
    pub id: CompanionId,
    pub emotion_system: EmotionSystem,
    pub internal_state: State,
}
}

Documentation Style

Module Documentation

#![allow(unused)]
fn main() {
//! # Emotion System
//!
//! This module provides the core emotion processing system for AI companions.
//! Emotions are represented as continuous values that decay over time and
//! blend based on stimuli.
//!
//! ## Example
//!
//! ```
//! use astraweave_ai::emotion::EmotionSystem;
//!
//! let mut system = EmotionSystem::new();
//! system.add_emotion("joy", 0.8);
//! system.update(0.016);
//! ```
}

Type Documentation

#![allow(unused)]
fn main() {
/// Represents an AI companion with emotion and behavior systems.
///
/// A companion processes stimuli through its perception system,
/// updates its emotional state, and exhibits behaviors based on
/// its internal state and environment.
///
/// # Examples
///
/// ```
/// use astraweave_ai::Companion;
///
/// let mut companion = Companion::new("Buddy");
/// companion.update(0.016);
///
/// if let Some(emotion) = companion.get_emotion("joy") {
///     println!("Joy level: {}", emotion.intensity);
/// }
/// ```
///
/// # Performance
///
/// Companion updates are O(n) where n is the number of active
/// emotions and behaviors.
pub struct Companion {
    // ...
}
}

Function Documentation

#![allow(unused)]
fn main() {
/// Calculates the blended emotion intensity from two emotions.
///
/// Uses linear interpolation to blend two emotion intensities
/// based on a blend factor.
///
/// # Arguments
///
/// * `emotion_a` - First emotion intensity (0.0 to 1.0)
/// * `emotion_b` - Second emotion intensity (0.0 to 1.0)
/// * `blend_factor` - Blend weight (0.0 = all A, 1.0 = all B)
///
/// # Returns
///
/// Blended emotion intensity clamped to [0.0, 1.0]
///
/// # Examples
///
/// ```
/// use astraweave_ai::emotion::blend_emotions;
///
/// let joy = 0.8;
/// let calm = 0.6;
/// let blended = blend_emotions(joy, calm, 0.5);
/// assert_eq!(blended, 0.7);
/// ```
///
/// # Panics
///
/// Panics if `blend_factor` is not in range [0.0, 1.0] in debug builds.
pub fn blend_emotions(emotion_a: f32, emotion_b: f32, blend_factor: f32) -> f32 {
    debug_assert!(
        (0.0..=1.0).contains(&blend_factor),
        "blend_factor must be in [0.0, 1.0]"
    );
    
    (emotion_a * (1.0 - blend_factor) + emotion_b * blend_factor).clamp(0.0, 1.0)
}
}

Documentation Sections

Use these sections in order:

  1. Summary: One-line description
  2. Detailed description: Multi-paragraph explanation
  3. Arguments: Parameter descriptions
  4. Returns: Return value description
  5. Examples: Code examples
  6. Errors: Possible errors (for Result)
  7. Panics: Panic conditions
  8. Safety: Safety invariants (for unsafe)
  9. Performance: Performance characteristics

Error Handling

Use Result for Recoverable Errors

#![allow(unused)]
fn main() {
// Good: Use Result for expected errors
pub fn load_companion(path: &Path) -> Result<Companion, LoadError> {
    let data = std::fs::read_to_string(path)
        .map_err(|e| LoadError::FileRead(path.to_path_buf(), e))?;
    
    serde_json::from_str(&data)
        .map_err(LoadError::Parse)
}

// Bad: Panic on expected errors
pub fn load_companion(path: &Path) -> Companion {
    let data = std::fs::read_to_string(path).unwrap(); // Don't do this
    serde_json::from_str(&data).unwrap()
}
}

Define Custom Error Types

#![allow(unused)]
fn main() {
use thiserror::Error;

#[derive(Debug, Error)]
pub enum CompanionError {
    #[error("Companion not found: {0}")]
    NotFound(CompanionId),
    
    #[error("Invalid companion state: {0}")]
    InvalidState(String),
    
    #[error("IO error: {0}")]
    Io(#[from] std::io::Error),
    
    #[error("Parse error: {0}")]
    Parse(#[from] serde_json::Error),
}
}

Provide Context

#![allow(unused)]
fn main() {
use anyhow::{Context, Result};

pub fn save_companion(&self, path: &Path) -> Result<()> {
    let json = serde_json::to_string_pretty(self)
        .context("Failed to serialize companion")?;
    
    std::fs::write(path, json)
        .with_context(|| format!("Failed to write to {}", path.display()))?;
    
    Ok(())
}
}

Performance Guidelines

Prefer Iteration over Indexing

#![allow(unused)]
fn main() {
// Good: Iterator-based
fn sum_emotions(emotions: &[Emotion]) -> f32 {
    emotions.iter().map(|e| e.intensity).sum()
}

// Less efficient: Index-based
fn sum_emotions_indexed(emotions: &[Emotion]) -> f32 {
    let mut sum = 0.0;
    for i in 0..emotions.len() {
        sum += emotions[i].intensity;
    }
    sum
}
}

Avoid Unnecessary Allocations

#![allow(unused)]
fn main() {
// Good: Reuse buffer
pub struct EmotionProcessor {
    buffer: Vec<f32>,
}

impl EmotionProcessor {
    pub fn process(&mut self, emotions: &[Emotion]) {
        self.buffer.clear();
        self.buffer.extend(emotions.iter().map(|e| e.intensity));
        // Process buffer...
    }
}

// Bad: Allocate every time
pub fn process(emotions: &[Emotion]) -> Vec<f32> {
    emotions.iter().map(|e| e.intensity).collect() // New allocation
}
}

Use References When Possible

#![allow(unused)]
fn main() {
// Good: Borrow instead of clone
pub fn analyze_emotion(&self, emotion: &Emotion) -> Analysis {
    Analysis {
        intensity: emotion.intensity,
        category: emotion.category.clone(), // Only clone when necessary
    }
}

// Bad: Unnecessary clone
pub fn analyze_emotion(&self, emotion: Emotion) -> Analysis {
    // Took ownership, forcing caller to clone
    Analysis {
        intensity: emotion.intensity,
        category: emotion.category,
    }
}
}

Document Performance Characteristics

#![allow(unused)]
fn main() {
/// Finds a companion by ID.
///
/// # Performance
///
/// O(1) average case using HashMap lookup.
pub fn find_companion(&self, id: CompanionId) -> Option<&Companion> {
    self.companions.get(&id)
}

/// Sorts companions by emotion intensity.
///
/// # Performance
///
/// O(n log n) where n is the number of companions.
/// Consider using `find_max_emotion` for single maximum lookup.
pub fn sort_by_emotion(&mut self, emotion_name: &str) {
    self.companions.sort_by(|a, b| {
        a.get_emotion(emotion_name)
            .cmp(&b.get_emotion(emotion_name))
    });
}
}

Clippy and Formatting

Running Clippy

# Run clippy with all features
cargo clippy --all-targets --all-features

# Fix clippy warnings automatically (where possible)
cargo clippy --fix --all-targets --all-features

# Deny all warnings (for CI)
cargo clippy --all-targets --all-features -- -D warnings

Common Clippy Lints

Enable project-wide lints in lib.rs:

#![allow(unused)]
#![warn(
    fn main() {
clippy::all,
    clippy::pedantic,
    clippy::cargo,
    missing_docs,
    rust_2018_idioms,
)]

// Allow specific lints when justified
#![allow(
    clippy::module_name_repetitions, // CompanionBuilder in companion module is fine
    clippy::must_use_candidate,      // Not all pure functions need #[must_use]
)]
}

Code Formatting

# Format all code
cargo fmt

# Check formatting without applying
cargo fmt -- --check

# Format with custom config
cargo fmt -- --config max_width=100

rustfmt Configuration

Create rustfmt.toml:

max_width = 100
hard_tabs = false
tab_spaces = 4
newline_style = "Unix"
use_small_heuristics = "Default"
reorder_imports = true
reorder_modules = true
remove_nested_parens = true
edition = "2021"

Pre-commit Hook

Create .git/hooks/pre-commit:

#!/bin/sh

# Run formatter
cargo fmt -- --check
if [ $? -ne 0 ]; then
    echo "Code is not formatted. Run 'cargo fmt' first."
    exit 1
fi

# Run clippy
cargo clippy --all-targets --all-features -- -D warnings
if [ $? -ne 0 ]; then
    echo "Clippy found issues. Fix them before committing."
    exit 1
fi

exit 0
Consistent style makes code easier to read, review, and maintain. Use automated tools to enforce standards.

Adding New Features

This guide walks you through the process of contributing new features to AstraWeave, from initial proposal through implementation, testing, and documentation.

Feature Development Workflow

graph LR
    subgraph Proposal["1. Proposal"]
        RFC[RFC Discussion] --> DESIGN[Design Doc]
        DESIGN --> APPROVAL[Team Review]
    end
    
    subgraph Implementation["2. Implementation"]
        APPROVAL --> BRANCH[Feature Branch]
        BRANCH --> CODE[Code + Tests]
        CODE --> DOCS[Documentation]
    end
    
    subgraph Review["3. Review"]
        DOCS --> PR[Pull Request]
        PR --> CI[CI Validation]
        CI --> REVIEW[Code Review]
        REVIEW --> MERGE[Merge]
    end

Phase 1: Proposal

Feature Request Format

Before implementing a significant feature, create a discussion or RFC:

## Feature: [Name]

### Summary
One paragraph describing the feature.

### Motivation
- Why is this needed?
- What problem does it solve?
- Who benefits?

### Design Overview
- High-level approach
- Key components affected
- API surface changes

### Alternatives Considered
- Option A: ...
- Option B: ...
- Why chosen approach is better

### Breaking Changes
- None / List any breaking changes

### Implementation Plan
1. Step 1
2. Step 2
3. ...

Design Document

For complex features, create a design document in docs/design/:

# Design: [Feature Name]

## Goals
- Primary objective
- Secondary objectives

## Non-Goals
- Explicitly out of scope

## Architecture

[Mermaid diagram or description]

## API Design

```rust
// Public API examples

Migration Path

How existing code adapts to the new feature.

Testing Strategy

How the feature will be tested.

Timeline

Estimated implementation phases.


## Phase 2: Implementation

### Branch Setup

```bash
git checkout main
git pull origin main
git checkout -b feature/my-feature-name

Project Structure

Understand where your feature belongs:

astraweave/
├── astraweave-ecs/          # Core ECS (rare modifications)
├── astraweave-ai/           # AI systems
├── astraweave-render/       # Rendering
├── astraweave-physics/      # Physics
├── astraweave-gameplay/     # Gameplay systems
├── astraweave-*/            # Other subsystems
└── examples/                # Feature examples

Creating a New Crate

For substantial features, create a new crate:

cargo new --lib astraweave-myfeature

Cargo.toml template:

[package]
name = "astraweave-myfeature"
version = "0.1.0"
edition = "2021"
license = "MIT OR Apache-2.0"
description = "Brief description"
repository = "https://github.com/astraweave/astraweave"
keywords = ["gamedev", "astraweave"]

[dependencies]
astraweave-ecs = { path = "../astraweave-ecs" }

[dev-dependencies]
criterion = "0.5"

[features]
default = []

[[bench]]
name = "my_benchmark"
harness = false

Adding to Workspace

Update root Cargo.toml:

[workspace]
members = [
    # ... existing crates
    "astraweave-myfeature",
]

Module Structure

Follow the standard module layout:

astraweave-myfeature/
├── src/
│   ├── lib.rs           # Public API, re-exports
│   ├── prelude.rs       # Common imports
│   ├── component.rs     # ECS components
│   ├── system.rs        # ECS systems
│   ├── resource.rs      # ECS resources
│   └── internal/        # Private implementation
│       └── mod.rs
├── tests/
│   └── integration.rs   # Integration tests
├── benches/
│   └── benchmark.rs     # Performance benchmarks
└── Cargo.toml

Implementation Guidelines

Public API Design

#![allow(unused)]
fn main() {
pub mod prelude {
    pub use crate::MyComponent;
    pub use crate::MyResource;
    pub use crate::my_system;
}

#[derive(Component, Debug, Clone)]
pub struct MyComponent {
    pub visible_field: f32,
}

impl MyComponent {
    pub fn new(value: f32) -> Self {
        Self { visible_field: value }
    }
    
    pub fn with_option(mut self, option: bool) -> Self {
        self
    }
}

#[derive(Resource, Default)]
pub struct MyResource {
    config: MyConfig,
}

pub fn my_system(
    query: Query<&MyComponent>,
    resource: Res<MyResource>,
) {
    for component in query.iter() {
        // Implementation
    }
}
}

Error Handling

Define clear error types:

#![allow(unused)]
fn main() {
#[derive(Debug, thiserror::Error)]
pub enum MyFeatureError {
    #[error("Invalid configuration: {0}")]
    InvalidConfig(String),
    
    #[error("Resource not found: {0}")]
    NotFound(String),
    
    #[error("Operation failed: {0}")]
    OperationFailed(#[from] std::io::Error),
}

pub type Result<T> = std::result::Result<T, MyFeatureError>;
}

Feature Flags

Use feature flags for optional functionality:

[features]
default = ["standard"]
standard = []
advanced = ["dep:optional-crate"]
serde = ["dep:serde"]

[dependencies]
optional-crate = { version = "1.0", optional = true }
serde = { version = "1.0", features = ["derive"], optional = true }
#![allow(unused)]
fn main() {
#[cfg(feature = "advanced")]
pub mod advanced {
    pub fn advanced_function() {
        // Only available with "advanced" feature
    }
}

#[cfg(feature = "serde")]
impl serde::Serialize for MyComponent {
    // Serialization implementation
}
}

Phase 3: Testing

Unit Tests

Include tests alongside implementation:

#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
    use super::*;
    
    #[test]
    fn test_component_creation() {
        let component = MyComponent::new(42.0);
        assert_eq!(component.visible_field, 42.0);
    }
    
    #[test]
    fn test_builder_pattern() {
        let component = MyComponent::new(1.0)
            .with_option(true);
        // Assertions
    }
    
    #[test]
    #[should_panic(expected = "Invalid value")]
    fn test_invalid_input() {
        MyComponent::new(-1.0);
    }
}
}

Integration Tests

Create tests/integration.rs:

#![allow(unused)]
fn main() {
use astraweave_ecs::prelude::*;
use astraweave_myfeature::prelude::*;

#[test]
fn test_system_integration() {
    let mut world = World::new();
    
    world.spawn(MyComponent::new(10.0));
    world.insert_resource(MyResource::default());
    
    let mut schedule = Schedule::default();
    schedule.add_system(my_system);
    schedule.run(&mut world);
    
    // Verify results
}

#[test]
fn test_full_workflow() {
    // Test complete feature workflow
}
}

Benchmarks

Create benches/benchmark.rs:

#![allow(unused)]
fn main() {
use criterion::{criterion_group, criterion_main, Criterion, BenchmarkId};
use astraweave_myfeature::*;

fn benchmark_creation(c: &mut Criterion) {
    c.bench_function("component_create", |b| {
        b.iter(|| MyComponent::new(42.0))
    });
}

fn benchmark_system(c: &mut Criterion) {
    let mut group = c.benchmark_group("system_throughput");
    
    for count in [100, 1000, 10000].iter() {
        group.bench_with_input(
            BenchmarkId::from_parameter(count),
            count,
            |b, &count| {
                let mut world = setup_world(count);
                let mut schedule = setup_schedule();
                
                b.iter(|| schedule.run(&mut world));
            },
        );
    }
    
    group.finish();
}

criterion_group!(benches, benchmark_creation, benchmark_system);
criterion_main!(benches);
}

Run benchmarks:

cargo bench -p astraweave-myfeature

Phase 4: Documentation

API Documentation

Document all public items:

#![allow(unused)]
fn main() {
/// A component that tracks feature state.
///
/// # Examples
///
/// ```rust
/// use astraweave_myfeature::MyComponent;
///
/// let component = MyComponent::new(42.0);
/// assert_eq!(component.visible_field, 42.0);
/// ```
///
/// # Panics
///
/// Panics if `value` is negative.
#[derive(Component)]
pub struct MyComponent {
    /// The primary value tracked by this component.
    pub visible_field: f32,
}

/// Creates a new component with the given value.
///
/// # Arguments
///
/// * `value` - The initial value (must be non-negative)
///
/// # Returns
///
/// A new `MyComponent` instance.
///
/// # Examples
///
/// ```rust
/// let component = MyComponent::new(10.0);
/// ```
impl MyComponent {
    pub fn new(value: f32) -> Self {
        assert!(value >= 0.0, "Invalid value");
        Self { visible_field: value }
    }
}
}

User Guide

Add documentation to docs/src/:

# My Feature

Brief introduction explaining what the feature does.

## Quick Start

```rust
use astraweave_myfeature::prelude::*;

// Minimal example

Concepts

Explain key concepts and terminology.

Usage

Basic Usage

Step-by-step instructions.

Advanced Usage

More complex scenarios.

Configuration

Available options and settings.

Best Practices

Tips for effective use.


### Update SUMMARY.md

Add your documentation to `docs/src/SUMMARY.md`:

```markdown
- [Core Systems](core-systems/index.md)
    - [My Feature](core-systems/myfeature.md)  # Add here

Phase 5: Pull Request

Pre-Submit Checklist

Before opening a PR:

cargo fmt --all
cargo clippy --all-targets --all-features -- -D warnings
cargo test --all
cargo doc --no-deps --all-features

PR Template

## Summary

Brief description of changes.

## Changes

- Added `MyComponent` for tracking X
- Implemented `my_system` for processing Y
- Added benchmarks for performance validation

## Testing

- [x] Unit tests added
- [x] Integration tests added
- [x] Benchmarks added
- [x] Manual testing completed

## Documentation

- [x] API docs complete
- [x] User guide added
- [x] SUMMARY.md updated

## Breaking Changes

None / List any breaking changes.

## Related Issues

Fixes #123
Related to #456

CI Requirements

Your PR must pass:

  1. Format check: cargo fmt --check
  2. Lint check: cargo clippy
  3. Test suite: cargo test --all
  4. Doc generation: cargo doc
  5. Benchmark regression: No significant slowdowns

Code Review

Expect reviewers to check:

  • API design and ergonomics
  • Performance implications
  • Test coverage
  • Documentation quality
  • Breaking change management

Best Practices

1. **Start Small**: Begin with minimal viable feature, iterate
2. **Design for Extension**: Make it easy to add functionality later
3. **Follow Conventions**: Match existing code style and patterns
4. **Test Edge Cases**: Cover error conditions and boundaries
5. **Document Intent**: Explain why, not just what
- **Scope Creep**: Resist adding "just one more thing"
- **Missing Tests**: Every public function needs tests
- **Breaking Changes**: Avoid unless absolutely necessary
- **Performance Regression**: Always benchmark critical paths

Example: Adding a New System

Here’s a complete example of adding a health regeneration system:

#![allow(unused)]
fn main() {
// astraweave-gameplay/src/health_regen.rs

use astraweave_ecs::prelude::*;

/// Configuration for health regeneration.
#[derive(Resource)]
pub struct HealthRegenConfig {
    /// Base regeneration rate (HP per second).
    pub base_rate: f32,
    /// Delay before regeneration starts after damage.
    pub regen_delay: f32,
}

impl Default for HealthRegenConfig {
    fn default() -> Self {
        Self {
            base_rate: 5.0,
            regen_delay: 3.0,
        }
    }
}

/// Enables health regeneration on an entity.
#[derive(Component)]
pub struct HealthRegen {
    /// Multiplier applied to base regeneration rate.
    pub rate_multiplier: f32,
    /// Time since last damage taken.
    pub time_since_damage: f32,
}

impl Default for HealthRegen {
    fn default() -> Self {
        Self {
            rate_multiplier: 1.0,
            time_since_damage: 0.0,
        }
    }
}

/// System that regenerates health over time.
pub fn health_regen_system(
    config: Res<HealthRegenConfig>,
    time: Res<Time>,
    mut query: Query<(&mut Health, &mut HealthRegen)>,
) {
    let dt = time.delta_seconds();
    
    for (mut health, mut regen) in query.iter_mut() {
        regen.time_since_damage += dt;
        
        if regen.time_since_damage >= config.regen_delay {
            let regen_amount = config.base_rate * regen.rate_multiplier * dt;
            health.current = (health.current + regen_amount).min(health.max);
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    
    #[test]
    fn test_regen_after_delay() {
        // Test implementation
    }
}
}

Performance Optimization

This guide covers profiling, benchmarking, and optimization techniques for AstraWeave games and the engine itself.

Performance Philosophy

AstraWeave targets consistent 60+ FPS gameplay with AI-native features. Key principles:

  1. Measure First: Always profile before optimizing
  2. Budget Time: Allocate frame time across systems
  3. Batch Operations: Minimize per-entity overhead
  4. Cache Strategically: Trade memory for speed where appropriate
  5. Offload to Threads: Parallelize independent work

Frame Budget

At 60 FPS, each frame has ~16.67ms. Recommended allocation:

SystemBudgetNotes
Game Logic2-3msECS systems, gameplay
AI2-4msPerception, planning, behaviors
Physics2-3msCollision, dynamics
Rendering6-8msDraw calls, GPU submission
Audio0.5-1msMixing, spatial
Buffer1-2msHeadroom for spikes

Profiling Tools

Tracy Integration

AstraWeave integrates with Tracy for real-time profiling:

[dependencies]
astraweave-profiling = { version = "0.1", features = ["tracy"] }
#![allow(unused)]
fn main() {
use astraweave_profiling::*;

fn my_system(query: Query<&MyComponent>) {
    profile_scope!("my_system");
    
    for component in query.iter() {
        profile_scope!("process_entity");
        // Work
    }
}
}

Run with Tracy:

cargo run --release --features tracy

Built-in Profiler

Enable the debug overlay:

#![allow(unused)]
fn main() {
use astraweave_profiling::prelude::*;

app.add_plugin(ProfilingPlugin::default());
}

Press F3 in-game to toggle the performance overlay showing:

  • Frame time graph
  • System timing breakdown
  • Memory usage
  • Draw call count

CPU Profiling

For detailed CPU analysis:

# Linux (perf)
perf record -g cargo run --release
perf report

# Windows (ETW)
cargo build --release
# Use Windows Performance Analyzer

# macOS (Instruments)
cargo instruments --release -t time

GPU Profiling

# NVIDIA Nsight
cargo run --release
# Attach Nsight Graphics

# RenderDoc
cargo run --release --features renderdoc
# Press F12 to capture frame

Optimization Techniques

ECS Optimization

Query Optimization

#![allow(unused)]
fn main() {
// Bad: Iterating all entities
fn slow_system(query: Query<&Transform>) {
    for transform in query.iter() {
        // Processes all entities with Transform
    }
}

// Good: Filter to relevant entities
fn fast_system(
    query: Query<&Transform, (With<Enemy>, Without<Dead>)>,
) {
    for transform in query.iter() {
        // Only active enemies
    }
}
}

Parallel Iteration

#![allow(unused)]
fn main() {
use rayon::prelude::*;

fn parallel_system(query: Query<&mut Transform>) {
    query.par_iter_mut().for_each(|mut transform| {
        // Thread-safe processing
        transform.translation.y += 0.1;
    });
}
}

Change Detection

#![allow(unused)]
fn main() {
fn efficient_update(
    query: Query<&MyComponent, Changed<MyComponent>>,
) {
    for component in query.iter() {
        // Only processes recently changed entities
    }
}
}

Archetypes

Group components that are commonly accessed together:

#![allow(unused)]
fn main() {
// Good: Components often queried together
#[derive(Bundle)]
struct EnemyBundle {
    transform: Transform,
    health: Health,
    ai: AiAgent,
    collider: Collider,
}

// Avoid: Rarely used components on common entities
struct RarelyUsedData { /* ... */ }
}

Memory Optimization

Component Size

#![allow(unused)]
fn main() {
// Bad: Large component
#[derive(Component)]
struct LargeComponent {
    data: [f32; 1000],  // 4KB per entity
    name: String,
}

// Good: Split into data and reference
#[derive(Component)]
struct SmallComponent {
    data_handle: Handle<LargeData>,  // 8 bytes
    flags: u8,
}
}

Object Pools

#![allow(unused)]
fn main() {
use astraweave_ecs::pool::*;

#[derive(Resource)]
struct BulletPool {
    pool: EntityPool<BulletBundle>,
}

impl BulletPool {
    fn spawn(&mut self, commands: &mut Commands) -> Entity {
        self.pool.get_or_spawn(commands, || BulletBundle::default())
    }
    
    fn despawn(&mut self, entity: Entity) {
        self.pool.return_entity(entity);
    }
}
}

Arena Allocation

For temporary allocations:

#![allow(unused)]
fn main() {
use bumpalo::Bump;

fn batch_process(entities: &[Entity]) {
    let arena = Bump::new();
    
    let temp_data: &mut [Vec3] = arena.alloc_slice_fill_default(entities.len());
    
    // Work with temp_data
    // Arena automatically freed at scope end
}
}

Rendering Optimization

Batching

#![allow(unused)]
fn main() {
// Enable instanced rendering for repeated meshes
#[derive(Component)]
struct InstancedMesh {
    mesh: Handle<Mesh>,
    material: Handle<Material>,
    instances: Vec<Transform>,
}
}

Level of Detail

#![allow(unused)]
fn main() {
#[derive(Component)]
struct LodGroup {
    distances: [f32; 3],
    meshes: [Handle<Mesh>; 3],
}

fn lod_system(
    camera: Query<&Transform, With<Camera>>,
    mut lod_query: Query<(&Transform, &LodGroup, &mut Handle<Mesh>)>,
) {
    let camera_pos = camera.single().translation;
    
    for (transform, lod, mut mesh) in lod_query.iter_mut() {
        let distance = transform.translation.distance(camera_pos);
        
        let lod_level = if distance < lod.distances[0] { 0 }
            else if distance < lod.distances[1] { 1 }
            else { 2 };
        
        *mesh = lod.meshes[lod_level].clone();
    }
}
}

Culling

#![allow(unused)]
fn main() {
#[derive(Component)]
struct Visibility {
    pub visible: bool,
    pub render_layers: u32,
}

fn frustum_culling_system(
    camera: Query<(&Camera, &Transform)>,
    mut renderables: Query<(&Transform, &Aabb, &mut Visibility)>,
) {
    let (camera, cam_transform) = camera.single();
    let frustum = camera.compute_frustum(cam_transform);
    
    for (transform, aabb, mut visibility) in renderables.iter_mut() {
        let world_aabb = aabb.transformed(transform);
        visibility.visible = frustum.intersects_aabb(&world_aabb);
    }
}
}

AI Optimization

Tick Budgeting

#![allow(unused)]
fn main() {
#[derive(Resource)]
pub struct AiBudget {
    pub max_ms_per_frame: f32,
    pub agents_processed: usize,
}

fn budgeted_ai_system(
    mut budget: ResMut<AiBudget>,
    mut agents: Query<&mut AiAgent>,
    time: Res<Time>,
) {
    let start = std::time::Instant::now();
    budget.agents_processed = 0;
    
    for mut agent in agents.iter_mut() {
        if start.elapsed().as_secs_f32() * 1000.0 > budget.max_ms_per_frame {
            break;
        }
        
        agent.tick();
        budget.agents_processed += 1;
    }
}
}

LOD for AI

#![allow(unused)]
fn main() {
#[derive(Component)]
pub struct AiLod {
    pub distance_from_player: f32,
    pub update_frequency: u32,
    pub frames_since_update: u32,
}

fn ai_lod_system(
    player: Query<&Transform, With<Player>>,
    mut ai_query: Query<(&Transform, &mut AiLod, &mut AiAgent)>,
) {
    let player_pos = player.single().translation;
    
    for (transform, mut lod, mut agent) in ai_query.iter_mut() {
        lod.distance_from_player = transform.translation.distance(player_pos);
        
        lod.update_frequency = match lod.distance_from_player {
            d if d < 20.0 => 1,   // Every frame
            d if d < 50.0 => 2,   // Every 2 frames
            d if d < 100.0 => 5,  // Every 5 frames
            _ => 10,              // Every 10 frames
        };
        
        lod.frames_since_update += 1;
        if lod.frames_since_update >= lod.update_frequency {
            lod.frames_since_update = 0;
            agent.tick();
        }
    }
}
}

LLM Caching

#![allow(unused)]
fn main() {
#[derive(Resource)]
pub struct LlmCache {
    cache: LruCache<u64, String>,
    hit_count: u64,
    miss_count: u64,
}

impl LlmCache {
    pub fn get_or_generate<F>(
        &mut self,
        prompt: &str,
        generator: F,
    ) -> &str
    where
        F: FnOnce() -> String,
    {
        let hash = hash_prompt(prompt);
        
        if !self.cache.contains(&hash) {
            self.miss_count += 1;
            let response = generator();
            self.cache.put(hash, response);
        } else {
            self.hit_count += 1;
        }
        
        self.cache.get(&hash).unwrap()
    }
}
}

Physics Optimization

Broad Phase

#![allow(unused)]
fn main() {
#[derive(Resource)]
pub struct PhysicsConfig {
    pub broad_phase: BroadPhaseType,
    pub substeps: u32,
    pub velocity_iterations: u32,
}

pub enum BroadPhaseType {
    BruteForce,     // < 100 entities
    SpatialHash,    // 100-1000 entities
    BvhTree,        // > 1000 entities
}
}

Sleeping

#![allow(unused)]
fn main() {
#[derive(Component)]
pub struct RigidBody {
    pub sleeping: bool,
    pub sleep_threshold: f32,
    pub sleep_timer: f32,
}

fn sleep_system(mut bodies: Query<(&Velocity, &mut RigidBody)>) {
    for (velocity, mut body) in bodies.iter_mut() {
        if velocity.linear.length_squared() < body.sleep_threshold {
            body.sleep_timer += delta;
            if body.sleep_timer > 0.5 {
                body.sleeping = true;
            }
        } else {
            body.sleep_timer = 0.0;
            body.sleeping = false;
        }
    }
}
}

Benchmarking

Criterion Benchmarks

#![allow(unused)]
fn main() {
use criterion::{criterion_group, criterion_main, Criterion, BenchmarkId};

fn bench_ecs_iteration(c: &mut Criterion) {
    let mut group = c.benchmark_group("ecs_iteration");
    
    for entity_count in [1000, 10000, 100000] {
        let world = create_world_with_entities(entity_count);
        
        group.bench_with_input(
            BenchmarkId::new("query", entity_count),
            &world,
            |b, world| {
                b.iter(|| {
                    let mut count = 0;
                    for _ in world.query::<&Transform>().iter() {
                        count += 1;
                    }
                    count
                });
            },
        );
    }
    
    group.finish();
}

criterion_group!(benches, bench_ecs_iteration);
criterion_main!(benches);
}

Performance Regression Testing

# .github/workflows/bench.yml
name: Benchmarks
on: [push, pull_request]

jobs:
  benchmark:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Run benchmarks
        run: cargo bench --all -- --save-baseline new
      - name: Compare with main
        run: |
          git fetch origin main
          cargo bench --all -- --baseline main --save-baseline new

Common Performance Issues

Issue: Frame Spikes

Symptoms: Occasional stutters, inconsistent frame times

Causes:

  • GC in scripting
  • Asset loading on main thread
  • Large allocations

Solutions:

#![allow(unused)]
fn main() {
// Pre-warm asset loading
fn warmup_system(asset_server: Res<AssetServer>) {
    asset_server.load::<Mesh>("meshes/common.gltf");
    asset_server.load::<Texture>("textures/atlas.png");
}

// Use streaming for large assets
let handle = asset_server.load_async::<LargeAsset>("path").await;
}

Issue: High CPU Usage

Symptoms: High CPU, low GPU utilization

Causes:

  • Inefficient queries
  • Too many systems
  • Excessive allocations

Solutions:

#![allow(unused)]
fn main() {
// Combine related systems
fn combined_system(
    mut query: Query<(&mut Transform, &Velocity, &mut Health)>,
) {
    for (mut transform, velocity, mut health) in query.iter_mut() {
        transform.translation += velocity.0;
        health.regen();
    }
}
}

Issue: Memory Growth

Symptoms: Increasing memory over time

Causes:

  • Entity leaks
  • Cache growth
  • Asset retention

Solutions:

#![allow(unused)]
fn main() {
// Periodic cleanup
fn cleanup_system(
    mut commands: Commands,
    dead_entities: Query<Entity, With<Dead>>,
    time: Res<Time>,
    mut cleanup_timer: ResMut<CleanupTimer>,
) {
    cleanup_timer.tick(time.delta());
    
    if cleanup_timer.just_finished() {
        for entity in dead_entities.iter() {
            commands.entity(entity).despawn_recursive();
        }
    }
}
}

Best Practices

1. **Profile in Release**: Debug builds are 10-50x slower
2. **Measure Realistically**: Test with actual content, not empty scenes
3. **Test on Target Hardware**: Don't only test on dev machines
4. **Budget Early**: Set performance targets before development
5. **Automate Testing**: Catch regressions in CI
- **Premature Optimization**: Don't optimize without profiling data
- **Micro-benchmarks**: Real-world performance may differ
- **Ignoring Memory**: CPU speed means nothing if you're thrashing cache
- **Single-threaded Thinking**: Utilize all cores

API Reference

Documentation Accuracy: All exports listed below are verified against actual lib.rs files as of v0.4.0. Coverage percentages sourced from Master Coverage Report.

Browse the Rust API documentation for all AstraWeave crates.

CrateDescriptionCoverageStatus
astraweave-ecsEntity Component System implementation83.2%Production
astraweave-coreCore ECS, simulation, and world systems79.1%Production
astraweave-aiAI orchestration and planning71.3%Production
astraweave-renderwgpu 25-based renderer67.4%Production
astraweave-physicsRapier3D 0.22 integration76.8%Production
astraweave-fluidsSPH fluid simulation94.2%Production
astraweave-navNavigation and pathfinding72.1%Production
astraweave-gameplayCombat physics, damage68.9%Production
astraweave-terrainVoxel terrain generation71.5%Production
astraweave-llmLLM integration (Qwen3-8B)58.3%Beta
astraweave-behaviorBehavior trees, utility AI74.2%Production

Core Engine Crates

astraweave-ecs (83.2% coverage)

High-performance Entity Component System:

ExportDescription
WorldMain container for all ECS data
EntityLightweight entity handles with generational indices
AppApplication builder and runner
ScheduleSystem scheduling and ordering
ComponentTrait for component data
ResourceSingleton data storage
QueryEfficient component access patterns
CommandBufferDeferred entity operations
EventsEvent queues and readers
#![allow(unused)]
fn main() {
use astraweave_ecs::{World, Entity, App, Schedule, Component, Resource, Query, CommandBuffer};
}

View Full Documentation →


astraweave-core (79.1% coverage)

Core engine systems and AI infrastructure:

ModuleDescription
capture_replayDeterministic frame recording/playback
perceptionAI world observation and filtering
schemaWorldSnapshot, PlanIntent, ActionStep
simGame state management
tool_sandboxSecure AI action execution
tool_vocabulary37-tool AI action vocabulary
validationInput/output validation
worldWorld state management
#![allow(unused)]
fn main() {
use astraweave_core::{
    schema::{WorldSnapshot, PlanIntent, ActionStep},
    capture_replay::CaptureReplay,
    perception::PerceptionFilter,
};
}

Generate rustdoc: cargo doc -p astraweave-core --no-deps --open


AI & Behavior Crates

astraweave-ai (71.3% coverage)

AI orchestration layer with GOAP+LLM hybrid support:

ModuleFeature GateDescription
core_loopPerception-Reasoning-Planning-Action loop
orchestratorAI coordination trait
tool_sandboxTool validation and sandboxing
async_taskllm_orchestratorAsync LLM task wrapper
llm_executorllm_orchestratorLLM plan generation
goapllm_orchestratorGoal-oriented action planning
AIArbiterllm_orchestratorGOAP+LLM hybrid (101.7ns control)
#![allow(unused)]
fn main() {
use astraweave_ai::{core_loop, orchestrator::Orchestrator};

// With llm_orchestrator feature:
#[cfg(feature = "llm_orchestrator")]
use astraweave_ai::{AIArbiter, goap::GoapOrchestrator};
}

View Full Documentation → | Arbiter Guide →


astraweave-behavior (74.2% coverage)

Behavior systems:

  • BehaviorGraph - Hierarchical behavior nodes (BehaviorNode enum)
  • BehaviorContext - Shared context for tick execution
  • BehaviorStatus - Success/Failure/Running states
  • UtilityAI - Score-based decision making
#![allow(unused)]
fn main() {
use astraweave_behavior::{BehaviorGraph, BehaviorNode, BehaviorContext, BehaviorStatus};
}

Generate rustdoc: cargo doc -p astraweave-behavior --no-deps --open


astraweave-llm (58.3% coverage)

LLM integration (Qwen3-8B via Ollama):

ExportDescription
LlmOrchestratorProvider-agnostic LLM coordination
FallbackChain4-tier fallback (Full LLM → Simplified → Heuristic → Emergency)
OllamaClientOllama API client
#![allow(unused)]
fn main() {
use astraweave_llm::{LlmOrchestrator, FallbackChain};
}

Generate rustdoc: cargo doc -p astraweave-llm --no-deps --open

Rendering & Graphics Crates

astraweave-render (67.4% coverage)

wgpu 25.0.2 based rendering (40+ modules):

CategoryKey Modules
Pipelinecamera, clustered, deferred, forward, renderer
Animationanimation, animation_blending, skeleton, skinning_gpu
Lightinglight, shadow, ibl_manager, post_fx_shader
Geometrymesh, culling, lod_generator, instancing
Materialsmaterial, texture_array, vertex_compression
Effectswater, volumetric, particle_system
#![allow(unused)]
fn main() {
use astraweave_render::{
    camera::Camera,
    material::Material,
    mesh::Mesh,
    renderer::Renderer,
};
}

View Full Documentation →


Physics & Navigation Crates

astraweave-physics (76.8% coverage)

Rapier3D 0.22 integration:

ModulePerformanceDescription
rigid_bodyPhysics bodies and dynamics
colliderCollision shapes and detection
character_controller114ns/movePlayer movement
spatial_hash99.96% fewer checksBroad-phase acceleration
async_schedulerParallel physics stepping
#![allow(unused)]
fn main() {
use astraweave_physics::{
    rigid_body::RigidBody,
    collider::Collider,
    character_controller::CharacterController,
};
}

View Full Documentation →


astraweave-fluids (94.2% coverage, 2,404 tests)

SPH fluid simulation (A+ grade):

ModuleDescription
solverSPH pressure/viscosity solver
surface_tensionSurface tension forces
boundaryDomain boundary handling
spatial_hashNeighbor lookup acceleration
#![allow(unused)]
fn main() {
use astraweave_fluids::{FluidWorld, Particle, FluidConfig};
}

View Full Documentation → | Fluids Guide →


astraweave-nav (72.1% coverage)

Navigation and pathfinding:

ModuleDescription
navmeshNavigation mesh generation
pathfindingA* and hierarchical planning
portal_graphRoom-to-room navigation
agentNavigation agent component
#![allow(unused)]
fn main() {
use astraweave_nav::{Navmesh, PathQuery, Agent};
}

View Full Documentation →


Gameplay Crates

astraweave-gameplay (68.9% coverage)

Combat and game mechanics:

ModuleDescription
combat_physicsRaycast attacks, parry, iframes
damage_systemDamage calculation
ability_systemAbility cooldowns and effects
#![allow(unused)]
fn main() {
use astraweave_gameplay::combat_physics::perform_attack_sweep;
}

Generate rustdoc: cargo doc -p astraweave-gameplay --no-deps --open


astraweave-terrain (71.5% coverage)

Procedural terrain generation:

ModuleDescription
voxel_meshMarching cubes (256 configurations)
biomeBiome distribution and blending
chunkTerrain chunking (15.06ms/chunk)
#![allow(unused)]
fn main() {
use astraweave_terrain::{VoxelMesh, Biome, TerrainChunk};
}

Generate rustdoc: cargo doc -p astraweave-terrain --no-deps --open | Terrain Guide →


Infrastructure Crates

astraweave-audio (69.2% coverage)

Spatial audio with rodio:

ModuleDescription
audio_engine4-bus mixer system (master, music, SFX, voice)
spatial3D audio positioning
crossfadeMusic transitions
#![allow(unused)]
fn main() {
use astraweave_audio::{AudioEngine, SpatialAudio};
}

View Full Documentation →


astraweave-scene (74.6% coverage)

World streaming and partitioning:

ModuleDescription
streamingAsync cell loading
partitionWorld partitioning
#![allow(unused)]
fn main() {
use astraweave_scene::{WorldCell, CellLoader};
}

Generate rustdoc: cargo doc -p astraweave-scene --no-deps --open


Building API Docs Locally

# Generate all API documentation
cargo doc --workspace --no-deps --open

# Generate for specific crate with all features
cargo doc --package astraweave-ai --all-features --no-deps --open

# Generate with private items (for internal development)
cargo doc --package astraweave-core --document-private-items --no-deps --open

Coverage by Tier

TierCratesAvg CoverageStatus
Tier 1 (Critical)ecs, core, ai, render75.3%✅ Production
Tier 2 (Important)physics, nav, gameplay72.6%✅ Production
Tier 3 (Supporting)audio, scene, terrain71.8%✅ Production
Tier 4 (Specialized)fluids, llm, prompts71.5%✅ Production

See Master Coverage Report for detailed breakdown.

See Also

ECS API Reference

Crate: astraweave-ecs
Coverage: 94.26%
Tests: 1,200+

The Entity Component System is the foundation of AstraWeave, providing deterministic, cache-friendly entity management with archetype-based storage.


Core Types

World

The central container for all entities, components, and resources.

#![allow(unused)]
fn main() {
use astraweave_ecs::World;

let mut world = World::new();

// Spawn entities
let entity = world.spawn((Position::default(), Velocity::default()));

// Access resources
world.insert_resource(GameTime::default());
let time = world.resource::<GameTime>();
}

Key Methods:

  • new()World - Create empty world
  • spawn(bundle)Entity - Create entity with components
  • despawn(entity) - Remove entity
  • insert_resource<R>(resource) - Add singleton resource
  • resource<R>()&R - Get resource reference
  • query<Q>()Query<Q> - Create component query

Entity

Lightweight 64-bit identifier (32-bit index + 32-bit generation).

#![allow(unused)]
fn main() {
use astraweave_ecs::Entity;

// Entities are created via World::spawn()
let entity = world.spawn(MyBundle::default());

// Check if entity exists
if world.contains(entity) {
    // Entity is alive
}

// Create from raw (unsafe, for FFI)
let raw = Entity::from_raw(42, 1);
}

Properties:

  • 8 bytes memory footprint
  • Copy, Clone, Hash, Eq
  • Safe to store in collections
  • Generation prevents use-after-free

App

Application builder with plugin support.

#![allow(unused)]
fn main() {
use astraweave_ecs::App;

App::new()
    .add_plugin(DefaultPlugins)
    .add_startup_system(setup)
    .add_system(update)
    .run();
}

Key Methods:

  • add_plugin<P>(plugin) - Add functionality bundle
  • add_startup_system(system) - Run once at start
  • add_system(system) - Add to main loop
  • add_system_to_stage(stage, system) - Add to specific stage
  • run() - Start main loop

Schedule

System execution scheduler with parallel support.

#![allow(unused)]
fn main() {
use astraweave_ecs::{Schedule, SystemStage};

let mut schedule = Schedule::new();
schedule.add_system_to_stage(SystemStage::Update, my_system);
schedule.run(&mut world);
}

System Stages (in execution order):

  1. First - Pre-frame setup
  2. PreUpdate - Input processing
  3. Update - Main game logic
  4. PostUpdate - Physics, AI
  5. Last - Rendering prep

Component

Trait for data attached to entities (auto-implemented for 'static + Send + Sync).

#![allow(unused)]
fn main() {
use astraweave_ecs::Component;

#[derive(Component, Default)]
struct Position {
    x: f32,
    y: f32,
    z: f32,
}

#[derive(Component)]
struct Velocity(Vec3);

#[derive(Component)]
struct Name(String);
}

Requirements:

  • 'static lifetime
  • Send + Sync (for parallel systems)
  • Prefer small, focused components

Resource

Singleton data shared across systems.

#![allow(unused)]
fn main() {
use astraweave_ecs::Resource;

#[derive(Resource, Default)]
struct GameTime {
    elapsed: f32,
    delta: f32,
}

#[derive(Resource)]
struct Settings {
    volume: f32,
    difficulty: Difficulty,
}

// Access in systems
fn update_time(mut time: ResMut<GameTime>) {
    time.elapsed += time.delta;
}
}

Query

Efficient iteration over component combinations.

#![allow(unused)]
fn main() {
use astraweave_ecs::Query;

// Read-only query
fn read_system(query: Query<&Position>) {
    for pos in query.iter() {
        println!("Position: {:?}", pos);
    }
}

// Mutable query
fn write_system(mut query: Query<&mut Position>) {
    for mut pos in query.iter_mut() {
        pos.x += 1.0;
    }
}

// Multiple components
fn complex_query(query: Query<(&Position, &Velocity, Option<&Name>)>) {
    for (pos, vel, name) in query.iter() {
        // name is Option<&Name>
    }
}

// Filters
fn filtered_query(query: Query<&Position, With<Player>>) {
    // Only entities with Player component
}
}

Filter Types:

  • With<T> - Entity must have T
  • Without<T> - Entity must not have T
  • Or<(A, B)> - Entity has A or B
  • Added<T> - T was just added
  • Changed<T> - T was modified

CommandBuffer

Deferred entity/component modifications (thread-safe).

#![allow(unused)]
fn main() {
use astraweave_ecs::CommandBuffer;

fn spawn_system(mut commands: Commands) {
    // Spawn entity with components
    commands.spawn((Position::default(), Velocity::default()));
    
    // Modify existing entity
    commands.entity(some_entity)
        .insert(Health(100))
        .remove::<Poisoned>();
    
    // Despawn
    commands.entity(dead_entity).despawn();
}
}

Key Methods:

  • spawn(bundle)EntityCommands - Queue entity spawn
  • entity(entity)EntityCommands - Get entity commands
  • insert_resource<R>(resource) - Queue resource insert
  • remove_resource<R>() - Queue resource removal

Events

Type-safe event channel for system communication.

#![allow(unused)]
fn main() {
use astraweave_ecs::{Events, EventReader, EventWriter};

struct DamageEvent {
    target: Entity,
    amount: f32,
}

fn damage_sender(mut events: EventWriter<DamageEvent>) {
    events.send(DamageEvent { target: enemy, amount: 50.0 });
}

fn damage_receiver(mut events: EventReader<DamageEvent>) {
    for event in events.iter() {
        println!("Entity {:?} took {} damage", event.target, event.amount);
    }
}
}

Event Lifecycle:

  1. Events sent via EventWriter
  2. Events read via EventReader
  3. Events cleared at end of frame
  4. Double-buffered for reliable delivery

Archetype Storage

AstraWeave uses archetype-based storage for optimal cache performance:

Archetype A: [Position, Velocity]
┌──────────┬──────────┐
│ Position │ Velocity │
├──────────┼──────────┤
│ (1,2,3)  │ (0,1,0)  │  Entity 1
│ (4,5,6)  │ (1,0,0)  │  Entity 2
│ (7,8,9)  │ (0,0,1)  │  Entity 3
└──────────┴──────────┘

Archetype B: [Position, Velocity, Health]
┌──────────┬──────────┬────────┐
│ Position │ Velocity │ Health │
├──────────┼──────────┼────────┤
│ (0,0,0)  │ (0,0,0)  │  100   │  Entity 4
└──────────┴──────────┴────────┘

Benefits:

  • Cache-friendly iteration
  • No pointer chasing
  • Parallel-safe by design

Performance

OperationLatencyNotes
World creation25.8 nsEmpty world
Entity spawn420 nsWith components
Component access<1 nsDirect archetype access
Query iteration~2 ns/entityCache-optimal

Feature Flags

FeatureDescriptionDefault
parallelParallel system execution
tracingTracy profiling integration
serdeSerialization support
[dependencies]
astraweave-ecs = { version = "0.4", features = ["serde"] }

See Also

AI API Reference

Crate: astraweave-ai
Coverage: ~75%
Tests: 400+

The AI system provides the core perception-reasoning-planning-action loop, orchestration, and tool validation for AI-native game development.


Core Modules

core_loop

The fundamental perception → reasoning → planning → action cycle.

#![allow(unused)]
fn main() {
use astraweave_ai::core_loop::{CoreLoop, WorldSnapshot, PlanIntent, ActionStep};

// Build perception
let snapshot = WorldSnapshot {
    t: game_time,
    player: player_state,
    me: companion_state,
    enemies: detected_enemies,
    pois: points_of_interest,
    obstacles: obstacle_positions,
    objective: current_objective,
};

// Execute AI cycle
let plan = orchestrator.plan(&mut world, &snapshot)?;

// Execute first action
if let Some(action) = plan.steps.first() {
    execute_action(action);
}
}

Key Types:

  • WorldSnapshot - Filtered world state for AI perception
  • PlanIntent - Validated action sequence from AI
  • ActionStep - Individual executable action

orchestrator

Trait-based AI planning abstraction.

#![allow(unused)]
fn main() {
use astraweave_ai::orchestrator::Orchestrator;

pub trait Orchestrator: Send + Sync {
    fn plan(&self, world: &mut World, snap: &WorldSnapshot) -> Result<PlanIntent>;
}

// Implementations provided:
// - RuleOrchestrator (classical if-then rules)
// - GoapOrchestrator (goal-oriented planning)
// - LlmOrchestrator (LLM-based reasoning, feature-gated)
// - HybridOrchestrator (GOAP + LLM)
}

tool_sandbox

Secure action validation preventing impossible/cheating actions.

#![allow(unused)]
fn main() {
use astraweave_ai::tool_sandbox::{ToolSandbox, ToolResult};

let sandbox = ToolSandbox::new();

// Validate action before execution
match sandbox.validate(&action, &world_state) {
    ToolResult::Valid => execute(action),
    ToolResult::Invalid(reason) => {
        log::warn!("AI attempted invalid action: {}", reason);
    }
}
}

Validation Examples:

  • Movement range limits
  • Line-of-sight requirements
  • Resource availability checks
  • Cooldown enforcement

Feature-Gated Types

AIArbiter (requires llm_orchestrator)

Hybrid GOAP+LLM system with zero user-facing latency.

#![allow(unused)]
fn main() {
#[cfg(feature = "llm_orchestrator")]
use astraweave_ai::{AIArbiter, LlmExecutor, GoapOrchestrator, RuleOrchestrator};

let mut arbiter = AIArbiter::new(llm_executor, goap, bt);

// Game loop - returns instantly (101.7 ns)
let action = arbiter.update(&snapshot);
}

Performance: 101.7 ns GOAP control, 575 ns LLM polling

See Arbiter System for full documentation.


goap

Goal-Oriented Action Planning implementation.

#![allow(unused)]
fn main() {
use astraweave_ai::goap::{GoapPlanner, Goal, Action, WorldState};

let mut planner = GoapPlanner::new();

// Define goals
planner.add_goal(Goal::new("kill_enemy")
    .with_precondition("enemy_visible", true)
    .with_effect("enemy_dead", true));

// Define actions
planner.add_action(Action::new("attack")
    .with_precondition("has_weapon", true)
    .with_precondition("in_range", true)
    .with_effect("enemy_dead", true)
    .with_cost(1.0));

// Plan
let plan = planner.plan(&current_state, &goal_state)?;
}

veilweaver

Integration for the Veilweaver game mechanics (fate-weaving).

#![allow(unused)]
fn main() {
use astraweave_ai::veilweaver::{FateThread, Prophecy, ThreadWeaver};

let mut weaver = ThreadWeaver::new();
let thread = weaver.create_thread(prophecy);

// Weave fate during gameplay
weaver.weave(&mut world, thread)?;
}

WorldSnapshot

The AI’s view of the world (filtered for perception).

#![allow(unused)]
fn main() {
pub struct WorldSnapshot {
    pub t: f32,                        // Current game time
    pub player: PlayerState,           // Player information
    pub me: CompanionState,            // This AI's state
    pub enemies: Vec<EnemyState>,      // Detected enemies
    pub pois: Vec<Poi>,                // Points of interest
    pub obstacles: Vec<IVec2>,         // Obstacle positions
    pub objective: Option<String>,     // Current objective
}

pub struct CompanionState {
    pub pos: IVec2,                    // Position
    pub ammo: i32,                     // Ammunition
    pub cooldowns: BTreeMap<String, f32>, // Ability cooldowns
    pub morale: f32,                   // Morale level
}
}

PlanIntent

Validated action sequence from AI reasoning.

#![allow(unused)]
fn main() {
pub struct PlanIntent {
    pub plan_id: String,               // Unique plan identifier
    pub steps: Vec<ActionStep>,        // Ordered actions
}

pub enum ActionStep {
    MoveTo { x: i32, y: i32 },
    Attack { target: u32, stance: String },
    TakeCover { position: Option<(i32, i32)> },
    UseAbility { ability: String, target: Option<u32> },
    Wait { duration: f32 },
    Interact { object: u32 },
}
}

Orchestrator Implementations

OrchestratorLatencyUse Case
RuleOrchestrator~100 nsSimple if-then logic
GoapOrchestrator3-50 µsGoal-oriented planning
BehaviorTreeOrchestrator~200 nsBehavior trees
UtilityOrchestrator~500 nsUtility-based scoring
LlmOrchestrator13-21sDeep reasoning (async)
HybridOrchestrator~100 nsGOAP + async LLM

Feature Flags

FeatureDescriptionDefault
llm_orchestratorLLM-based planning
goapGoal-oriented planning
behavior_treeBT integration
utilityUtility AI
[dependencies]
astraweave-ai = { version = "0.4", features = ["llm_orchestrator"] }

Performance

OperationLatencyNotes
WorldSnapshot build~500 nsPerception gathering
GOAP planning3-50 µsDepends on action space
Tool validation~100 nsPer action
Full AI cycle~5 µsTypical case

See Also

Physics API Reference

Crate: astraweave-physics
Coverage: ~82%
Tests: 657+

Physics simulation built on Rapier3D with character controllers, vehicles, destruction, and deterministic replay support.


Core Types

PhysicsWorld

Central physics simulation container.

#![allow(unused)]
fn main() {
use astraweave_physics::{PhysicsWorld, RigidBodyDesc, ColliderDesc};

let mut physics = PhysicsWorld::new();

// Create rigid body
let body = physics.create_rigid_body(RigidBodyDesc {
    position: Vec3::new(0.0, 5.0, 0.0),
    body_type: RigidBodyType::Dynamic,
    mass: 1.0,
    ..Default::default()
});

// Add collider
physics.add_collider(body, ColliderDesc::sphere(0.5));

// Step simulation
physics.step(1.0 / 60.0);
}

CharacterController

Physics-based character movement with slopes, stairs, and ground detection.

#![allow(unused)]
fn main() {
use astraweave_physics::CharacterController;

let mut controller = CharacterController::new(CharacterConfig {
    height: 1.8,
    radius: 0.3,
    step_height: 0.3,
    max_slope: 45.0,
    ..Default::default()
});

// Move character
let movement = controller.move_character(
    &physics,
    desired_velocity,
    delta_time,
);

// Check ground state
if controller.is_grounded() {
    // Can jump
}
}

Vehicle

Vehicle physics with tire model and suspension.

#![allow(unused)]
fn main() {
use astraweave_physics::vehicle::{Vehicle, VehicleConfig, WheelConfig};

let vehicle = Vehicle::new(VehicleConfig {
    chassis_mass: 1500.0,
    wheels: vec![
        WheelConfig { position: Vec3::new(-0.8, 0.0, 1.5), ..Default::default() },
        WheelConfig { position: Vec3::new(0.8, 0.0, 1.5), ..Default::default() },
        WheelConfig { position: Vec3::new(-0.8, 0.0, -1.5), ..Default::default() },
        WheelConfig { position: Vec3::new(0.8, 0.0, -1.5), ..Default::default() },
    ],
    ..Default::default()
});

// Apply input
vehicle.set_steering(steering_angle);
vehicle.set_throttle(throttle);
vehicle.set_brake(brake);
}

SpatialHash

Efficient broad-phase collision detection.

#![allow(unused)]
fn main() {
use astraweave_physics::SpatialHash;

let mut spatial = SpatialHash::new(10.0); // Cell size

// Insert objects
spatial.insert(entity_a, aabb_a);
spatial.insert(entity_b, aabb_b);

// Query nearby objects
let nearby = spatial.query_sphere(position, radius);

// Query potential collisions
let pairs = spatial.get_collision_pairs();
}

Performance: 99.96% collision check reduction vs brute force


Ragdoll

Ragdoll physics with joint constraints.

#![allow(unused)]
fn main() {
use astraweave_physics::ragdoll::{Ragdoll, RagdollConfig, BoneConfig};

let ragdoll = Ragdoll::new(RagdollConfig {
    bones: vec![
        BoneConfig { name: "spine", parent: None, .. },
        BoneConfig { name: "head", parent: Some(0), .. },
        BoneConfig { name: "arm_l", parent: Some(0), .. },
        // ...
    ],
    ..Default::default()
});

// Activate ragdoll on character death
ragdoll.activate(&mut physics, character_pose);
}

Cloth

Cloth simulation with wind and collision.

#![allow(unused)]
fn main() {
use astraweave_physics::cloth::{Cloth, ClothConfig};

let cloth = Cloth::new(ClothConfig {
    width: 10,
    height: 10,
    particle_mass: 0.1,
    stiffness: 1000.0,
    damping: 0.1,
    ..Default::default()
});

// Pin corners
cloth.pin_particle(0, 0);
cloth.pin_particle(9, 0);

// Apply wind
cloth.apply_force(wind_force);
}

Destruction

Destructible objects with fracturing.

#![allow(unused)]
fn main() {
use astraweave_physics::destruction::{Destructible, FractureConfig};

let destructible = Destructible::new(FractureConfig {
    health: 100.0,
    fracture_threshold: 50.0,
    piece_count: 8,
    ..Default::default()
});

// Apply damage
let fragments = destructible.apply_damage(damage, hit_point)?;

// Spawn fragment entities
for fragment in fragments {
    commands.spawn(fragment);
}
}

Projectile

Projectile physics with penetration and ricochet.

#![allow(unused)]
fn main() {
use astraweave_physics::projectile::{Projectile, ProjectileConfig};

let projectile = Projectile::new(ProjectileConfig {
    velocity: Vec3::new(0.0, 0.0, 500.0),
    mass: 0.01,
    drag: 0.1,
    gravity_scale: 1.0,
    penetration_power: 50.0,
    ..Default::default()
});

// Step projectile
let hit = projectile.step(&physics, delta_time);
if let Some(hit_info) = hit {
    // Handle impact
}
}

Collision Detection

Raycasting

#![allow(unused)]
fn main() {
use astraweave_physics::RaycastResult;

// Single ray
if let Some(hit) = physics.raycast(origin, direction, max_distance) {
    println!("Hit at {:?}, normal: {:?}", hit.point, hit.normal);
}

// Ray with filter
let hit = physics.raycast_filtered(origin, direction, max_distance, |entity| {
    !entity.has::<Ghost>()
});
}

Shape Casts

#![allow(unused)]
fn main() {
// Sphere cast
let hit = physics.spherecast(origin, radius, direction, max_distance);

// Box cast
let hit = physics.boxcast(origin, half_extents, direction, max_distance);
}

Overlap Queries

#![allow(unused)]
fn main() {
// Get all entities in sphere
let entities = physics.overlap_sphere(center, radius);

// Get all entities in box
let entities = physics.overlap_box(center, half_extents, rotation);
}

Collision Shapes

ShapeConstructorUse Case
SphereColliderDesc::sphere(radius)Characters, projectiles
BoxColliderDesc::cuboid(hx, hy, hz)Crates, buildings
CapsuleColliderDesc::capsule(half_height, radius)Characters
CylinderColliderDesc::cylinder(half_height, radius)Pillars
Convex HullColliderDesc::convex_hull(points)Complex objects
TrimeshColliderDesc::trimesh(vertices, indices)Static geometry
HeightfieldColliderDesc::heightfield(heights, scale)Terrain

Collision Groups

#![allow(unused)]
fn main() {
use astraweave_physics::{CollisionGroups, Group};

// Define groups
const PLAYER: Group = Group::GROUP_1;
const ENEMY: Group = Group::GROUP_2;
const PROJECTILE: Group = Group::GROUP_3;
const ENVIRONMENT: Group = Group::GROUP_4;

// Player collides with enemies and environment
let player_groups = CollisionGroups::new(
    PLAYER,
    ENEMY | ENVIRONMENT,
);

// Projectile collides with everything
let projectile_groups = CollisionGroups::new(
    PROJECTILE,
    PLAYER | ENEMY | ENVIRONMENT,
);
}

Determinism

AstraWeave physics is fully deterministic for replay and networking:

#![allow(unused)]
fn main() {
use astraweave_physics::PhysicsWorld;

// Same seed = same results
let mut physics_a = PhysicsWorld::with_seed(12345);
let mut physics_b = PhysicsWorld::with_seed(12345);

// Run simulation
for _ in 0..1000 {
    physics_a.step(1.0 / 60.0);
    physics_b.step(1.0 / 60.0);
}

// Results are bit-identical
assert_eq!(physics_a.checksum(), physics_b.checksum());
}

Performance

OperationLatencyNotes
Rigid body step2.97 µsSingle body
Full physics tick6.52 µs100 bodies
Character move114 nsController step
Raycast~500 nsTypical scene
Spatial hash query~50 nsPer cell

Feature Flags

FeatureDescriptionDefault
debug-renderPhysics visualization
simdSIMD acceleration
parallelParallel simulation
serializeState serialization

See Also

Fluids API Reference

Crate: astraweave-fluids
Coverage: ~91%
Tests: 2,404

Production-ready SPH fluid simulation with real-time rendering, terrain integration, and editor support.


Core Types

WaterEffectsManager

Central manager for all water visual effects.

#![allow(unused)]
fn main() {
use astraweave_fluids::{WaterEffectsManager, WaterQualityPreset};

let mut manager = WaterEffectsManager::new(WaterQualityPreset::High);

// Configure effects
manager.enable_caustics(true);
manager.enable_god_rays(true);
manager.enable_foam(true);

// Update each frame
manager.update(delta_time, &water_state);

// Render
manager.render(&mut render_context);
}

WaterQualityPreset

Quality presets for different hardware targets.

#![allow(unused)]
fn main() {
use astraweave_fluids::WaterQualityPreset;

// Available presets
let ultra = WaterQualityPreset::Ultra;    // 64 rays, 1024 particles
let high = WaterQualityPreset::High;      // 32 rays, 512 particles
let medium = WaterQualityPreset::Medium;  // 16 rays, 256 particles
let low = WaterQualityPreset::Low;        // 8 rays, 128 particles

// Apply preset
let manager = WaterEffectsManager::new(high);
}

PcisphSystem

Predictive-Corrective Incompressible SPH solver.

#![allow(unused)]
fn main() {
use astraweave_fluids::pcisph_system::{PcisphSystem, PcisphConfig};

let config = PcisphConfig {
    particle_count: 10000,
    particle_radius: 0.1,
    rest_density: 1000.0,
    viscosity: 0.01,
    surface_tension: 0.0728,
    ..Default::default()
};

let mut solver = PcisphSystem::new(config);

// Add particles
solver.spawn_particles(position, count);

// Step simulation
solver.step(delta_time);

// Get particle data for rendering
let positions = solver.positions();
let velocities = solver.velocities();
}

UnifiedSolver

Unified particle solver handling fluids, foam, and spray.

#![allow(unused)]
fn main() {
use astraweave_fluids::unified_solver::{UnifiedSolver, ParticleType};

let mut solver = UnifiedSolver::new(config);

// Spawn different particle types
solver.spawn(ParticleType::Water, position, count);
solver.spawn(ParticleType::Foam, position, count);
solver.spawn(ParticleType::Spray, position, count);

// Step all particles together
solver.step(delta_time);
}

Visual Effects

Caustics

Underwater light caustic patterns.

#![allow(unused)]
fn main() {
use astraweave_fluids::caustics::{CausticRenderer, CausticConfig};

let config = CausticConfig {
    resolution: 512,
    intensity: 1.0,
    speed: 0.5,
    scale: 10.0,
    ..Default::default()
};

let mut caustics = CausticRenderer::new(config);
caustics.update(time, water_surface);

// Apply to underwater objects
caustics.apply_to_surface(&mut material);
}

God Rays

Volumetric underwater light shafts.

#![allow(unused)]
fn main() {
use astraweave_fluids::god_rays::{GodRayRenderer, GodRayConfig};

let config = GodRayConfig {
    ray_count: 32,
    ray_length: 50.0,
    intensity: 0.8,
    decay: 0.95,
    ..Default::default()
};

let mut god_rays = GodRayRenderer::new(config);
god_rays.render(sun_direction, water_surface);
}

Foam

Surface foam simulation.

#![allow(unused)]
fn main() {
use astraweave_fluids::foam::{FoamSystem, FoamConfig};

let config = FoamConfig {
    spawn_threshold: 2.0,    // Velocity threshold
    lifetime: 3.0,           // Seconds
    size: 0.1,
    ..Default::default()
};

let mut foam = FoamSystem::new(config);
foam.update(delta_time, &water_particles);
foam.render(&mut render_context);
}

Terrain Integration

WaterTerrainIntegration

Seamless water-terrain interaction.

#![allow(unused)]
fn main() {
use astraweave_fluids::terrain_integration::{WaterTerrainIntegration, WaterBodyConfig};

let integration = WaterTerrainIntegration::new();

// Define water body
let lake = WaterBodyConfig {
    center: Vec3::new(100.0, 50.0, 100.0),
    radius: 50.0,
    depth: 10.0,
    flow_direction: Vec3::ZERO,
    ..Default::default()
};

// Query water at position
let water_info = integration.sample_water(position, &terrain);
if water_info.is_submerged {
    apply_buoyancy(water_info.depth);
}
}

Building Integration

Water interaction with structures.

#![allow(unused)]
fn main() {
use astraweave_fluids::building::{WaterBuildingIntegration, FloodConfig};

let mut building_water = WaterBuildingIntegration::new();

// Configure flooding
building_water.set_flood_config(FloodConfig {
    water_level: 45.0,
    rise_rate: 0.1,
    ..Default::default()
});

// Update with building geometry
building_water.update(&buildings, delta_time);

// Query flood state
let flood = building_water.get_flood_level(building_id);
}

Volume Grid

GpuVolumeGrid

GPU-accelerated 3D volume grid for water simulation.

#![allow(unused)]
fn main() {
use astraweave_fluids::gpu_volume::{GpuVolumeGrid, VolumeConfig};

let config = VolumeConfig {
    resolution: [128, 64, 128],
    cell_size: 0.5,
    ..Default::default()
};

let mut volume = GpuVolumeGrid::new(&device, config);

// Update from particles
volume.splat_particles(&particles);

// Read density
let density = volume.sample(position);
}

Editor Support

Undo/Redo

Full undo/redo support for editor integration.

#![allow(unused)]
fn main() {
use astraweave_fluids::editor::{WaterEditor, EditorCommand};

let mut editor = WaterEditor::new();

// Make changes
editor.execute(EditorCommand::SetWaterLevel(50.0));
editor.execute(EditorCommand::AddEmitter(emitter));

// Undo
editor.undo();

// Redo
editor.redo();
}

Validation

Real-time validation of water configurations.

#![allow(unused)]
fn main() {
use astraweave_fluids::editor::validation::{validate_water_body, ValidationResult};

let result = validate_water_body(&config);
match result {
    ValidationResult::Valid => { /* OK */ }
    ValidationResult::Warning(msg) => {
        ui.show_warning(&msg);
    }
    ValidationResult::Error(msg) => {
        ui.show_error(&msg);
        return;
    }
}
}

Serialization

Save/Load

Full water state serialization.

#![allow(unused)]
fn main() {
use astraweave_fluids::serialization::{save_water_state, load_water_state};

// Save
let data = save_water_state(&manager)?;
std::fs::write("water.bin", &data)?;

// Load
let data = std::fs::read("water.bin")?;
let manager = load_water_state(&data)?;
}

LOD System

Adaptive Quality

Distance-based quality adjustment.

#![allow(unused)]
fn main() {
use astraweave_fluids::lod::{WaterLod, LodConfig};

let lod = WaterLod::new(LodConfig {
    lod0_distance: 50.0,   // Full quality
    lod1_distance: 100.0,  // Medium
    lod2_distance: 200.0,  // Low
    ..Default::default()
});

// Update based on camera
lod.update(camera_position);

// Get quality for water body
let quality = lod.get_quality(water_position);
}

Performance

OperationLatencyNotes
PCISPH step (10K particles)~2 msGPU accelerated
Caustic update~0.5 ms512×512 texture
God ray render~0.8 ms32 rays
Foam update~0.3 ms1K particles
Volume splat~0.2 ms128³ grid

Feature Flags

FeatureDescriptionDefault
gpuGPU acceleration
editorEditor integration
serializeState serialization
simdSIMD optimization

See Also

Render API Reference

Crate: astraweave-render
Coverage: ~68%
Tests: 300+

Modern wgpu-based rendering with PBR materials, shadows, post-processing, GPU skinning, and mesh optimization.


Core Types

Renderer

Central rendering coordinator.

#![allow(unused)]
fn main() {
use astraweave_render::{Renderer, RendererConfig};

let config = RendererConfig {
    width: 1920,
    height: 1080,
    vsync: true,
    msaa_samples: 4,
    ..Default::default()
};

let mut renderer = Renderer::new(&window, config).await?;

// Render frame
renderer.begin_frame();
renderer.draw_scene(&scene);
renderer.end_frame();
}

MeshRegistry

GPU mesh resource management.

#![allow(unused)]
fn main() {
use astraweave_render::MeshRegistry;

let mut registry = MeshRegistry::new(&device);

// Register mesh
let mesh_id = registry.register(MeshData {
    vertices: vertex_data,
    indices: index_data,
    bounds: aabb,
});

// Draw mesh
renderer.draw_mesh(mesh_id, &transform, &material);
}

MaterialManager

PBR material system with hot reloading.

#![allow(unused)]
fn main() {
use astraweave_render::{MaterialManager, PbrMaterial};

let mut materials = MaterialManager::new(&device);

// Create material
let material = PbrMaterial {
    albedo: Color::rgb(0.8, 0.2, 0.2),
    metallic: 0.0,
    roughness: 0.5,
    normal_map: Some(normal_texture),
    ..Default::default()
};

let material_id = materials.register(material);
}

IblManager

Image-Based Lighting for environment reflections.

#![allow(unused)]
fn main() {
use astraweave_render::IblManager;

let mut ibl = IblManager::new(&device);

// Load environment map
ibl.load_hdr("environment.hdr").await?;

// Bind to shader
ibl.bind(&mut render_pass);
}

GPU Skinning

SkinnedVertex

Vertex format with bone weights.

#![allow(unused)]
fn main() {
use astraweave_render::skinning_gpu::{SkinnedVertex, SkinnedMesh};

let vertex = SkinnedVertex {
    position: [0.0, 1.0, 0.0],
    normal: [0.0, 1.0, 0.0],
    uv: [0.5, 0.5],
    bone_indices: [0, 1, 0, 0],  // Up to 4 bones
    bone_weights: [0.7, 0.3, 0.0, 0.0],
};

let skinned_mesh = SkinnedMesh::new(&device, vertices, indices);
}

BoneMatrixBuffer

GPU bone matrix upload.

#![allow(unused)]
fn main() {
use astraweave_render::skinning_gpu::BoneMatrixBuffer;

let mut bone_buffer = BoneMatrixBuffer::new(&device, max_bones);

// Update bone matrices each frame
bone_buffer.update(&queue, &bone_matrices);

// Bind for skinning
bone_buffer.bind(&mut render_pass);
}

Mesh Optimization

Vertex Compression

37% memory reduction via octahedral normals and half-float UVs.

#![allow(unused)]
fn main() {
use astraweave_render::vertex_compression::{compress_vertices, CompressedVertex};

let compressed = compress_vertices(&vertices);

// 32 bytes → 20 bytes per vertex
assert!(std::mem::size_of::<CompressedVertex>() < std::mem::size_of::<Vertex>());
}

LOD Generation

Automatic level-of-detail mesh generation.

#![allow(unused)]
fn main() {
use astraweave_render::lod_generator::{generate_lods, LodConfig};

let config = LodConfig {
    target_ratios: vec![1.0, 0.5, 0.25, 0.1],
    min_triangles: 100,
    ..Default::default()
};

let lods = generate_lods(&mesh, config);

// lods[0] = original
// lods[1] = 50% triangles
// lods[2] = 25% triangles
// lods[3] = 10% triangles
}

Instancing

GPU instanced rendering for repeated objects.

#![allow(unused)]
fn main() {
use astraweave_render::instancing::{InstanceBuffer, InstanceData};

let mut instances = InstanceBuffer::new(&device, max_instances);

// Update instance data
let instance_data: Vec<InstanceData> = trees.iter()
    .map(|t| InstanceData {
        transform: t.transform.to_cols_array_2d(),
        color: t.color.into(),
    })
    .collect();

instances.update(&queue, &instance_data);

// Draw all instances in one call
renderer.draw_instanced(mesh_id, &instances);
}

Shadows

CascadedShadowMaps

Multi-cascade shadow mapping for large scenes.

#![allow(unused)]
fn main() {
use astraweave_render::shadows::{CascadedShadowMaps, CsmConfig};

let config = CsmConfig {
    cascade_count: 4,
    resolution: 2048,
    split_lambda: 0.5,
    ..Default::default()
};

let mut csm = CascadedShadowMaps::new(&device, config);

// Update cascades
csm.update(&camera, &sun_direction);

// Render shadow maps
for cascade in csm.cascades() {
    renderer.render_shadow_pass(cascade, &shadow_casters);
}
}

Post-Processing

PostFxPipeline

Post-processing effect chain.

#![allow(unused)]
fn main() {
use astraweave_render::post_fx::{PostFxPipeline, PostFxConfig};

let config = PostFxConfig {
    bloom_enabled: true,
    bloom_intensity: 0.5,
    tonemapping: Tonemapping::Aces,
    exposure: 1.0,
    ..Default::default()
};

let mut post_fx = PostFxPipeline::new(&device, config);

// Apply post-processing
post_fx.process(&mut render_context, &scene_texture);
}

Available Effects:

  • Bloom
  • Tonemapping (ACES, Reinhard, Filmic)
  • Color grading
  • Vignette
  • Film grain
  • SSAO (optional)

Camera

Camera3D

3D camera with multiple projection modes.

#![allow(unused)]
fn main() {
use astraweave_render::camera::{Camera3D, Projection};

let camera = Camera3D {
    position: Vec3::new(0.0, 5.0, -10.0),
    target: Vec3::ZERO,
    up: Vec3::Y,
    projection: Projection::Perspective {
        fov: 60.0_f32.to_radians(),
        aspect: 16.0 / 9.0,
        near: 0.1,
        far: 1000.0,
    },
};

let view_proj = camera.view_projection_matrix();
}

Debug Rendering

DebugDraw

Immediate-mode debug visualization.

#![allow(unused)]
fn main() {
use astraweave_render::debug_draw::DebugDraw;

let mut debug = DebugDraw::new(&device);

// Draw primitives
debug.line(start, end, Color::RED);
debug.sphere(center, radius, Color::GREEN);
debug.box3d(&aabb, Color::BLUE);
debug.arrow(origin, direction, Color::YELLOW);

// Submit
debug.flush(&mut render_pass);
}

Render Graph

RenderGraph

Declarative render pipeline.

#![allow(unused)]
fn main() {
use astraweave_render::graph::{RenderGraph, RenderPass};

let mut graph = RenderGraph::new();

// Define passes
graph.add_pass(RenderPass::new("shadow")
    .with_output("shadow_map", Format::Depth32Float));

graph.add_pass(RenderPass::new("gbuffer")
    .with_output("albedo", Format::Rgba8Unorm)
    .with_output("normal", Format::Rgba16Float)
    .with_output("depth", Format::Depth32Float));

graph.add_pass(RenderPass::new("lighting")
    .with_input("albedo")
    .with_input("normal")
    .with_input("shadow_map")
    .with_output("hdr_color", Format::Rgba16Float));

// Execute
graph.execute(&mut renderer);
}

Performance

OperationLatencyNotes
Mesh draw21 nsPer instance
Vertex compression21 nsPer vertex
Instancing overhead2 nsPer instance
Shadow pass (2K)~1 msPer cascade
Post-FX (full)~2 msAll effects

Feature Flags

FeatureDescriptionDefault
pbrPBR materials
shadowsShadow mapping
post-fxPost-processing
debug-renderDebug visualization
hot-reloadShader hot reload

See Also

Navigation API Reference

Crate: astraweave-nav
Coverage: ~78%
Tests: 200+

Navmesh-based pathfinding with A*, portal graphs, and dynamic obstacle avoidance.


Core Types

Navigation mesh for pathfinding.

#![allow(unused)]
fn main() {
use astraweave_nav::{NavMesh, NavMeshConfig};

let config = NavMeshConfig {
    cell_size: 0.3,
    cell_height: 0.2,
    agent_height: 2.0,
    agent_radius: 0.6,
    max_slope: 45.0,
    ..Default::default()
};

// Build from geometry
let navmesh = NavMesh::build(&vertices, &indices, config)?;

// Query path
let path = navmesh.find_path(start, goal)?;
}

Agent with steering behaviors.

#![allow(unused)]
fn main() {
use astraweave_nav::{NavAgent, AgentConfig};

let mut agent = NavAgent::new(AgentConfig {
    speed: 5.0,
    acceleration: 10.0,
    radius: 0.5,
    ..Default::default()
});

// Set destination
agent.set_destination(target_position);

// Update each frame
agent.update(delta_time, &navmesh);

// Get movement vector
let velocity = agent.velocity();
}

PortalGraph

Hierarchical pathfinding for large worlds.

#![allow(unused)]
fn main() {
use astraweave_nav::PortalGraph;

let mut graph = PortalGraph::new();

// Add regions
graph.add_region("forest", forest_navmesh);
graph.add_region("castle", castle_navmesh);

// Connect regions via portals
graph.add_portal("forest", "castle", portal_polygon);

// Find cross-region path
let path = graph.find_path(start, goal)?;
}

ObstacleAvoidance

Dynamic obstacle avoidance using velocity obstacles.

#![allow(unused)]
fn main() {
use astraweave_nav::ObstacleAvoidance;

let mut avoidance = ObstacleAvoidance::new();

// Add moving obstacles
for other in nearby_agents {
    avoidance.add_obstacle(other.position, other.velocity, other.radius);
}

// Compute safe velocity
let safe_velocity = avoidance.compute(
    agent.position,
    agent.desired_velocity,
    agent.radius,
    delta_time,
);
}

Pathfinding

#![allow(unused)]
fn main() {
use astraweave_nav::astar::{astar_search, PathResult};

let result = astar_search(&navmesh, start, goal);

match result {
    PathResult::Found(path) => {
        for waypoint in path {
            // Follow path
        }
    }
    PathResult::Partial(path) => {
        // Closest reachable point
    }
    PathResult::NotFound => {
        // No path exists
    }
}
}

Path Smoothing

#![allow(unused)]
fn main() {
use astraweave_nav::path_smoothing::smooth_path;

let raw_path = navmesh.find_path(start, goal)?;
let smooth = smooth_path(&raw_path, &navmesh);

// Smooth path has fewer waypoints
assert!(smooth.len() <= raw_path.len());
}

Crowd Simulation

CrowdManager

Large-scale agent coordination.

#![allow(unused)]
fn main() {
use astraweave_nav::crowd::{CrowdManager, CrowdAgent};

let mut crowd = CrowdManager::new(&navmesh, max_agents);

// Add agents
let agent_id = crowd.add_agent(CrowdAgent {
    position: spawn_point,
    target: destination,
    speed: 5.0,
    ..Default::default()
});

// Update all agents
crowd.update(delta_time);

// Get agent state
let agent = crowd.get_agent(agent_id);
}

Performance

OperationLatencyNotes
A* pathfind50-500 µsDepends on distance
NavMesh query~1 µsPoint location
Agent update~100 nsPer agent
Crowd update (100)~50 µsAll agents

See Also

Audio API Reference

Crate: astraweave-audio
Coverage: ~72%
Tests: 150+

Spatial audio with mixer buses, crossfading, reverb zones, and audio occlusion.


Core Types

AudioEngine

Central audio system with mixer.

#![allow(unused)]
fn main() {
use astraweave_audio::{AudioEngine, AudioConfig};

let config = AudioConfig {
    sample_rate: 48000,
    buffer_size: 512,
    ..Default::default()
};

let mut audio = AudioEngine::new(config)?;

// Play sound
audio.play("sfx/explosion.ogg", PlayConfig::default());

// Update each frame
audio.update(listener_position, listener_orientation);
}

AudioMixer

4-bus mixing system.

#![allow(unused)]
fn main() {
use astraweave_audio::mixer::{AudioMixer, Bus};

let mut mixer = AudioMixer::new();

// Configure bus volumes
mixer.set_volume(Bus::Master, 1.0);
mixer.set_volume(Bus::Music, 0.8);
mixer.set_volume(Bus::Sfx, 1.0);
mixer.set_volume(Bus::Voice, 1.0);

// Mute bus
mixer.set_muted(Bus::Music, true);
}

SpatialSource

3D positioned audio source.

#![allow(unused)]
fn main() {
use astraweave_audio::SpatialSource;

let mut source = SpatialSource::new("ambient/wind.ogg");
source.set_position(Vec3::new(10.0, 0.0, 5.0));
source.set_looping(true);
source.set_min_distance(1.0);
source.set_max_distance(50.0);
source.play();
}

MusicLayer

Dynamic music with layers and crossfading.

#![allow(unused)]
fn main() {
use astraweave_audio::music::{MusicPlayer, MusicLayer};

let mut music = MusicPlayer::new();

// Add layers
music.add_layer(MusicLayer::new("music/ambient.ogg"));
music.add_layer(MusicLayer::new("music/combat.ogg"));
music.add_layer(MusicLayer::new("music/boss.ogg"));

// Crossfade between layers
music.crossfade_to(1, Duration::from_secs(2)); // Combat music
}

ReverbZone

Environment-based reverb.

#![allow(unused)]
fn main() {
use astraweave_audio::reverb::{ReverbZone, ReverbPreset};

let zone = ReverbZone::new(ReverbPreset::Cave)
    .with_bounds(aabb)
    .with_blend_distance(5.0);

// Add to audio system
audio.add_reverb_zone(zone);
}

Presets: Small, Medium, Large, Hall, Cave, Outdoor, Underwater


AudioOcclusion

Raycast-based sound occlusion.

#![allow(unused)]
fn main() {
use astraweave_audio::occlusion::AudioOcclusion;

let mut occlusion = AudioOcclusion::new(&physics);

// Update occlusion for all sources
occlusion.update(listener_position, &sources);

// Apply to source
let factor = occlusion.get_factor(source_id);
source.set_volume(base_volume * factor);
}

Performance

OperationLatencyNotes
Source play~100 µsInitial setup
Mixer update~50 µsPer frame
Spatial update~1 µsPer source
Occlusion raycast~5 µsPer source

See Also

Crate Reference

AstraWeave is organized as a workspace of interconnected crates, each providing focused functionality. This reference documents the crate hierarchy, dependencies, and public APIs.

Workspace Overview

graph TB
    subgraph Core["Core Layer"]
        ECS[astraweave-ecs]
        MATH[astraweave-math]
        SDK[astraweave-sdk]
    end
    
    subgraph AI["AI Layer"]
        AI_CORE[astraweave-ai]
        LLM[astraweave-llm]
        MEMORY[astraweave-memory]
        PERSONA[astraweave-persona]
        BEHAVIOR[astraweave-behavior]
    end
    
    subgraph Rendering["Rendering Layer"]
        RENDER[astraweave-render]
        MATERIALS[astraweave-materials]
        ASSET[astraweave-asset]
        UI[astraweave-ui]
    end
    
    subgraph Simulation["Simulation Layer"]
        PHYSICS[astraweave-physics]
        NAV[astraweave-nav]
        AUDIO[astraweave-audio]
    end
    
    subgraph Gameplay["Gameplay Layer"]
        GAMEPLAY[astraweave-gameplay]
        DIALOGUE[astraweave-dialogue]
        QUESTS[astraweave-quests]
        PCG[astraweave-pcg]
    end
    
    SDK --> ECS
    SDK --> AI_CORE
    SDK --> RENDER
    SDK --> PHYSICS
    
    AI_CORE --> ECS
    AI_CORE --> LLM
    AI_CORE --> MEMORY
    
    RENDER --> ECS
    RENDER --> ASSET
    RENDER --> MATERIALS
    
    GAMEPLAY --> ECS
    GAMEPLAY --> AI_CORE
    GAMEPLAY --> PHYSICS

Core Crates

astraweave-ecs

The foundation Entity Component System providing deterministic, high-performance entity management.

[dependencies]
astraweave-ecs = "0.1"

Key Types:

TypeDescription
WorldContainer for all entities, components, and resources
EntityLightweight identifier for game objects
ComponentData attached to entities (derive macro available)
ResourceSingleton data shared across systems
QueryEfficient iteration over component combinations
CommandsDeferred entity/component modifications

Example:

#![allow(unused)]
fn main() {
use astraweave_ecs::prelude::*;

#[derive(Component)]
struct Position(Vec3);

#[derive(Component)]
struct Velocity(Vec3);

fn movement_system(mut query: Query<(&mut Position, &Velocity)>) {
    for (mut pos, vel) in query.iter_mut() {
        pos.0 += vel.0;
    }
}
}

Features:

  • parallel - Enable parallel system execution (default)
  • tracing - Add performance tracing instrumentation
  • serde - Serialization support for components

astraweave-math

Mathematics library optimized for game development with SIMD acceleration.

[dependencies]
astraweave-math = "0.1"

Key Types:

TypeDescription
Vec2, Vec3, Vec4Vector types with operator overloading
Mat3, Mat4Matrix types for transforms
QuatQuaternion for rotations
TransformPosition + rotation + scale
AabbAxis-aligned bounding box
RayRay for intersection tests

Example:

#![allow(unused)]
fn main() {
use astraweave_math::prelude::*;

let pos = Vec3::new(1.0, 2.0, 3.0);
let rotation = Quat::from_axis_angle(Vec3::Y, 45.0_f32.to_radians());
let transform = Transform::from_translation(pos).with_rotation(rotation);

let world_pos = transform.transform_point(Vec3::ZERO);
}

astraweave-sdk

High-level SDK that re-exports commonly used types and provides convenience APIs.

[dependencies]
astraweave-sdk = "0.1"

Re-exports:

#![allow(unused)]
fn main() {
pub use astraweave_ecs::prelude::*;
pub use astraweave_ai::prelude::*;
pub use astraweave_render::prelude::*;
pub use astraweave_physics::prelude::*;
pub use astraweave_audio::prelude::*;
pub use astraweave_input::prelude::*;
}

App Builder:

use astraweave_sdk::prelude::*;

fn main() {
    App::new()
        .add_plugin(DefaultPlugins)
        .add_startup_system(setup)
        .add_system(game_logic)
        .run();
}

AI Crates

astraweave-ai

Core AI framework with perception, planning, and behavior systems.

[dependencies]
astraweave-ai = "0.1"

Key Types:

TypeDescription
AiAgentAI-controlled entity component
PerceptionBusSensory input aggregation
PlannerGoal-oriented action planning
BehaviorTreeBehavior tree execution
BlackboardShared AI state storage
AiToolTool definition for LLM agents

Example:

#![allow(unused)]
fn main() {
use astraweave_ai::prelude::*;

let mut agent = AiAgent::new()
    .with_perception_radius(50.0)
    .with_tick_budget_ms(8);

agent.add_goal(AiGoal::Patrol { 
    waypoints: vec![point_a, point_b, point_c] 
});
}

Features:

  • llm - Enable LLM integration (requires astraweave-llm)
  • goap - Goal-Oriented Action Planning
  • utility - Utility AI scoring system

astraweave-llm

LLM integration for AI agents with tool calling and validation.

[dependencies]
astraweave-llm = "0.1"

Key Types:

TypeDescription
LlmClientHTTP client for LLM endpoints
LlmConfigConfiguration for model and endpoint
ToolCallStructured tool invocation from LLM
ToolResultValidated tool execution result
PromptBuilderFluent prompt construction

Example:

#![allow(unused)]
fn main() {
use astraweave_llm::prelude::*;

let config = LlmConfig {
    endpoint: "http://localhost:11434".into(),
    model: "hermes2-pro-mistral".into(),
    temperature: 0.7,
    max_tokens: 256,
};

let client = LlmClient::new(config);
let response = client.complete("What should I do next?").await?;
}

Supported Backends:

  • Ollama (local)
  • OpenAI-compatible APIs
  • Custom endpoints

astraweave-memory

Memory systems for AI agents including short-term, long-term, and episodic memory.

[dependencies]
astraweave-memory = "0.1"

Key Types:

TypeDescription
MemoryStoreCentral memory management
ShortTermMemoryRecent observations with decay
LongTermMemoryPersistent important memories
EpisodicMemoryEvent sequences and narratives
MemoryQuerySemantic memory retrieval

astraweave-behavior

Behavior tree implementation with visual editor support.

[dependencies]
astraweave-behavior = "0.1"

Node Types:

CategoryNodes
CompositeSequence, Selector, Parallel, RandomSelector
DecoratorInverter, Repeater, Succeeder, UntilFail
LeafAction, Condition, Wait, SubTree

Example:

#![allow(unused)]
fn main() {
use astraweave_behavior::prelude::*;

let tree = BehaviorTree::new(
    Selector::new(vec![
        Sequence::new(vec![
            Condition::new("has_target"),
            Action::new("attack_target"),
        ]).into(),
        Action::new("patrol").into(),
    ])
);
}

Rendering Crates

astraweave-render

GPU rendering with Vulkan/DX12/Metal backends via wgpu.

[dependencies]
astraweave-render = "0.1"

Key Types:

TypeDescription
RendererMain rendering context
RenderPassConfigurable render pass
MeshVertex/index buffer pair
MaterialSurface properties and shaders
CameraView and projection configuration
LightPoint, directional, spot lights

Features:

  • pbr - Physically-based rendering (default)
  • shadows - Shadow mapping with CSM
  • post-process - Bloom, SSAO, tone mapping
  • skeletal - Skeletal animation

astraweave-materials

PBR material system with shader graph support.

[dependencies]
astraweave-materials = "0.1"

Material Properties:

PropertyTypeDescription
albedoColor or TextureBase color
metallicf32 or TextureMetallic factor (0-1)
roughnessf32 or TextureSurface roughness (0-1)
normalTextureNormal map
emissionColorEmissive color
aoTextureAmbient occlusion

astraweave-asset

Asset loading, caching, and hot-reloading.

[dependencies]
astraweave-asset = "0.1"

Key Types:

TypeDescription
AssetServerAsync asset loading
Handle<T>Reference-counted asset handle
AssetLoaderCustom loader trait
AssetEventLoad/unload notifications

Supported Formats:

  • Meshes: glTF 2.0, OBJ, FBX
  • Textures: PNG, JPEG, KTX2, DDS
  • Audio: WAV, OGG, MP3
  • Fonts: TTF, OTF

astraweave-ui

Immediate-mode UI with retained state for game interfaces.

[dependencies]
astraweave-ui = "0.1"

Key Types:

TypeDescription
UiContextUI state and input handling
WidgetBase widget trait
LayoutFlexbox-style layout
StyleVisual styling properties

Built-in Widgets:

  • Button, Label, TextInput
  • Slider, Checkbox, RadioGroup
  • Panel, ScrollView, Modal
  • ProgressBar, Tooltip

Simulation Crates

astraweave-physics

3D physics with Rapier backend.

[dependencies]
astraweave-physics = "0.1"

Key Types:

TypeDescription
RigidBodyDynamic, kinematic, or static body
ColliderCollision shape
PhysicsWorldPhysics simulation context
RayCastRay intersection queries
JointConstraints between bodies

Collider Shapes:

  • Ball, Cuboid, Capsule, Cylinder
  • ConvexHull, TriMesh, HeightField
  • Compound (multiple shapes)

astraweave-nav

Navigation mesh and pathfinding.

[dependencies]
astraweave-nav = "0.1"

Key Types:

TypeDescription
NavMeshNavigation mesh geometry
NavAgentPathfinding agent component
PathQueryPath computation request
NavObstacleDynamic obstacle

Features:

  • A* pathfinding with string pulling
  • Dynamic obstacle avoidance
  • Off-mesh links for jumps/ladders
  • Hierarchical pathfinding for large worlds

astraweave-audio

Spatial audio with multiple backends.

[dependencies]
astraweave-audio = "0.1"

Key Types:

TypeDescription
AudioSourcePositional audio emitter
AudioListenerSpatial audio receiver
AudioClipLoaded audio data
MixerAudio mixing and effects

Features:

  • 3D spatial audio with HRTF
  • Reverb zones
  • Audio occlusion
  • Streaming for music

Gameplay Crates

astraweave-gameplay

High-level gameplay systems and components.

[dependencies]
astraweave-gameplay = "0.1"

Systems:

  • Combat and damage
  • Inventory management
  • Status effects
  • Interactable objects
  • Save/load integration

astraweave-dialogue

Dialogue tree and conversation systems.

[dependencies]
astraweave-dialogue = "0.1"

Key Types:

TypeDescription
DialogueTreeBranching conversation graph
DialogueNodeSingle dialogue entry
DialogueControllerRuntime dialogue state
DynamicDialogueLLM-powered conversations

astraweave-quests

Quest tracking and objective systems.

[dependencies]
astraweave-quests = "0.1"

Key Types:

TypeDescription
QuestQuest definition
QuestLogPlayer’s active quests
ObjectiveQuest goal/task
QuestEventQuest state changes

astraweave-pcg

Procedural content generation framework.

[dependencies]
astraweave-pcg = "0.1"

Generators:

  • Terrain heightmaps with erosion
  • Dungeon layouts
  • Item properties
  • NPC backstories (AI-enhanced)
  • Quest generation (AI-enhanced)

Tool Crates

aw_editor

Visual editor for scenes, behavior trees, and materials.

cargo run -p aw_editor

Features:

  • Scene hierarchy view
  • Component inspector
  • Behavior tree editor
  • Material graph editor
  • Asset browser

aw_asset_cli

Command-line asset processing.

cargo run -p aw_asset_cli -- --help

Commands:

  • import - Convert assets to engine format
  • pack - Create asset bundles
  • validate - Check asset integrity
  • optimize - Compress and optimize assets

aw_debug

Runtime debugging tools.

[dependencies]
aw_debug = "0.1"

Features:

  • Entity inspector overlay
  • Performance graphs
  • Physics debug visualization
  • AI state visualization
  • Console commands

Feature Flags Summary

CrateFeatureDescription
astraweave-ecsparallelParallel system execution
astraweave-ecstracingPerformance instrumentation
astraweave-aillmLLM integration
astraweave-aigoapGoal-oriented planning
astraweave-renderpbrPBR materials
astraweave-rendershadowsShadow mapping
astraweave-physicsdebug-renderPhysics visualization
astraweave-audiospatial3D audio

Dependency Graph

graph LR
    subgraph External["External Dependencies"]
        wgpu[wgpu]
        rapier[rapier3d]
        tokio[tokio]
        serde[serde]
    end
    
    ECS[astraweave-ecs] --> serde
    RENDER[astraweave-render] --> wgpu
    RENDER --> ECS
    PHYSICS[astraweave-physics] --> rapier
    PHYSICS --> ECS
    LLM[astraweave-llm] --> tokio
    AI[astraweave-ai] --> ECS
    AI --> LLM

Configuration

This reference documents all configuration options for AstraWeave projects.

Project Configuration

Cargo.toml

Standard Rust project configuration with AstraWeave dependencies:

[package]
name = "my_game"
version = "0.1.0"
edition = "2021"

[dependencies]
astraweave-core = "0.1"
astraweave-ecs = "0.1"
astraweave-ai = "0.1"
astraweave-physics = "0.1"
astraweave-render = "0.1"
astraweave-audio = "0.1"
astraweave-input = "0.1"

[features]
default = ["graphics"]
graphics = ["astraweave-render/wgpu"]
headless = []
profiling = ["astraweave-core/tracy"]

[profile.release]
lto = true
codegen-units = 1
opt-level = 3

Runtime Configuration

Engine Configuration

Configure the engine at startup:

#![allow(unused)]
fn main() {
use astraweave_core::config::*;

let config = EngineConfig {
    tick_rate: TickRate::Fixed(60),
    
    window: WindowConfig {
        title: "My Game".into(),
        width: 1920,
        height: 1080,
        vsync: true,
        fullscreen: false,
    },
    
    graphics: GraphicsConfig {
        backend: GraphicsBackend::Auto,
        msaa_samples: 4,
        shadow_quality: ShadowQuality::High,
        max_lights: 128,
    },
    
    physics: PhysicsConfig {
        gravity: Vec3::new(0.0, -9.81, 0.0),
        substeps: 4,
        solver_iterations: 8,
    },
    
    audio: AudioConfig {
        sample_rate: 48000,
        channels: 2,
        buffer_size: 1024,
    },
    
    ai: AiConfig {
        tick_budget_ms: 8,
        max_concurrent_plans: 4,
        ollama_endpoint: "http://localhost:11434".into(),
        default_model: "hermes2-pro-mistral".into(),
    },
};

Engine::run(config, |world| {
    // Game setup
});
}

Configuration File (TOML)

Load configuration from file:

# config.toml

[engine]
tick_rate = 60

[window]
title = "My Game"
width = 1920
height = 1080
vsync = true
fullscreen = false

[graphics]
backend = "vulkan"
msaa_samples = 4
shadow_quality = "high"
max_lights = 128
hdr = true
bloom = true

[physics]
gravity = [0.0, -9.81, 0.0]
substeps = 4
solver_iterations = 8
ccd_enabled = true

[audio]
sample_rate = 48000
channels = 2
buffer_size = 1024
master_volume = 1.0

[ai]
tick_budget_ms = 8
max_concurrent_plans = 4
ollama_endpoint = "http://localhost:11434"
default_model = "hermes2-pro-mistral"
fallback_enabled = true

[navigation]
cell_size = 0.3
cell_height = 0.2
agent_radius = 0.5
agent_height = 1.8
max_slope = 45.0

[networking]
tick_rate = 30
interpolation_delay_ms = 100
max_clients = 32

Load in code:

#![allow(unused)]
fn main() {
use astraweave_core::config::EngineConfig;

let config = EngineConfig::from_file("config.toml")?;
}

System-Specific Configuration

Graphics Configuration

#![allow(unused)]
fn main() {
let graphics = GraphicsConfig {
    backend: GraphicsBackend::Vulkan,
    
    msaa_samples: 4,
    anisotropic_filtering: 16,
    
    shadow_quality: ShadowQuality::High,
    shadow_cascade_count: 4,
    shadow_map_size: 2048,
    
    max_lights: 128,
    clustered_lighting: true,
    cluster_dimensions: [16, 9, 24],
    
    hdr: true,
    bloom: true,
    bloom_intensity: 0.5,
    
    ambient_occlusion: AmbientOcclusion::Ssao,
    
    vsync: VsyncMode::Enabled,
    frame_limit: None,
};
}

Physics Configuration

#![allow(unused)]
fn main() {
let physics = PhysicsConfig {
    gravity: Vec3::new(0.0, -9.81, 0.0),
    
    substeps: 4,
    solver_iterations: 8,
    
    ccd_enabled: true,
    ccd_max_substeps: 4,
    
    broad_phase: BroadPhase::SweepAndPrune,
    
    contact_skin: 0.01,
    
    sleep_threshold_linear: 0.1,
    sleep_threshold_angular: 0.05,
    
    debug_render: false,
};
}

AI Configuration

#![allow(unused)]
fn main() {
let ai = AiConfig {
    tick_budget_ms: 8,
    
    max_concurrent_plans: 4,
    plan_queue_size: 16,
    
    ollama_endpoint: "http://localhost:11434".into(),
    default_model: "hermes2-pro-mistral".into(),
    
    temperature: 0.7,
    max_tokens: 256,
    request_timeout_ms: 100,
    
    plan_cache_enabled: true,
    plan_cache_duration_ms: 2000,
    
    fallback_enabled: true,
    
    context_window_size: 4096,
    
    tool_validation_strict: true,
};
}

Audio Configuration

#![allow(unused)]
fn main() {
let audio = AudioConfig {
    sample_rate: 48000,
    channels: 2,
    buffer_size: 1024,
    
    master_volume: 1.0,
    music_volume: 0.8,
    sfx_volume: 1.0,
    voice_volume: 1.0,
    
    max_simultaneous_sounds: 32,
    
    spatial_audio: true,
    hrtf_enabled: true,
    
    reverb_enabled: true,
    
    distance_model: DistanceModel::InverseSquare,
    rolloff_factor: 1.0,
    reference_distance: 1.0,
    max_distance: 100.0,
};
}
#![allow(unused)]
fn main() {
let navigation = NavigationConfig {
    cell_size: 0.3,
    cell_height: 0.2,
    
    agent_radius: 0.5,
    agent_height: 1.8,
    agent_max_climb: 0.4,
    agent_max_slope: 45.0,
    
    region_min_size: 8,
    region_merge_size: 20,
    
    edge_max_len: 12.0,
    edge_max_error: 1.3,
    
    verts_per_poly: 6,
    detail_sample_distance: 6.0,
    detail_sample_max_error: 1.0,
    
    tile_size: 64,
    
    pathfinder: PathfinderConfig {
        max_iterations: 2048,
        heuristic_scale: 1.0,
    },
    
    crowd: CrowdConfig {
        max_agents: 128,
        avoidance_quality: AvoidanceQuality::High,
    },
};
}

Input Configuration

#![allow(unused)]
fn main() {
let input = InputConfig {
    deadzone: 0.15,
    
    mouse_sensitivity: 1.0,
    mouse_invert_y: false,
    mouse_smoothing: true,
    
    gamepad_enabled: true,
    gamepad_vibration: true,
    
    action_repeat_delay_ms: 500,
    action_repeat_rate_ms: 50,
};
}

Environment Variables

Override configuration with environment variables:

VariableDescriptionDefault
ASTRAWEAVE_LOGLog level (trace, debug, info, warn, error)info
ASTRAWEAVE_GRAPHICS_BACKENDGraphics backend (vulkan, dx12, metal)Auto
ASTRAWEAVE_OLLAMA_ENDPOINTOllama API endpointhttp://localhost:11434
ASTRAWEAVE_OLLAMA_MODELDefault LLM modelhermes2-pro-mistral
ASTRAWEAVE_TICK_RATESimulation tick rate60
ASTRAWEAVE_HEADLESSRun without graphicsfalse

Example:

ASTRAWEAVE_LOG=debug ASTRAWEAVE_HEADLESS=true cargo run -p my_game

Platform-Specific Configuration

Windows

[target.'cfg(windows)'.graphics]
backend = "dx12"

Linux

[target.'cfg(target_os = "linux")'.graphics]
backend = "vulkan"

macOS

[target.'cfg(target_os = "macos")'.graphics]
backend = "metal"

Debug Configuration

#![allow(unused)]
fn main() {
#[cfg(debug_assertions)]
let debug_config = DebugConfig {
    show_fps: true,
    show_entity_count: true,
    physics_debug_draw: true,
    navmesh_debug_draw: false,
    ai_debug_overlay: true,
    memory_stats: true,
    profiler_enabled: true,
};
}

See Also

CLI Tools

AstraWeave provides command-line tools for development, asset processing, debugging, and deployment.

Overview

ToolPurpose
aw_buildBuild and package games
aw_asset_cliAsset processing and optimization
aw_debugRuntime debugging and inspection
aw_editorVisual editor (GUI)
aw_headlessHeadless server for testing
aw_save_cliSave file management

aw_build

Build and package games for distribution.

Installation

cargo install --path tools/aw_build

Usage

# Build for current platform
aw_build

# Build release version
aw_build --release

# Build for specific platform
aw_build --target windows
aw_build --target linux
aw_build --target macos
aw_build --target web

# Full package with assets
aw_build --release --package

# Custom output directory
aw_build --release --output dist/

Configuration

Create aw_build.toml in your project root:

[package]
name = "my-game"
version = "1.0.0"
author = "Developer Name"
description = "An awesome game"

[build]
features = ["release-optimizations"]
target_dir = "target"

[assets]
source = "assets"
compress = true
strip_unused = true

[platforms.windows]
icon = "assets/icons/game.ico"
installer = true

[platforms.linux]
appimage = true

[platforms.macos]
bundle = true
icon = "assets/icons/game.icns"

[platforms.web]
wasm_opt = true

Build Profiles

# Debug build (fast compile, slow runtime)
aw_build --profile debug

# Release build (slow compile, optimized)
aw_build --profile release

# Distribution build (maximum optimization)
aw_build --profile dist

aw_asset_cli

Process and optimize game assets.

Installation

cargo install --path tools/aw_asset_cli

Commands

Import Assets

# Import a single asset
aw_asset_cli import models/character.fbx

# Import with options
aw_asset_cli import models/character.fbx --format gltf --optimize

# Batch import directory
aw_asset_cli import-dir raw_assets/ --recursive

Optimize Assets

# Optimize textures
aw_asset_cli optimize textures/ --compress --generate-mipmaps

# Optimize meshes
aw_asset_cli optimize-mesh models/ --simplify 0.5

# Full optimization pass
aw_asset_cli optimize-all assets/

Pack Assets

# Create asset bundle
aw_asset_cli pack assets/ --output game.pak

# Pack with compression
aw_asset_cli pack assets/ --output game.pak --compression zstd

# Pack specific types
aw_asset_cli pack assets/textures/ --output textures.pak --type texture

Validate Assets

# Check asset integrity
aw_asset_cli validate assets/

# Check specific asset
aw_asset_cli validate models/character.gltf --verbose

# Validate bundle
aw_asset_cli validate-bundle game.pak

Texture Processing

# Convert to KTX2 format
aw_asset_cli texture convert input.png --format ktx2

# Generate mipmaps
aw_asset_cli texture mipmaps input.png

# Compress textures
aw_asset_cli texture compress textures/ --format bc7

# Create texture atlas
aw_asset_cli texture atlas sprites/*.png --output atlas.png --json atlas.json

Mesh Processing

# Simplify mesh
aw_asset_cli mesh simplify model.gltf --ratio 0.5 --output model_lod1.gltf

# Generate LODs
aw_asset_cli mesh lod model.gltf --levels 3

# Optimize vertex cache
aw_asset_cli mesh optimize model.gltf --vertex-cache --output optimized.gltf

aw_debug

Runtime debugging and inspection tools.

Installation

cargo install --path tools/aw_debug

Attach to Running Game

# Connect to local game
aw_debug attach

# Connect to specific port
aw_debug attach --port 9999

# Connect to remote game
aw_debug attach --host 192.168.1.100 --port 9999

Commands (Interactive Mode)

> help                    # Show available commands
> entities                # List all entities
> entity 42               # Inspect entity 42
> components Player       # Find entities with Player component
> resource GameState      # Inspect resource
> systems                 # List running systems
> perf                    # Show performance stats
> mem                     # Show memory usage
> spawn Enemy 10.0 0.0 5.0  # Spawn entity at position
> despawn 42              # Despawn entity
> pause                   # Pause game
> resume                  # Resume game
> step                    # Step one frame
> quit                    # Disconnect

Scripted Commands

# Run commands from file
aw_debug attach --script debug_commands.txt

# Run single command
aw_debug attach --exec "entities | grep Player"

# Dump state to file
aw_debug attach --exec "dump world.json"

Profiling

# Start profiling
aw_debug attach --profile

# Capture Tracy trace
aw_debug attach --tracy --output trace.tracy

# Memory profiling
aw_debug attach --memory-profile --output memory.json

aw_editor

Visual editor for scene creation and game development.

Installation

cargo install --path tools/aw_editor

Launch

# Open editor
aw_editor

# Open specific project
aw_editor --project path/to/project

# Open specific scene
aw_editor --project myproject --scene levels/main.scene

Features

  • Scene hierarchy view
  • Component inspector
  • Asset browser
  • Behavior tree editor
  • Material editor
  • Play-in-editor

Keyboard Shortcuts

ShortcutAction
Ctrl+SSave scene
Ctrl+Shift+SSave all
Ctrl+ZUndo
Ctrl+Shift+ZRedo
F5Play
Shift+F5Stop
FFocus selected
DeleteDelete selected

aw_headless

Run games without rendering for testing and CI.

Installation

cargo install --path tools/aw_headless

Usage

# Run game headlessly
aw_headless --project path/to/game

# Run for specific duration
aw_headless --project game --duration 60

# Run specific test scene
aw_headless --project game --scene test_level.scene

# Run with custom config
aw_headless --project game --config test_config.toml

Testing

# Run integration tests
aw_headless --project game --test integration

# Run performance benchmark
aw_headless --project game --benchmark --output results.json

# Run AI behavior tests
aw_headless --project game --test ai-behaviors --verbose

CI Integration

# GitHub Actions example
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Build
        run: cargo build --release
      - name: Headless Tests
        run: aw_headless --project . --test all --timeout 300

aw_save_cli

Manage game save files.

Installation

cargo install --path tools/aw_save_cli

Commands

# List saves
aw_save_cli list

# Inspect save
aw_save_cli inspect save_001.dat

# Validate save
aw_save_cli validate save_001.dat

# Export to JSON
aw_save_cli export save_001.dat --format json --output save.json

# Import from JSON
aw_save_cli import save.json --output save_new.dat

# Migrate save to new version
aw_save_cli migrate save_001.dat --to-version 2

Save File Locations

PlatformDefault Location
Windows%APPDATA%/GameName/saves/
Linux~/.local/share/GameName/saves/
macOS~/Library/Application Support/GameName/saves/

Common Workflows

Development Build

# Quick iteration
cargo build
cargo run

# With asset hot-reload
ASTRAWEAVE_HOT_RELOAD=1 cargo run

Release Build

# Build optimized release
aw_build --release

# Process assets
aw_asset_cli optimize-all assets/

# Package for distribution
aw_build --release --package

CI/CD Pipeline

# Install tools
cargo install --path tools/aw_build
cargo install --path tools/aw_asset_cli
cargo install --path tools/aw_headless

# Build
aw_build --release

# Test
aw_headless --test all

# Package
aw_build --release --package --output artifacts/

Environment Variables

VariableDescription
ASTRAWEAVE_LOGLog level (trace, debug, info, warn, error)
ASTRAWEAVE_ASSETSAsset directory path
ASTRAWEAVE_CONFIGConfig file path
ASTRAWEAVE_HOT_RELOADEnable hot-reload (1/0)
ASTRAWEAVE_DEBUG_PORTDebug server port

Platform Support

AstraWeave targets multiple platforms with varying levels of support and feature availability.

Support Tiers

TierDefinitionGuarantees
Tier 1Fully supported, tested in CIFull feature support, regular testing, priority bug fixes
Tier 2Supported, community testedMost features work, issues addressed on best-effort basis
Tier 3ExperimentalMay work, no guarantees, community-maintained

Platform Matrix

Desktop Platforms

PlatformTierRenderingAI/LLMAudioNetworking
Windows 10/11 (x64)Tier 1Vulkan, DX12FullFullFull
Linux (x64)Tier 1VulkanFullFullFull
macOS (x64)Tier 2MetalFullFullFull
macOS (Apple Silicon)Tier 2MetalFullFullFull

Web Platforms

PlatformTierRenderingAI/LLMAudioNetworking
WebAssemblyTier 2WebGPU, WebGL2Remote onlyWeb AudioWebSocket

Console Platforms

PlatformTierNotes
PlayStation 5PlannedPost-1.0
Xbox Series X/SPlannedPost-1.0
Nintendo SwitchPlannedPost-1.0

Mobile Platforms

PlatformTierNotes
AndroidTier 3Experimental
iOSTier 3Experimental

System Requirements

Minimum Requirements

ComponentWindowsLinuxmacOS
OSWindows 10 (1909+)Ubuntu 20.04+ / Fedora 34+macOS 11+
CPUx64, 4 coresx64, 4 coresx64 / Apple M1+
RAM8 GB8 GB8 GB
GPUVulkan 1.2 / DX12Vulkan 1.2Metal 2
Storage2 GB2 GB2 GB
ComponentSpecification
CPU8+ cores, modern (2020+)
RAM16 GB+
GPURTX 2060 / RX 5700 / M1 Pro+
StorageSSD, 10 GB+

For AI/LLM Features

FeatureAdditional Requirements
Local LLM (Ollama)16 GB+ RAM, fast SSD
GPU-accelerated LLMNVIDIA GPU with 8 GB+ VRAM, CUDA
Large context windows32 GB+ RAM

Graphics API Support

Vulkan (Windows, Linux)

Primary rendering backend for Windows and Linux.

[dependencies]
astraweave-render = { version = "0.1", features = ["vulkan"] }

Requirements:

  • Vulkan 1.2+
  • Up-to-date GPU drivers

Tested GPUs:

  • NVIDIA: GTX 1000+, RTX series
  • AMD: RX 400+, RX 5000+, RX 6000+, RX 7000+
  • Intel: UHD 600+, Arc series

DirectX 12 (Windows)

Alternative backend for Windows.

[dependencies]
astraweave-render = { version = "0.1", features = ["dx12"] }

Requirements:

  • Windows 10 1909+
  • DirectX 12 capable GPU

Metal (macOS)

Primary backend for macOS.

[dependencies]
astraweave-render = { version = "0.1", features = ["metal"] }

Requirements:

  • macOS 11 (Big Sur)+
  • Metal 2 capable GPU

WebGPU (Web)

Modern web rendering API.

[dependencies]
astraweave-render = { version = "0.1", features = ["webgpu"] }

Requirements:

  • Chrome 113+ / Firefox 115+ / Safari 17+
  • WebGPU enabled

WebGL2 (Web Fallback)

Fallback for browsers without WebGPU.

[dependencies]
astraweave-render = { version = "0.1", features = ["webgl2"] }

Limitations:

  • Reduced shader capabilities
  • Lower texture limits
  • No compute shaders

Feature Availability by Platform

Core Features

FeatureWindowsLinuxmacOSWeb
ECSYesYesYesYes
PhysicsYesYesYesYes
AudioYesYesYesYes
InputYesYesYesPartial
NetworkingYesYesYesWebSocket

Rendering Features

FeatureWindowsLinuxmacOSWeb
PBR MaterialsYesYesYesYes
Shadow MappingYesYesYesLimited
Post-ProcessingYesYesYesLimited
Skeletal AnimationYesYesYesYes
Compute ShadersYesYesYesWebGPU only

AI Features

FeatureWindowsLinuxmacOSWeb
Behavior TreesYesYesYesYes
GOAPYesYesYesYes
Local LLM (Ollama)YesYesYesNo
Remote LLMYesYesYesYes
GPU LLM (CUDA)YesLimitedNoNo

Building for Platforms

Windows

# Native build
cargo build --release

# Cross-compile from Linux
cargo build --release --target x86_64-pc-windows-gnu

Linux

# Native build
cargo build --release

# With specific features
cargo build --release --features "vulkan,llm"

macOS

# Native build
cargo build --release

# Universal binary (x64 + ARM)
cargo build --release --target x86_64-apple-darwin
cargo build --release --target aarch64-apple-darwin
lipo -create -output libgame.dylib \
  target/x86_64-apple-darwin/release/libgame.dylib \
  target/aarch64-apple-darwin/release/libgame.dylib

WebAssembly

# Install wasm-pack
cargo install wasm-pack

# Build for web
wasm-pack build --target web --release

# Optimize WASM
wasm-opt -O3 -o game_opt.wasm game.wasm

Platform-Specific Configuration

Windows Configuration

# astraweave.toml
[platform.windows]
renderer = "dx12"  # or "vulkan"
vsync = true
fullscreen_mode = "borderless"

[platform.windows.audio]
backend = "wasapi"

Linux Configuration

[platform.linux]
renderer = "vulkan"
vsync = true
wayland = true  # Enable Wayland support

[platform.linux.audio]
backend = "pulseaudio"  # or "alsa", "pipewire"

macOS Configuration

[platform.macos]
renderer = "metal"
retina = true
native_fullscreen = true

[platform.macos.audio]
backend = "coreaudio"

Web Configuration

[platform.web]
renderer = "webgpu"  # or "webgl2"
canvas_id = "game-canvas"

[platform.web.audio]
backend = "webaudio"

Known Limitations

Windows

  • DX12 may require Windows Update for latest features
  • Some anti-virus software may flag debug builds

Linux

  • Wayland compositor compatibility varies
  • Audio backend may need manual selection

macOS

  • Notarization required for distribution
  • Some features limited without Rosetta on Apple Silicon

Web

  • No local file system access
  • LLM requires remote server
  • Thread support limited (SharedArrayBuffer)
  • Binary size considerations

Troubleshooting Platform Issues

Windows: DX12 Errors

Error: Failed to create DX12 device

Solution: Update GPU drivers and Windows.

Linux: Vulkan Not Found

Error: Failed to load Vulkan library

Solution:

# Ubuntu/Debian
sudo apt install vulkan-tools mesa-vulkan-drivers

# Fedora
sudo dnf install vulkan-loader vulkan-tools

macOS: Metal Errors

Error: Metal device not found

Solution: Ensure running on macOS 11+ with Metal-capable GPU.

Web: WebGPU Not Supported

Error: WebGPU not available

Solution: Enable WebGPU in browser settings or use WebGL2 fallback:

[platform.web]
renderer = "webgl2"

Platform Testing

CI/CD Matrix

# GitHub Actions example
strategy:
  matrix:
    os: [ubuntu-latest, windows-latest, macos-latest]
    include:
      - os: ubuntu-latest
        target: x86_64-unknown-linux-gnu
      - os: windows-latest
        target: x86_64-pc-windows-msvc
      - os: macos-latest
        target: x86_64-apple-darwin

Platform-Specific Tests

#![allow(unused)]
fn main() {
#[cfg(target_os = "windows")]
#[test]
fn test_windows_specific() {
    // Windows-only test
}

#[cfg(target_os = "linux")]
#[test]
fn test_linux_specific() {
    // Linux-only test
}

#[cfg(target_arch = "wasm32")]
#[test]
fn test_wasm_specific() {
    // WebAssembly-only test
}
}

Glossary

A comprehensive glossary of terms used throughout the AstraWeave documentation.

A

Action
The final stage of the AI loop where an agent executes planned behaviors through validated tool calls.
Admonish
An mdBook preprocessor for creating callout blocks (warnings, notes, tips).
Agent
An AI-controlled entity in the game world with perception, planning, and action capabilities.
Arbiter
The AI system responsible for validating tool calls and ensuring agents can only perform sanctioned actions within game rules.
Archetype
In ECS, a unique combination of component types. Entities with the same components share an archetype for cache-efficient storage.
Asset Pipeline
The content processing workflow that converts raw assets (textures, models, audio) into engine-optimized formats.

B

Batch Executor
A system for grouping multiple LLM inference requests to improve throughput.
Behavior Tree
A hierarchical AI decision-making structure where nodes represent conditions, actions, and control flow.
Blackboard
A shared data structure for AI agents to read and write state information.
Biome
A terrain zone with distinct environmental characteristics (forest, desert, etc.).

C

Character Controller
A physics component that handles player/NPC movement with collision detection.
Clustered Lighting
A rendering technique that divides the view frustum into clusters for efficient many-light rendering.
Component
In ECS, a piece of data attached to an entity. Components contain only data, not behavior.
Core Loop
The fundamental AI cycle: Perception -> Reasoning -> Planning -> Action.

D

Deterministic Simulation
A simulation where given the same inputs, the outputs are always identical. Essential for replay and networking.
Director
An AI system that orchestrates game pacing, difficulty, and narrative events.

E

ECS (Entity Component System)
A data-oriented architecture pattern where entities are IDs, components are data, and systems are logic operating on components.
Entity
In ECS, a unique identifier (typically an integer with a generation counter) that components are attached to.
Episode
In memory systems, a recorded sequence of events that can be recalled for learning.

F

Fixed Timestep
A simulation update rate that runs at a constant interval (e.g., 60Hz) regardless of frame rate.
Frame
One complete update cycle of the game, including input, simulation, and rendering.

G

GOAP (Goal-Oriented Action Planning)
An AI planning algorithm that finds optimal action sequences to achieve goals by searching through possible world states.
Generational Index
An entity ID with a generation counter to detect use-after-free bugs when entities are recycled.

H

HDRI (High Dynamic Range Image)
An image format storing extended brightness values, commonly used for environment lighting.

I

IBL (Image-Based Lighting)
A technique using environment maps to provide realistic ambient lighting.

K

KTX2
A texture container format supporting GPU compression and mipmaps.

L

LLM (Large Language Model)
AI models (like GPT, Llama, Phi) used for natural language understanding and generation.
LOD (Level of Detail)
Rendering optimization that uses simpler geometry for distant objects.

M

Mermaid
A JavaScript-based diagramming tool integrated into mdBook for rendering flowcharts and diagrams.
Mipmap
Pre-calculated, progressively smaller versions of a texture for efficient rendering at different distances.
MRA
Metallic-Roughness-AO texture packing format used in PBR materials.

N

Nanite
Unreal Engine 5’s virtualized geometry system. AstraWeave implements similar techniques for GPU-driven LOD.
A polygon mesh representing walkable areas for AI pathfinding.

O

Orchestrator
The AI component that coordinates multiple agents and manages their interactions.

P

PBR (Physically Based Rendering)
A rendering approach that simulates realistic light behavior using physical principles.
Perception
The first stage of the AI loop where agents observe the game world state.
Planning
The AI stage where goals are decomposed into action sequences.
Preprocessor
An mdBook tool that transforms source files before rendering.

Q

Query
In ECS, a request for entities matching specific component patterns.

R

RAG (Retrieval-Augmented Generation)
A technique combining information retrieval with LLM generation for context-aware responses.
Ragdoll
A physics-based character simulation for realistic death/knockback animations.
Rapier
The Rust physics engine used by AstraWeave.
Rhai
An embedded scripting language for Rust, used in AstraWeave for game logic.

S

Sandbox
An isolated execution environment for AI tool calls, preventing unauthorized actions.
SIMD (Single Instruction Multiple Data)
CPU instructions processing multiple data points simultaneously for performance.
Spatial Hash
A data structure for efficient spatial queries (collision detection, neighbor finding).
System
In ECS, logic that operates on entities with specific component combinations.

T

Tick
One fixed timestep update of the simulation.
Tool
In AI systems, an action an agent can take, validated by the Arbiter.
Tool Vocabulary
The set of actions available to AI agents, each with schema and validation rules.

U

Utility AI
An AI approach where actions are scored by utility functions and the highest-scoring action is selected.

V

Voxel
A 3D pixel, used in terrain systems for destructible/modifiable environments.

W

WGPU
A Rust graphics library providing a cross-platform abstraction over Vulkan, Metal, DX12, and WebGPU.
WGSL (WebGPU Shading Language)
The shader language used with WGPU.
World
The container for all entities, components, and resources in the engine.

See Also

FAQ

Frequently asked questions about AstraWeave, the AI-native game engine.

General

What is AstraWeave?

AstraWeave is an AI-native game engine built in Rust. Unlike traditional engines that add AI as an afterthought, AstraWeave places intelligent agents at the core of its architecture. AI companions, enemies, and NPCs use the same validated game systems as players, ensuring fair and emergent gameplay.

How is AstraWeave different from Unity or Unreal?

FeatureAstraWeaveUnity/Unreal
AI ArchitectureAI-first, tool-validatedAI as addon/plugin
LLM IntegrationNative, first-classThird-party plugins
DeterminismGuaranteed by designOptional, complex
LanguageRust (safe, fast)C#/C++
Multiplayer AISame validation for AI & playersSeparate systems

Is AstraWeave production-ready?

AstraWeave is in active development (pre-1.0). Core systems are functional and tested, but APIs may change. We recommend it for:

  • Research and prototyping
  • Indie game development
  • Learning AI game development
  • Contributing to an open-source engine

What platforms does AstraWeave support?

  • Windows: Full support (primary development platform)
  • Linux: Full support
  • macOS: Supported (Apple Silicon native)
  • WebAssembly: Experimental (rendering only)
  • Consoles: Planned for post-1.0

AI System

Which LLMs does AstraWeave support?

AstraWeave uses Ollama for local LLM inference. Recommended models:

ModelUse CaseVRAM Required
hermes2-pro-mistralTool calling, dialogue8GB
phi3:miniLightweight inference4GB
llama3:8bGeneral purpose8GB
mistral:7bFast inference8GB

Can I use cloud LLMs like GPT-4 or Claude?

Not currently. AstraWeave is designed for local inference to ensure:

  • Deterministic behavior
  • Low latency (8ms budget per tick)
  • Privacy (no data leaves the machine)
  • Offline gameplay

Cloud support may be added post-1.0 for specific use cases.

How does AI validation work?

All AI actions go through the Tool Validation System:

#![allow(unused)]
fn main() {
let tool_call = ToolCall {
    tool: "move_to",
    params: json!({ "target": [10.0, 0.0, 5.0] }),
};

match validator.validate(&tool_call, &world_state) {
    ToolResult::Success(action) => execute(action),
    ToolResult::Blocked(reason) => handle_failure(reason),
}
}

This prevents AI from cheating - it can only perform actions that would be valid for a player.

Why does my AI companion not respond?

Common causes:

  1. Ollama not running: Start with ollama serve
  2. Model not pulled: Run ollama pull hermes2-pro-mistral
  3. Port conflict: Check Ollama is on port 11434
  4. Timeout: AI has an 8ms budget per tick; complex queries may timeout

Performance

What are the minimum system requirements?

ComponentMinimumRecommended
CPU4 cores, 2.5 GHz8 cores, 3.5 GHz
RAM8 GB16 GB
GPUGTX 1060 / RX 580RTX 3070 / RX 6800
VRAM4 GB8 GB (for LLM)
Storage10 GB50 GB SSD

Why is my game running slowly?

  1. Debug builds: Always use --release for playable performance
  2. LLM inference: Check Ollama is GPU-accelerated
  3. Physics overhead: Reduce collision complexity
  4. AI agents: Limit concurrent planning operations

How do I profile my game?

AstraWeave integrates with Tracy profiler:

cargo run -p your_game --release --features profiling

See Performance Optimization for details.

ECS Architecture

Why does AstraWeave use its own ECS?

AstraWeave’s ECS is designed for:

  • Deterministic iteration: Same order every tick
  • Generational entities: Safe entity references
  • AI integration: Perception and planning systems
  • Networking: State synchronization

It’s inspired by Bevy but optimized for AI-native gameplay.

How do I create entities?

#![allow(unused)]
fn main() {
let entity = world.spawn((
    Transform::from_xyz(0.0, 1.0, 0.0),
    RigidBody::Dynamic,
    Collider::sphere(0.5),
    Health { current: 100.0, max: 100.0 },
));
}

How do I query entities?

#![allow(unused)]
fn main() {
fn damage_system(
    mut query: Query<(&mut Health, &Transform), With<Enemy>>,
) {
    for (mut health, transform) in query.iter_mut() {
        if in_danger_zone(transform) {
            health.current -= 10.0;
        }
    }
}
}

Building and Development

How do I build from source?

git clone https://github.com/lazyxeon/AstraWeave-AI-Native-Gaming-Engine
cd AstraWeave-AI-Native-Gaming-Engine
cargo build --release

Why is the first build so slow?

AstraWeave has many dependencies. First builds compile:

  • wgpu (graphics)
  • rapier (physics)
  • rodio (audio)
  • tokio (async runtime)

Subsequent builds are much faster due to incremental compilation.

How do I run the examples?

cargo run -p hello_companion --release
cargo run -p physics_demo3d --release
cargo run -p unified_showcase --release

How do I contribute?

See Contributing Guide. In brief:

  1. Fork the repository
  2. Create a feature branch
  3. Make changes with tests
  4. Submit a pull request

Troubleshooting

“LosBlocked” error in hello_companion

This is expected behavior! The example demonstrates that AI cannot perform invalid actions. The companion tries to move but line-of-sight is blocked, proving the validation system works.

Shader compilation errors

Ensure your GPU driver is up to date. WGPU requires:

  • Vulkan 1.1+ on Windows/Linux
  • Metal on macOS
  • WebGPU on browsers

“Entity not found” panic

This usually means an entity was despawned while still referenced. Use generational entities:

#![allow(unused)]
fn main() {
if world.get::<Health>(entity).is_some() {
    // Entity still exists
}
}

Audio not playing

Check audio device availability and ensure the audio example works:

cargo run -p audio_spatial_demo --release

Licensing

What license is AstraWeave under?

AstraWeave uses a dual license:

  • Apache 2.0: For most use cases
  • MIT: Alternative option

Commercial use is permitted under both licenses.

Can I use AstraWeave for commercial games?

Yes! Both Apache 2.0 and MIT licenses permit commercial use. Attribution is appreciated but not required.

Getting Help

Where can I get help?

  • GitHub Issues: Bug reports and feature requests
  • GitHub Discussions: Questions and community chat
  • Documentation: This site
  • Examples: examples/ directory in the repository

How do I report a bug?

Open a GitHub issue with:

  1. AstraWeave version
  2. Operating system
  3. Steps to reproduce
  4. Expected vs actual behavior
  5. Error messages or logs

Performance Tips

Quick reference for optimizing your AstraWeave games. For in-depth performance optimization, see the Performance Guide.

Quick Wins

Build Configuration

Always use release builds for performance testing:

cargo run --release -p your_game

Common Optimizations

IssueSolution
Low FPSUse release builds, reduce entity count
High memoryLimit AI memory buffers, use asset streaming
AI lagIncrease planning interval, reduce perception range
Render stutterEnable frustum culling, use LODs

AI Performance

#![allow(unused)]
fn main() {
ai_config.planning_interval = Duration::from_millis(500);
ai_config.perception_range = 15.0;
ai_config.max_concurrent_llm_requests = 2;
}

ECS Batching

Process entities in batches:

#![allow(unused)]
fn main() {
fn process_entities(query: Query<(&Transform, &mut Velocity)>) {
    query.par_iter_mut().for_each(|(transform, mut velocity)| {
        velocity.0 = calculate_velocity(transform);
    });
}
}

Performance Targets

MetricTargetMeasurement
Frame time< 16.67ms60 FPS
AI tick< 5msPlanning + execution
Physics step< 4msCollision + dynamics
Render< 8msDraw calls + GPU

Profiling Tools

  • Tracy: Real-time frame profiler
  • Cargo flamegraph: CPU profiling
  • RenderDoc: GPU debugging
cargo install cargo-flamegraph
cargo flamegraph --release -p your_game

See Also

Best Practices

This guide covers recommended practices for building production-quality games with AstraWeave, covering architecture, performance, AI integration, and maintainability.

Project Architecture

Modular Design

Organize game code into focused modules:

my_game/
├── src/
│   ├── main.rs              # Entry point
│   ├── lib.rs               # Game library root
│   ├── player/              # Player systems
│   │   ├── mod.rs
│   │   ├── controller.rs
│   │   └── inventory.rs
│   ├── enemy/               # Enemy systems
│   │   ├── mod.rs
│   │   ├── ai.rs
│   │   └── spawner.rs
│   ├── world/               # World management
│   │   ├── mod.rs
│   │   ├── terrain.rs
│   │   └── weather.rs
│   └── ui/                  # User interface
│       ├── mod.rs
│       ├── hud.rs
│       └── menus.rs
├── assets/
├── config/
└── Cargo.toml

Plugin Pattern

Encapsulate features as plugins:

pub struct PlayerPlugin;

impl Plugin for PlayerPlugin {
    fn build(&self, app: &mut App) {
        app
            .init_resource::<PlayerConfig>()
            .add_systems(Startup, spawn_player)
            .add_systems(Update, (
                player_movement,
                player_combat,
                player_inventory,
            ).chain());
    }
}

pub struct EnemyPlugin;

impl Plugin for EnemyPlugin {
    fn build(&self, app: &mut App) {
        app
            .add_systems(Update, (
                enemy_ai_tick,
                enemy_spawning,
                enemy_cleanup,
            ));
    }
}

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_plugins(PlayerPlugin)
        .add_plugins(EnemyPlugin)
        .run();
}

Dependency Injection

Use resources for configuration and shared state:

#![allow(unused)]
fn main() {
#[derive(Resource)]
pub struct GameConfig {
    pub difficulty: Difficulty,
    pub render_distance: f32,
    pub ai_budget_ms: f32,
}

#[derive(Resource)]
pub struct GameState {
    pub current_level: String,
    pub player_score: u64,
    pub elapsed_time: f32,
}

fn game_system(
    config: Res<GameConfig>,
    mut state: ResMut<GameState>,
) {
    // Access shared configuration and state
}
}

ECS Best Practices

Component Design

Do:

#![allow(unused)]
fn main() {
// Small, focused components
#[derive(Component)]
struct Health(f32);

#[derive(Component)]
struct MaxHealth(f32);

#[derive(Component)]
struct Damage(f32);

// Use marker components
#[derive(Component)]
struct Player;

#[derive(Component)]
struct Enemy;

#[derive(Component)]
struct Hostile;
}

Don’t:

#![allow(unused)]
fn main() {
// Avoid monolithic components
#[derive(Component)]
struct Entity {
    health: f32,
    max_health: f32,
    damage: f32,
    position: Vec3,
    rotation: Quat,
    velocity: Vec3,
    ai_state: AiState,
    inventory: Vec<Item>,
    // ... more fields
}
}

System Organization

Group related systems with clear ordering:

#![allow(unused)]
fn main() {
app.add_systems(Update, (
    // Input phase
    read_player_input,
    
    // AI phase (can run in parallel)
    (enemy_perception, companion_perception).run_if(should_run_ai),
    enemy_decision,
    companion_decision,
    
    // Movement phase
    apply_movement,
    resolve_collisions,
    
    // Combat phase
    process_attacks,
    apply_damage,
    check_deaths,
    
    // Cleanup phase
    despawn_dead_entities,
).chain());
}

Query Best Practices

#![allow(unused)]
fn main() {
// Good: Specific queries with filters
fn enemy_update(
    enemies: Query<(&mut Transform, &mut AiAgent), (With<Enemy>, Without<Dead>)>,
) {
    for (mut transform, mut ai) in enemies.iter_mut() {
        // Only active enemies
    }
}

// Good: Read-only where possible
fn render_system(
    renderables: Query<(&Transform, &Mesh, &Material)>,
) {
    for (transform, mesh, material) in renderables.iter() {
        // Read-only access allows parallelism
    }
}

// Good: Use change detection
fn on_health_changed(
    query: Query<(Entity, &Health), Changed<Health>>,
) {
    for (entity, health) in query.iter() {
        // Only runs when health actually changes
    }
}
}

AI Integration

LLM Usage Guidelines

When to use LLM:

  • Dynamic dialogue and conversation
  • Procedural narrative generation
  • Player behavior analysis
  • Creative content generation

When NOT to use LLM:

  • Real-time combat decisions (too slow)
  • Deterministic game logic
  • Anything requiring < 100ms response

AI Architecture

#![allow(unused)]
fn main() {
// Layer AI appropriately
pub struct AiStack {
    // Fast: Behavior trees for immediate reactions
    pub behavior_tree: BehaviorTree,
    
    // Medium: GOAP for tactical planning
    pub planner: GoalPlanner,
    
    // Slow: LLM for strategic/narrative decisions
    pub llm_client: Option<LlmClient>,
}

impl AiStack {
    pub fn update(&mut self, context: &AiContext) {
        // Always run fast layer
        self.behavior_tree.tick(context);
        
        // Run planner when needed
        if self.needs_replan(context) {
            self.planner.plan(context);
        }
        
        // Async LLM calls for non-critical decisions
        if self.should_consult_llm(context) {
            self.queue_llm_request(context);
        }
    }
}
}

Tool Validation

Always validate LLM tool calls:

#![allow(unused)]
fn main() {
pub fn validate_tool_call(call: &ToolCall, context: &GameContext) -> ToolResult {
    match call.name.as_str() {
        "move_to" => {
            let target: Vec3 = call.parse_param("target")?;
            
            // Validate target is reachable
            if !context.navmesh.is_reachable(context.position, target) {
                return ToolResult::Error("Target unreachable".into());
            }
            
            // Validate target is within range
            let distance = context.position.distance(target);
            if distance > context.max_move_distance {
                return ToolResult::Error("Target too far".into());
            }
            
            ToolResult::Success(json!({ "path_found": true }))
        }
        "attack" => {
            let target_id: u64 = call.parse_param("target")?;
            
            // Validate target exists and is attackable
            if !context.can_attack(target_id) {
                return ToolResult::Error("Cannot attack target".into());
            }
            
            ToolResult::Success(json!({ "attack_initiated": true }))
        }
        _ => ToolResult::Error("Unknown tool".into()),
    }
}
}

Performance

Frame Budget Management

#![allow(unused)]
fn main() {
#[derive(Resource)]
pub struct FrameBudget {
    pub target_fps: f32,
    pub ai_budget_pct: f32,
    pub physics_budget_pct: f32,
    pub render_budget_pct: f32,
}

impl FrameBudget {
    pub fn frame_time_ms(&self) -> f32 {
        1000.0 / self.target_fps
    }
    
    pub fn ai_budget_ms(&self) -> f32 {
        self.frame_time_ms() * self.ai_budget_pct
    }
}
}

Async Operations

#![allow(unused)]
fn main() {
// Load assets asynchronously
fn load_level_async(
    mut commands: Commands,
    asset_server: Res<AssetServer>,
    mut loading_state: ResMut<LoadingState>,
) {
    if loading_state.started {
        return;
    }
    
    loading_state.handles.push(asset_server.load_async::<Scene>("levels/level1.scn"));
    loading_state.started = true;
}

// Check loading progress
fn check_loading(
    asset_server: Res<AssetServer>,
    loading_state: Res<LoadingState>,
    mut next_state: ResMut<NextState<GameState>>,
) {
    if loading_state.handles.iter().all(|h| asset_server.is_loaded(h)) {
        next_state.set(GameState::Playing);
    }
}
}

Memory Management

#![allow(unused)]
fn main() {
// Use object pools for frequent spawn/despawn
#[derive(Resource)]
pub struct ProjectilePool {
    available: Vec<Entity>,
    active: HashSet<Entity>,
}

impl ProjectilePool {
    pub fn get(&mut self, commands: &mut Commands) -> Entity {
        if let Some(entity) = self.available.pop() {
            commands.entity(entity).insert(Visible);
            self.active.insert(entity);
            entity
        } else {
            let entity = commands.spawn(ProjectileBundle::default()).id();
            self.active.insert(entity);
            entity
        }
    }
    
    pub fn return_entity(&mut self, entity: Entity, commands: &mut Commands) {
        commands.entity(entity).remove::<Visible>();
        self.active.remove(&entity);
        self.available.push(entity);
    }
}
}

Error Handling

Graceful Degradation

#![allow(unused)]
fn main() {
pub fn ai_system_with_fallback(
    mut agents: Query<&mut AiAgent>,
    llm: Option<Res<LlmClient>>,
) {
    for mut agent in agents.iter_mut() {
        let decision = if let Some(ref llm) = llm {
            // Try LLM-based decision
            match llm.try_decide(&agent.context) {
                Ok(decision) => decision,
                Err(e) => {
                    warn!("LLM failed: {}, using fallback", e);
                    agent.fallback_decision()
                }
            }
        } else {
            // No LLM available, use behavior tree
            agent.behavior_tree.tick()
        };
        
        agent.execute(decision);
    }
}
}

Structured Errors

#![allow(unused)]
fn main() {
#[derive(Debug, thiserror::Error)]
pub enum GameError {
    #[error("Asset not found: {0}")]
    AssetNotFound(String),
    
    #[error("Invalid game state: {0}")]
    InvalidState(String),
    
    #[error("Network error: {0}")]
    Network(#[from] NetworkError),
    
    #[error("AI error: {0}")]
    Ai(#[from] AiError),
}

pub type GameResult<T> = Result<T, GameError>;
}

Testing

Unit Testing Systems

#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
    use super::*;
    
    #[test]
    fn test_damage_system() {
        let mut world = World::new();
        
        let entity = world.spawn((Health(100.0), DamageQueue::default())).id();
        
        world.get_mut::<DamageQueue>(entity).unwrap().push(Damage(30.0));
        
        let mut schedule = Schedule::default();
        schedule.add_systems(apply_damage_system);
        schedule.run(&mut world);
        
        assert_eq!(world.get::<Health>(entity).unwrap().0, 70.0);
    }
}
}

Integration Testing

#![allow(unused)]
fn main() {
#[test]
fn test_combat_flow() {
    let mut app = App::new();
    app.add_plugins(MinimalPlugins);
    app.add_plugins(CombatPlugin);
    
    app.world.spawn((
        Player,
        Health(100.0),
        Transform::default(),
    ));
    
    app.world.spawn((
        Enemy,
        Health(50.0),
        Transform::from_xyz(5.0, 0.0, 0.0),
    ));
    
    // Simulate multiple frames
    for _ in 0..100 {
        app.update();
    }
    
    // Verify expected state
}
}

Debugging

Logging

#![allow(unused)]
fn main() {
use tracing::{info, warn, error, debug, trace, instrument};

#[instrument(skip(query))]
fn my_system(query: Query<(&Transform, &Health)>) {
    for (transform, health) in query.iter() {
        trace!("Processing entity at {:?}", transform.translation);
        
        if health.0 < 10.0 {
            warn!("Entity at low health: {}", health.0);
        }
    }
}
}

Debug Visualization

#![allow(unused)]
fn main() {
fn debug_draw_system(
    mut gizmos: Gizmos,
    agents: Query<(&Transform, &AiAgent)>,
) {
    #[cfg(debug_assertions)]
    for (transform, agent) in agents.iter() {
        // Draw perception radius
        gizmos.circle(transform.translation, Vec3::Y, agent.perception_radius, Color::YELLOW);
        
        // Draw path
        if let Some(path) = &agent.current_path {
            for window in path.windows(2) {
                gizmos.line(window[0], window[1], Color::GREEN);
            }
        }
    }
}
}

Security

Input Validation

#![allow(unused)]
fn main() {
pub fn validate_player_input(input: &PlayerInput) -> Result<(), InputError> {
    // Validate movement speed
    if input.movement.length() > MAX_MOVEMENT_SPEED {
        return Err(InputError::InvalidMovement);
    }
    
    // Validate action cooldowns
    if input.action.is_some() && !can_perform_action() {
        return Err(InputError::ActionOnCooldown);
    }
    
    Ok(())
}
}

Save Data Integrity

#![allow(unused)]
fn main() {
use sha2::{Sha256, Digest};

pub fn save_game(state: &GameState) -> Result<(), SaveError> {
    let data = bincode::serialize(state)?;
    let hash = Sha256::digest(&data);
    
    let save = SaveFile {
        version: SAVE_VERSION,
        data,
        checksum: hash.to_vec(),
    };
    
    std::fs::write("save.dat", bincode::serialize(&save)?)?;
    Ok(())
}

pub fn load_game() -> Result<GameState, LoadError> {
    let bytes = std::fs::read("save.dat")?;
    let save: SaveFile = bincode::deserialize(&bytes)?;
    
    // Verify checksum
    let hash = Sha256::digest(&save.data);
    if hash.as_slice() != save.checksum {
        return Err(LoadError::CorruptedSave);
    }
    
    Ok(bincode::deserialize(&save.data)?)
}
}

Summary Checklist

- [ ] All systems have error handling
- [ ] Performance profiled on target hardware
- [ ] AI fallbacks tested
- [ ] Save/load tested extensively
- [ ] Memory usage profiled
- [ ] Input validation complete
- [ ] Logging appropriate for production
- [ ] All assets properly loaded
- [ ] Multiplayer desync handled (if applicable)

Design Patterns

This guide covers common design patterns used in AstraWeave game development, with practical examples and when to apply each pattern.

Entity Patterns

Marker Components

Use empty components to categorize entities:

#![allow(unused)]
fn main() {
#[derive(Component)]
struct Player;

#[derive(Component)]
struct Enemy;

#[derive(Component)]
struct NPC;

#[derive(Component)]
struct Interactable;

// Query with markers
fn player_system(query: Query<&Transform, With<Player>>) {
    for transform in query.iter() {
        // Only players
    }
}

fn enemy_system(query: Query<&Transform, (With<Enemy>, Without<Dead>)>) {
    for transform in query.iter() {
        // Only living enemies
    }
}
}

Component Bundles

Group related components for entity archetypes:

#![allow(unused)]
fn main() {
#[derive(Bundle)]
struct CharacterBundle {
    transform: Transform,
    health: Health,
    velocity: Velocity,
    collider: Collider,
}

#[derive(Bundle)]
struct PlayerBundle {
    character: CharacterBundle,
    player: Player,
    inventory: Inventory,
    controller: PlayerController,
}

#[derive(Bundle)]
struct EnemyBundle {
    character: CharacterBundle,
    enemy: Enemy,
    ai: AiAgent,
    loot_table: LootTable,
}

// Spawn with bundles
fn spawn_player(mut commands: Commands) {
    commands.spawn(PlayerBundle {
        character: CharacterBundle {
            transform: Transform::default(),
            health: Health(100.0),
            velocity: Velocity::default(),
            collider: Collider::capsule(0.5, 1.8),
        },
        player: Player,
        inventory: Inventory::new(20),
        controller: PlayerController::default(),
    });
}
}

Entity References

Store references between entities:

#![allow(unused)]
fn main() {
#[derive(Component)]
struct Parent(Entity);

#[derive(Component)]
struct Children(Vec<Entity>);

#[derive(Component)]
struct Target(Option<Entity>);

#[derive(Component)]
struct Owner(Entity);

fn targeting_system(
    mut agents: Query<(&Transform, &mut Target), With<Enemy>>,
    targets: Query<(Entity, &Transform), With<Player>>,
) {
    for (agent_transform, mut target) in agents.iter_mut() {
        let nearest = targets
            .iter()
            .min_by_key(|(_, t)| {
                OrderedFloat(t.translation.distance(agent_transform.translation))
            })
            .map(|(e, _)| e);
        
        target.0 = nearest;
    }
}
}

State Patterns

State Machine

Implement entity state machines:

#![allow(unused)]
fn main() {
#[derive(Component, Default)]
enum CharacterState {
    #[default]
    Idle,
    Walking,
    Running,
    Jumping,
    Attacking,
    Stunned { duration: f32 },
    Dead,
}

fn state_machine_system(
    time: Res<Time>,
    mut query: Query<(&mut CharacterState, &Velocity, &Health)>,
) {
    let dt = time.delta_seconds();
    
    for (mut state, velocity, health) in query.iter_mut() {
        // Check for death
        if health.0 <= 0.0 && !matches!(*state, CharacterState::Dead) {
            *state = CharacterState::Dead;
            continue;
        }
        
        // State transitions
        *state = match &*state {
            CharacterState::Stunned { duration } => {
                let remaining = duration - dt;
                if remaining <= 0.0 {
                    CharacterState::Idle
                } else {
                    CharacterState::Stunned { duration: remaining }
                }
            }
            CharacterState::Dead => CharacterState::Dead,
            _ => {
                if velocity.0.length() > 5.0 {
                    CharacterState::Running
                } else if velocity.0.length() > 0.1 {
                    CharacterState::Walking
                } else {
                    CharacterState::Idle
                }
            }
        };
    }
}
}

Global Game State

Use app states for high-level game flow:

#![allow(unused)]
fn main() {
#[derive(States, Debug, Clone, PartialEq, Eq, Hash, Default)]
enum GameState {
    #[default]
    Loading,
    MainMenu,
    Playing,
    Paused,
    GameOver,
}

fn setup_states(app: &mut App) {
    app
        .add_state::<GameState>()
        .add_systems(OnEnter(GameState::Loading), start_loading)
        .add_systems(OnEnter(GameState::MainMenu), show_menu)
        .add_systems(OnExit(GameState::MainMenu), hide_menu)
        .add_systems(Update, game_logic.run_if(in_state(GameState::Playing)))
        .add_systems(OnEnter(GameState::Paused), show_pause_menu)
        .add_systems(OnEnter(GameState::GameOver), show_game_over);
}
}

Resource Patterns

Configuration Resources

Store runtime configuration:

#![allow(unused)]
fn main() {
#[derive(Resource, Default)]
struct GameSettings {
    master_volume: f32,
    music_volume: f32,
    sfx_volume: f32,
    mouse_sensitivity: f32,
    difficulty: Difficulty,
}

#[derive(Resource)]
struct LevelConfig {
    spawn_rate: f32,
    max_enemies: usize,
    boss_health_multiplier: f32,
}

fn load_settings(mut settings: ResMut<GameSettings>) {
    if let Ok(data) = std::fs::read_to_string("settings.toml") {
        if let Ok(loaded) = toml::from_str(&data) {
            *settings = loaded;
        }
    }
}
}

Service Resources

Encapsulate complex functionality:

#![allow(unused)]
fn main() {
#[derive(Resource)]
struct AudioManager {
    music_channel: AudioChannel,
    sfx_channels: Vec<AudioChannel>,
    ambient_channel: AudioChannel,
}

impl AudioManager {
    fn play_music(&mut self, track: &str, fade_in: f32) {
        self.music_channel.fade_in(track, fade_in);
    }
    
    fn play_sfx(&mut self, sound: &str, position: Vec3) {
        if let Some(channel) = self.sfx_channels.iter_mut().find(|c| c.is_available()) {
            channel.play_spatial(sound, position);
        }
    }
}

#[derive(Resource)]
struct DialogueManager {
    active_dialogue: Option<DialogueTree>,
    history: Vec<DialogueEntry>,
}
}

Event Patterns

Game Events

Use events for decoupled communication:

#![allow(unused)]
fn main() {
#[derive(Event)]
struct DamageEvent {
    target: Entity,
    amount: f32,
    damage_type: DamageType,
    source: Option<Entity>,
}

#[derive(Event)]
struct DeathEvent {
    entity: Entity,
    killer: Option<Entity>,
    position: Vec3,
}

#[derive(Event)]
struct ItemPickupEvent {
    player: Entity,
    item: ItemId,
    quantity: u32,
}

fn damage_system(
    mut damage_events: EventReader<DamageEvent>,
    mut death_events: EventWriter<DeathEvent>,
    mut query: Query<(&mut Health, &Transform)>,
) {
    for event in damage_events.iter() {
        if let Ok((mut health, transform)) = query.get_mut(event.target) {
            health.0 -= event.amount;
            
            if health.0 <= 0.0 {
                death_events.send(DeathEvent {
                    entity: event.target,
                    killer: event.source,
                    position: transform.translation,
                });
            }
        }
    }
}

fn death_system(
    mut commands: Commands,
    mut death_events: EventReader<DeathEvent>,
    loot_query: Query<&LootTable>,
) {
    for event in death_events.iter() {
        // Spawn loot
        if let Ok(loot) = loot_query.get(event.entity) {
            spawn_loot(&mut commands, loot, event.position);
        }
        
        // Play death effects
        commands.spawn(ParticleEffect::death(event.position));
        
        // Mark entity for removal
        commands.entity(event.entity).insert(Dead);
    }
}
}

Event Chains

Chain events for complex interactions:

#![allow(unused)]
fn main() {
// Event flow: Attack -> Hit -> Damage -> Death -> Loot -> XP

#[derive(Event)]
struct AttackEvent { attacker: Entity, weapon: Entity }

#[derive(Event)]
struct HitEvent { attacker: Entity, target: Entity, damage: f32 }

#[derive(Event)]
struct XpGainEvent { player: Entity, amount: u32 }

fn attack_resolution(
    mut attacks: EventReader<AttackEvent>,
    mut hits: EventWriter<HitEvent>,
    weapons: Query<&Weapon>,
    transforms: Query<&Transform>,
) {
    for attack in attacks.iter() {
        if let Ok(weapon) = weapons.get(attack.weapon) {
            // Perform hit detection
            for (entity, hit_point) in detect_hits(attack.attacker, weapon, &transforms) {
                hits.send(HitEvent {
                    attacker: attack.attacker,
                    target: entity,
                    damage: weapon.damage,
                });
            }
        }
    }
}
}

AI Patterns

Behavior Tree

Structure AI decisions hierarchically:

#![allow(unused)]
fn main() {
pub enum BtNode {
    Selector(Vec<BtNode>),
    Sequence(Vec<BtNode>),
    Condition(Box<dyn Fn(&AiContext) -> bool>),
    Action(Box<dyn Fn(&mut AiContext) -> BtStatus>),
}

#[derive(Clone, Copy, PartialEq)]
pub enum BtStatus {
    Success,
    Failure,
    Running,
}

fn create_enemy_bt() -> BtNode {
    BtNode::Selector(vec![
        // Priority 1: Flee if low health
        BtNode::Sequence(vec![
            BtNode::Condition(Box::new(|ctx| ctx.health_percent() < 0.2)),
            BtNode::Action(Box::new(|ctx| ctx.flee())),
        ]),
        // Priority 2: Attack if in range
        BtNode::Sequence(vec![
            BtNode::Condition(Box::new(|ctx| ctx.has_target())),
            BtNode::Condition(Box::new(|ctx| ctx.in_attack_range())),
            BtNode::Action(Box::new(|ctx| ctx.attack())),
        ]),
        // Priority 3: Chase target
        BtNode::Sequence(vec![
            BtNode::Condition(Box::new(|ctx| ctx.has_target())),
            BtNode::Action(Box::new(|ctx| ctx.chase_target())),
        ]),
        // Priority 4: Patrol
        BtNode::Action(Box::new(|ctx| ctx.patrol())),
    ])
}
}

Blackboard

Share data between AI systems:

#![allow(unused)]
fn main() {
#[derive(Component, Default)]
pub struct Blackboard {
    values: HashMap<String, BlackboardValue>,
}

#[derive(Clone)]
pub enum BlackboardValue {
    Bool(bool),
    Float(f32),
    Int(i32),
    Entity(Entity),
    Vec3(Vec3),
    String(String),
}

impl Blackboard {
    pub fn set<K: Into<String>>(&mut self, key: K, value: BlackboardValue) {
        self.values.insert(key.into(), value);
    }
    
    pub fn get(&self, key: &str) -> Option<&BlackboardValue> {
        self.values.get(key)
    }
    
    pub fn get_entity(&self, key: &str) -> Option<Entity> {
        match self.get(key) {
            Some(BlackboardValue::Entity(e)) => Some(*e),
            _ => None,
        }
    }
}

fn perception_system(
    mut agents: Query<(&Transform, &Perception, &mut Blackboard)>,
    targets: Query<(Entity, &Transform), With<Player>>,
) {
    for (transform, perception, mut blackboard) in agents.iter_mut() {
        let nearest = find_nearest_in_range(transform, &targets, perception.radius);
        
        if let Some((entity, distance)) = nearest {
            blackboard.set("target", BlackboardValue::Entity(entity));
            blackboard.set("target_distance", BlackboardValue::Float(distance));
            blackboard.set("has_target", BlackboardValue::Bool(true));
        } else {
            blackboard.set("has_target", BlackboardValue::Bool(false));
        }
    }
}
}

System Patterns

System Sets

Organize systems with explicit ordering:

#![allow(unused)]
fn main() {
#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
enum GameSystems {
    Input,
    Ai,
    Movement,
    Combat,
    Effects,
    Cleanup,
}

fn configure_systems(app: &mut App) {
    app.configure_sets(Update, (
        GameSystems::Input,
        GameSystems::Ai,
        GameSystems::Movement,
        GameSystems::Combat,
        GameSystems::Effects,
        GameSystems::Cleanup,
    ).chain());
    
    app
        .add_systems(Update, read_input.in_set(GameSystems::Input))
        .add_systems(Update, (
            ai_perception,
            ai_planning,
            ai_execution,
        ).chain().in_set(GameSystems::Ai))
        .add_systems(Update, (
            apply_velocity,
            resolve_collisions,
        ).chain().in_set(GameSystems::Movement));
}
}

Run Conditions

Control when systems execute:

#![allow(unused)]
fn main() {
fn run_if_playing(state: Res<State<GameState>>) -> bool {
    *state.get() == GameState::Playing
}

fn run_if_has_enemies(enemies: Query<(), With<Enemy>>) -> bool {
    !enemies.is_empty()
}

fn run_every_n_frames(mut counter: Local<u32>) -> bool {
    *counter = (*counter + 1) % 5;
    *counter == 0
}

app
    .add_systems(Update, game_logic.run_if(run_if_playing))
    .add_systems(Update, enemy_ai.run_if(run_if_has_enemies))
    .add_systems(Update, expensive_system.run_if(run_every_n_frames));
}

Data Patterns

Flyweight

Share immutable data across entities:

#![allow(unused)]
fn main() {
#[derive(Resource)]
struct WeaponDatabase {
    weapons: HashMap<WeaponId, WeaponData>,
}

#[derive(Clone)]
struct WeaponData {
    name: String,
    base_damage: f32,
    attack_speed: f32,
    mesh: Handle<Mesh>,
    icon: Handle<Texture>,
}

#[derive(Component)]
struct Weapon {
    id: WeaponId,
    level: u32,
    durability: f32,
}

impl Weapon {
    fn get_data<'a>(&self, db: &'a WeaponDatabase) -> &'a WeaponData {
        db.weapons.get(&self.id).expect("Invalid weapon ID")
    }
    
    fn calculate_damage(&self, db: &WeaponDatabase) -> f32 {
        let data = self.get_data(db);
        data.base_damage * (1.0 + self.level as f32 * 0.1)
    }
}
}

Observer

React to component changes:

#![allow(unused)]
fn main() {
fn on_health_changed(
    query: Query<(Entity, &Health, &MaxHealth), Changed<Health>>,
    mut damage_flash: Query<&mut DamageFlash>,
) {
    for (entity, health, max_health) in query.iter() {
        let health_percent = health.0 / max_health.0;
        
        // Trigger damage flash
        if let Ok(mut flash) = damage_flash.get_mut(entity) {
            flash.trigger();
        }
        
        // Update health bar
        if health_percent < 0.25 {
            // Critical health effects
        }
    }
}

fn on_entity_added(
    query: Query<Entity, Added<Enemy>>,
    mut enemy_count: ResMut<EnemyCount>,
) {
    for _entity in query.iter() {
        enemy_count.0 += 1;
    }
}
}

Command Pattern

Deferred Commands

Queue operations for later execution:

#![allow(unused)]
fn main() {
#[derive(Resource, Default)]
struct CommandQueue {
    commands: Vec<GameCommand>,
}

enum GameCommand {
    SpawnEnemy { position: Vec3, enemy_type: EnemyType },
    DealDamage { target: Entity, amount: f32 },
    GiveItem { player: Entity, item: ItemId },
    TriggerEvent { event_name: String },
}

fn queue_command(queue: &mut CommandQueue, command: GameCommand) {
    queue.commands.push(command);
}

fn execute_commands(
    mut commands: Commands,
    mut queue: ResMut<CommandQueue>,
    mut damage_events: EventWriter<DamageEvent>,
) {
    for command in queue.commands.drain(..) {
        match command {
            GameCommand::SpawnEnemy { position, enemy_type } => {
                commands.spawn(EnemyBundle::new(enemy_type, position));
            }
            GameCommand::DealDamage { target, amount } => {
                damage_events.send(DamageEvent { target, amount, .. });
            }
            // Handle other commands
            _ => {}
        }
    }
}
}

Summary

PatternUse Case
Marker ComponentsEntity categorization
BundlesEntity archetypes
State MachineEntity behavior
App StatesGame flow
EventsDecoupled communication
Behavior TreeAI decisions
BlackboardAI data sharing
System SetsExecution ordering
Run ConditionsConditional execution
FlyweightShared data
ObserverChange reactions
Command QueueDeferred execution

Troubleshooting

This guide helps you solve common issues when working with AstraWeave. Issues are organized by category with specific solutions.

Build Issues

Rust Toolchain Problems

Error: “rustc version mismatch”

error: rustc version doesn't match the expected version

Solution:

# Remove existing toolchain and reinstall
rustup toolchain uninstall stable
rustup toolchain install 1.89.0
rustup default 1.89.0

# Verify version
rustc --version  # Should show 1.89.0

Error: “rust-toolchain.toml not respected”

Solution:

# Force toolchain installation
rustup toolchain install 1.89.0
rustup override set 1.89.0

# Clean and rebuild
cargo clean
cargo build -p astraweave-core

Dependency Issues

Error: “linker cc not found”

Linux Solution:

sudo apt-get install build-essential

macOS Solution:

xcode-select --install

Windows Solution: Install Visual Studio with C++ build tools.

Error: “failed to find required package”

error: could not find `wgpu` in the registry

Solution:

# Update Cargo registry
cargo update

# If still failing, clear cache
rm -rf ~/.cargo/registry
cargo update

Graphics Dependencies

Error: “Vulkan not found”

Linux Solution:

# Ubuntu/Debian
sudo apt-get install mesa-vulkan-drivers vulkan-tools

# Arch Linux  
sudo pacman -S vulkan-devel mesa

# Fedora
sudo dnf install vulkan-devel mesa-dri-drivers

# Verify Vulkan
vulkaninfo | head -20

Windows Solution: Update your graphics drivers from manufacturer website:

  • NVIDIA: Download latest drivers
  • AMD: Download Adrenalin drivers
  • Intel: Download latest graphics drivers

macOS Solution: Vulkan support requires MoltenVK:

brew install molten-vk

Error: “wgpu adapter not found”

thread 'main' panicked at 'No suitable graphics adapter found'

Solutions:

  1. Check GPU compatibility:

    # Linux: Check Vulkan support
    vulkaninfo
    
    # Should show at least one device
    
  2. Force software rendering:

    export WGPU_BACKEND=gl
    cargo run -p hello_companion
    
  3. Update graphics drivers

Audio Dependencies

Error: “ALSA lib errors” (Linux)

ALSA lib pcm_dmix.c:1089:(snd_pcm_dmix_open) unable to open slave

Solution:

# Install audio libraries
sudo apt-get install libasound2-dev libpulse-dev

# Check audio devices
aplay -l

# If no devices, check PulseAudio
pulseaudio --check

Error: “No audio output device found”

Linux Solution:

# Restart audio services
systemctl --user restart pulseaudio

Windows Solution: Check that Windows Audio service is running in Services.

macOS Solution: Usually works out of the box. Check System Preferences > Sound.

Example Compilation Issues

Error: “examples fail to compile”

Many examples have known compilation issues. Use only the working examples:

Working Examples:

cargo build -p hello_companion      # ✅ Works (expected panic)
cargo build -p ipc_loopback        # ✅ Should work  
cargo test -p astraweave-input      # ✅ Tests pass

Known Broken Examples:

# ❌ These have compilation issues:
# cargo build -p debug_toolkit_demo  # egui/winit/renderer API mismatches
# cargo build -p aw_editor           # eframe/glutin sync/send trait issues  
# cargo build -p aw_debug            # eframe API mismatches
# cargo build -p visual_3d           # clippy deny-level errors
# cargo build -p navmesh_demo        # clippy deny-level errors (approx_constant)
# cargo build -p physics_demo3d      # clippy deny-level errors (approx_constant)

Workaround: Focus on the working core components and use the provided aliases:

cargo build-core                    # Build core components only
cargo check-all                     # Check workspace (excluding problematic crates)
cargo clippy-all                    # Run clippy on working crates

Runtime Issues

Graphics Issues

Error: “Validation error in wgpu”

wgpu validation error: Buffer usage VERTEX | COPY_DST is not valid

Solutions:

  1. Update graphics drivers
  2. Use older wgpu backend:
    export WGPU_BACKEND=vulkan  # or gl, metal, dx12
    
  3. Reduce graphics settings in your code

Error: “Surface creation failed”

Error creating surface: SurfaceError(OutOfMemory)

Solutions:

  1. Reduce window size:
    #![allow(unused)]
    fn main() {
    // In your window configuration
    .with_inner_size(winit::dpi::LogicalSize::new(800, 600))
    }
  2. Lower graphics quality settings
  3. Check available VRAM:
    # Linux
    nvidia-smi  # for NVIDIA
    radeontop   # for AMD
    

AI Model Issues

Error: “AI model not found”

Error: Could not load AI model 'companion-7b'

Solutions:

  1. Use mock AI for testing:

    #![allow(unused)]
    fn main() {
    // In your configuration
    ai_agent.ai_model = AIModel::Mock;
    }
  2. Download required models:

    # Models not included in repository
    # Use mock or implement your own model loader
    
  3. Configure model path:

    #![allow(unused)]
    fn main() {
    ai_agent.ai_model = AIModel::Local("path/to/your/model".to_string());
    }

Performance Issues

Issue: “Low FPS / Stuttering”

Diagnosis:

# Always use release builds for performance testing
cargo run -p hello_companion --release

# Check if running in debug mode
cargo run -p hello_companion  # This is debug mode - will be slow

Solutions:

  1. Always use release builds:

    cargo build --release
    cargo run --release -p your_example
    
  2. Check system resources:

    # Linux
    htop
    
    # Monitor GPU usage
    nvidia-smi  # NVIDIA
    radeontop   # AMD
    
  3. Reduce AI complexity:

    #![allow(unused)]
    fn main() {
    // Lower AI planning frequency
    ai_agent.planning_interval = Duration::from_millis(1000); // Instead of 500
    
    // Reduce perception range
    ai_agent.perception_range = 5.0; // Instead of 10.0
    }

Issue: “High memory usage”

Diagnosis:

# Check memory usage
cargo run --release -p hello_companion &
ps aux | grep hello_companion

Solutions:

  1. Limit AI memory:

    #![allow(unused)]
    fn main() {
    ai_memory.max_episodic_memories = 100;
    ai_memory.max_working_memory = 10;
    }
  2. Use memory profiling:

    # Install valgrind (Linux)
    sudo apt-get install valgrind
    valgrind --tool=massif cargo run --release -p hello_companion
    

Network Issues

Error: “Connection refused” (multiplayer examples)

Error: Connection refused (os error 111)

Solutions:

  1. Check if server is running:

    # Terminal 1 - Start server first
    cargo run -p coop_server --release
    
    # Terminal 2 - Then client
    cargo run -p coop_client --release
    
  2. Check firewall settings:

    # Linux: Check if port is open
    sudo ufw status
    
    # Allow port if needed
    sudo ufw allow 8080
    
  3. Use localhost:

    #![allow(unused)]
    fn main() {
    // Make sure client connects to localhost
    let server_addr = "127.0.0.1:8080";
    }

Development Issues

IDE Problems

Issue: “rust-analyzer not working”

Solution:

# Restart rust-analyzer
# In VS Code: Ctrl+Shift+P > "Rust Analyzer: Restart Server"

# Or reinstall
rustup component add rust-analyzer

Issue: “Slow code completion”

Solutions:

  1. Exclude target directory from indexing
  2. Reduce project scope:
    // In VS Code settings.json
    {
      "rust-analyzer.cargo.allFeatures": false,
      "rust-analyzer.checkOnSave.allFeatures": false
    }
    

Testing Issues

Error: “Tests hanging”

cargo test -p astraweave-input
# Hangs indefinitely

Solutions:

  1. Run with timeout:

    timeout 30s cargo test -p astraweave-input
    
  2. Run single test:

    cargo test -p astraweave-input test_input_system
    
  3. Use single-threaded execution:

    cargo test -p astraweave-input -- --test-threads=1
    

Error: “Test failures due to timing”

thread 'ai_planning_test' panicked at 'assertion failed: plan.is_some()'

Solution: Tests involving AI may have timing dependencies:

#![allow(unused)]
fn main() {
// Add delays in tests
#[test]
fn ai_planning_test() {
    let mut world = create_test_world();
    world.step(); // Let one frame pass
    
    std::thread::sleep(Duration::from_millis(100)); // Give AI time to plan
    
    let plan = world.get_ai_plan();
    assert!(plan.is_some());
}
}

Platform-Specific Issues

Linux Issues

Issue: “Wayland compatibility”

Some features may not work correctly on Wayland:

# Force X11 if needed
export WAYLAND_DISPLAY=""
export DISPLAY=:0

# Or force Wayland if X11 is causing issues
export DISPLAY=""

Issue: “Audio permission denied”

# Add user to audio group
sudo usermod -a -G audio $USER

# Restart session or reboot

macOS Issues

Issue: “Code signing errors”

error: codesign failed with exit code 1

Solution:

# For development, disable code signing
export MACOSX_DEPLOYMENT_TARGET=11.0

# Or sign manually
codesign --force --deep --sign - target/release/hello_companion

Issue: “Metal validation errors”

Use software rendering if Metal causes issues:

export WGPU_BACKEND=gl
cargo run -p hello_companion --release

Windows Issues

Issue: “MSVC runtime missing”

Install Microsoft Visual C++ Redistributable:

  • Download from Microsoft’s website
  • Or install Visual Studio with C++ tools

Issue: “Antivirus blocking execution”

Add exclusions for:

  • Project directory
  • %USERPROFILE%\.cargo
  • target\ directory

Issue: “Path too long errors”

# Enable long paths in Windows
# Run as Administrator in PowerShell:
New-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem" -Name "LongPathsEnabled" -Value 1 -PropertyType DWORD -Force

Getting Help

Before Asking for Help

  1. Check this troubleshooting guide
  2. Verify your setup:
    rustc --version  # Should be 1.89.0
    cargo --version
    
  3. Try with minimal example:
    cargo run -p hello_companion --release
    
  4. Check system dependencies

Information to Include

When reporting issues, include:

  1. System information:

    # Linux
    uname -a
    lsb_release -a
    
    # macOS
    sw_vers
    
    # Windows
    systeminfo
    
  2. Rust version:

    rustc --version
    cargo --version
    
  3. Graphics information:

    # Linux
    lspci | grep VGA
    vulkaninfo | head -20
    
    # Windows
    dxdiag
    
    # macOS
    system_profiler SPDisplaysDataType
    
  4. Full error output:

    # Include full error with backtrace
    RUST_BACKTRACE=full cargo run -p hello_companion 2>&1 | tee error.log
    
  5. Steps to reproduce

Community Resources

  • GitHub Issues: For bug reports and feature requests
  • Discussions: For questions and general help
  • Matrix/Discord: For real-time community support (if available)

Known Limitations

Current Development State

AstraWeave is under active development. Known limitations:

  1. Many examples don’t compile due to API evolution
  2. Limited AI model integration - mostly uses mock AI
  3. Graphics API compatibility - some newer GPU features not supported
  4. Documentation gaps - some advanced features lack documentation

Workarounds

  1. Focus on working examples (hello_companion, core components)
  2. Use mock AI for learning the architecture
  3. Stick to stable APIs in core crates
  4. Contribute fixes for broken examples

If you’re still having issues after trying these solutions, please create an issue on GitHub with the requested information. The community is here to help!

Community

Welcome to the AstraWeave community! This page lists resources for connecting with other developers, getting help, and contributing to the project.

Official Channels

GitHub

The primary home for AstraWeave development:

ResourceDescription
RepositorySource code, issues, and pull requests
DiscussionsQ&A, ideas, and general discussion
IssuesBug reports and feature requests
ProjectsDevelopment roadmap and tracking

Discord

Join our Discord server for real-time chat:

  • #general - Introduce yourself and chat with the community
  • #help - Get help with AstraWeave questions
  • #showcase - Share your projects and creations
  • #ai-discussion - Discuss AI/LLM integration topics
  • #development - Engine development discussion
  • #announcements - Official news and updates

Social Media

Getting Help

Before Asking

  1. Search existing resources:

  2. Prepare your question:

    • Describe what you’re trying to do
    • Show what you’ve tried
    • Include relevant code snippets
    • Share error messages

Where to Ask

Question TypeBest Channel
Quick questionsDiscord #help
Detailed technical questionsGitHub Discussions
Bug reportsGitHub Issues
Feature requestsGitHub Issues or Discussions
Security issuessecurity@astraweave.dev (private)

How to Ask Good Questions

## What I'm trying to do
I want to create an AI companion that follows the player and helps in combat.

## What I've tried
```rust
// My current code
fn companion_follow(
    companion: Query<&mut Transform, With<Companion>>,
    player: Query<&Transform, With<Player>>,
) {
    // This doesn't work as expected
}

What happens

The companion doesn’t move at all.

What I expected

The companion should move toward the player.

Environment

  • AstraWeave 0.1.0
  • Windows 11

## Contributing

### Ways to Contribute

Everyone can contribute, regardless of experience level:

| Contribution | Description |
|--------------|-------------|
| **Bug Reports** | Found a bug? Report it on GitHub Issues |
| **Documentation** | Improve or add documentation |
| **Code** | Fix bugs or implement features |
| **Examples** | Create example projects |
| **Testing** | Test pre-release versions |
| **Translations** | Help translate documentation |
| **Community** | Help others in Discord/Discussions |

### Getting Started

1. **Read the contributing guide**: [Contributing](../dev/contributing.md)
2. **Set up your development environment**: [Building](../dev/building.md)
3. **Find a good first issue**:
   - Look for `good first issue` labels on GitHub
   - Check the `help wanted` label for more challenging tasks

### Code Contribution Workflow

```bash
# 1. Fork and clone
git clone https://github.com/YOUR_USERNAME/astraweave.git
cd astraweave

# 2. Create a branch
git checkout -b feature/my-feature

# 3. Make changes
# ... edit files ...

# 4. Run tests
cargo test --all

# 5. Commit
git commit -am "feat: add my feature"

# 6. Push and create PR
git push origin feature/my-feature

Pull Request Guidelines

  • Keep PRs focused: One feature or fix per PR
  • Write tests: Cover new functionality with tests
  • Update docs: Add documentation for new features
  • Follow style: Match existing code style
  • Be patient: Reviews take time

Showcase

Share your AstraWeave projects!

How to Submit

  1. Discord: Post in #showcase with:

    • Project name
    • Brief description
    • Screenshots or video
    • Link (if public)
  2. GitHub Discussions: Create a post in the “Show and Tell” category

Projects using AstraWeave:

ProjectDescription
hello_companionOfficial demo showcasing AI companion systems
adaptive-bossBoss AI that learns and adapts to player strategies

Have a project to share? Post in #showcase on Discord or open a Discussion on GitHub!

Events

Community Events

  • Monthly Showcase: First Friday of each month
  • AI Game Jam: Quarterly jam focusing on AI-native games
  • Office Hours: Weekly Q&A with maintainers

Conferences

AstraWeave may be presented at:

  • GDC (Game Developers Conference)
  • RustConf
  • Various game development meetups

Governance

Project Leadership

RoleResponsibility
Core TeamMajor decisions, releases, roadmap
MaintainersCode review, issue triage
ContributorsFeature development, bug fixes

Decision Making

  • Minor changes: Maintainer approval
  • Major changes: Core team discussion
  • Breaking changes: RFC process with community input

Code of Conduct

We follow a Code of Conduct to ensure a welcoming community:

  • Be respectful and inclusive
  • Focus on constructive feedback
  • Help others learn
  • No harassment or discrimination

Full Code of Conduct: CODE_OF_CONDUCT.md

Learning Resources

Official Resources

Community Resources

  • Tutorials: Community-created tutorials (see Discord pinned messages)
  • Templates: Starter templates for common project types
  • Libraries: Community extensions and plugins
  1. Getting Started: Installation
  2. First Project: Building Your First Game
  3. Core Concepts: Architecture Overview
  4. AI Features: AI System
  5. Advanced Topics: Explore specific system documentation

Staying Updated

Release Notes

Changelog

Each release includes a detailed changelog covering:

  • New features
  • Bug fixes
  • Breaking changes
  • Migration guides

Roadmap

See Roadmap for upcoming features and long-term plans.

Sponsorship

Supporting Development

If AstraWeave helps you, consider supporting its development:

  • GitHub Sponsors: Support individual maintainers
  • Corporate Sponsorship: Contact sponsorship@astraweave.dev

Sponsors

Thank you to our sponsors! Interested in sponsoring AstraWeave? Contact sponsorship@astraweave.dev to learn about sponsorship tiers and benefits.

Contact

PurposeContact
General inquirieshello@astraweave.dev
Security issuessecurity@astraweave.dev
Sponsorshipsponsorship@astraweave.dev
Presspress@astraweave.dev

Roadmap

This document outlines the development roadmap for AstraWeave, including current status, planned features, and long-term vision.

Current Status

Version: 0.1.0 (Alpha)

AstraWeave is currently in active development. The core systems are functional but APIs may change before 1.0.

Stability Matrix

SystemStatusAPI Stability
ECSStableHigh
RenderingBetaMedium
PhysicsStableHigh
AI/LLMBetaMedium
AudioStableHigh
NetworkingAlphaLow
EditorAlphaLow

Recently Completed Milestones

MilestoneDateTestsHighlights
Blueprint Zone EditorMar 2026125+Polygon zone editor, Replica/Inspired placement, heightmap injection, 3D overlay
Blend Import PipelineFeb 202697.blend scene decomposition, BiomePack bridge, texture processing, editor panel
Veilweaver Vertical SliceFeb 20263205-phase game slice, dialogue, cinematics, boss HUD, determinism validation
Miri Memory SafetyFeb 20269774 crates validated, zero undefined behavior
Fluids System (A+)Jan 20264,907SPH/FLIP simulation, comprehensive test suite
AI-Orchestrated TerrainDec 20252,536TerrainSolver, NavMesh rebaking, terrain persistence
Rendering PipelineDec 2025806+PBR/IBL, CSM shadows, headless support, post-processing

Near-Term (Q1-Q2 2026)

v0.2.0 - Polish Release

Focus: Stability and developer experience.

  • API Stabilization

    • Finalize core ECS APIs
    • Stabilize component derive macros
    • Lock down resource patterns
  • Documentation

    • Complete API documentation
    • Add more tutorials
    • Video walkthroughs
  • Tooling

    • Improved editor prototype
    • Better debugging tools
    • Asset pipeline improvements
  • Performance

    • Rendering optimization
    • AI tick budgeting improvements
    • Memory usage reduction

v0.3.0 - AI Enhancement

Focus: Advanced AI capabilities.

  • LLM Improvements

    • Multi-provider support (OpenAI, Anthropic, local)
    • Streaming responses
    • Function calling standardization
    • Context window management
  • Behavior Systems

    • Visual behavior tree editor
    • GOAP improvements
    • Utility AI integration
  • Memory Systems

    • Improved episodic memory
    • Semantic search with embeddings
    • Memory persistence
  • AI Characters

    • Personality templates
    • Dynamic relationship modeling
    • Emotion simulation

Mid-Term (Q3-Q4 2026)

v0.4.0 - Content Creation

Focus: Procedural content and world building.

  • PCG Framework

    • Dungeon generation improvements
    • Terrain generation enhancements
    • AI-assisted content creation
  • World Streaming

    • Large world support
    • Seamless level streaming
    • Background loading
  • Quest System

    • Dynamic quest generation
    • Branching narratives
    • AI-driven story adaptation

v0.5.0 - Editor & Tools

Focus: Complete development environment.

  • Visual Editor

    • Scene editing
    • Prefab system
    • Play-in-editor
  • Asset Pipeline

    • Automated texture processing
    • Model optimization
    • Audio processing
  • Debugging

    • In-game console
    • Entity inspector
    • Performance profiler
  • Scripting

    • Rhai scripting integration
    • Hot-reload for scripts
    • Visual scripting (prototype)

Long-Term (2027+)

v1.0.0 - Production Ready

Focus: Stability for production games.

  • API Freeze

    • Stable, documented APIs
    • Semantic versioning
    • Deprecation policy
  • Platform Support

    • Windows, Linux, macOS (native)
    • WebAssembly
    • Console support (planned)
  • Performance Guarantees

    • Documented performance characteristics
    • Benchmark suite
    • Regression testing
  • Security

    • Security audit
    • Sandboxed scripting
    • Save file validation

Future Vision

Features being considered for post-1.0:

  • Console Ports: PlayStation, Xbox, Nintendo Switch support
  • VR/AR: Virtual and augmented reality support
  • Cloud Gaming: Streaming and cloud save integration
  • Multiplayer: Advanced networking and matchmaking
  • Marketplace: Asset and plugin marketplace
  • AI Cloud: Optional cloud-based AI processing
  • Mobile: iOS and Android support

Feature Requests

How to Request Features

  1. Check existing requests: Search GitHub Issues
  2. Create a discussion: Use GitHub Discussions for initial feedback
  3. Submit formal request: Create an issue with the feature-request label

Prioritization Criteria

Features are prioritized based on:

FactorWeight
Community interestHigh
Development effortMedium
Strategic alignmentHigh
MaintainabilityMedium
Performance impactMedium

Most Requested Features

Current top community requests:

  1. Visual scripting
  2. More platform support
  3. VR integration
  4. Advanced networking
  5. Asset marketplace

Deprecation Policy

Pre-1.0

During alpha/beta:

  • APIs may change with each minor version
  • Breaking changes documented in changelog
  • Migration guides provided for significant changes

Post-1.0

After 1.0 release:

  • Deprecated features marked with #[deprecated]
  • 2 minor versions before removal
  • Clear migration documentation

Release Schedule

Versioning

We follow Semantic Versioning:

  • MAJOR: Breaking changes
  • MINOR: New features, backward compatible
  • PATCH: Bug fixes

Release Cadence

PhaseCadence
AlphaAs needed
BetaMonthly
StableQuarterly

Support Policy

VersionSupport Duration
CurrentFull support
Previous minorSecurity fixes
OlderCommunity only

Contributing to Roadmap

How to Help

  1. Implement Features: Check help wanted issues
  2. Provide Feedback: Test pre-release versions
  3. Write Documentation: Help document new features
  4. Create Examples: Build showcase projects

RFC Process

For major changes:

  1. Create RFC in Discussions
  2. Gather community feedback
  3. Core team review
  4. Implementation (if approved)

Roadmap Updates

This roadmap is updated:

  • After each major release
  • Quarterly for minor adjustments
  • As needed for significant changes

Milestones

Completed Milestones

MilestoneDateHighlights
Initial Release2025 Q4Core ECS, basic rendering
AI Integration2025 Q4LLM support, behavior trees

Upcoming Milestones

MilestoneTargetGoals
Beta Release2026 Q2Stable APIs, documentation
Editor Preview2026 Q3Basic visual editor
1.0 Release2027 Q1Production ready

Risk Factors

Technical Risks

RiskMitigation
LLM API changesAbstraction layer, multiple providers
Performance challengesContinuous benchmarking, optimization focus
Platform compatibilityEarly testing on all platforms

Project Risks

RiskMitigation
Scope creepClear prioritization, phased releases
Maintainer burnoutCommunity building, shared ownership
FundingExploring sustainability options

Community Involvement

The roadmap reflects community input:

  • Surveys: Periodic community surveys
  • Voting: Feature request voting on GitHub
  • Discussions: Open planning discussions
  • Contributors: Active contributors help shape direction