diff --git a/Cargo.lock b/Cargo.lock index d9294e6658dbf52bbc981a9b09841fa131e5e185..69ee6f4e22a2e5a2e60f737cb3da05d87144a3a3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2081,7 +2081,7 @@ dependencies = [ [[package]] name = "kayak_font" version = "0.1.0" -source = "git+https://github.com/StarArawn/kayak_ui.git?rev=a85cab76839f22e5f5a1002b864bad3923e0f375#a85cab76839f22e5f5a1002b864bad3923e0f375" +source = "git+https://github.com/StarArawn/kayak_ui.git?rev=c8437b382b0cd1ce950c15cacd170a1c7c7fe5dc#c8437b382b0cd1ce950c15cacd170a1c7c7fe5dc" dependencies = [ "anyhow", "bevy", @@ -2098,7 +2098,7 @@ dependencies = [ [[package]] name = "kayak_ui" version = "0.1.0" -source = "git+https://github.com/StarArawn/kayak_ui.git?rev=a85cab76839f22e5f5a1002b864bad3923e0f375#a85cab76839f22e5f5a1002b864bad3923e0f375" +source = "git+https://github.com/StarArawn/kayak_ui.git?rev=c8437b382b0cd1ce950c15cacd170a1c7c7fe5dc#c8437b382b0cd1ce950c15cacd170a1c7c7fe5dc" dependencies = [ "bevy", "bitflags", @@ -2116,7 +2116,7 @@ dependencies = [ [[package]] name = "kayak_ui_macros" version = "0.1.0" -source = "git+https://github.com/StarArawn/kayak_ui.git?rev=a85cab76839f22e5f5a1002b864bad3923e0f375#a85cab76839f22e5f5a1002b864bad3923e0f375" +source = "git+https://github.com/StarArawn/kayak_ui.git?rev=c8437b382b0cd1ce950c15cacd170a1c7c7fe5dc#c8437b382b0cd1ce950c15cacd170a1c7c7fe5dc" dependencies = [ "find-crate", "proc-macro-crate", diff --git a/Cargo.toml b/Cargo.toml index 5c5bbe743e9d41985d6440325d04de0118476a6a..241f06520e667393bc53de889689aec2daeefc86 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,8 +19,8 @@ iyes_loopless = "0.9.1" micro_musicbox = { version = "0.5.0", features = ["mp3"] } micro_banimate = { version = "0.2.1", features = ["ecs_tilemap"] } -kayak_ui = { rev = "a85cab76839f22e5f5a1002b864bad3923e0f375", git = "https://github.com/StarArawn/kayak_ui" } -kayak_font = { rev = "a85cab76839f22e5f5a1002b864bad3923e0f375", git = "https://github.com/StarArawn/kayak_ui.git" } +kayak_ui = { rev = "c8437b382b0cd1ce950c15cacd170a1c7c7fe5dc", git = "https://github.com/StarArawn/kayak_ui" } +kayak_font = { rev = "c8437b382b0cd1ce950c15cacd170a1c7c7fe5dc", git = "https://github.com/StarArawn/kayak_ui.git" } [workspace.dependencies.bevy] version = "0.9.0" diff --git a/assets/resources.apack b/assets/resources.apack index 3a58606d63933002d84ec36009055b0bb10e0347..b86836b74e6ca4c0ab25130fde60a57cbfb88de4 100644 --- a/assets/resources.apack +++ b/assets/resources.apack @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3d2dab5fb4b4a900bcb5a9eee1e01cf5a143441de1a424c9f4b120377de915f1 -size 1581436 +oid sha256:0104e32f553d939d5f9d7c0346282da2705062194ccca0540cdeab540fdadf30 +size 1930616 diff --git a/assets/trade_manifests/monastery.manifest.toml b/assets/trade_manifests/monastery.manifest.toml new file mode 100644 index 0000000000000000000000000000000000000000..e909957b5cd813175998e5fd07eb7d661cf5a3d3 --- /dev/null +++ b/assets/trade_manifests/monastery.manifest.toml @@ -0,0 +1,157 @@ +["Spare Wheel"] +tick_decay = 0 +natural_limit = 0 +cost_multipliers = [ + [99999, 0.0], +] +# When there are more wheels than the natural limit (e.g. player sells a lot of wheels) +# This strategy will be applied to the inventory. "Halt" means nothing will be done +oversupply_trend = "halt" + +[Armour] +# It takes 10 world ticks to consume one unit of armour. +# 10 ticks = 50km +tick_decay = 0.1 +natural_limit = 10 +cost_multipliers = [ + [5, 1.4], + [9, 1.25], + [10, 1], + [15, 0.8], + [30, 0.7], + [99999, 0.6] +] +oversupply_trend = "halt" + +[Weapons] +tick_decay = 0.1 +natural_limit = 10 +cost_multipliers = [ + [5, 1.4], + [9, 1.25], + [10, 1], + [15, 0.8], + [30, 0.7], + [99999, 0.6] +] + +["Luxury Goods"] +tick_decay = 0.05 +natural_limit = 5 +cost_multipliers = [ + [5, 1.4], + [9, 1.25], + [10, 1], + [15, 0.8], + [30, 0.7], + [99999, 0.6] +] +oversupply_trend = "halt" + +[Ale] +tick_decay = 0.25 +natural_limit = 0 +cost_multipliers = [ + [5, 1.4], + [9, 1.25], + [10, 1], + [15, 0.8], + [30, 0.7], + [99999, 0.6] +] + +[Milk] +tick_decay = 0.25 +natural_limit = 0 +cost_multipliers = [ + [5, 1.4], + [9, 1.25], + [10, 1], + [15, 0.8], + [30, 0.7], + [99999, 0.6] +] + +[Cheese] +tick_decay = 0.25 +natural_limit = 0 +cost_multipliers = [ + [5, 1.4], + [9, 1.25], + [10, 1], + [15, 0.8], + [30, 0.7], + [99999, 0.6] +] + +[Berries] +tick_decay = 0.25 +natural_limit = 0 +cost_multipliers = [ + [5, 1.4], + [9, 1.25], + [10, 1], + [15, 0.8], + [30, 0.7], + [99999, 0.6] +] + +[Corn] +tick_decay = 0.25 +natural_limit = 0 +cost_multipliers = [ + [5, 1.4], + [9, 1.25], + [10, 1], + [15, 0.8], + [30, 0.7], + [99999, 0.6] +] + +[Tomato] +tick_decay = 0.25 +natural_limit = 0 +cost_multipliers = [ + [5, 1.4], + [9, 1.25], + [10, 1], + [15, 0.8], + [30, 0.7], + [99999, 0.6] +] + +[Chili] +tick_decay = 0.25 +natural_limit = 0 +cost_multipliers = [ + [5, 1.4], + [9, 1.25], + [10, 1], + [15, 0.8], + [30, 0.7], + [99999, 0.6] +] + +[Grapes] +tick_decay = 0.25 +natural_limit = 0 +cost_multipliers = [ + [5, 1.4], + [9, 1.25], + [10, 1], + [15, 0.8], + [30, 0.7], + [99999, 0.6] +] + +[Wheat] +tick_decay = 0.25 +natural_limit = 0 +cost_multipliers = [ + [5, 1.4], + [9, 1.25], + [10, 1], + [15, 0.8], + [30, 0.7], + [99999, 0.6] +] diff --git a/game_core/src/assets/apack_handler.rs b/game_core/src/assets/apack_handler.rs index d9b7604c8ea288ccf53ba0c6bdf1f7c00ce119a8..d62d58562f2cb77d7aad5b1a43279b798d90fcfd 100644 --- a/game_core/src/assets/apack_handler.rs +++ b/game_core/src/assets/apack_handler.rs @@ -151,6 +151,9 @@ pub fn handle_apack_process_events(world: &mut World) { } }; + let mut success_count = 0; + log::info!("Processing apack file with {} paths", pack.len()); + if let Some(raw) = pack.get("./manifest.toml") { let manifest: APackManifest = toml::from_slice(raw.as_slice()).expect("Badly formatted apack manifest"); @@ -179,6 +182,7 @@ pub fn handle_apack_process_events(world: &mut World) { } else { log::warn!("Malformed image {}", &image.path); } + success_count += 1; } for font in &manifest.fonts { @@ -252,6 +256,8 @@ pub fn handle_apack_process_events(world: &mut World) { ) } } + + success_count += 1; } for spritesheet in &manifest.spritesheets { @@ -297,6 +303,8 @@ pub fn handle_apack_process_events(world: &mut World) { } else { log::warn!("Malformed spritesheet {}", &spritesheet.path); } + + success_count += 1; } for ldtk_entry in &manifest.ldtk { @@ -315,6 +323,8 @@ pub fn handle_apack_process_events(world: &mut World) { params.p1().ldtk.insert(ldtk_entry.name.clone(), handle); }, ); + + success_count += 1; } for sound_entry in &manifest.sounds { @@ -338,9 +348,12 @@ pub fn handle_apack_process_events(world: &mut World) { }, ); } + + success_count += 1; } - log::info!("LOADED AN APACK"); + log::info!("Successfully loaded {} resources from apack", success_count); + { let mut state: SystemState<ResMut<Assets<APack>>> = SystemState::new(world); let mut packs = state.get_mut(world); diff --git a/game_core/src/assets/startup.rs b/game_core/src/assets/startup.rs index e98c997db710626dfb77125003d066a99d9184b6..5c607ed230fcd210435be99f527502875eb2c50d 100644 --- a/game_core/src/assets/startup.rs +++ b/game_core/src/assets/startup.rs @@ -15,7 +15,10 @@ pub fn start_load_resources(mut loader: AssetTypeLoader) { loader.load_images(&[("splash.png", "splash")]); loader.load_audio(&[("splash_sting.mp3", "splash_sting")]); loader.load_apack(&[("resources.apack", "resources")]); - loader.load_trade_manifest(&[("trade_manifests/whitestone.manifest.toml", "whitestone")]); + loader.load_trade_manifest(&[ + ("trade_manifests/whitestone.manifest.toml", "whitestone"), + ("trade_manifests/monastery.manifest.toml", "monastery"), + ]); } pub fn check_load_resources(mut commands: Commands, loader: AssetTypeLoader) { diff --git a/game_core/src/ui/screens/main_menu.rs b/game_core/src/ui/screens/main_menu.rs index d5b0a69b07dfb2cc7df7b834788402bf8ab9ee98..7e815562436411ed6b8d87a0cc0d66605c297949 100644 --- a/game_core/src/ui/screens/main_menu.rs +++ b/game_core/src/ui/screens/main_menu.rs @@ -63,7 +63,8 @@ pub fn render_main_menu_layout( ..Default::default() }; - let image = KImage(assets.image("menu_background")); + let background_idx = fastrand::usize(1..6); + let image = KImage(assets.image(format!("menu_background_{}", background_idx))); let button_style = KStyle { width: px(300.0), @@ -79,7 +80,6 @@ pub fn render_main_menu_layout( let on_continue = on_button_click!(EventWriter<LoadFileEvent>, |mut events: EventWriter< LoadFileEvent, >| { - log::info!("SENDING LOAD FILE EVENT"); events.send(LoadFileEvent::autosave()); }); @@ -117,6 +117,24 @@ pub fn render_main_menu_layout( }} on_event={on_continue} /> + <ButtonWidget + styles={button_style.clone()} + props={ButtonWidgetProps { + text: String::from("Tips"), + font_size: 32.0, + is_disabled: true, + ..Default::default() + }} + /> + <ButtonWidget + styles={button_style.clone()} + props={ButtonWidgetProps { + text: String::from("Settings"), + font_size: 32.0, + is_disabled: true, + ..Default::default() + }} + /> </ElementBundle> </ElementBundle> }; diff --git a/game_core/src/ui/sync/sync_trade.rs b/game_core/src/ui/sync/sync_trade.rs index 660dc629f54afbb41e5ebfd48ecbbe892ca0f58b..494286aca51a68b93dc4f2a58060b27b37beb5a5 100644 --- a/game_core/src/ui/sync/sync_trade.rs +++ b/game_core/src/ui/sync/sync_trade.rs @@ -118,7 +118,9 @@ pub fn sync_ui_trade_data( let mut entries = trade_entries .into_values() - .filter(|listing| listing.town_amount > 0 || listing.player_amount > 0) + .filter(|listing| { + listing.current_cost > 0 && (listing.town_amount > 0 || listing.player_amount > 0) + }) .collect::<Vec<ItemTradeData>>(); entries.sort_by(|a, b| a.definition.name.cmp(&b.definition.name)); trade_data.shop_items = entries; diff --git a/game_core/src/ui/widgets/town_menu.rs b/game_core/src/ui/widgets/town_menu.rs index c3460e4b71d417261de3e7d3d88f14d04b98e123..af5b3072aae5677978d1843d802edb4066c5daba 100644 --- a/game_core/src/ui/widgets/town_menu.rs +++ b/game_core/src/ui/widgets/town_menu.rs @@ -215,7 +215,6 @@ pub fn render_town_menu_panel( }; if let Some(next) = state.next_tab { - log::info!("SWAPPGIN {:?} ;; {:?}", next, state.tab); state.tab = next; state.next_tab = None; } diff --git a/game_core/src/world/hunger.rs b/game_core/src/world/hunger.rs new file mode 100644 index 0000000000000000000000000000000000000000..1c363e28ec2dd5263c5bdbe053b60ba30e3b3576 --- /dev/null +++ b/game_core/src/world/hunger.rs @@ -0,0 +1,127 @@ +use std::collections::HashMap; + +use bevy::prelude::{Component, EventReader, Mut, Query, With, Without}; +use serde::{Deserialize, Serialize}; + +use crate::const_data::get_goods_from_name_checked; +use crate::states::Player; +use crate::world::travel::WorldTickEvent; +use crate::world::{TownName, TradeGood, TradingState}; + +#[derive(Clone, Debug, Default, Serialize, Deserialize, Component)] +pub struct HungerState { + pub sustenance: usize, + pub starvation_ticks: f32, +} + +impl HungerState { + pub fn initial_player() -> Self { + Self { + sustenance: PLAYER_FOOD_TARGET, + starvation_ticks: 0.0, + } + } + pub fn initial_town() -> Self { + Self { + sustenance: TOWN_FOOD_TARGET, + starvation_ticks: 0.0, + } + } +} + +pub struct StarvationMarker; + +pub const PLAYER_FOOD_TARGET: usize = 50; +pub const TOWN_FOOD_TARGET: usize = 150; + +pub fn process_player_hunger_ticks( + world_ticks: EventReader<WorldTickEvent>, + mut query: Query<(&mut TradingState, &mut HungerState), (With<Player>, Without<TownName>)>, +) { + if !world_ticks.is_empty() { + log::info!("Processing player hunger"); + process_hunger_state(PLAYER_FOOD_TARGET, query.iter_mut()); + } +} + +pub fn process_towns_hunger_ticks( + world_ticks: EventReader<WorldTickEvent>, + mut query: Query<(&mut TradingState, &mut HungerState), (With<TownName>, Without<Player>)>, +) { + if !world_ticks.is_empty() { + log::info!("Processing town hunger"); + process_hunger_state(TOWN_FOOD_TARGET, query.iter_mut()); + } +} + +/// Run system logic for processing hunger state. We extract this out to a separate function +/// so that we can parallelise player & town hunger processing as they are identical but have +/// different target values. The systems have disjoint query sets (second tuple in the Query params), +/// so will run in parallel despite mutably accessing the same components +fn process_hunger_state<'a>( + target: usize, + mut entries: impl Iterator<Item = (Mut<'a, TradingState>, Mut<'a, HungerState>)>, +) { + for (mut trading_state, mut hunger_state) in &mut entries { + // Tick down our sustenance every time we check our food state + hunger_state.sustenance = hunger_state.sustenance.saturating_sub(1); + // We don't need to consume if we somehow exceed our target + if hunger_state.sustenance >= target { + continue; + } + + // The amount of sustenance that we need to get from food in the inventory + let mut deficit = target - hunger_state.sustenance; + + // A list of what items we have that satisfy the following conditions: + // - Has a food_value of at least 1 (We aren't eating spare wheels over here) + // - Has a food value _less_ than the deficit we're trying to fill. We won't eat foods + // that we can't take full advantage of + let mut foodstuffs = trading_state + .items + .iter() + .filter_map(|(name, amount)| { + get_goods_from_name_checked(name) + .filter(|good| good.food_value > 0 && good.food_value <= deficit) + .map(|good| (name, good, amount)) + }) + .collect::<Vec<(&String, TradeGood, &usize)>>(); + + // Sort by highest food value. We eat the most nourishing foods first + foodstuffs.sort_by(|(_, tga, _), (_, tgb, _)| tga.food_value.cmp(&tgb.food_value)); + + // Track what we're going to consume + let mut to_consume = Vec::with_capacity(foodstuffs.len() / 4); + 'consumption_loop: for (name, trade_good, available_amount) in &foodstuffs { + let mut available = **available_amount; + let mut consume_amount = 0; + + 'deficit_loop: while deficit > 0 && available > 0 { + if trade_good.food_value > deficit { + break 'deficit_loop; + } + available -= 1; + consume_amount += 1; + deficit -= trade_good.food_value; + } + + if consume_amount > 0 { + to_consume.push(((*name).clone(), consume_amount)); + } + + // If we've satisfied our deficit, or we no longer have foods we can consume to take + // full effect of their food_value (no wastage) + if deficit == 0 || trade_good.food_value > deficit { + break 'consumption_loop; + } + } + + // Actually remove items we're consuming for food + for (item_name, amount) in to_consume { + trading_state.remove_items(item_name, amount); + } + + // Restore hunger to max, less any deficit we haven't satisfied + hunger_state.sustenance = target - deficit; + } +} diff --git a/game_core/src/world/mod.rs b/game_core/src/world/mod.rs index 971f5d44bf368db28e93286880057cc363aa1bc3..3e43af408252ffeb481986e6a31ce1e24c0afee6 100644 --- a/game_core/src/world/mod.rs +++ b/game_core/src/world/mod.rs @@ -1,10 +1,11 @@ -use bevy::app::App; -use bevy::prelude::{Commands, Plugin}; +use bevy::app::{App, CoreStage}; +use bevy::prelude::{Commands, IntoSystemDescriptor, Plugin, SystemSet, SystemStage}; use iyes_loopless::prelude::{AppLooplessStateExt, ConditionSet}; mod debug; mod encounters; mod generators; +mod hunger; mod spawning; mod towns; mod trading; @@ -13,11 +14,12 @@ mod utils; mod world_query; pub use encounters::{EncounterState, WorldZones}; +pub use hunger::HungerState; pub use spawning::PendingLoadState; pub use towns::{CurrentResidence, PathingResult, TownName, TownPaths, TravelPath, TravelTarget}; pub use trading::{ - HungerState, ItemName, RosterEntry, TradeGood, TradeManifest, TradeManifestTickState, - TradeRoster, TradingState, + ItemName, RosterEntry, TradeGood, TradeManifest, TradeManifestTickState, TradeRoster, + TradingState, }; pub use travel::DistanceTravelled; pub use utils::{grid_to_px, px_to_grid, ActiveLevel, WorldLinked}; @@ -30,6 +32,11 @@ use crate::world::travel::WorldTickEvent; pub struct WorldPlugin; impl Plugin for WorldPlugin { fn build(&self, app: &mut App) { + app.add_stage_after(CoreStage::PreUpdate, "travel", SystemStage::parallel()); + app.add_stage_after("travel", "trade", SystemStage::parallel()); + app.add_stage_after("trade", "hunger", SystemStage::parallel()); + app.add_stage_after(CoreStage::Update, "cleanup", SystemStage::parallel()); + app.init_resource::<TownPaths>() .init_resource::<WorldZones>() .init_resource::<EncounterState>() @@ -42,17 +49,50 @@ impl Plugin for WorldPlugin { }); }) .add_enter_system(AppState::Menu, spawning::clean_game_state) - .add_system_set( + .add_system_set_to_stage( + "travel", ConditionSet::new() + .label("tick_travelling_merchant") .run_in_state(AppState::InGame) - // .with_system(debug::create_tombstones) - .with_system(spawning::spawn_world_data) - .with_system(spawning::populate_world) .with_system(travel::tick_travelling_merchant) + .into(), + ) + .add_system_set_to_stage( + "travel", + ConditionSet::new() + .after("tick_travelling_merchant") + .run_in_state(AppState::InGame) .with_system(travel::track_travel) - .with_system(encounters::notify_new_zone) + .into(), + ) + .add_system_set_to_stage( + "trade", + ConditionSet::new() + .run_in_state(AppState::InGame) .with_system(trading::tick_manifests) .into(), + ) + .add_system_set_to_stage( + "hunger", + ConditionSet::new() + .run_in_state(AppState::InGame) + .with_system(hunger::process_player_hunger_ticks) + .with_system(hunger::process_towns_hunger_ticks) + .into(), + ) + .add_system_set_to_stage( + "cleanup", + ConditionSet::new() + .run_in_state(AppState::InGame) + .with_system(encounters::notify_new_zone) + .into(), + ) + .add_system_set( + ConditionSet::new() + .run_in_state(AppState::InGame) + .with_system(spawning::spawn_world_data) + .with_system(spawning::populate_world) + .into(), ); } } diff --git a/game_core/src/world/trading.rs b/game_core/src/world/trading.rs index 5f8f9b4aa7912b62641661f54b2b8dcc57400941..d2a96c03ee10c7b804dbaba1c7721a5f4b160393 100644 --- a/game_core/src/world/trading.rs +++ b/game_core/src/world/trading.rs @@ -19,12 +19,6 @@ pub struct TradingState { pub gold: isize, } -#[derive(Clone, Debug, Default, Serialize, Deserialize, Component)] -pub struct HungerState { - pub sustenance: usize, - pub starvation_ticks: f32, -} - #[derive(Clone, Debug, Default, Serialize, Deserialize)] pub struct TradeGood { pub name: String, @@ -201,7 +195,8 @@ pub fn tick_manifests( if events.is_empty() { return; } - log::info!("TICK MANIFESTS"); + + log::info!("Processing trade manifests"); for (mut state, manifest, mut tick_state) in &mut town_query { if let Some(manifest) = manifests.get(manifest) { @@ -221,10 +216,10 @@ pub fn tick_manifests( while ts > &mut 1.0 { *ts -= 1.0; if quantity > entry.natural_limit { - let mut item = state.items.entry(good.clone()).or_insert(0); + let item = state.items.entry(good.clone()).or_insert(0); *item = item.saturating_sub(1); } else { - let mut item = state.items.entry(good.clone()).or_insert(0); + let item = state.items.entry(good.clone()).or_insert(0); *item = item.saturating_add(1); } } @@ -234,7 +229,7 @@ pub fn tick_manifests( *ts += entry.tick_decay; while ts > &mut 1.0 { *ts -= 1.0; - let mut item = state.items.entry(good.clone()).or_insert(0); + let item = state.items.entry(good.clone()).or_insert(0); *item = item.saturating_sub(1); } } @@ -242,7 +237,7 @@ pub fn tick_manifests( *ts += entry.tick_decay; while ts > &mut 1.0 { *ts -= 1.0; - let mut item = state.items.entry(good.clone()).or_insert(0); + let item = state.items.entry(good.clone()).or_insert(0); *item = item.saturating_add(1); } } diff --git a/game_core/src/world/travel.rs b/game_core/src/world/travel.rs index e9e6e37948293be6b76011d374a6d328c95e26ce..7b8fd12ca5ae440699e5dfbf971187cff928a5d1 100644 --- a/game_core/src/world/travel.rs +++ b/game_core/src/world/travel.rs @@ -88,7 +88,7 @@ pub fn track_travel( }; if format_distance(before) % 5.0 > format_distance(after) % 5.0 { - log::info!("ANOTHER 5KM HAS PASSED"); + log::info!("Ticking World Objects"); events.send(WorldTickEvent); } } diff --git a/raw_assets/manifest.toml b/raw_assets/manifest.toml index 41bb26226d291b164b8fae8d2495e3406d78ee2c..3bfc120b90732581652ede1f48297d355d5e5804 100644 --- a/raw_assets/manifest.toml +++ b/raw_assets/manifest.toml @@ -9,8 +9,28 @@ name = "icons" tiles = { size = 8, columns = 16, rows = 16 } [[images]] -path = "backgrounds/main_menu.png" -name = "menu_background" +path = "backgrounds/main_menu_1.png" +name = "menu_background_1" +format = "png" + +[[images]] +path = "backgrounds/main_menu_2.png" +name = "menu_background_2" +format = "png" + +[[images]] +path = "backgrounds/main_menu_3.png" +name = "menu_background_3" +format = "png" + +[[images]] +path = "backgrounds/main_menu_4.png" +name = "menu_background_4" +format = "png" + +[[images]] +path = "backgrounds/main_menu_5.png" +name = "menu_background_5" format = "png" [[images]]