use std::collections::HashMap; use std::time::Duration; use bevy::math::vec3; use bevy::prelude::*; use bevy_ecs_tilemap::prelude::*; use bevy_tweening::lens::TransformPositionLens; use bevy_tweening::{ Animator, EaseFunction, EaseMethod, RepeatCount, RepeatStrategy, Sequence, Tracks, Tween, }; use ldtk_rust::EntityInstance; use micro_banimate::definitions::SimpleAnimationBundle; use num_traits::AsPrimitive; use serde_json::Value; use crate::assets::{AssetHandles, LdtkProject, LevelIndex, TilesetIndex}; use crate::const_data::SPECIALISMS; use crate::persistance::PersistenceState; use crate::states::Player; use crate::system::camera::ChaseCam; use crate::world::encounters::WorldZones; use crate::world::towns::{CurrentResidence, TownBundle, TownPaths}; use crate::world::travel::DistanceTravelled; use crate::world::utils::{ entity_to_worldspace, grid_to_px, px_to_grid, ActiveLevel, WorldLinked, TILE_SCALE_F32, }; use crate::world::world_query::MapQuery; use crate::world::{ EncounterState, HungerState, TownName, TradeManifest, TradeManifestTickState, TradingState, TravelPath, TravelTarget, }; #[derive(Default, Debug, Copy, Clone, Eq, PartialEq)] pub struct PopulateWorldEvent; pub fn spawn_world_data( mut commands: Commands, mut active_level: Option<ResMut<ActiveLevel>>, assets: Res<AssetHandles>, projects: Res<Assets<LdtkProject>>, level_index: Res<LevelIndex>, tileset_index: Res<TilesetIndex>, mut last_spawned_level: Local<String>, mut events: EventWriter<PopulateWorldEvent>, ) { let mut active_level = match active_level { Some(l) => l, None => return, }; let mut tileset = match tileset_index.get(&String::from("Overworld")) { Some(l) => l, None => return, }; if *last_spawned_level == active_level.map && !active_level.dirty { return; } else { *last_spawned_level = active_level.map.clone(); } if active_level.is_changed() || active_level.is_added() { let level = match level_index.get(&active_level.map) { Some(l) => l, _ => return, }; let map_tile_width = px_to_grid(level.px_wid); let map_tile_height = px_to_grid(level.px_hei); let tilemap_size = TilemapSize { x: map_tile_width.as_(), y: map_tile_height.as_(), }; let tilemap_tile_size = TilemapTileSize { x: TILE_SCALE_F32, y: TILE_SCALE_F32, }; MapQuery::for_each_layer_of(level, |layer| { if !layer.has_tiles() { return; } let map_entity = commands.spawn(WorldLinked).id(); let mut storage = TileStorage::empty(tilemap_size); layer.for_each_tile(|x, y, tile| { let mut tile_pos = TilePos { x: px_to_grid(x).as_(), y: px_to_grid(y).as_(), }; tile_pos.y = map_tile_height as u32 - tile_pos.y - 1; let tile_entity = if let Some(tile_entity) = tileset.0.get(&tile.tile.t) { match tile_entity.get("animations") { Some(Value::Array(frames)) => commands .spawn(( TileBundle { position: tile_pos, tilemap_id: TilemapId(map_entity), texture_index: TileTextureIndex(tile.tile.t as u32), ..Default::default() }, SimpleAnimationBundle::new( frames .iter() .map(|val| val.as_u64().unwrap() as usize) .collect(), 0.5, ), )) .id(), _ => commands .spawn(TileBundle { position: tile_pos, tilemap_id: TilemapId(map_entity), texture_index: TileTextureIndex(tile.tile.t as u32), ..Default::default() }) .id(), } } else { commands .spawn(TileBundle { position: tile_pos, tilemap_id: TilemapId(map_entity), texture_index: TileTextureIndex(tile.tile.t as u32), ..Default::default() }) .id() }; storage.set(&tile_pos, tile_entity); }); let grid_size = tilemap_tile_size.into(); let map_type = TilemapType::Square; let bg = level .level_bg_color .clone() .unwrap_or_else(|| level.bg_color.clone()); match bg.len() { 6 => commands.insert_resource(ClearColor( Color::hex(bg.clone()).expect("Failed to set background color"), )), 7 => commands.insert_resource(ClearColor( Color::hex(&bg[1..]).expect("Failed to set background color"), )), _ => { log::warn!("Strange colour {}", &bg) } } commands.entity(map_entity).insert((TilemapBundle { grid_size, map_type, size: tilemap_size, storage, texture: TilemapTexture::Single(assets.image("overworld")), tile_size: tilemap_tile_size, transform: Transform::from_xyz(0.0, 0.0, 5.0 + layer.get_z_delta()), // get_tilemap_center_transform(&tilemap_size, &grid_size, 5.0), ..Default::default() },)); }); events.send(PopulateWorldEvent); } } #[derive(Resource)] pub struct PendingLoadState(pub PersistenceState); pub fn populate_world( mut commands: Commands, mut events: ResMut<Events<PopulateWorldEvent>>, mut active_level: Option<ResMut<ActiveLevel>>, assets: Res<AssetHandles>, level_index: Res<LevelIndex>, existing_player: Query<Entity, With<Player>>, pending_load: Option<Res<PendingLoadState>>, manifests: Res<Assets<TradeManifest>>, ) { let should_populate = !events .drain() .collect::<Vec<PopulateWorldEvent>>() .is_empty(); if should_populate { let mut active_level = match active_level { Some(l) => l, None => return, }; let level = match level_index.get(&active_level.map) { Some(l) => l, None => return, }; let trade_routes = TownPaths::from(MapQuery::get_entities_of(level)); let start = trade_routes .routes .get(&String::from("The Royal Lampoon")) .unwrap(); if let Some(route) = start.routes.values().next() { let point = route.nodes[0]; let mut cmds = commands.spawn(( SpriteSheetBundle { transform: pending_load .as_ref() .map(|pending| Transform::from_translation(pending.0.player_location)) .unwrap_or_else(|| { Transform::from_xyz( grid_to_px(point.tile_x), level.px_hei as f32 - grid_to_px(point.tile_y), 400.0, ) }), texture_atlas: assets.atlas("icons"), sprite: TextureAtlasSprite { index: 0, ..Default::default() }, ..Default::default() }, Player, ChaseCam, WorldLinked, )); match &pending_load { Some(ref state) => { match ( &state.0.previous_location, &state.0.travel_path, &state.0.travel_target, ) { (res, Some(path), Some(target)) => { cmds.insert((res.clone(), path.clone(), target.clone())); } (res, None, Some(target)) => { cmds.insert((res.clone(), target.clone())); } (res, Some(path), None) => { cmds.insert((res.clone(), path.clone())); } (res, None, None) => { cmds.insert(res.clone()); } } } None => { if let Some(bundle) = start.create_route_bundle_for( start .routes .keys() .nth(fastrand::usize(0..start.routes.len())) .cloned() .unwrap(), level, ) { cmds.insert(bundle); } } } cmds.insert( pending_load .as_ref() .map(|pending| pending.0.player_total_distance) .unwrap_or_else(|| DistanceTravelled(0.0)), ); cmds.insert( pending_load .as_ref() .map(|pending| pending.0.player_inventory.clone()) .unwrap_or_else(|| TradingState { gold: 500, items: Default::default(), }), ); cmds.insert( pending_load .as_ref() .map(|pending| pending.0.player_hunger.clone()) .unwrap_or_else(HungerState::initial_player), ); } let world_zones = WorldZones::from_entities(level.px_hei, MapQuery::get_entities_of(level)); MapQuery::get_entities_of(level) .into_iter() .filter(|instance| instance.identifier == *"Town") .map(|instance| { let mut name = String::new(); let mut manifest_name = String::new(); let (cx, cy) = entity_to_worldspace(level.px_hei, instance); instance .field_instances .iter() .for_each(|inst| match &*inst.identifier { "name" => name = inst.value.as_ref().unwrap().as_str().unwrap().to_string(), "trade_manifest" => { manifest_name = inst .value .as_ref() .map(|val| String::from(val.as_str().unwrap())) .unwrap_or_default() } _ => {} }); if manifest_name.is_empty() { (name, String::from("whitestone"), Vec3::new(cx, cy, 500.0)) } else { (name, manifest_name, Vec3::new(cx, cy, 500.0)) } }) .map(|(name, manifest, transform)| { let handle = assets.trade_manifest(manifest); let asset = manifests.get(&handle).unwrap(); match pending_load .as_ref() .and_then(|pl| pl.0.town_states.get(&name)) { Some((trade, hunger, tick)) => TownBundle { town_name: TownName(name.clone()), manifest: handle, trade_state: trade.clone(), hunger_state: hunger.clone(), manifest_state: tick.clone(), world_linked: WorldLinked, location: TransformBundle::from_transform(Transform::from_translation( transform, )), }, None => TownBundle { town_name: TownName(name.clone()), manifest: handle, trade_state: TradingState { gold: fastrand::isize(0..250) + 250, items: asset.create_randomised_inventory(), }, hunger_state: HungerState::initial_town(), manifest_state: TradeManifestTickState::default(), world_linked: WorldLinked, location: TransformBundle::from_transform(Transform::from_translation( transform, )), }, } }) .for_each(|bundle| { let ent = commands.spawn((bundle, VisibilityBundle::default())).id(); apply_skull_marker(&mut commands, &assets, ent); }); log::info!("Spec {:#?}", *SPECIALISMS); commands.insert_resource(trade_routes); commands.insert_resource(world_zones); } } pub fn apply_skull_marker(commands: &mut Commands, assets: &Res<AssetHandles>, entity: Entity) { commands.entity(entity).with_children(|builder| { let tween = Tween::new( EaseFunction::SineInOut, Duration::from_secs(2), TransformPositionLens { start: vec3(0.0, 0.0, 400.0), end: vec3(0.0, 2.0, 400.0), }, ) .with_repeat_strategy(RepeatStrategy::MirroredRepeat) .with_repeat_count(RepeatCount::Infinite); builder.spawn(( SpriteSheetBundle { sprite: TextureAtlasSprite::new(42), texture_atlas: assets.atlas("icons"), transform: Transform::from_xyz(0.0, 0.0, 400.0), ..Default::default() }, Animator::new(tween), )); }); } pub fn clean_game_state( mut commands: Commands, world_entities: Query<Entity, (Without<TileStorage>, With<WorldLinked>)>, mut tile_storages: Query<Entity, Or<(With<TilemapId>, With<TileStorage>)>>, ) { commands.remove_resource::<PendingLoadState>(); commands.remove_resource::<PersistenceState>(); commands.insert_resource(TownPaths::default()); commands.insert_resource(WorldZones::default()); commands.insert_resource(EncounterState::default()); // log::info!("Cleaning {} world objects", world_entities.iter().len()); for entity in &world_entities { commands.entity(entity).despawn_recursive(); } // log::info!("Cleaning {} tile storages", tile_storages.iter().len()); for entity in &mut tile_storages { commands.entity(entity).despawn(); } }