1pub mod messages;
2pub mod rpy_asset_loader;
3
4use std::collections::HashMap;
5
6use bevy::prelude::*;
7use bevy_kira_audio::prelude::*;
8
9use renpy_parser::parsers::{AST, inject_node};
10
11use messages::*;
12
13#[derive(Component)]
14pub struct NovelBackground;
15
16#[derive(Component)]
17pub struct NovelImage;
18
19#[derive(Component)]
20pub struct NovelText;
21
22#[derive(Component)]
23pub struct NovelTextWhat;
24
25#[derive(Component)]
26pub struct NovelTextWho;
27pub struct NovelPlugin;
28
29#[derive(Resource, Clone)]
30pub struct MusicHandle(Option<Handle<AudioInstance>>);
31
32#[derive(Resource, Default)]
33pub struct NovelData {
34 pub ast: Vec<AST>,
35 pub current_index: usize,
36 pub cached_images: HashMap<String, Sprite>,
37}
38
39impl NovelData {
40 pub fn push_text_node(&mut self, who: Option<String>, what: String, index: usize) {
43 let node = AST::Say(index, who, what);
44 self.ast = inject_node(self.ast.clone(), node.clone());
45 for a in self.ast.iter_mut() {
46 if let AST::Label(node_index, label, node_ast, opts) = a {
47 let first_index = node_ast.first().unwrap().index();
48 let last_index = node_ast.last().unwrap().index();
49
50 if index >= first_index && index <= last_index {
51 *a = AST::Label(
52 *node_index,
53 label.clone(),
54 inject_node(node_ast.clone(), node.clone()).clone(),
55 opts.clone(),
56 )
57 }
58 }
59 }
60 }
61
62 pub fn push_show_node(&mut self, image: String, index: usize) {
63 let node = AST::Show(index, image);
64 self.ast = inject_node(self.ast.clone(), node.clone());
65 for a in self.ast.iter_mut() {
66 if let AST::Label(node_index, label, node_ast, opts) = a {
67 let first_index = node_ast.first().unwrap().index();
68 let last_index = node_ast.last().unwrap().index();
69
70 if index >= first_index && index <= last_index {
71 *a = AST::Label(
72 *node_index,
73 label.clone(),
74 inject_node(node_ast.clone(), node.clone()).clone(),
75 opts.clone(),
76 )
77 }
78 }
79 }
80 }
81
82 pub fn push_hide_node(&mut self, image: String, index: usize) {
83 let node = AST::Hide(index, image);
84 self.ast = inject_node(self.ast.clone(), node.clone());
85 for a in self.ast.iter_mut() {
86 if let AST::Label(node_index, label, node_ast, opts) = a {
87 let first_index = node_ast.first().unwrap().index();
88 let last_index = node_ast.last().unwrap().index();
89
90 if index >= first_index && index <= last_index {
91 *a = AST::Label(
92 *node_index,
93 label.clone(),
94 inject_node(node_ast.clone(), node.clone()).clone(),
95 opts.clone(),
96 )
97 }
98 }
99 }
100 }
101
102 pub fn push_scene_node(&mut self, image: String, index: usize) {
103 let node = AST::Scene(index, Some(image), "master".into());
104 self.ast = inject_node(self.ast.clone(), node.clone());
105 for a in self.ast.iter_mut() {
106 if let AST::Label(node_index, label, node_ast, opts) = a {
107 let first_index = node_ast.first().unwrap().index();
108 let last_index = node_ast.last().unwrap().index();
109
110 if index >= first_index && index <= last_index {
111 *a = AST::Label(
112 *node_index,
113 label.clone(),
114 inject_node(node_ast.clone(), node.clone()).clone(),
115 opts.clone(),
116 )
117 }
118 }
119 }
120 }
121
122 pub fn write_image_cache(&mut self, image_name: String, sprite: Sprite) {
125 self.cached_images.insert(image_name, sprite);
126 }
127}
128
129#[derive(Resource, Default)]
130pub struct NovelSettings {
131 pub assets_path: String,
132 pub pause_handle_switch_node: bool,
133}
134
135impl Plugin for NovelPlugin {
136 fn build(&self, app: &mut App) {
137 app.add_systems(Startup, setup)
138 .add_systems(
139 Update,
140 (
141 adjust_text_position,
142 handle_start_scenario,
143 handle_switch_next_node,
144 handle_new_node,
145 (handle_hide_image_node, handle_hide_text_node).chain(),
146 (handle_show_image_node, handle_show_text_node).chain(),
147 handle_press_key,
148 handle_play_audio,
149 scale_images,
150 )
151 .chain(),
152 )
153 .add_message::<EventHandleNode>()
154 .add_message::<EventHideImageNode>()
155 .add_message::<EventHideTextNode>()
156 .add_message::<EventJump>()
157 .add_message::<EventLabel>()
158 .add_message::<EventPlayAudio>()
159 .add_message::<EventReturn>()
160 .add_message::<EventSay>()
161 .add_message::<EventShow>()
162 .add_message::<EventShowImageNode>()
163 .add_message::<EventShowTextNode>()
164 .add_message::<EventStartScenario>()
165 .add_message::<EventSwitchNextNode>()
166 .add_message::<EventNovelEnd>()
167 .init_resource::<NovelData>()
168 .insert_resource(MusicHandle(None))
169 .insert_resource(NovelSettings::default())
170 .init_asset_loader::<rpy_asset_loader::RpyAssetLoader>()
171 .init_asset::<rpy_asset_loader::Rpy>();
172 }
173}
174
175fn setup(mut commands: Commands, _novel_settings: Res<NovelSettings>) {
176 commands
177 .spawn((
178 Text2d::default(),
179 NovelText,
180 Name::new("Novel Text"),
181 ZIndex(10),
182 Transform::from_translation(Vec3::Z),
183 TextLayout::new(Justify::Left, LineBreak::WordBoundary),
184 ))
185 .with_children(|p| {
186 p.spawn((
187 TextSpan::new(""),
188 NovelTextWho {},
189 Name::new("Novel Text Who"),
190 Visibility::Visible,
191 ));
192
193 p.spawn((TextSpan::new("\n"), Name::new("Novel Text Newline")));
194
195 p.spawn((
196 TextSpan::new(""),
197 NovelTextWhat {},
198 Name::new("Novel Text What"),
199 Transform::from_translation(Vec3::Z),
200 Visibility::Visible,
201 ));
202 });
203
204 commands.spawn((
205 Name::new("Background Image"),
206 Sprite::default(),
207 NovelBackground,
208 Node {
209 position_type: PositionType::Absolute,
210 width: Val::Auto,
211 height: Val::Auto,
212 ..default()
213 },
214 Visibility::Hidden,
215 ));
216
217 commands.spawn((
218 Name::new("Character Image"),
219 Sprite::default(),
220 NovelImage,
221 Node {
223 position_type: PositionType::Absolute,
224 width: Val::Auto,
225 height: Val::Auto,
226 ..default()
227 },
228 Visibility::Hidden,
229 ));
230}
231
232fn adjust_text_position(
233 mut windows: Query<&Window>,
234 mut q_novel_text: Query<(Entity, &mut Transform, &NovelText)>,
235) {
236 let window = windows.single_mut().unwrap(); for (_, mut transform, _) in q_novel_text.iter_mut() {
239 transform.translation.y = -window.resolution.height() / 2.0 + 50.0;
240 transform.translation.x = -window.resolution.width() / 3.0 + 50.0;
241 }
242}
243
244pub fn find_element_with_index(ast: Vec<AST>, index: usize) -> Option<AST> {
245 for ast in ast.iter() {
246 let ast_index = match ast {
247 AST::Return(i, _) => *i,
248 AST::Jump(i, _, _) => *i,
249 AST::Scene(i, _, _) => *i,
250 AST::Show(i, _) => *i,
251 AST::Hide(i, _) => *i,
252 AST::Label(i, _, _, _) => *i,
253 AST::Say(i, _, _) => *i,
254 AST::Play(i, _, _) => *i,
255 AST::Define(i, _) => *i,
256 AST::Stop(i, _, _, _) => *i,
257 AST::GameMechanic(i, _) => *i,
258 AST::LLMGenerate(i, _, _) => *i,
259 AST::Comment(i, _) => *i,
260 AST::Error => panic!(),
261 };
262
263 if index == ast_index {
264 return Some(ast.clone());
265 }
266 }
267
268 None
269}
270
271fn list_ast_indices(ast: Vec<AST>) -> Vec<usize> {
272 let mut indices: Vec<usize> = ast
273 .iter()
274 .map(|a| match a {
275 AST::Return(i, _) => *i,
276 AST::Jump(i, _, _) => *i,
277 AST::Scene(i, _, _) => *i,
278 AST::Show(i, _) => *i,
279 AST::Hide(i, _) => *i,
280 AST::Label(i, _, _, _) => *i,
281 AST::Say(i, _, _) => *i,
282 AST::Play(i, _, _) => *i,
283 AST::Define(i, _) => *i,
284 AST::Stop(i, _, _, _) => *i,
285 AST::GameMechanic(i, _) => *i,
286 AST::LLMGenerate(i, _, _) => *i,
287 AST::Comment(i, _) => *i,
288 AST::Error => panic!(),
289 })
290 .collect();
291
292 for ast in ast {
293 match ast {
294 AST::Label(_, _, vec, _) => {
295 indices.extend_from_slice(list_ast_indices(vec).as_slice());
296 }
297 _ => {}
298 }
299 }
300
301 indices
302}