oxygengine_integration_vn_ui/
lib.rs

1pub mod gui;
2
3use crate::gui::{
4    backgrounds::*, characters::*, dialogue::*, game_menu::*, main_menu::*, overlay::*,
5    visual_novel_gui,
6};
7use oxygengine_animation::phase::{Ease, Phase};
8use oxygengine_core::{
9    app::AppBuilder,
10    ecs::{
11        pipeline::{PipelineBuilder, PipelineBuilderError},
12        AccessType, Comp, ResQuery, Universe, WorldRef,
13    },
14    prefab::{Prefab as CorePrefab, PrefabComponent, PrefabManager},
15};
16use oxygengine_user_interface::{
17    component::UserInterfaceView,
18    raui::{core::prelude::*, material::prelude::*},
19    resource::UserInterface,
20};
21use oxygengine_visual_novel::{
22    background::Background, character::Character, dialogue::DialogueOption,
23    resource::VnStoryManager, Position,
24};
25use serde::{Deserialize, Serialize};
26
27pub mod prelude {
28    pub use crate::{
29        gui::{backgrounds::*, characters::*, dialogue::*, game_menu::*, main_menu::*, overlay::*},
30        *,
31    };
32}
33
34fn make_true() -> bool {
35    true
36}
37
38/// (story ID)
39#[derive(Default, Debug, Clone, Serialize, Deserialize)]
40pub struct VisualNovelSyncUserInterface(#[serde(default)] pub String);
41
42impl CorePrefab for VisualNovelSyncUserInterface {}
43impl PrefabComponent for VisualNovelSyncUserInterface {}
44
45pub type ApplyVisualNovelToUserInterfaceSystemResources<'a> = (
46    WorldRef,
47    &'a mut UserInterface,
48    &'a mut VnStoryManager,
49    Comp<&'a VisualNovelSyncUserInterface>,
50    Comp<&'a UserInterfaceView>,
51);
52
53pub fn apply_visual_novel_to_user_interface_system(universe: &mut Universe) {
54    let (world, mut ui, mut manager, ..) =
55        universe.query_resources::<ApplyVisualNovelToUserInterfaceSystemResources>();
56
57    for (_, (sync, view)) in world
58        .query::<(&VisualNovelSyncUserInterface, &UserInterfaceView)>()
59        .iter()
60    {
61        if let Some(data) = ui.get_mut(view.app_id()) {
62            if let Some(story) = manager.get(&sync.0) {
63                if story.is_dirty() {
64                    data.application.mark_dirty();
65                }
66            }
67            for (_, msg) in data.signals_received() {
68                if let Some(VisualNovelSignal { story, action }) = msg.as_any().downcast_ref() {
69                    if let Some(story) = manager.get_mut(story) {
70                        match action {
71                            VisualNovelAction::None => {}
72                            VisualNovelAction::SelectDialogueOption(None) => {
73                                story.unselect_dialogue_option();
74                            }
75                            VisualNovelAction::FocusDialogueOption(index) => {
76                                let _ = story.focus_dialogue_option(*index);
77                            }
78                            VisualNovelAction::SelectDialogueOption(Some(index)) => {
79                                let _ = story.select_dialogue_option(*index);
80                            }
81                        }
82                    }
83                }
84            }
85        }
86    }
87}
88
89#[derive(MessageData, Debug, Clone, Serialize, Deserialize)]
90pub struct VisualNovelSignal {
91    #[serde(default)]
92    pub story: String,
93    #[serde(default)]
94    pub action: VisualNovelAction,
95}
96
97#[derive(Debug, Clone, Serialize, Deserialize)]
98pub enum VisualNovelAction {
99    None,
100    FocusDialogueOption(Option<usize>),
101    SelectDialogueOption(Option<usize>),
102}
103
104impl Default for VisualNovelAction {
105    fn default() -> Self {
106        Self::None
107    }
108}
109
110#[derive(PropsData, Default, Debug, Clone, Serialize, Deserialize)]
111pub struct VisualNovelStoryUsed(pub String);
112
113#[derive(PropsData, Debug, Clone, Serialize, Deserialize)]
114pub struct VisualNovelOverlayPhase(
115    #[serde(default = "VisualNovelOverlayPhase::default_phase")] pub Phase,
116);
117
118impl Default for VisualNovelOverlayPhase {
119    fn default() -> Self {
120        Self(Self::default_phase())
121    }
122}
123
124impl VisualNovelOverlayPhase {
125    fn default_phase() -> Phase {
126        Phase::ease(Ease::InOutCubic, 0.0..1.0, 0.0..1.0).unwrap()
127    }
128}
129
130#[derive(PropsData, Default, Debug, Clone, Serialize, Deserialize)]
131pub struct VisualNovelOverlayMaterial(#[serde(default)] pub ImageBoxMaterial);
132
133#[derive(PropsData, Default, Debug, Copy, Clone, Serialize, Deserialize)]
134pub struct VisualNovelCharacterContentAlignment(pub Option<Position>);
135
136#[derive(PropsData, Debug, Clone, Serialize, Deserialize)]
137pub enum VisualNovelTextTransition {
138    Instant,
139    Fade,
140    Unfold,
141}
142
143impl Default for VisualNovelTextTransition {
144    fn default() -> Self {
145        Self::Fade
146    }
147}
148
149#[derive(PropsData, Default, Debug, Clone, Serialize, Deserialize)]
150pub struct VisualNovelDialogueOptionsList(pub VerticalBoxProps);
151
152#[derive(PropsData, Default, Debug, Clone, Serialize, Deserialize)]
153pub struct VisualNovelDialogueMessageLayout(pub Option<ContentBoxItemLayout>);
154
155#[derive(PropsData, Default, Debug, Clone, Serialize, Deserialize)]
156pub struct VisualNovelDialogueCharacterLayout(pub Option<ContentBoxItemLayout>);
157
158#[derive(PropsData, Default, Debug, Clone, Serialize, Deserialize)]
159pub struct VisualNovelDialogueTextLayout(pub Option<ContentBoxItemLayout>);
160
161#[derive(PropsData, Default, Debug, Clone, Serialize, Deserialize)]
162pub struct VisualNovelDialogueOptionsLayout(pub Option<ContentBoxItemLayout>);
163
164#[derive(PropsData, Debug, Clone, Serialize, Deserialize)]
165pub struct VisualNovelDialogueMessageThemed {
166    #[serde(default)]
167    pub use_main_color: bool,
168    #[serde(default)]
169    pub color: ThemeColor,
170    #[serde(default)]
171    pub background_variant: Option<String>,
172    #[serde(default)]
173    pub text_variant: Option<String>,
174    #[serde(default = "VisualNovelDialogueMessageThemed::default_margin")]
175    pub margin: Rect,
176}
177
178impl Default for VisualNovelDialogueMessageThemed {
179    fn default() -> Self {
180        Self {
181            use_main_color: false,
182            color: Default::default(),
183            background_variant: None,
184            text_variant: None,
185            margin: Self::default_margin(),
186        }
187    }
188}
189
190impl VisualNovelDialogueMessageThemed {
191    fn default_margin() -> Rect {
192        64.0.into()
193    }
194}
195
196#[derive(PropsData, Debug, Clone, Serialize, Deserialize)]
197pub struct VisualNovelDialogueCharacterThemed {
198    #[serde(default)]
199    pub use_main_color: bool,
200    #[serde(default)]
201    pub color: ThemeColor,
202    #[serde(default)]
203    pub background_variant: Option<String>,
204    #[serde(default)]
205    pub text_variant: Option<String>,
206    #[serde(default = "VisualNovelDialogueCharacterThemed::default_margin")]
207    pub margin: Rect,
208}
209
210impl Default for VisualNovelDialogueCharacterThemed {
211    fn default() -> Self {
212        Self {
213            use_main_color: false,
214            color: Default::default(),
215            margin: Self::default_margin(),
216            background_variant: None,
217            text_variant: None,
218        }
219    }
220}
221
222impl VisualNovelDialogueCharacterThemed {
223    fn default_margin() -> Rect {
224        Rect {
225            left: 32.0,
226            right: 32.0,
227            top: 18.0,
228            bottom: 18.0,
229        }
230    }
231}
232
233#[derive(PropsData, Debug, Clone, Serialize, Deserialize)]
234pub struct VisualNovelDialogueOptionThemed {
235    #[serde(default = "VisualNovelDialogueOptionThemed::default_height")]
236    pub height: SizeBoxSizeValue,
237    #[serde(default)]
238    pub use_main_color: bool,
239    #[serde(default)]
240    pub color_default: ThemeColor,
241    #[serde(default = "VisualNovelDialogueOptionThemed::default_color_focused")]
242    pub color_focused: ThemeColor,
243    #[serde(default)]
244    pub button_variant: Option<String>,
245    #[serde(default)]
246    pub text_variant_default: Option<String>,
247    #[serde(default)]
248    pub text_variant_focused: Option<String>,
249    #[serde(default = "VisualNovelDialogueOptionThemed::default_margin")]
250    pub margin: Rect,
251}
252
253impl Default for VisualNovelDialogueOptionThemed {
254    fn default() -> Self {
255        Self {
256            height: Self::default_height(),
257            use_main_color: false,
258            color_default: Default::default(),
259            color_focused: Self::default_color_focused(),
260            button_variant: None,
261            text_variant_default: None,
262            text_variant_focused: None,
263            margin: Self::default_margin(),
264        }
265    }
266}
267
268impl VisualNovelDialogueOptionThemed {
269    fn default_height() -> SizeBoxSizeValue {
270        SizeBoxSizeValue::Exact(48.0)
271    }
272
273    fn default_color_focused() -> ThemeColor {
274        ThemeColor::Primary
275    }
276
277    fn default_margin() -> Rect {
278        Rect {
279            left: 32.0,
280            right: 32.0,
281            top: 14.0,
282            bottom: 14.0,
283        }
284    }
285}
286
287#[derive(PropsData, Debug, Copy, Clone, Serialize, Deserialize)]
288pub struct VisualNovelGuiProps {
289    #[serde(default = "make_true")]
290    pub overlay: bool,
291    #[serde(default = "make_true")]
292    pub main_menu: bool,
293    #[serde(default = "make_true")]
294    pub game_menu: bool,
295    #[serde(default = "make_true")]
296    pub dialogue: bool,
297    #[serde(default = "make_true")]
298    pub characters: bool,
299    #[serde(default = "make_true")]
300    pub backgrounds: bool,
301}
302
303impl Default for VisualNovelGuiProps {
304    fn default() -> Self {
305        Self {
306            overlay: true,
307            main_menu: true,
308            game_menu: true,
309            dialogue: true,
310            characters: true,
311            backgrounds: true,
312        }
313    }
314}
315
316#[derive(PropsData, Default, Debug, Clone, Serialize, Deserialize)]
317pub struct VisualNovelBackgroundsProps {
318    #[serde(default)]
319    pub phase: Scalar,
320    #[serde(default)]
321    pub from: Option<Background>,
322    #[serde(default)]
323    pub to: Option<Background>,
324}
325
326#[derive(PropsData, Default, Debug, Clone, Serialize, Deserialize)]
327pub struct VisualNovelCharacterProps(#[serde(default)] pub Character);
328
329#[derive(PropsData, Default, Debug, Clone, Serialize, Deserialize)]
330pub struct VisualNovelDialogueMessageProps {
331    #[serde(default)]
332    pub character: String,
333    #[serde(default)]
334    pub text: String,
335    #[serde(default)]
336    pub container_alpha: Scalar,
337    #[serde(default)]
338    pub message_alpha: Scalar,
339}
340
341#[derive(PropsData, Default, Debug, Clone, Serialize, Deserialize)]
342pub struct VisualNovelDialogueOptionsProps {
343    #[serde(default)]
344    pub options: Vec<DialogueOption>,
345    #[serde(default)]
346    pub alpha: Scalar,
347}
348
349#[derive(PropsData, Default, Debug, Clone, Serialize, Deserialize)]
350pub struct VisualNovelDialogueOptionProps {
351    #[serde(default)]
352    pub index: usize,
353    #[serde(default)]
354    pub text: String,
355    #[serde(default)]
356    pub alpha: Scalar,
357    #[serde(default)]
358    pub focused: bool,
359    #[serde(default)]
360    pub focus_phase: Scalar,
361}
362
363pub fn ui_setup(app: &mut Application) {
364    app.register_props::<VisualNovelStoryUsed>("VisualNovelStoryUsed");
365    app.register_props::<VisualNovelOverlayMaterial>("VisualNovelOverlayMaterial");
366    app.register_props::<VisualNovelCharacterContentAlignment>(
367        "VisualNovelCharacterContentAlignment",
368    );
369    app.register_props::<VisualNovelTextTransition>("VisualNovelTextTransition");
370    app.register_props::<VisualNovelDialogueOptionsList>("VisualNovelDialogueOptionsList");
371    app.register_props::<VisualNovelDialogueMessageLayout>("VisualNovelDialogueMessageLayout");
372    app.register_props::<VisualNovelDialogueCharacterLayout>("VisualNovelDialogueCharacterLayout");
373    app.register_props::<VisualNovelDialogueTextLayout>("VisualNovelDialogueTextLayout");
374    app.register_props::<VisualNovelDialogueOptionsLayout>("VisualNovelDialogueOptionsLayout");
375    app.register_props::<VisualNovelDialogueMessageThemed>("VisualNovelDialogueMessageThemed");
376    app.register_props::<VisualNovelDialogueCharacterThemed>("VisualNovelDialogueCharacterThemed");
377    app.register_props::<VisualNovelDialogueOptionThemed>("VisualNovelDialogueOptionThemed");
378    app.register_props::<VisualNovelGuiProps>("VisualNovelGuiProps");
379    app.register_props::<VisualNovelBackgroundsProps>("VisualNovelBackgroundsProps");
380    app.register_props::<VisualNovelCharacterProps>("VisualNovelCharacterProps");
381    app.register_props::<VisualNovelDialogueMessageProps>("VisualNovelDialogueMessageProps");
382    app.register_props::<VisualNovelDialogueOptionsProps>("VisualNovelDialogueOptionsProps");
383    app.register_props::<VisualNovelDialogueOptionProps>("VisualNovelDialogueOptionProps");
384
385    app.register_component("visual_novel_gui", visual_novel_gui);
386    app.register_component(
387        "visual_novel_overlay_container",
388        visual_novel_overlay_container,
389    );
390    app.register_component("visual_novel_overlay", visual_novel_overlay);
391    app.register_component("visual_novel_main_menu", visual_novel_main_menu);
392    app.register_component("visual_novel_game_menu", visual_novel_game_menu);
393    app.register_component(
394        "visual_novel_dialogue_container",
395        visual_novel_dialogue_container,
396    );
397    app.register_component(
398        "visual_novel_dialogue_message",
399        visual_novel_dialogue_message,
400    );
401    app.register_component(
402        "visual_novel_dialogue_options",
403        visual_novel_dialogue_options,
404    );
405    app.register_component("visual_novel_dialogue_option", visual_novel_dialogue_option);
406    app.register_component(
407        "visual_novel_characters_container",
408        visual_novel_characters_container,
409    );
410    app.register_component("visual_novel_character", visual_novel_character);
411    app.register_component(
412        "visual_novel_backgrounds_container",
413        visual_novel_backgrounds_container,
414    );
415    app.register_component("visual_novel_backgrounds", visual_novel_backgrounds);
416}
417
418pub fn bundle_installer<PB, Q>(
419    builder: &mut AppBuilder<PB>,
420    _: (),
421) -> Result<(), PipelineBuilderError>
422where
423    PB: PipelineBuilder,
424    Q: AccessType + ResQuery + 'static,
425{
426    builder.install_system::<ApplyVisualNovelToUserInterfaceSystemResources>(
427        "apply-visual-novel-to-user-interface",
428        apply_visual_novel_to_user_interface_system,
429        &["vn-story"],
430    )?;
431    Ok(())
432}
433
434pub fn prefabs_installer(prefabs: &mut PrefabManager) {
435    prefabs
436        .register_component_factory::<VisualNovelSyncUserInterface>("VisualNovelSyncUserInterface");
437}