Skip to main content

claude_native/rules/project_specific/
game_dev.rs

1use crate::detection::{PrimaryType, ProjectType};
2use crate::rules::*;
3use crate::scan::ProjectContext;
4
5fn is_game(pt: &ProjectType) -> bool {
6    matches!(pt.primary, PrimaryType::GameDev(_))
7}
8
9// ── Rule GAME1: Binary scene/asset files ignored ────────────────────
10
11pub struct BinaryAssetsIgnored;
12
13impl Rule for BinaryAssetsIgnored {
14    fn id(&self) -> &str { "GAME1" }
15    fn name(&self) -> &str { "Binary scene/asset files are ignored" }
16    fn dimension(&self) -> Dimension { Dimension::ContextEfficiency }
17    fn severity(&self) -> Severity { Severity::Critical }
18
19    fn applies_to(&self, pt: &ProjectType) -> bool { is_game(pt) }
20
21    fn check(&self, ctx: &ProjectContext) -> RuleResult {
22        let binary_patterns = [
23            "*.unity", "*.prefab", "*.asset", "*.tscn", "*.tres",
24            "*.blend", "*.fbx", "*.png", "*.wav", "*.mp3",
25        ];
26
27        if ctx.claudeignore_content.is_none() {
28            return self.fail(
29                "No .claudeignore — binary game assets (scenes, models, textures) clutter searches",
30                Suggestion {
31                    priority: SuggestionPriority::QuickWin,
32                    title: "Create .claudeignore for game project".into(),
33                    description: format!("Add to .claudeignore: {}\nClaude can't read binary files — they only waste search results.", binary_patterns.join(", ")),
34                    effort: Effort::Minutes,
35                },
36            );
37        }
38
39        let has_patterns = binary_patterns.iter().any(|p| {
40            let p = p.trim_start_matches("*.");
41            ctx.claudeignore_contains(p)
42        });
43
44        if has_patterns {
45            self.pass()
46        } else {
47            self.fail(
48                "Binary game asset patterns not in .claudeignore",
49                Suggestion {
50                    priority: SuggestionPriority::QuickWin,
51                    title: "Ignore binary game assets".into(),
52                    description: format!("Add to .claudeignore: {}", binary_patterns.join(", ")),
53                    effort: Effort::Minutes,
54                },
55            )
56        }
57    }
58}
59
60// ── Rule GAME2: Editor metadata ignored ─────────────────────────────
61
62pub struct EditorMetadataIgnored;
63
64impl Rule for EditorMetadataIgnored {
65    fn id(&self) -> &str { "GAME2" }
66    fn name(&self) -> &str { "Editor-generated metadata is ignored" }
67    fn dimension(&self) -> Dimension { Dimension::ContextEfficiency }
68    fn severity(&self) -> Severity { Severity::High }
69
70    fn applies_to(&self, pt: &ProjectType) -> bool { is_game(pt) }
71
72    fn check(&self, ctx: &ProjectContext) -> RuleResult {
73        let metadata = ["*.meta", "Library/", ".godot/", ".import/"];
74        let relevant: Vec<&&str> = metadata.iter()
75            .filter(|m| {
76                let m_clean = m.trim_end_matches('/').trim_start_matches("*.");
77                ctx.all_files.iter().any(|f| {
78                    let path_str = f.path.to_string_lossy();
79                    path_str.contains(m_clean)
80                }) || ctx.root.join(m.trim_end_matches('/')).is_dir()
81            })
82            .collect();
83
84        if relevant.is_empty() {
85            return self.pass();
86        }
87
88        let all_ignored = relevant.iter().all(|m| {
89            let m_clean = m.trim_end_matches('/').trim_start_matches("*.");
90            ctx.claudeignore_contains(m_clean)
91        });
92
93        if all_ignored {
94            self.pass()
95        } else {
96            self.fail(
97                "Editor metadata not in .claudeignore",
98                Suggestion {
99                    priority: SuggestionPriority::QuickWin,
100                    title: "Ignore editor metadata".into(),
101                    description: "Add *.meta, Library/, .godot/, .import/ to .claudeignore. Unity alone generates a .meta file per asset — 500 assets = 500 extra search results.".into(),
102                    effort: Effort::Minutes,
103                },
104            )
105        }
106    }
107}
108
109// ── Rule GAME3: Scripts are the primary target ──────────────────────
110
111pub struct ScriptsArePrimary;
112
113impl Rule for ScriptsArePrimary {
114    fn id(&self) -> &str { "GAME3" }
115    fn name(&self) -> &str { "CLAUDE.md directs to script directories" }
116    fn dimension(&self) -> Dimension { Dimension::Foundation }
117    fn severity(&self) -> Severity { Severity::High }
118
119    fn applies_to(&self, pt: &ProjectType) -> bool { is_game(pt) }
120
121    fn check(&self, ctx: &ProjectContext) -> RuleResult {
122        let content = match &ctx.claude_md_content {
123            Some(c) => c.to_lowercase(),
124            None => return self.skip(),
125        };
126
127        let has_script_ref = content.contains("script")
128            || content.contains("src/")
129            || content.contains("assets/scripts")
130            || content.contains("shader");
131
132        if has_script_ref {
133            self.pass()
134        } else {
135            self.warn(
136                "CLAUDE.md doesn't reference script directories",
137                Suggestion {
138                    priority: SuggestionPriority::QuickWin,
139                    title: "Point CLAUDE.md to scripts".into(),
140                    description: "Add to CLAUDE.md: 'Game logic: Assets/Scripts/ — Shaders: Assets/Shaders/'. Claude can only read code, not scenes or prefabs.".into(),
141                    effort: Effort::Minutes,
142                },
143            )
144        }
145    }
146}
147
148// ── Rule GAME4: Tests exist for game logic ──────────────────────────
149
150pub struct GameLogicTests;
151
152impl Rule for GameLogicTests {
153    fn id(&self) -> &str { "GAME4" }
154    fn name(&self) -> &str { "Tests for non-visual game logic" }
155    fn dimension(&self) -> Dimension { Dimension::CodeQuality }
156    fn severity(&self) -> Severity { Severity::Medium }
157
158    fn applies_to(&self, pt: &ProjectType) -> bool { is_game(pt) }
159
160    fn check(&self, ctx: &ProjectContext) -> RuleResult {
161        if !ctx.test_files.is_empty() {
162            self.pass()
163        } else {
164            self.warn(
165                "No tests for game logic (state machines, inventory, damage calculations)",
166                Suggestion {
167                    priority: SuggestionPriority::HighImpact,
168                    title: "Add tests for pure game logic".into(),
169                    description: "Pure logic (math, state, rules) is perfectly testable. Add unit tests for game systems — Claude can verify changes without needing the game editor.".into(),
170                    effort: Effort::HalfDay,
171                },
172            )
173        }
174    }
175}