1#![allow(clippy::uninlined_format_args)]
2use std::path::Path;
7use std::collections::HashMap;
8use thyme::{Context, ContextBuilder, Frame, bench, ShowElement, Renderer};
9
10pub fn register_assets(context_builder: &mut ContextBuilder) {
11 context_builder.register_theme_from_files(
13 &[
14 Path::new("examples/data/themes/base.yml"),
15 Path::new("examples/data/themes/demo.yml"),
16 Path::new("examples/data/themes/pixel.yml"),
18 ],
19 ).unwrap();
20 context_builder.register_texture_from_file("pixel", Path::new("examples/data/images/pixel.png"));
21 context_builder.register_texture_from_file("fantasy", Path::new("examples/data/images/fantasy.png"));
22 context_builder.register_texture_from_file("transparent", Path::new("examples/data/images/transparent.png"));
23 context_builder.register_texture_from_file("golden", Path::new("examples/data/images/golden.png"));
24 context_builder.register_font_from_file("Roboto-Medium", Path::new("examples/data/fonts/Roboto-Medium.ttf"));
25 context_builder.register_font_from_file("Roboto-Italic", Path::new("examples/data/fonts/Roboto-Italic.ttf"));
26 context_builder.register_font_from_file("Roboto-Bold", Path::new("examples/data/fonts/Roboto-Bold.ttf"));
27 context_builder.register_font_from_file("Roboto-BoldItalic", Path::new("examples/data/fonts/Roboto-BoldItalic.ttf"));
28}
29
30#[derive(Debug, Copy, Clone, Default)]
31enum ThemeChoice {
32 #[default]
33 Pixels,
34 Fantasy,
35 Transparent,
36 Golden,
37 NoImage,
38}
39
40const THEME_CHOICES: [ThemeChoice; 5] = [
41 ThemeChoice::Pixels,
42 ThemeChoice::Fantasy,
43 ThemeChoice::Transparent,
44 ThemeChoice::Golden,
45 ThemeChoice::NoImage
46];
47
48impl ThemeChoice {
49 fn path(self) -> &'static str {
50 match self {
51 ThemeChoice::Fantasy => "examples/data/themes/fantasy.yml",
52 ThemeChoice::Pixels => "examples/data/themes/pixel.yml",
53 ThemeChoice::Transparent => "examples/data/themes/transparent.yml",
54 ThemeChoice::Golden => "examples/data/themes/golden.yml",
55 ThemeChoice::NoImage => "examples/data/themes/no_image.yml",
56 }
57 }
58}
59
60impl std::fmt::Display for ThemeChoice {
61 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62 write!(f, "{:?}", self)
63 }
64}
65
66#[derive(Default)]
67pub struct Party {
68 members: Vec<Character>,
69 editing_index: Option<usize>,
70
71 live_reload_disabled: bool,
72 reload_assets: bool,
73 old_theme_choice: Option<ThemeChoice>,
74 theme_choice: ThemeChoice,
75}
76
77impl Party {
78 pub fn theme_has_mouse_cursor(&self) -> bool {
79 match self.theme_choice {
80 ThemeChoice::Pixels | ThemeChoice::Fantasy | ThemeChoice::Transparent | ThemeChoice::Golden => true,
81 ThemeChoice::NoImage => false,
82 }
83 }
84
85 pub fn check_context_changes<R: Renderer>(&mut self, context: &mut Context, renderer: &mut R) {
86 if let Some(old_choice) = self.old_theme_choice.take() {
87 context.remove_theme_file(old_choice.path());
88 context.add_theme_file(self.theme_choice.path());
89 }
90
91 if self.reload_assets {
92 if let Err(e) = context.rebuild_all(renderer) {
93 log::error!("Unable to rebuild theme: {}", e);
94 }
95 self.reload_assets = false;
96 } else if !self.live_reload_disabled {
97 if let Err(e) = context.check_live_reload(renderer) {
98 log::error!("Unable to live reload theme: {}", e);
99 }
100 }
101 }
102}
103
104const MIN_AGE: f32 = 18.0;
105const DEFAULT_AGE: f32 = 25.0;
106const MAX_AGE: f32 = 50.0;
107const INITIAL_GP: u32 = 100;
108const MIN_STAT: u32 = 3;
109const MAX_STAT: u32 = 18;
110const STAT_POINTS: u32 = 75;
111
112struct Character {
113 name: String,
114 age: f32,
115 stats: HashMap<Stat, u32>,
116
117 race: Race,
118 gp: u32,
119 items: Vec<Item>,
120}
121
122impl Character {
123 fn generate(index: usize) -> Character {
124 Character {
125 name: format!("Charname {}", index),
126 age: DEFAULT_AGE,
127 stats: HashMap::default(),
128 gp: INITIAL_GP,
129 items: Vec::default(),
130 race: Race::default(),
131 }
132 }
133}
134
135#[derive(Debug, Copy, Clone, Default)]
136enum Race {
137 #[default]
138 Human,
139 Elf,
140 Dwarf,
141 Halfling,
142}
143
144impl Race {
145 fn all() -> &'static [Race] {
146 use Race::*;
147 &[Human, Elf, Dwarf, Halfling]
148 }
149}
150
151impl std::fmt::Display for Race {
152 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
153 write!(f, "{:?}", self)
154 }
155}
156
157#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
158enum Stat {
159 Strength,
160 Dexterity,
161 Constitution,
162 Intelligence,
163 Wisdom,
164 Charisma,
165}
166
167impl Stat {
168 fn iter() -> impl Iterator<Item=Stat> + 'static {
169 use Stat::*;
170 [Strength, Dexterity, Constitution, Intelligence, Wisdom, Charisma].iter().copied()
171 }
172}
173
174#[derive(Clone)]
175struct Item {
176 name: &'static str,
177 price: u32,
178}
179
180const ITEMS: [Item; 3] = [
181 Item { name: "Sword", price: 50 },
182 Item { name: "Shield", price: 20 },
183 Item { name: "Torch", price: 2 }
184];
185
186pub fn build_ui(ui: &mut Frame, party: &mut Party) {
190 match party.theme_choice {
191 ThemeChoice::Pixels | ThemeChoice::Fantasy | ThemeChoice::Transparent | ThemeChoice::Golden => {
192 ui.set_mouse_cursor("gui/cursor", thyme::Align::TopLeft);
194 },
195 ThemeChoice::NoImage => {
196 }
198 }
199
200 ui.label("bench", format!(
201 "{}\n{}\n{}",
202 bench::short_report("thyme"),
203 bench::short_report("frame"),
204 bench::short_report("draw"),
205 ));
206
207 ui.start("theme_panel").children(|ui| {
208 if ui.start("live_reload").active(!party.live_reload_disabled).finish().clicked {
209 party.live_reload_disabled = !party.live_reload_disabled;
210 }
211
212 if let Some(choice) = ui.combo_box("theme_choice", "theme_choice", &party.theme_choice, &THEME_CHOICES) {
213 party.old_theme_choice = Some(party.theme_choice);
214 party.theme_choice = *choice;
215 party.reload_assets = true;
216 }
217 });
218
219 ui.start("party_window")
220 .window("party_window")
221 .with_close_button(false)
222 .moveable(false)
223 .resizable(false)
224 .children(|ui| {
225 ui.scrollpane("members_panel", "party_content", |ui| {
226 party_members_panel(ui, party);
227 });
228 });
229
230 if let Some(index) = party.editing_index {
231 let character = &mut party.members[index];
232
233 ui.window("character_window", |ui| {
234 ui.scrollpane("pane", "character_content", |ui| {
235 ui.start("name_panel")
236 .children(|ui| {
237 if let Some(new_name) = ui.input_field("name_input", "name_input", None) {
238 character.name = new_name;
239 }
240 });
241
242 ui.gap(10.0);
243 ui.label("age_label", format!("Age: {}", character.age.round() as u32));
244 if let Some(age) = ui.horizontal_slider("age_slider", MIN_AGE, MAX_AGE, character.age) {
245 character.age = age;
246 }
247
248 for stat in Stat::iter() {
249 let value = format!("{}", character.stats.get(&stat).unwrap_or(&10));
250 let key = format!("{:?}", stat);
251 ui.set_variable(key, value);
252 }
253
254 ui.scrollpane("description_panel", "description_pane", |ui| {
255 ui.text_area("description_box");
256 });
257
258 ui.gap(10.0);
259
260 if let Some(race) = ui.combo_box("race_selector", "race_selector", &character.race, Race::all()) {
261 character.race = *race;
262 }
263
264 ui.gap(10.0);
265
266 ui.tree("stats_panel", "stats_panel", true,
267 |ui| {
268 ui.child("title");
269 },|ui| {
270 stats_panel(ui, character);
271 });
272
273 ui.gap(10.0);
274
275 ui.tree("inventory_panel", "inventory_panel", true,
276 |ui| {
277 ui.child("title");
278 }, |ui| {
279 inventory_panel(ui, character);
280 });
281 });
282 });
283
284 ui.window("item_picker", |ui| {
285 let display_size = ui.display_size();
286
287 ui.start("greyed_out")
288 .unclip()
289 .unparent()
290 .size(display_size.x, display_size.y)
291 .screen_pos(0.0, 0.0).finish();
292
293 item_picker(ui, character);
294 });
295 }
296}
297
298fn party_members_panel(ui: &mut Frame, party: &mut Party) {
299 for (index, member) in party.members.iter_mut().enumerate() {
300 let clicked = ui.start("filled_slot_button")
301 .text(&member.name)
302 .active(Some(index) == party.editing_index)
303 .finish().clicked;
304
305 if clicked {
306 set_active_character(ui, member);
307 party.editing_index = Some(index);
308 }
309 }
310
311 if ui.start("add_character_button").finish().clicked {
312 let new_member = Character::generate(party.members.len());
313 set_active_character(ui, &new_member);
314 party.members.push(new_member);
315 party.editing_index = Some(party.members.len() - 1);
316 }
317}
318
319fn set_active_character(ui: &mut Frame, character: &Character) {
320 ui.open("character_window");
321 ui.modify("name_input", |state| {
322 state.text = Some(character.name.clone());
323 });
324 ui.close("item_picker");
325}
326
327fn stats_panel(ui: &mut Frame, character: &mut Character) {
328 let points_used: u32 = character.stats.values().sum();
329 let points_available: u32 = STAT_POINTS - points_used;
330 let frac = ((ui.cur_time_millis() - ui.base_time_millis("stat_roll")) as f32 / 1000.0).min(1.0);
331
332 let roll = ui.start("roll_button")
333 .enabled(frac > 0.99)
334 .children(|ui| {
335 ui.progress_bar("progress_bar", frac);
336 });
337
338 if roll.clicked {
339 ui.set_base_time_now("stat_roll");
340 }
341
342 for stat in Stat::iter() {
343 let value = character.stats.entry(stat).or_insert(10);
344
345 ui.tree(
346 "stat_panel",
347 &format!("stat_panel_{:?}", stat),
348 false,
349 |ui| {
350 ui.label("label", format!("{:?}", stat));
351
352 match ui.spinner("spinner", *value, MIN_STAT, if points_available == 0 { *value } else { MAX_STAT }) {
353 1 => *value += 1,
354 -1 => *value -= 1,
355 _ => (),
356 }
357 }, |ui| {
358 ui.child("description");
359 });
360 }
361
362 ui.label("points_available", format!("Points Remaining: {}", points_available));
363}
364
365fn item_picker(ui: &mut Frame, character: &mut Character) {
366 for item in ITEMS.iter() {
367 let clicked = ui.start("item_button")
368 .enabled(character.gp >= item.price)
369 .children(|ui| {
370 ui.label("name", item.name);
371 ui.child("icon");
373 ui.label("price", format!("{} Gold", item.price));
374 }).clicked;
375
376 if clicked {
377 character.gp -= item.price;
378 character.items.push(item.clone());
379 ui.close("item_picker");
380 }
381 }
382}
383
384fn inventory_panel(ui: &mut Frame, character: &mut Character) {
385 ui.start("top_panel")
386 .children(|ui| {
387 if ui.child("buy").clicked {
388 ui.open_modal("item_picker");
389 }
390
391 ui.label("gold", format!("{} Gold", character.gp));
392 });
393
394 ui.start("items_panel")
395 .scrollpane("items_content")
396 .show_vertical_scrollbar(ShowElement::Always)
397 .children(|ui| {
398 items_panel(ui, character);
399 });
400}
401
402fn items_panel(ui: &mut Frame, character: &mut Character) {
403 let mut sell = None;
404 for (index, item) in character.items.iter().enumerate() {
405 let result = ui.button("item_button", item.name);
406 if result.clicked {
407 sell = Some(index);
408 }
409
410 if result.hovered {
411 ui.tooltip_label("tooltip", "Remove Item");
413 }
414 }
415
416 if let Some(index) = sell {
417 let item = character.items.remove(index);
418 character.gp += item.price;
419 }
420}