1mod utils;
13use utils::*;
14
15use std::{collections::HashMap, sync::OnceLock};
16
17use bevy::prelude::*;
18use bevy_asset_loader::prelude::*;
19use haalka::{prelude::*, raw::DeferredUpdaterAppendDirection};
20use rand::{
21 Rng,
22 distr::{Bernoulli, Distribution},
23};
24
25fn main() {
26 App::new()
27 .add_plugins(examples_plugin)
28 .init_state::<AssetState>()
29 .add_loading_state(
30 LoadingState::new(AssetState::Loading)
31 .continue_to_state(AssetState::Loaded)
32 .load_collection::<RpgIconSheet>(),
33 )
34 .add_systems(Startup, |mut commands: Commands| {
38 commands.spawn((Camera2d, IsDefaultUiCamera));
39 })
40 .add_systems(
41 OnEnter(AssetState::Loaded),
42 (set_icon_texture_atlas, |world: &mut World| {
43 ui_root()
44 .update_raw_el(|raw_el| {
45 raw_el.on_spawn_with_system(
46 move |In(entity): In<_>,
47 camera: Single<Entity, With<IsDefaultUiCamera>>,
48 mut commands: Commands| {
49 if let Ok(mut commands) = commands.get_entity(entity) {
51 commands.try_insert(UiTargetCamera(*camera));
52 }
53 },
54 )
55 })
56 .spawn(world);
57 })
58 .chain(),
59 )
60 .run();
61}
62
63const CELL_WIDTH: f32 = 70.;
64const INVENTORY_BACKGROUND_COLOR: Color = Color::hsl(0., 0., 0.78);
65const CELL_BACKGROUND_COLOR: Color = Color::hsl(0., 0., 0.55);
66const CELL_HIGHLIGHT_COLOR: Color = Color::hsl(0., 0., 0.83);
67const CELL_GAP: f32 = 5.;
68const INVENTORY_SIZE: f32 = 700.;
69const CELL_BORDER_WIDTH: f32 = 2.;
70const CELL_DARK_BORDER_COLOR: Color = Color::hsl(0., 0., 0.19);
71static ITEM_NAMES: LazyLock<HashMap<usize, &'static str>> = LazyLock::new(|| {
74 HashMap::from([
75 (0, "copper dagger"),
76 (1, "copper sword"),
77 (2, "shortbow"),
78 (3, "copper spear"),
79 (4, "copper axe"),
80 (5, "copper mace"),
81 (6, "copper shovel"),
82 (7, "copper pickaxe"),
83 (8, "copper hammer"),
84 (9, "copper scythe"),
85 (10, "steel dagger"),
86 (11, "steel sword"),
87 (12, "longbow"),
88 (13, "steel spear"),
89 (14, "steel axe"),
90 (15, "steel mace"),
91 (16, "steel shovel"),
92 (17, "steel pickaxe"),
93 (18, "steel hammer"),
94 (19, "steel scythe"),
95 (20, "golden dagger"),
96 (21, "golden sword"),
97 (22, "golden longbow"),
98 (23, "golden spear"),
99 (24, "golden axe"),
100 (25, "golden mace"),
101 (26, "golden shovel"),
102 (27, "golden pickaxe"),
103 (28, "golden hammer"),
104 (29, "golden scythe"),
105 (30, "copper arrow"),
106 (31, "steel arrow"),
107 (32, "golden arrow"),
108 (33, "poison arrow"),
109 (34, "fire arrow"),
110 (35, "ice arrow"),
111 (36, "electric arrow"),
112 (37, "charm arrow"),
113 (38, "leather quiver"),
114 (39, "elven quiver"),
115 (40, "apprentice robes"),
116 (41, "common shirt"),
117 (42, "copper armor"),
118 (43, "turtle buckler"),
119 (44, "wooden shield"),
120 (45, "plank shield"),
121 (46, "shoes"),
122 (47, "apprentice hat"),
123 (48, "cloth cap"),
124 (49, "copper helmet"),
125 (50, "mage robes"),
126 (51, "leather armor"),
127 (52, "steel armor"),
128 (53, "wooden buckler"),
129 (54, "reinforced wooden shield"),
130 (55, "steel shield"),
131 (56, "leather boots"),
132 (57, "mage hat"),
133 (58, "leather helmet"),
134 (59, "steel helmet"),
135 (60, "archmage robes"),
136 (61, "elven armor"),
137 (62, "golden armor"),
138 (63, "steel buckler"),
139 (64, "steel round shield"),
140 (65, "golden shield"),
141 (66, "elven boots"),
142 (67, "archmage hat"),
143 (68, "elven helmet"),
144 (69, "golden helmet"),
145 (70, "wooden staff"),
146 (71, "fire staff"),
147 (72, "lightning staff"),
148 (73, "ice staff"),
149 (74, "fire ring"),
150 (75, "lightning ring"),
151 (76, "ice ring"),
152 (77, "fire necklace"),
153 (78, "lightning necklace"),
154 (79, "ice necklace"),
155 (80, "minor healing potion"),
156 (81, "healing potion"),
157 (82, "greater healing potion"),
158 (83, "minor mana potion"),
159 (84, "mana potion"),
160 (85, "greater mana potion"),
161 (86, "yellow potion"),
162 (87, "green potion"),
163 (88, "purple potion"),
164 (89, "flying potion"),
165 (90, "gold coins (small)"),
166 (91, "gold coins (medium)"),
167 (92, "gold coins (big)"),
168 (93, "gold pouch"),
169 (94, "gold chest"),
170 (95, "ruby"),
171 (96, "topaz"),
172 (97, "emerald"),
173 (98, "sapphire"),
174 (99, "diamond"),
175 (100, "map"),
176 (101, "journal"),
177 (102, "satchel"),
178 (103, "backpack"),
179 (104, "pouch"),
180 (105, "chest (small)"),
181 (106, "chest (big)"),
182 (107, "bronze key"),
183 (108, "silver key"),
184 (109, "golden key"),
185 (110, "wood log"),
186 (111, "stone"),
187 (112, "meat"),
188 (113, "cheese"),
189 (114, "apple"),
190 (115, "poisoned apple"),
191 (116, "milk glass"),
192 (117, "egg (white)"),
193 (118, "egg (brown)"),
194 (119, "egg (golden)"),
195 (120, "carrot"),
196 (121, "berries"),
197 (122, "sunflower"),
198 (123, "flower (yellow)"),
199 (124, "flower (blue)"),
200 (125, "flower (red)"),
201 (126, "fishing rod"),
202 (127, "worm"),
203 (128, "fish_1"),
204 (129, "fish_2"),
205 ])
206});
207
208static ICON_TEXTURE_ATLAS: OnceLock<RpgIconSheet> = OnceLock::new();
210
211fn icon_sheet() -> &'static RpgIconSheet {
214 ICON_TEXTURE_ATLAS
215 .get()
216 .expect("expected ICON_TEXTURE_ATLAS to be initialized")
217}
218
219#[derive(AssetCollection, Resource, Clone, Debug)]
220struct RpgIconSheet {
221 #[asset(texture_atlas(tile_size_x = 48, tile_size_y = 48, columns = 10, rows = 27))]
222 layout: Handle<TextureAtlasLayout>,
223 #[asset(path = "rpg_icon_sheet.png")]
224 image: Handle<Image>,
225}
226
227fn icon(
228 index_signal: impl Signal<Item = usize> + Send + 'static,
229 count_signal: impl Signal<Item = usize> + Send + 'static,
230) -> Stack<Node> {
231 Stack::new()
232 .layer(
233 El::<ImageNode>::new()
234 .image_node(ImageNode {
235 image: icon_sheet().image.clone(),
236 texture_atlas: Some(TextureAtlas::from(icon_sheet().layout.clone())),
237 ..default()
238 })
239 .on_signal_with_image_node(index_signal, |mut image_node: Mut<ImageNode>, index| {
240 if let Some(ref mut texture_atlas) = image_node.texture_atlas {
241 texture_atlas.index = index;
242 }
243 }),
244 )
245 .layer(
246 El::<Text>::new()
247 .with_node(|mut node| node.top = Val::Px(6.))
248 .align(Align::new().bottom().right())
249 .text_font(TextFont::from_font_size(33.33))
250 .text_signal(count_signal.map(|count| Text(count.to_string()))),
251 )
252}
253
254#[derive(Clone, Component)]
255struct CellData {
256 index: Mutable<usize>,
257 count: Mutable<usize>,
258}
259
260#[derive(Component)]
261struct BlockClick;
262
263fn cell(cell_data_option: Mutable<Option<CellData>>, insertable: bool) -> impl Element {
264 let hovered = Mutable::new(false);
265 let original_position: Mutable<Option<Vec2>> = Mutable::new(None);
266 let down = Mutable::new(false);
267 El::<Node>::new()
268 .update_raw_el(clone!((cell_data_option, down) move |mut raw_el| {
269 if insertable {
270 raw_el = raw_el
271 .insert(Pickable::default())
272 .on_event_disableable::<Pointer<Click>, BlockClick>(
273 clone!((cell_data_option => self_cell_data_option) move |click| {
274 let mut consume = false;
275 if let Some(dragging_cell_data_option) = &*DRAGGING_OPTION.lock_ref() {
276 if self_cell_data_option.lock_ref().is_none() && let Some(dragging_cell_data) = &*dragging_cell_data_option.lock_ref() {
277 self_cell_data_option.set(Some(CellData {
278 index: Mutable::new(dragging_cell_data.index.get()),
279 count: Mutable::new(0),
280 }));
281 }
282 if let Some((dragging_cell_data, self_cell_data)) = dragging_cell_data_option.lock_ref().as_ref().zip(self_cell_data_option.lock_ref().as_ref()) {
283 if self_cell_data.index.get() == dragging_cell_data.index.get() {
284 let to_add = {
285 if matches!(click.button, PointerButton::Secondary) {
286 *dragging_cell_data.count.lock_mut() -= 1;
287 if dragging_cell_data.count.get() == 0 {
288 consume = true;
289 }
290 1
291 } else {
292 let count = dragging_cell_data.count.take();
293 consume = true;
294 count
295 }
296 };
297 self_cell_data.count.update(|count| count + to_add);
298 } else {
299 self_cell_data.index.swap(&dragging_cell_data.index);
300 self_cell_data.count.swap(&dragging_cell_data.count);
301 }
302 }
303 }
304 if consume && let Some(cell_data_option) = DRAGGING_OPTION.take() {
305 cell_data_option.take();
306 }
307 }),
308 );
309 }
310 raw_el
311 .on_event_with_system::<Pointer<Pressed>, _>(|In((entity, _)), mut commands: Commands| { commands.entity(entity).insert(BlockClick); })
314 .on_event_with_system::<Pointer<Released>, _>(|In((entity, _)), mut commands: Commands| { commands.entity(entity).remove::<BlockClick>(); })
315 .on_event_disableable_signal::<Pointer<Pressed>>(
316 clone!((cell_data_option, down) move |pointer_down| {
317 let to_drag_option = {
318 if pointer_down.button == PointerButton::Secondary {
319 if let Some(cell_data) = &*cell_data_option.lock_ref() {
320 let to_take = (cell_data.count.get() / 2).max(1);
321 cell_data.count.update(|count| count - to_take);
322 Some(CellData {
323 index: Mutable::new(cell_data.index.get()),
324 count: Mutable::new(to_take),
325 })
326 } else {
327 None
328 }
329 } else {
330 cell_data_option.take()
331 }
332 };
333 if cell_data_option.lock_ref().as_ref().map(|cell_data| cell_data.count.get() == 0).unwrap_or(false) {
334 cell_data_option.take();
335 }
336 DRAGGING_OPTION.set(Some(Mutable::new(to_drag_option)));
337 POINTER_POSITION.set(pointer_down.pointer_location.position.into());
338 down.set_neq(true);
339 }),
340 signal::or(is_dragging(), cell_data_option.signal_ref(Option::is_none)).dedupe()
341 )
342 }))
343 .cursor_signal(
345 map_ref! {
346 let &populated = cell_data_option.signal_ref(Option::is_some),
347 let &is_dragging = is_dragging() => {
348 if is_dragging {
349 CursorIcon::System(SystemCursorIcon::Grabbing)
350 } else if populated {
351 CursorIcon::System(SystemCursorIcon::Grab)
352 } else {
353 CursorIcon::default()
354 }
355 }
356 }
357 )
358 .hovered_sync(hovered.clone())
361 .with_node(|mut node| {
362 node.width = Val::Px(CELL_WIDTH);
363 node.height = Val::Px(CELL_WIDTH);
364 node.border = UiRect::all(Val::Px(CELL_BORDER_WIDTH));
365 })
366 .background_color_signal(
367 hovered.signal()
368 .map_bool(|| CELL_HIGHLIGHT_COLOR, || CELL_BACKGROUND_COLOR).map(BackgroundColor),
369 )
370 .border_color(BorderColor(CELL_DARK_BORDER_COLOR))
371 .child_signal(
372 cell_data_option
373 .signal_cloned()
374 .map_some(move |cell_data| {
375 Stack::<Node>::new()
376 .layer(icon(cell_data.index.signal(), cell_data.count.signal()))
377 .layer_signal(
378 signal::and(hovered.signal(), signal::not(is_dragging())).dedupe()
379 .map_true(clone!((original_position) move || {
380 El::<Node>::new()
381 .with_node(|mut node| {
384 node.height = Val::Px(CELL_WIDTH);
385 node.position_type = PositionType::Absolute;
386 node.border = UiRect::all(Val::Px(CELL_BORDER_WIDTH));
387 node.padding = UiRect::horizontal(Val::Px(10.));
388 })
389 .visibility(Visibility::Hidden)
390 .update_raw_el(clone!((original_position) move |raw_el| {
391 raw_el
392 .on_signal_with_entity(POINTER_POSITION.signal(), move |mut entity, (mut left, mut top)| {
393 if let Some(transform) = entity.get::<GlobalTransform>() {
394 if original_position.get().is_none() {
396 original_position.set(Some(transform.compute_transform().translation.xy()));
397 }
398 let original_position = original_position.get().unwrap();
399 left -= original_position.x - CELL_WIDTH / 2.;
400 top -= original_position.y + CELL_WIDTH / 2.;
401 entity.insert(Visibility::Visible);
403 }
404 if let Some(mut node) = entity.get_mut::<Node>() {
405 node.left = Val::Px(left);
406 node.top = Val::Px(top);
407 }
408 })
409 }))
410 .global_z_index(GlobalZIndex(1))
411 .background_color(BackgroundColor(CELL_BACKGROUND_COLOR))
412 .border_color(BorderColor(CELL_DARK_BORDER_COLOR))
413 .child(
414 El::<Text>::new()
415 .align(Align::center())
416 .text_font(TextFont::from_font_size(41.67))
417 .text_layout(TextLayout::new_with_no_wrap())
418 .text_signal(
419 cell_data.index.signal()
420 .map(|i| Text(ITEM_NAMES.get(&i).unwrap().to_string()))
421 )
422 )
423 }))
424 )
425 })
426 )
427}
428
429fn random_cell_data(rng: &mut impl Rng) -> CellData {
430 CellData {
431 index: Mutable::new(rng.random_range(0..ITEM_NAMES.len())),
432 count: Mutable::new(rng.random_range(1..=64)),
433 }
434}
435
436fn bern_cell_data_option(bern: f64) -> Mutable<Option<CellData>> {
437 Mutable::new('block: {
438 let distribution = Bernoulli::new(bern).unwrap();
439 let mut rng = rand::rng();
440 if distribution.sample(&mut rng) {
441 break 'block Some(random_cell_data(&mut rng));
442 }
443 None
444 })
445}
446
447fn bern_cell(bern: f64, insertable: bool) -> impl Element {
448 cell(bern_cell_data_option(bern), insertable)
449}
450
451fn grid<I: IntoIterator<Item = Mutable<Option<CellData>>>>(cell_data_options: I) -> impl Element
452where
453 <I as IntoIterator>::IntoIter: std::marker::Send + 'static,
454{
455 Grid::<Node>::new()
456 .with_node(|mut node| {
457 node.width = Val::Percent(100.);
458 node.height = Val::Percent(100.);
459 node.column_gap = Val::Px(CELL_GAP);
460 node.row_gap = Val::Px(CELL_GAP);
461 })
462 .row_wrap_cell_width(CELL_WIDTH)
463 .cells(
464 cell_data_options
465 .into_iter()
466 .map(move |cell_data_option| cell(cell_data_option, true)),
467 )
468}
469
470fn set_icon_texture_atlas(rpg_icon_sheet: Res<RpgIconSheet>) {
471 ICON_TEXTURE_ATLAS
472 .set(rpg_icon_sheet.clone())
473 .expect("failed to initialize ICON_TEXTURE_ATLAS");
474}
475
476fn dot() -> impl Element {
519 El::<Node>::new()
520 .with_node(|mut node| {
521 node.width = Val::Px(CELL_BORDER_WIDTH * 2.);
522 node.height = Val::Px(CELL_BORDER_WIDTH * 2.);
523 })
524 .background_color(BackgroundColor(CELL_BACKGROUND_COLOR))
525}
526
527fn dot_row(n: usize) -> impl Element {
528 Row::<Node>::new().items((0..n).map(|_| dot()))
529}
530
531fn arrow() -> impl Element {
532 Column::<Node>::new()
533 .align_content(Align::center())
534 .items((0..=6).map(|i| dot_row(2 * i + 1)))
535 .items((0..6).map(|_| dot_row(3)))
536}
537
538fn side_column() -> impl Element {
539 Column::<Node>::new()
540 .with_node(|mut node| node.row_gap = Val::Px(CELL_GAP))
541 .items((0..4).map(|_| bern_cell(0.5, true)))
542}
543
544fn inventory() -> impl Element {
545 El::<Node>::new()
546 .align(Align::center())
547 .with_node(|mut node| {
548 node.height = Val::Px(INVENTORY_SIZE);
549 node.width = Val::Px(INVENTORY_SIZE);
550 })
551 .child(
552 Column::<Node>::new()
553 .with_node(|mut node| {
554 node.height = Val::Percent(100.);
555 node.width = Val::Percent(100.);
556 node.row_gap = Val::Px(CELL_GAP * 4.);
557 })
558 .background_color(BackgroundColor(INVENTORY_BACKGROUND_COLOR))
559 .align_content(Align::center())
560 .item(
561 Row::<Node>::new()
562 .with_node(|mut node| {
563 node.width = Val::Percent(100.);
564 node.column_gap = Val::Px(CELL_GAP);
565 })
566 .item(
567 Row::<Node>::new()
568 .align_content(Align::center())
569 .with_node(|mut node| {
570 node.width = Val::Percent(60.);
571 node.column_gap = Val::Px(CELL_GAP);
572 node.padding = UiRect::horizontal(Val::Px(CELL_GAP * 3.));
573 })
574 .item(side_column())
575 .item(
576 El::<Node>::new()
577 .with_node(|mut node| {
578 node.height = Val::Px(CELL_WIDTH * 4. + CELL_GAP * 3.);
579 node.width = Val::Percent(100.);
580 })
581 .background_color(BackgroundColor(Color::BLACK)),
582 )
583 .item(side_column())
584 )
585 .item(
586 El::<Node>::new()
587 .with_node(|mut node| {
588 node.width = Val::Percent(40.);
589 node.height = Val::Percent(100.);
590 })
591 .align_content(Align::center())
592 .child({
593 let inputs = MutableVec::new_with_values(
594 (0..4).map(|_| bern_cell_data_option(0.2)).collect(),
595 );
596 let output: Mutable<Option<CellData>> = default();
597 let outputter = spawn(clone!((inputs, output) async move {
598 inputs.signal_vec_cloned()
600 .map_signal(|input|
601 input.signal_cloned()
602 .map_some(|cell_data| map_ref! {
605 let _ = cell_data.index.signal_ref(|_|()),
606 let _ = cell_data.count.signal_ref(|_|()) => ()
607 })
608 .switch(signal::option)
609 )
610 .to_signal_map(|filleds| filleds.iter().all(Option::is_some))
611 .for_each_sync(move |all_filled| {
612 output.set(all_filled.then(|| random_cell_data(&mut rand::rng())));
613 })
614 .await;
615 }));
616 Column::<Node>::new()
617 .update_raw_el(|raw_el| raw_el.hold_tasks([outputter]))
618 .with_node(|mut node| {
619 node.row_gap = Val::Px(CELL_GAP * 2.);
620 })
621 .item(
622 El::<Node>::new()
625 .child(cell(output.clone(), false).align(Align::center()))
626 .update_raw_el(clone!((inputs) move |raw_el| {
627 raw_el
628 .on_event_disableable_signal::<Pointer<Pressed>>(
629 clone!((inputs) move |_| {
630 for input in inputs.lock_ref().iter() {
631 input.take();
632 }
633 }),
634 signal::not(signal::and(DRAGGING_OPTION.signal_ref(Option::is_none), output.signal_ref(Option::is_some))).dedupe()
635 )
636 }))
637 )
638 .item(arrow())
639 .item({
640 let cell_data_options = inputs.lock_ref().iter().cloned().collect::<Vec<_>>();
641 El::<Node>::new()
642 .with_node(|mut node| node.width = Val::Px(CELL_WIDTH * 2. + CELL_GAP))
643 .child(grid(cell_data_options).align_content(Align::new().center_x()))
644 })
645 }),
646 ),
647 )
648 .item(
649 El::<Node>::new()
650 .with_node(|mut node| node.width = Val::Percent(100.))
651 .child(
652 grid((0..27).map(|_| bern_cell_data_option(0.5)))
653 .align_content(Align::new().center_x()),
654 ),
655 )
656 .item(
657 Row::<Node>::new()
658 .with_node(|mut node| {
659 node.column_gap = Val::Px(CELL_GAP);
660 })
661 .items((0..9).map(|_| bern_cell(0.5, true))),
662 ),
663 )
664}
665
666static DRAGGING_OPTION: LazyLock<Mutable<Option<Mutable<Option<CellData>>>>> = LazyLock::new(default);
667
668static POINTER_POSITION: LazyLock<Mutable<(f32, f32)>> = LazyLock::new(default);
669
670fn is_dragging() -> impl Signal<Item = bool> {
671 DRAGGING_OPTION.signal_ref(Option::is_some)
672}
673
674fn ui_root() -> impl Element {
675 Stack::<Node>::new()
676 .cursor_disableable_signal(CursorIcon::default(), is_dragging())
677 .with_node(|mut node| {
678 node.width = Val::Percent(100.);
679 node.height = Val::Percent(100.);
680 })
681 .update_raw_el(|raw_el| {
682 raw_el
683 .on_event_with_system::<Pointer<Move>, _>(|In((_, move_)): In<(_, Pointer<Move>)>| {
684 POINTER_POSITION.set(move_.pointer_location.position.into());
685 })
686 .component_signal::<Pickable, _>(is_dragging().map_true(default))
687 })
688 .align_content(Align::center())
689 .layer(inventory())
690 .layer_signal(
691 DRAGGING_OPTION
692 .signal_cloned()
693 .map_some(|cell_data_option| cell_data_option.signal_cloned())
694 .switch(signal::option)
695 .map(Option::flatten)
696 .map_some(move |cell_data| {
697 icon(cell_data.index.signal(), cell_data.count.signal())
698 .update_raw_el(|raw_el| {
699 raw_el.defer_update(DeferredUpdaterAppendDirection::Front, |raw_el| {
700 raw_el.insert(Pickable {
701 should_block_lower: false,
703 is_hoverable: true,
704 })
705 })
706 })
707 .cursor(CursorIcon::System(SystemCursorIcon::Grabbing))
708 .with_node(|mut node| {
709 node.width = Val::Px(CELL_WIDTH);
710 node.height = Val::Px(CELL_WIDTH);
711 node.position_type = PositionType::Absolute;
712 let pointer_position = POINTER_POSITION.get();
713 set_dragging_position(node, pointer_position);
723 })
724 .global_z_index(GlobalZIndex(1))
725 .on_signal_with_node(POINTER_POSITION.signal(), set_dragging_position)
726 }),
727 )
728}
729
730fn set_dragging_position(mut node: Mut<Node>, pointer_position: (f32, f32)) {
731 node.left = Val::Px(pointer_position.0 - CELL_WIDTH / 2.);
732 node.top = Val::Px(pointer_position.1 - CELL_WIDTH / 2.);
733}
734
735#[derive(Clone, Eq, PartialEq, Debug, Hash, Default, States)]
736enum AssetState {
737 #[default]
738 Loading,
739 Loaded,
740}