# Phase 8-1: AI System Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** 수동 내비메시 + A* 패스파인딩 + 스티어링 행동으로 AI 에이전트가 경로를 따라 이동 **Architecture:** `voltex_ai` crate 신규 생성. NavMesh(삼각형 그래프) + A*(삼각형 중심 경로) + 스티어링(순수 함수). 모두 voltex_math::Vec3 기반. **Tech Stack:** Rust, voltex_math (Vec3) **Spec:** `docs/superpowers/specs/2026-03-25-phase8-1-ai.md` --- ## File Structure ### voltex_ai (신규) - `crates/voltex_ai/Cargo.toml` (Create) - `crates/voltex_ai/src/lib.rs` (Create) - `crates/voltex_ai/src/navmesh.rs` — NavMesh, NavTriangle (Create) - `crates/voltex_ai/src/pathfinding.rs` — A* find_path (Create) - `crates/voltex_ai/src/steering.rs` — SteeringAgent, seek/flee/arrive/wander/follow_path (Create) ### Workspace (수정) - `Cargo.toml` — members + dependencies (Modify) --- ## Task 1: Crate 설정 + NavMesh **Files:** - Create: `crates/voltex_ai/Cargo.toml` - Create: `crates/voltex_ai/src/lib.rs` - Create: `crates/voltex_ai/src/navmesh.rs` - Modify: `Cargo.toml` (workspace) - [ ] **Step 1: Cargo.toml** ```toml [package] name = "voltex_ai" version = "0.1.0" edition = "2021" [dependencies] voltex_math.workspace = true ``` - [ ] **Step 2: workspace에 추가** members에 `"crates/voltex_ai"`, workspace.dependencies에 `voltex_ai = { path = "crates/voltex_ai" }`. - [ ] **Step 3: navmesh.rs 작성** ```rust // crates/voltex_ai/src/navmesh.rs use voltex_math::Vec3; #[derive(Debug, Clone)] pub struct NavTriangle { pub indices: [usize; 3], pub neighbors: [Option; 3], } pub struct NavMesh { pub vertices: Vec, pub triangles: Vec, } impl NavMesh { pub fn new(vertices: Vec, triangles: Vec) -> Self { Self { vertices, triangles } } /// Find which triangle contains the point (XZ plane projection, Y ignored). pub fn find_triangle(&self, point: Vec3) -> Option { for (i, tri) in self.triangles.iter().enumerate() { let a = self.vertices[tri.indices[0]]; let b = self.vertices[tri.indices[1]]; let c = self.vertices[tri.indices[2]]; if point_in_triangle_xz(point, a, b, c) { return Some(i); } } None } /// Center of a triangle (average of 3 vertices). pub fn triangle_center(&self, tri_idx: usize) -> Vec3 { let tri = &self.triangles[tri_idx]; let a = self.vertices[tri.indices[0]]; let b = self.vertices[tri.indices[1]]; let c = self.vertices[tri.indices[2]]; (a + b + c) * (1.0 / 3.0) } /// Midpoint of shared edge between triangle tri_idx and its neighbor on edge_idx. pub fn edge_midpoint(&self, tri_idx: usize, edge_idx: usize) -> Vec3 { let tri = &self.triangles[tri_idx]; let i0 = tri.indices[edge_idx]; let i1 = tri.indices[(edge_idx + 1) % 3]; (self.vertices[i0] + self.vertices[i1]) * 0.5 } } /// Point-in-triangle test on XZ plane using barycentric coordinates. fn point_in_triangle_xz(p: Vec3, a: Vec3, b: Vec3, c: Vec3) -> bool { let v0x = c.x - a.x; let v0z = c.z - a.z; let v1x = b.x - a.x; let v1z = b.z - a.z; let v2x = p.x - a.x; let v2z = p.z - a.z; let dot00 = v0x * v0x + v0z * v0z; let dot01 = v0x * v1x + v0z * v1z; let dot02 = v0x * v2x + v0z * v2z; let dot11 = v1x * v1x + v1z * v1z; let dot12 = v1x * v2x + v1z * v2z; let inv_denom = 1.0 / (dot00 * dot11 - dot01 * dot01); let u = (dot11 * dot02 - dot01 * dot12) * inv_denom; let v = (dot00 * dot12 - dot01 * dot02) * inv_denom; u >= 0.0 && v >= 0.0 && (u + v) <= 1.0 } #[cfg(test)] mod tests { use super::*; fn make_square_navmesh() -> NavMesh { // Square from (0,0,0) to (10,0,10), split into 2 triangles // Triangle 0: (0,0,0), (10,0,0), (10,0,10) — neighbors: [None, Some(1), None] // Triangle 1: (0,0,0), (10,0,10), (0,0,10) — neighbors: [Some(0), None, None] let vertices = vec![ Vec3::new(0.0, 0.0, 0.0), Vec3::new(10.0, 0.0, 0.0), Vec3::new(10.0, 0.0, 10.0), Vec3::new(0.0, 0.0, 10.0), ]; let triangles = vec![ NavTriangle { indices: [0, 1, 2], neighbors: [None, Some(1), None] }, NavTriangle { indices: [0, 2, 3], neighbors: [Some(0), None, None] }, ]; NavMesh::new(vertices, triangles) } #[test] fn test_find_triangle_inside() { let nm = make_square_navmesh(); // Point in bottom-right triangle (0) assert_eq!(nm.find_triangle(Vec3::new(8.0, 0.0, 2.0)), Some(0)); // Point in top-left triangle (1) assert_eq!(nm.find_triangle(Vec3::new(2.0, 0.0, 8.0)), Some(1)); } #[test] fn test_find_triangle_outside() { let nm = make_square_navmesh(); assert_eq!(nm.find_triangle(Vec3::new(-5.0, 0.0, 5.0)), None); assert_eq!(nm.find_triangle(Vec3::new(15.0, 0.0, 5.0)), None); } #[test] fn test_triangle_center() { let nm = make_square_navmesh(); let c = nm.triangle_center(0); // Center of (0,0,0), (10,0,0), (10,0,10) ≈ (6.67, 0, 3.33) assert!((c.x - 10.0 / 3.0 * 2.0).abs() < 0.01); assert!((c.z - 10.0 / 3.0).abs() < 0.01); } #[test] fn test_edge_midpoint() { let nm = make_square_navmesh(); // Edge 1 of triangle 0 connects vertices 1(10,0,0) and 2(10,0,10) let mid = nm.edge_midpoint(0, 1); assert!((mid.x - 10.0).abs() < 0.01); assert!((mid.z - 5.0).abs() < 0.01); } } ``` - [ ] **Step 4: lib.rs 작성** ```rust pub mod navmesh; pub use navmesh::{NavMesh, NavTriangle}; ``` - [ ] **Step 5: 테스트 실행** Run: `cargo test -p voltex_ai` Expected: 4 PASS - [ ] **Step 6: 커밋** ```bash git add crates/voltex_ai/ Cargo.toml git commit -m "feat(ai): add voltex_ai crate with NavMesh (manual triangle mesh)" ``` --- ## Task 2: A* 패스파인딩 **Files:** - Create: `crates/voltex_ai/src/pathfinding.rs` - Modify: `crates/voltex_ai/src/lib.rs` - [ ] **Step 1: pathfinding.rs 작성** ```rust // crates/voltex_ai/src/pathfinding.rs use voltex_math::Vec3; use crate::navmesh::NavMesh; use std::collections::BinaryHeap; use std::cmp::Ordering; #[derive(Debug)] struct AStarNode { tri_idx: usize, g_cost: f32, f_cost: f32, } impl PartialEq for AStarNode { fn eq(&self, other: &Self) -> bool { self.tri_idx == other.tri_idx } } impl Eq for AStarNode {} impl PartialOrd for AStarNode { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } impl Ord for AStarNode { fn cmp(&self, other: &Self) -> Ordering { // Min-heap: reverse comparison other.f_cost.partial_cmp(&self.f_cost).unwrap_or(Ordering::Equal) } } /// Find a path on the NavMesh from start to goal. /// Returns waypoints (triangle centers) or None if no path exists. pub fn find_path(navmesh: &NavMesh, start: Vec3, goal: Vec3) -> Option> { let start_tri = navmesh.find_triangle(start)?; let goal_tri = navmesh.find_triangle(goal)?; if start_tri == goal_tri { return Some(vec![start, goal]); } let tri_count = navmesh.triangles.len(); let mut g_costs = vec![f32::INFINITY; tri_count]; let mut came_from: Vec> = vec![None; tri_count]; let mut closed = vec![false; tri_count]; g_costs[start_tri] = 0.0; let mut open = BinaryHeap::new(); let h = distance_xz(navmesh.triangle_center(start_tri), navmesh.triangle_center(goal_tri)); open.push(AStarNode { tri_idx: start_tri, g_cost: 0.0, f_cost: h }); while let Some(current) = open.pop() { if current.tri_idx == goal_tri { // Reconstruct path let mut path = vec![goal]; let mut idx = goal_tri; while let Some(prev) = came_from[idx] { path.push(navmesh.triangle_center(idx)); idx = prev; } path.push(start); path.reverse(); return Some(path); } if closed[current.tri_idx] { continue; } closed[current.tri_idx] = true; let tri = &navmesh.triangles[current.tri_idx]; for neighbor_opt in &tri.neighbors { if let Some(neighbor) = *neighbor_opt { if closed[neighbor] { continue; } let edge_cost = distance_xz( navmesh.triangle_center(current.tri_idx), navmesh.triangle_center(neighbor), ); let tentative_g = g_costs[current.tri_idx] + edge_cost; if tentative_g < g_costs[neighbor] { g_costs[neighbor] = tentative_g; came_from[neighbor] = Some(current.tri_idx); let h = distance_xz(navmesh.triangle_center(neighbor), navmesh.triangle_center(goal_tri)); open.push(AStarNode { tri_idx: neighbor, g_cost: tentative_g, f_cost: tentative_g + h, }); } } } } None // No path found } fn distance_xz(a: Vec3, b: Vec3) -> f32 { let dx = a.x - b.x; let dz = a.z - b.z; (dx * dx + dz * dz).sqrt() } #[cfg(test)] mod tests { use super::*; use crate::navmesh::{NavMesh, NavTriangle}; fn make_square_navmesh() -> NavMesh { let vertices = vec![ Vec3::new(0.0, 0.0, 0.0), Vec3::new(10.0, 0.0, 0.0), Vec3::new(10.0, 0.0, 10.0), Vec3::new(0.0, 0.0, 10.0), ]; let triangles = vec![ NavTriangle { indices: [0, 1, 2], neighbors: [None, Some(1), None] }, NavTriangle { indices: [0, 2, 3], neighbors: [Some(0), None, None] }, ]; NavMesh::new(vertices, triangles) } fn make_three_triangle_strip() -> NavMesh { // Three triangles in a strip: 0-1-2 connected let vertices = vec![ Vec3::new(0.0, 0.0, 0.0), // 0 Vec3::new(5.0, 0.0, 0.0), // 1 Vec3::new(5.0, 0.0, 5.0), // 2 Vec3::new(10.0, 0.0, 0.0), // 3 Vec3::new(10.0, 0.0, 5.0), // 4 Vec3::new(15.0, 0.0, 0.0), // 5 Vec3::new(15.0, 0.0, 5.0), // 6 ]; let triangles = vec![ NavTriangle { indices: [0, 1, 2], neighbors: [Some(1), None, None] }, NavTriangle { indices: [1, 3, 4], neighbors: [Some(2), None, Some(0)] }, NavTriangle { indices: [3, 5, 6], neighbors: [None, None, Some(1)] }, ]; NavMesh::new(vertices, triangles) } #[test] fn test_same_triangle() { let nm = make_square_navmesh(); let path = find_path(&nm, Vec3::new(8.0, 0.0, 2.0), Vec3::new(9.0, 0.0, 1.0)); let p = path.unwrap(); assert_eq!(p.len(), 2); } #[test] fn test_adjacent_triangles() { let nm = make_square_navmesh(); let path = find_path(&nm, Vec3::new(8.0, 0.0, 2.0), Vec3::new(2.0, 0.0, 8.0)); let p = path.unwrap(); assert!(p.len() >= 2); // First point is start, last is goal assert!((p[0].x - 8.0).abs() < 0.01); assert!((p.last().unwrap().x - 2.0).abs() < 0.01); } #[test] fn test_three_triangle_path() { let nm = make_three_triangle_strip(); let path = find_path(&nm, Vec3::new(2.0, 0.0, 1.0), Vec3::new(14.0, 0.0, 1.0)); let p = path.unwrap(); assert!(p.len() >= 3); // start + midpoints + goal } #[test] fn test_no_path_outside() { let nm = make_square_navmesh(); assert!(find_path(&nm, Vec3::new(-5.0, 0.0, 0.0), Vec3::new(5.0, 0.0, 5.0)).is_none()); } #[test] fn test_disconnected() { // Two triangles not connected let vertices = vec![ Vec3::new(0.0, 0.0, 0.0), Vec3::new(5.0, 0.0, 0.0), Vec3::new(2.5, 0.0, 5.0), Vec3::new(20.0, 0.0, 0.0), Vec3::new(25.0, 0.0, 0.0), Vec3::new(22.5, 0.0, 5.0), ]; let triangles = vec![ NavTriangle { indices: [0, 1, 2], neighbors: [None, None, None] }, NavTriangle { indices: [3, 4, 5], neighbors: [None, None, None] }, ]; let nm = NavMesh::new(vertices, triangles); assert!(find_path(&nm, Vec3::new(2.0, 0.0, 1.0), Vec3::new(22.0, 0.0, 1.0)).is_none()); } } ``` - [ ] **Step 2: lib.rs에 모듈 등록** ```rust pub mod pathfinding; pub use pathfinding::find_path; ``` - [ ] **Step 3: 테스트 실행** Run: `cargo test -p voltex_ai` Expected: 9 PASS (4 navmesh + 5 pathfinding) - [ ] **Step 4: 커밋** ```bash git add crates/voltex_ai/src/pathfinding.rs crates/voltex_ai/src/lib.rs git commit -m "feat(ai): add A* pathfinding on NavMesh triangle graph" ``` --- ## Task 3: 스티어링 행동 **Files:** - Create: `crates/voltex_ai/src/steering.rs` - Modify: `crates/voltex_ai/src/lib.rs` - [ ] **Step 1: steering.rs 작성** ```rust // crates/voltex_ai/src/steering.rs use voltex_math::Vec3; #[derive(Debug, Clone, Copy)] pub struct SteeringAgent { pub position: Vec3, pub velocity: Vec3, pub max_speed: f32, pub max_force: f32, } impl SteeringAgent { pub fn new(position: Vec3, max_speed: f32, max_force: f32) -> Self { Self { position, velocity: Vec3::ZERO, max_speed, max_force } } } fn truncate(v: Vec3, max_len: f32) -> Vec3 { let len = v.length(); if len > max_len && len > 1e-6 { v * (max_len / len) } else { v } } /// Steer toward a target at max speed. pub fn seek(agent: &SteeringAgent, target: Vec3) -> Vec3 { let desired = target - agent.position; let len = desired.length(); if len < 1e-6 { return Vec3::ZERO; } let desired = desired * (agent.max_speed / len); truncate(desired - agent.velocity, agent.max_force) } /// Steer away from a threat. pub fn flee(agent: &SteeringAgent, threat: Vec3) -> Vec3 { let desired = agent.position - threat; let len = desired.length(); if len < 1e-6 { return Vec3::ZERO; } let desired = desired * (agent.max_speed / len); truncate(desired - agent.velocity, agent.max_force) } /// Steer toward target, decelerating within slow_radius. pub fn arrive(agent: &SteeringAgent, target: Vec3, slow_radius: f32) -> Vec3 { let to_target = target - agent.position; let dist = to_target.length(); if dist < 0.01 { return Vec3::ZERO; } let speed = if dist < slow_radius { agent.max_speed * (dist / slow_radius) } else { agent.max_speed }; let desired = to_target * (speed / dist); truncate(desired - agent.velocity, agent.max_force) } /// Steer in a wandering pattern. /// `angle` should be varied by the caller each frame (add small random delta). pub fn wander(agent: &SteeringAgent, wander_radius: f32, wander_distance: f32, angle: f32) -> Vec3 { let forward = if agent.velocity.length() > 1e-6 { agent.velocity.normalize() } else { Vec3::Z }; let circle_center = agent.position + forward * wander_distance; let offset = Vec3::new(angle.cos() * wander_radius, 0.0, angle.sin() * wander_radius); let wander_target = circle_center + offset; seek(agent, wander_target) } /// Follow a path of waypoints. Returns (steering_force, current_waypoint_index). pub fn follow_path( agent: &SteeringAgent, path: &[Vec3], current_waypoint: usize, waypoint_radius: f32, ) -> (Vec3, usize) { if path.is_empty() { return (Vec3::ZERO, 0); } let mut wp = current_waypoint.min(path.len() - 1); // Advance waypoint if close enough let dist = (path[wp] - agent.position).length(); if dist < waypoint_radius && wp < path.len() - 1 { wp += 1; } // Last waypoint: arrive let force = if wp == path.len() - 1 { arrive(agent, path[wp], waypoint_radius * 2.0) } else { seek(agent, path[wp]) }; (force, wp) } #[cfg(test)] mod tests { use super::*; fn approx_vec(a: Vec3, b: Vec3) -> bool { (a.x - b.x).abs() < 0.1 && (a.y - b.y).abs() < 0.1 && (a.z - b.z).abs() < 0.1 } #[test] fn test_seek_direction() { let agent = SteeringAgent::new(Vec3::ZERO, 5.0, 10.0); let force = seek(&agent, Vec3::new(10.0, 0.0, 0.0)); assert!(force.x > 0.0, "seek should steer toward target, got x={}", force.x); } #[test] fn test_flee_direction() { let agent = SteeringAgent::new(Vec3::ZERO, 5.0, 10.0); let force = flee(&agent, Vec3::new(10.0, 0.0, 0.0)); assert!(force.x < 0.0, "flee should steer away from threat, got x={}", force.x); } #[test] fn test_arrive_deceleration() { let agent = SteeringAgent::new(Vec3::ZERO, 5.0, 10.0); let far_force = arrive(&agent, Vec3::new(100.0, 0.0, 0.0), 10.0); let near_force = arrive(&agent, Vec3::new(5.0, 0.0, 0.0), 10.0); // Near force should be weaker (decelerating) assert!(near_force.length() < far_force.length(), "near={} should be < far={}", near_force.length(), far_force.length()); } #[test] fn test_arrive_at_target() { let agent = SteeringAgent::new(Vec3::new(5.0, 0.0, 0.0), 5.0, 10.0); let force = arrive(&agent, Vec3::new(5.0, 0.0, 0.0), 1.0); assert!(force.length() < 0.1, "at target, force should be ~zero"); } #[test] fn test_follow_path_advance() { let agent = SteeringAgent::new(Vec3::new(0.9, 0.0, 0.0), 5.0, 10.0); let path = vec![ Vec3::new(1.0, 0.0, 0.0), Vec3::new(5.0, 0.0, 0.0), Vec3::new(10.0, 0.0, 0.0), ]; let (_, wp) = follow_path(&agent, &path, 0, 0.5); assert_eq!(wp, 1, "should advance to next waypoint"); } #[test] fn test_follow_path_last_arrives() { let agent = SteeringAgent::new(Vec3::new(9.5, 0.0, 0.0), 5.0, 10.0); let path = vec![ Vec3::new(5.0, 0.0, 0.0), Vec3::new(10.0, 0.0, 0.0), ]; let (force, wp) = follow_path(&agent, &path, 1, 1.0); // Should be arriving (low force) assert!(force.length() < 5.0); } } ``` - [ ] **Step 2: lib.rs에 모듈 등록** ```rust pub mod steering; pub use steering::{SteeringAgent, seek, flee, arrive, wander, follow_path}; ``` - [ ] **Step 3: 테스트 실행** Run: `cargo test -p voltex_ai` Expected: 15 PASS (4 navmesh + 5 pathfinding + 6 steering) - [ ] **Step 4: 전체 workspace 테스트** Run: `cargo test --workspace` Expected: all pass - [ ] **Step 5: 커밋** ```bash git add crates/voltex_ai/src/steering.rs crates/voltex_ai/src/lib.rs git commit -m "feat(ai): add steering behaviors (seek, flee, arrive, wander, follow_path)" ``` --- ## Task 4: 문서 업데이트 **Files:** - Modify: `docs/STATUS.md` - Modify: `docs/DEFERRED.md` - [ ] **Step 1: STATUS.md에 Phase 8-1 추가** ```markdown ### Phase 8-1: AI System - voltex_ai: NavMesh (manual triangle mesh, find_triangle, edge/center queries) - voltex_ai: A* pathfinding on triangle graph (center-point path) - voltex_ai: Steering behaviors (seek, flee, arrive, wander, follow_path) ``` crate 구조에 voltex_ai 추가. 테스트 수 업데이트. - [ ] **Step 2: DEFERRED.md에 Phase 8-1 미뤄진 항목** ```markdown ## Phase 8-1 - **자동 내비메시 생성** — Recast 스타일 복셀화 미구현. 수동 정의만. - **String Pulling (Funnel)** — 삼각형 중심점 경로만. 최적 경로 스무딩 미구현. - **동적 장애물 회피** — 정적 내비메시만. 런타임 장애물 미처리. - **ECS 통합** — AI 컴포넌트 미구현. 함수 직접 호출. - **내비메시 직렬화** — 미구현. ``` - [ ] **Step 3: 커밋** ```bash git add docs/STATUS.md docs/DEFERRED.md git commit -m "docs: add Phase 8-1 AI system status and deferred items" ```