claude_native/rules/project_specific/
game_dev.rs1use 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
9pub 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
60pub 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
109pub 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
148pub 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}