diff --git a/Cargo.toml b/Cargo.toml index c841129..40352ca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,6 @@ authors = ["Alula"] edition = "2018" [lib] -#crate-type = ["lib", "cdylib"] crate-type = ["lib"] [[bin]] @@ -57,6 +56,7 @@ case_insensitive_hashmap = "1.0.0" chrono = "0.4" cpal = { git = "https://github.com/doukutsu-rs/cpal.git", rev = "4218ff23242834d36bcdcc0c2e3883985c15b5e0" } directories = "3" +downcast = "0.11" funty = "=1.1.0" # https://github.com/bitvecto-rs/bitvec/issues/105 glutin = { git = "https://github.com/doukutsu-rs/glutin.git", rev = "8dd457b9adb7dbac7ade337246b6356c784272d9", optional = true, default_features = false, features = ["x11"] } imgui = "0.8.0" @@ -76,7 +76,7 @@ serde = { version = "1", features = ["derive"] } serde_derive = "1" serde_cbor = { version = "0.11.2", optional = true } serde_json = "1.0" -simple_logger = { version = "1.13" } +simple_logger = { version = "1.16", features = ["colors", "threads"] } strum = "0.20" strum_macros = "0.20" tokio = { version = "1.12.0", features = ["net"], optional = true } diff --git a/src/components/background.rs b/src/components/background.rs index f6316c7..fb48d85 100644 --- a/src/components/background.rs +++ b/src/components/background.rs @@ -48,12 +48,13 @@ impl Background { BackgroundType::TiledStatic => { graphics::clear(ctx, stage.data.background_color); - let count_x = state.canvas_size.0 as usize / batch.width() + 1; - let count_y = state.canvas_size.1 as usize / batch.height() + 1; + let (bg_width, bg_height) = (batch.width() as i32, batch.height() as i32); + let count_x = state.canvas_size.0 as i32 / bg_width + 1; + let count_y = state.canvas_size.1 as i32 / bg_height + 1; - for y in 0..count_y { - for x in 0..count_x { - batch.add((x * batch.width()) as f32, (y * batch.height()) as f32); + for y in -1..count_y { + for x in -1..count_x { + batch.add((x * bg_width) as f32, (y * bg_height) as f32); } } } @@ -69,12 +70,13 @@ impl Background { ) }; - let count_x = state.canvas_size.0 as usize / batch.width() + 2; - let count_y = state.canvas_size.1 as usize / batch.height() + 2; + let (bg_width, bg_height) = (batch.width() as i32, batch.height() as i32); + let count_x = state.canvas_size.0 as i32 / bg_width + 2; + let count_y = state.canvas_size.1 as i32 / bg_height + 2; - for y in 0..count_y { - for x in 0..count_x { - batch.add((x * batch.width()) as f32 - off_x, (y * batch.height()) as f32 - off_y); + for y in -1..count_y { + for x in -1..count_x { + batch.add((x * bg_width) as f32 - off_x, (y * bg_height) as f32 - off_y); } } } diff --git a/src/editor/mod.rs b/src/editor/mod.rs new file mode 100644 index 0000000..0520c5b --- /dev/null +++ b/src/editor/mod.rs @@ -0,0 +1,285 @@ +use std::cell::RefCell; +use std::ops::Deref; +use std::rc::Rc; + +use imgui::{Image, MouseButton, Window, WindowFlags}; + +use crate::common::{Color, Rect}; +use crate::components::background::Background; +use crate::components::tilemap::{TileLayer, Tilemap}; +use crate::frame::Frame; +use crate::stage::{Stage, StageTexturePaths}; +use crate::{graphics, Context, GameResult, SharedGameState, I_MAG}; + +#[derive(Copy, Clone, Eq, PartialEq)] +pub enum CurrentTool { + Move, + Brush, + Fill, + Rectangle, +} + +pub struct EditorInstance { + pub stage: Stage, + pub stage_id: usize, + pub frame: Frame, + pub background: Background, + pub stage_textures: Rc>, + pub tilemap: Tilemap, + pub zoom: f32, + pub current_tile: u8, + pub mouse_pos: (f32, f32), + pub want_capture_mouse: bool, +} + +impl EditorInstance { + pub fn new(stage_id: usize, stage: Stage) -> EditorInstance { + let stage_textures = { + let mut textures = StageTexturePaths::new(); + textures.update(&stage); + Rc::new(RefCell::new(textures)) + }; + let mut frame = Frame::new(); + frame.x = -16 * 0x200; + frame.y = -48 * 0x200; + + EditorInstance { + stage, + stage_id, + frame, + background: Background::new(), + stage_textures, + tilemap: Tilemap::new(), + zoom: 2.0, + current_tile: 0, + mouse_pos: (0.0, 0.0), + want_capture_mouse: true, + } + } + + pub fn process(&mut self, state: &mut SharedGameState, ctx: &mut Context, ui: &mut imgui::Ui, tool: CurrentTool) { + self.frame.prev_x = self.frame.x; + self.frame.prev_y = self.frame.y; + self.mouse_pos = (ui.io().mouse_pos[0], ui.io().mouse_pos[1]); + self.want_capture_mouse = ui.io().want_capture_mouse; + + let mut drag = false; + + match tool { + CurrentTool::Move => { + if ui.io().want_capture_mouse { + return; + } + + drag |= ui.is_mouse_down(MouseButton::Left) || ui.is_mouse_down(MouseButton::Right); + } + CurrentTool::Brush => { + self.palette_window(state, ctx, ui); + + if ui.io().want_capture_mouse { + return; + } + + drag |= ui.is_mouse_down(MouseButton::Right); + + if !drag && ui.is_mouse_down(MouseButton::Left) { + let tile_size = self.stage.map.tile_size.as_int(); + let halft = tile_size / 2; + let stage_mouse_x = (self.frame.x / 0x200) + halft + (self.mouse_pos.0 / self.zoom) as i32; + let stage_mouse_y = (self.frame.y / 0x200) + halft + (self.mouse_pos.1 / self.zoom) as i32; + let tile_x = stage_mouse_x / tile_size; + let tile_y = stage_mouse_y / tile_size; + + if tile_x >= 0 + && tile_y >= 0 + && tile_x < self.stage.map.width as i32 + && tile_y < self.stage.map.height as i32 + { + self.stage.change_tile(tile_x as usize, tile_y as usize, self.current_tile); + } + } + } + CurrentTool::Fill => { + self.palette_window(state, ctx, ui); + drag |= ui.is_mouse_down(MouseButton::Right); + } + CurrentTool::Rectangle => { + self.palette_window(state, ctx, ui); + drag |= ui.is_mouse_down(MouseButton::Right); + } + } + + if drag { + self.frame.x -= (512.0 * ui.io().mouse_delta[0] as f32 / self.zoom) as i32; + self.frame.y -= (512.0 * ui.io().mouse_delta[1] as f32 / self.zoom) as i32; + self.frame.prev_x = self.frame.x; + self.frame.prev_y = self.frame.y; + } + } + + fn tile_cursor(&self, state: &mut SharedGameState, ctx: &mut Context) -> GameResult { + if self.want_capture_mouse { + return Ok(()); + } + + let tile_size = self.stage.map.tile_size.as_int(); + let halft = tile_size / 2; + let stage_mouse_x = (self.frame.x / 0x200) + halft + (self.mouse_pos.0 / self.zoom) as i32; + let stage_mouse_y = (self.frame.y / 0x200) + halft + (self.mouse_pos.1 / self.zoom) as i32; + let tile_x = stage_mouse_x / tile_size; + let tile_y = stage_mouse_y / tile_size; + let frame_x = self.frame.x as f32 / 512.0; + let frame_y = self.frame.y as f32 / 512.0; + + if tile_x < 0 || tile_y < 0 || tile_x >= self.stage.map.width as i32 || tile_y >= self.stage.map.height as i32 { + return Ok(()); + } + + let name = &self.stage_textures.deref().borrow().tileset_fg; + + if let Ok(batch) = state.texture_set.get_or_load_batch(ctx, &state.constants, name) { + let tile_size16 = tile_size as u16; + let rect = Rect::new_size( + (self.current_tile as u16 % 16) * tile_size16, + (self.current_tile as u16 / 16) * tile_size16, + tile_size16, + tile_size16, + ); + + batch.add_rect_tinted( + (tile_x * tile_size - halft) as f32 - frame_x, + (tile_y * tile_size - halft) as f32 - frame_y, + (255, 255, 255, 192), + &rect, + ); + + batch.draw(ctx)?; + } + + Ok(()) + } + + fn palette_window(&mut self, state: &mut SharedGameState, ctx: &mut Context, ui: &imgui::Ui) { + Window::new("Palette") + .size([260.0, 260.0], imgui::Condition::Always) + .position(ui.io().display_size, imgui::Condition::FirstUseEver) + .position_pivot([1.0, 1.0]) + .resizable(false) + .build(ui, || { + let name = &self.stage_textures.deref().borrow().tileset_fg; + let batch = state.texture_set.get_or_load_batch(ctx, &state.constants, name); + + let pos = ui.cursor_screen_pos(); + let tile_size = self.stage.map.tile_size.as_float(); + + if let Ok(batch) = batch { + let (scale_x, scale_y) = batch.scale(); + if let Some(tex) = batch.get_texture() { + let (width, height) = tex.dimensions(); + let (width, height) = (width as f32 / scale_x, height as f32 / scale_y); + + if let Ok(tex_id) = graphics::imgui_texture_id(ctx, tex) { + Image::new(tex_id, [width, height]).build(ui); + } + + ui.set_cursor_screen_pos(pos); + ui.invisible_button("##tiles", [width, height]); + } + } + + let draw_list = ui.get_window_draw_list(); + let cur_pos1 = [ + pos[0].floor() + tile_size * (self.current_tile % 16) as f32, + pos[1].floor() + tile_size * (self.current_tile / 16) as f32, + ]; + let cur_pos2 = [cur_pos1[0] + tile_size, cur_pos1[1] + tile_size]; + draw_list.add_rect(cur_pos1, cur_pos2, [1.0, 0.0, 0.0, 1.0]).thickness(2.0).build(); + + if ui.is_mouse_down(MouseButton::Left) { + let mouse_pos = ui.io().mouse_pos; + let x = (mouse_pos[0] - pos[0]) / tile_size; + let y = (mouse_pos[1] - pos[1]) / tile_size; + + if x >= 0.0 && x < 16.0 && y >= 0.0 && y < 16.0 { + self.current_tile = (y as u8 * 16 + x as u8) as u8; + } + } + }); + } + + pub fn draw(&self, state: &mut SharedGameState, ctx: &mut Context, tool: CurrentTool) -> GameResult { + let old_scale = state.scale; + set_scale(state, self.zoom); + + let paths = self.stage_textures.deref().borrow(); + self.background.draw(state, ctx, &self.frame, &*paths, &self.stage)?; + + self.tilemap.draw(state, ctx, &self.frame, TileLayer::Background, &*paths, &self.stage)?; + self.tilemap.draw(state, ctx, &self.frame, TileLayer::Middleground, &*paths, &self.stage)?; + self.tilemap.draw(state, ctx, &self.frame, TileLayer::Foreground, &*paths, &self.stage)?; + self.tilemap.draw(state, ctx, &self.frame, TileLayer::Snack, &*paths, &self.stage)?; + + self.draw_black_bars(state, ctx)?; + + match tool { + CurrentTool::Move => (), + CurrentTool::Brush | CurrentTool::Fill | CurrentTool::Rectangle => { + self.tile_cursor(state, ctx)?; + } + } + + set_scale(state, old_scale); + + Ok(()) + } + + fn draw_black_bars(&self, state: &mut SharedGameState, ctx: &mut Context) -> GameResult { + let color = Color::from_rgba(0, 0, 0, 128); + let (x, y) = self.frame.xy_interpolated(state.frame_time); + let (x, y) = (x * state.scale, y * state.scale); + let canvas_w_scaled = state.canvas_size.0 as f32 * state.scale; + let canvas_h_scaled = state.canvas_size.1 as f32 * state.scale; + let level_width = (self.stage.map.width as f32 - 1.0) * self.stage.map.tile_size.as_float(); + let level_height = (self.stage.map.height as f32 - 1.0) * self.stage.map.tile_size.as_float(); + let left_side = -x; + let right_side = -x + level_width * state.scale; + let upper_side = -y; + let lower_side = -y + level_height * state.scale; + + if left_side > 0.0 { + let rect = Rect::new(0, upper_side as isize, left_side as isize, lower_side as isize); + graphics::draw_rect(ctx, rect, color)?; + } + + if right_side < canvas_w_scaled { + let rect = Rect::new( + right_side as isize, + upper_side as isize, + (state.canvas_size.0 * state.scale) as isize, + lower_side as isize, + ); + graphics::draw_rect(ctx, rect, color)?; + } + + if upper_side > 0.0 { + let rect = Rect::new(0, 0, canvas_w_scaled as isize, upper_side as isize); + graphics::draw_rect(ctx, rect, color)?; + } + + if lower_side < canvas_h_scaled { + let rect = Rect::new(0, lower_side as isize, canvas_w_scaled as isize, canvas_h_scaled as isize); + graphics::draw_rect(ctx, rect, color)?; + } + + Ok(()) + } +} + +fn set_scale(state: &mut SharedGameState, scale: f32) { + state.scale = scale; + + unsafe { + I_MAG = state.scale; + state.canvas_size = (state.screen_size.0 / state.scale, state.screen_size.1 / state.scale); + } +} diff --git a/src/frame.rs b/src/frame.rs index 7cba8a2..364375f 100644 --- a/src/frame.rs +++ b/src/frame.rs @@ -23,6 +23,19 @@ pub struct Frame { } impl Frame { + pub fn new() -> Frame { + Frame { + x: 0, + y: 0, + prev_x: 0, + prev_y: 0, + update_target: UpdateTarget::Player, + target_x: 0, + target_y: 0, + wait: 16, + } + } + pub fn xy_interpolated(&self, frame_time: f64) -> (f32, f32) { if self.prev_x == self.x && self.prev_y == self.y { return (fix9_scale(self.x), fix9_scale(self.y)); diff --git a/src/lib.rs b/src/lib.rs index 3408c18..4cdbf6b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -32,15 +32,17 @@ mod builtin_fs; mod caret; mod common; mod components; +#[cfg(feature = "editor")] +mod editor; mod encoding; mod engine_constants; mod entity; mod frame; mod framework; -mod input; -mod inventory; #[cfg(feature = "hooks")] mod hooks; +mod input; +mod inventory; mod live_debugger; mod macros; mod map; @@ -65,6 +67,7 @@ mod weapon; pub struct LaunchOptions { pub server_mode: bool, + pub editor: bool, } lazy_static! { @@ -100,11 +103,12 @@ impl Game { if let Some(scene) = self.scene.as_mut() { let state_ref = unsafe { &mut *self.state.get() }; - let speed = if state_ref.textscript_vm.mode == ScriptMode::Map && state_ref.textscript_vm.flags.cutscene_skip() { - 4.0 * state_ref.settings.speed - } else { - 1.0 * state_ref.settings.speed - }; + let speed = + if state_ref.textscript_vm.mode == ScriptMode::Map && state_ref.textscript_vm.flags.cutscene_skip() { + 4.0 * state_ref.settings.speed + } else { + 1.0 * state_ref.settings.speed + }; match state_ref.settings.timing_mode { TimingMode::_50Hz | TimingMode::_60Hz => { @@ -114,8 +118,7 @@ impl Game { if (speed - 1.0).abs() < 0.01 { self.next_tick += state_ref.settings.timing_mode.get_delta() as u128; } else { - self.next_tick += - (state_ref.settings.timing_mode.get_delta() as f64 / speed) as u128; + self.next_tick += (state_ref.settings.timing_mode.get_delta() as f64 / speed) as u128; } self.loops += 1; } @@ -123,8 +126,8 @@ impl Game { if self.loops == 10 { log::warn!("Frame skip is way too high, a long system lag occurred?"); self.last_tick = self.start_time.elapsed().as_nanos(); - self.next_tick = self.last_tick - + (state_ref.settings.timing_mode.get_delta() as f64 / speed) as u128; + self.next_tick = + self.last_tick + (state_ref.settings.timing_mode.get_delta() as f64 / speed) as u128; self.loops = 0; } @@ -198,7 +201,11 @@ impl Game { } pub fn init(options: LaunchOptions) -> GameResult { - let _ = simple_logger::init_with_level(log::Level::Info); + let _ = simple_logger::SimpleLogger::new() + .without_timestamps() + .with_colors(true) + .with_level(log::Level::Info.to_level_filter()) + .init(); #[cfg(not(target_os = "android"))] let resource_dir = if let Ok(data_dir) = env::var("CAVESTORY_DATA_DIR") { @@ -264,17 +271,17 @@ pub fn init(options: LaunchOptions) -> GameResult { } #[cfg(not(target_os = "android"))] - { - if let Ok(_) = crate::framework::filesystem::open(&mut context, "/.drs_localstorage") { - let mut user_dir = resource_dir.clone(); - user_dir.push("_drs_profile"); + { + if let Ok(_) = crate::framework::filesystem::open(&mut context, "/.drs_localstorage") { + let mut user_dir = resource_dir.clone(); + user_dir.push("_drs_profile"); - let _ = std::fs::create_dir_all(&user_dir); - mount_user_vfs(&mut context, Box::new(PhysicalFS::new(&user_dir, false))); - } else { - mount_user_vfs(&mut context, Box::new(PhysicalFS::new(project_dirs.data_local_dir(), false))); - } + let _ = std::fs::create_dir_all(&user_dir); + mount_user_vfs(&mut context, Box::new(PhysicalFS::new(&user_dir, false))); + } else { + mount_user_vfs(&mut context, Box::new(PhysicalFS::new(project_dirs.data_local_dir(), false))); } + } if options.server_mode { log::info!("Running in server mode..."); diff --git a/src/main.rs b/src/main.rs index f8178e9..d4be069 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,13 +5,23 @@ use std::process::exit; fn main() { let args = std::env::args(); let mut options = doukutsu_rs::LaunchOptions { - server_mode: false + server_mode: false, + editor: false, }; for arg in args { if arg == "--server-mode" { options.server_mode = true; } + + if arg == "--editor" { + options.editor = true; + } + } + + if options.server_mode && options.editor { + eprintln!("Cannot run in server mode and editor mode at the same time."); + exit(1); } let result = doukutsu_rs::init(options); diff --git a/src/map.rs b/src/map.rs index d0e79ed..6a10be0 100644 --- a/src/map.rs +++ b/src/map.rs @@ -17,6 +17,7 @@ use crate::stage::{PxPackScroll, PxPackStageData, StageData}; static SUPPORTED_PXM_VERSIONS: [u8; 1] = [0x10]; static SUPPORTED_PXE_VERSIONS: [u8; 2] = [0, 0x10]; +#[derive(Clone)] pub struct Map { pub width: u16, pub height: u16, diff --git a/src/scene/editor_scene.rs b/src/scene/editor_scene.rs index d5c83a5..ffb1d26 100644 --- a/src/scene/editor_scene.rs +++ b/src/scene/editor_scene.rs @@ -1,19 +1,134 @@ -use imgui::MenuItem; +use std::cell::RefCell; +use std::rc::Rc; +use downcast::Downcast; +use imgui::{Condition, MenuItem, TabItem, TabItemFlags, Window}; + +use crate::editor::{CurrentTool, EditorInstance}; +use crate::framework::keyboard; +use crate::framework::keyboard::ScanCode; use crate::framework::ui::Components; +use crate::scene::game_scene::GameScene; use crate::scene::title_scene::TitleScene; +use crate::stage::Stage; use crate::{Context, GameResult, Scene, SharedGameState}; -pub struct EditorScene {} +struct ErrorList { + errors: Vec, +} + +impl ErrorList { + fn new() -> ErrorList { + Self { errors: Vec::new() } + } + + fn try_or_push_error(&mut self, func: impl FnOnce() -> GameResult<()>) { + if let Err(err) = func() { + self.errors.push(err.to_string()); + } + } +} + +fn catch(error_list: Rc>, func: impl FnOnce() -> GameResult<()>) { + error_list.borrow_mut().try_or_push_error(func); +} + +pub struct EditorScene { + stage_list: StageListWindow, + error_list: Rc>, + instances: Vec, + subscene: Option>, + current_tool: CurrentTool, + selected_instance: usize, + switch_tab: bool, +} impl EditorScene { pub fn new() -> Self { - EditorScene {} + EditorScene { + stage_list: StageListWindow::new(), + error_list: Rc::new(RefCell::new(ErrorList::new())), + instances: Vec::new(), + subscene: None, + current_tool: CurrentTool::Move, + selected_instance: 0, + switch_tab: false, + } } fn exit_editor(&mut self, state: &mut SharedGameState) { state.next_scene = Some(Box::new(TitleScene::new())); } + + fn open_stage(&mut self, state: &mut SharedGameState, ctx: &mut Context, stage_id: usize) { + catch(self.error_list.clone(), || { + for (idx, instance) in self.instances.iter().enumerate() { + if instance.stage_id == stage_id { + self.selected_instance = idx; + self.switch_tab = true; + return Ok(()); + } + } + + if let Some(stage) = state.stages.get(stage_id) { + let stage = Stage::load(&state.base_path, stage, ctx)?; + + let new_instance = EditorInstance::new(stage_id, stage); + self.instances.push(new_instance); + self.selected_instance = self.instances.len() - 1; + self.switch_tab = true; + } + + Ok(()) + }); + } + + fn test_stage(&mut self, state: &mut SharedGameState, ctx: &mut Context) { + catch(self.error_list.clone(), || { + if let Some(instance) = self.instances.get(self.selected_instance) { + state.reset(); + state.textscript_vm.start_script(94); + let mut game_scene = GameScene::from_stage(state, ctx, instance.stage.clone(), instance.stage_id)?; + game_scene.init(state, ctx)?; + game_scene.player1.cond.set_alive(true); + game_scene.player1.x = instance.frame.x + (state.canvas_size.0 * 256.0) as i32; + game_scene.player1.y = instance.frame.y + (state.canvas_size.1 * 256.0) as i32; + state.control_flags.set_control_enabled(true); + state.control_flags.set_tick_world(true); + state.textscript_vm.suspend = false; + self.subscene = Some(Box::new(game_scene)); + } + + Ok(()) + }); + } + + fn perform_actions(&mut self, state: &mut SharedGameState, ctx: &mut Context) { + let actions = std::mem::take(&mut self.stage_list.actions); + for action in actions.iter() { + match action { + StageListAction::OpenStage(idx) => self.open_stage(state, ctx, *idx), + } + } + } +} + +trait ExtraWidgetsExt { + fn tool_button(&self, label: impl AsRef, active: bool) -> bool; +} + +impl ExtraWidgetsExt for imgui::Ui<'_> { + fn tool_button(&self, label: impl AsRef, active: bool) -> bool { + if active { + let color = self.style_color(imgui::StyleColor::ButtonActive); + let _token1 = self.push_style_color(imgui::StyleColor::Button, color); + let _token2 = self.push_style_color(imgui::StyleColor::ButtonHovered, color); + let ret = self.button(label); + ret + } else { + self.button(label) + } + } } impl Scene for EditorScene { @@ -23,7 +138,64 @@ impl Scene for EditorScene { Ok(()) } - fn draw(&self, _state: &mut SharedGameState, _ctx: &mut Context) -> GameResult { + fn tick(&mut self, state: &mut SharedGameState, ctx: &mut Context) -> GameResult { + let subscene_ref = &mut self.subscene; + if subscene_ref.is_some() { + subscene_ref.as_mut().unwrap().tick(state, ctx)?; + + if keyboard::is_key_pressed(ctx, ScanCode::Escape) { + *subscene_ref = None; + } + + // hijack scene switches + let next_scene = std::mem::take(&mut state.next_scene); + if let Some(next_scene) = next_scene { + *subscene_ref = if let Ok(game_scene) = next_scene.downcast() { + let mut game_scene: Box = game_scene; + game_scene.init(state, ctx)?; + Some(game_scene) + } else { + None + }; + } + + if subscene_ref.is_none() { + state.sound_manager.play_song(0, &state.constants, &state.settings, ctx)?; + } + + return Ok(()); + } + + Ok(()) + } + + fn draw_tick(&mut self, state: &mut SharedGameState) -> GameResult { + if let Some(scene) = &mut self.subscene { + scene.draw_tick(state)?; + return Ok(()); + } + + Ok(()) + } + + fn draw(&self, state: &mut SharedGameState, ctx: &mut Context) -> GameResult { + if let Some(scene) = &self.subscene { + scene.draw(state, ctx)?; + state.font.draw_text( + "Press [ESC] to return.".chars(), + 4.0, + 4.0, + &state.constants, + &mut state.texture_set, + ctx, + )?; + return Ok(()); + } + + if let Some(instance) = self.instances.get(self.selected_instance) { + instance.draw(state, ctx, self.current_tool)?; + } + Ok(()) } @@ -31,12 +203,25 @@ impl Scene for EditorScene { &mut self, _game_ui: &mut Components, state: &mut SharedGameState, - _ctx: &mut Context, + ctx: &mut Context, ui: &mut imgui::Ui, ) -> GameResult { + self.perform_actions(state, ctx); + + if let Some(_) = self.subscene { + return Ok(()); + } + + let mut menu_bar_size = (0.0, 0.0); if let Some(menu_bar) = ui.begin_main_menu_bar() { + let [menu_bar_w, menu_bar_h] = ui.window_size(); + menu_bar_size = (menu_bar_w, menu_bar_h); + if let Some(menu) = ui.begin_menu("File") { - MenuItem::new("Open stage").shortcut("Ctrl+O").build(ui); + if MenuItem::new("Open stage").shortcut("Ctrl+O").build(ui) { + self.stage_list.show(); + } + ui.separator(); if MenuItem::new("Exit editor").build(ui) { @@ -48,6 +233,117 @@ impl Scene for EditorScene { menu_bar.end(); } + Window::new("Toolbar") + .title_bar(false) + .resizable(false) + .position([0.0, menu_bar_size.1], Condition::Always) + .size([menu_bar_size.0, 0.0], Condition::Always) + .build(ui, || { + if ui.tool_button("Move", self.current_tool == CurrentTool::Move) { + self.current_tool = CurrentTool::Move; + } + ui.same_line(); + if ui.tool_button("Brush", self.current_tool == CurrentTool::Brush) { + self.current_tool = CurrentTool::Brush; + } + ui.same_line(); + if ui.tool_button("Fill", self.current_tool == CurrentTool::Fill) { + self.current_tool = CurrentTool::Fill; + } + ui.same_line(); + if ui.tool_button("Rectangle", self.current_tool == CurrentTool::Rectangle) { + self.current_tool = CurrentTool::Rectangle; + } + + ui.same_line(); + ui.text("|"); + + ui.same_line(); + if ui.button("Test Stage") { + self.test_stage(state, ctx); + } + + if let Some(tab) = ui.tab_bar("Stages") { + for (idx, inst) in self.instances.iter().enumerate() { + let mut flags = TabItemFlags::NO_CLOSE_WITH_MIDDLE_MOUSE_BUTTON; + if self.switch_tab && self.selected_instance == idx { + self.switch_tab = false; + flags |= TabItemFlags::SET_SELECTED; + } + + if let Some(item) = TabItem::new(&inst.stage.data.name).flags(flags).begin(ui) { + if !self.switch_tab { + self.selected_instance = idx; + } + item.end(); + } + } + + tab.end(); + } + }); + + self.stage_list.action(state, ctx, ui); + + if let Some(instance) = self.instances.get_mut(self.selected_instance) { + instance.process(state, ctx, ui, self.current_tool); + } + Ok(()) } } + +struct StageListWindow { + visible: bool, + selected_stage: i32, + actions: Vec, +} + +enum StageListAction { + OpenStage(usize), +} + +impl StageListWindow { + fn new() -> Self { + StageListWindow { visible: false, selected_stage: 0, actions: Vec::new() } + } + + fn show(&mut self) { + self.visible = true; + } + + fn action(&mut self, state: &mut SharedGameState, ctx: &mut Context, ui: &mut imgui::Ui) { + if !self.visible { + return; + } + + Window::new("Stage list") + .resizable(false) + .collapsible(false) + .position_pivot([0.5, 0.5]) + .size([300.0, 352.0], Condition::FirstUseEver) + .build(ui, || { + let mut stages = Vec::with_capacity(state.stages.len()); + for stage in state.stages.iter() { + stages.push(stage.name.as_str()); + } + + ui.push_item_width(-1.0); + ui.list_box("", &mut self.selected_stage, &stages, 14); + + ui.disabled(self.selected_stage < 0, || { + if ui.button("Open") { + self.actions.push(StageListAction::OpenStage(self.selected_stage as usize)); + } + + ui.same_line(); + if ui.button("Edit table entry") {} + }); + + ui.same_line(); + if ui.button("Cancel") { + self.visible = false; + } + }); + } +} diff --git a/src/scene/game_scene.rs b/src/scene/game_scene.rs index 0b6a9ae..e66d84b 100644 --- a/src/scene/game_scene.rs +++ b/src/scene/game_scene.rs @@ -5,7 +5,7 @@ use std::rc::Rc; use log::info; use crate::caret::CaretType; -use crate::common::{interpolate_fix9_scale, Color, Direction, Rect}; +use crate::common::{Color, Direction, interpolate_fix9_scale, Rect}; use crate::components::background::Background; use crate::components::boss_life_bar::BossLifeBar; use crate::components::credits::Credits; @@ -21,30 +21,30 @@ use crate::components::tilemap::{TileLayer, Tilemap}; use crate::components::water_renderer::WaterRenderer; use crate::entity::GameEntity; use crate::frame::{Frame, UpdateTarget}; +use crate::framework::{filesystem, graphics}; use crate::framework::backend::SpriteBatchCommand; use crate::framework::context::Context; use crate::framework::error::GameResult; -use crate::framework::graphics::{draw_rect, BlendMode, FilterMode}; +use crate::framework::graphics::{BlendMode, draw_rect, FilterMode}; use crate::framework::ui::Components; -use crate::framework::{filesystem, graphics}; use crate::input::touch_controls::TouchControlType; use crate::inventory::{Inventory, TakeExperienceResult}; use crate::map::WaterParams; +use crate::npc::{NPC, NPCLayer}; use crate::npc::boss::BossNPC; use crate::npc::list::NPCList; -use crate::npc::{NPCLayer, NPC}; -use crate::physics::{PhysicalEntity, OFFSETS}; +use crate::physics::{OFFSETS, PhysicalEntity}; use crate::player::{Player, TargetPlayer}; use crate::rng::XorShift; -use crate::scene::title_scene::TitleScene; use crate::scene::Scene; +use crate::scene::title_scene::TitleScene; use crate::scripting::tsc::credit_script::CreditScriptVM; use crate::scripting::tsc::text_script::{ScriptMode, TextScriptExecutionState, TextScriptVM}; use crate::shared_game_state::{SharedGameState, TileSize}; use crate::stage::{BackgroundType, Stage, StageTexturePaths}; use crate::texture_set::SpriteBatch; -use crate::weapon::bullet::BulletManager; use crate::weapon::{Weapon, WeaponType}; +use crate::weapon::bullet::BulletManager; pub struct GameScene { pub tick: u32, @@ -96,11 +96,16 @@ impl GameScene { info!("Loading stage {} ({})", id, &state.stages[id].map); let stage = Stage::load(&state.base_path, &state.stages[id], ctx)?; info!("Loaded stage: {}", stage.data.name); + + GameScene::from_stage(state, ctx, stage, id) + } + + pub fn from_stage(state: &mut SharedGameState, ctx: &mut Context, stage: Stage, id: usize) -> GameResult { let mut water_params = WaterParams::new(); let mut water_renderer = WaterRenderer::new(); if let Ok(water_param_file) = - filesystem::open(ctx, [&state.base_path, "Stage/", &state.stages[id].tileset.name, ".pxw"].join("")) + filesystem::open(ctx, [&state.base_path, "Stage/", &state.stages[id].tileset.name, ".pxw"].join("")) { water_params.load_from(water_param_file)?; info!("Loaded water parameters file."); @@ -109,23 +114,9 @@ impl GameScene { } let stage_textures = { - let background = stage.data.background.filename(); - let (tileset_fg, tileset_mg, tileset_bg) = if let Some(pxpack_data) = stage.data.pxpack_data.as_ref() { - let t_fg = ["Stage/", &pxpack_data.tileset_fg].join(""); - let t_mg = ["Stage/", &pxpack_data.tileset_mg].join(""); - let t_bg = ["Stage/", &pxpack_data.tileset_bg].join(""); - - (t_fg, t_mg, t_bg) - } else { - let tex_tileset_name = ["Stage/", &stage.data.tileset.filename()].join(""); - - (tex_tileset_name.clone(), tex_tileset_name.clone(), tex_tileset_name) - }; - - let npc1 = ["Npc/", &stage.data.npc1.filename()].join(""); - let npc2 = ["Npc/", &stage.data.npc2.filename()].join(""); - - Rc::new(RefCell::new(StageTexturePaths { background, tileset_fg, tileset_mg, tileset_bg, npc1, npc2 })) + let mut textures = StageTexturePaths::new(); + textures.update(&stage); + Rc::new(RefCell::new(textures)) }; Ok(Self { @@ -149,16 +140,7 @@ impl GameScene { tilemap: Tilemap::new(), text_boxes: TextBoxes::new(), fade: Fade::new(), - frame: Frame { - x: 0, - y: 0, - prev_x: 0, - prev_y: 0, - update_target: UpdateTarget::Player, - target_x: 0, - target_y: 0, - wait: 16, - }, + frame: Frame::new(), stage_id: id, npc_list: NPCList::new(), boss: BossNPC::new(), diff --git a/src/scene/loading_scene.rs b/src/scene/loading_scene.rs index f6e763d..aadc475 100644 --- a/src/scene/loading_scene.rs +++ b/src/scene/loading_scene.rs @@ -5,9 +5,9 @@ use crate::npc::NPCTable; use crate::scene::no_data_scene::NoDataScene; use crate::scene::Scene; use crate::scripting::tsc::credit_script::CreditScript; +use crate::scripting::tsc::text_script::TextScript; use crate::shared_game_state::SharedGameState; use crate::stage::StageData; -use crate::scripting::tsc::text_script::TextScript; pub struct LoadingScene { tick: usize, @@ -15,9 +15,7 @@ pub struct LoadingScene { impl LoadingScene { pub fn new() -> Self { - Self { - tick: 0, - } + Self { tick: 0 } } fn load_stuff(&mut self, state: &mut SharedGameState, ctx: &mut Context) -> GameResult { @@ -71,8 +69,10 @@ impl Scene for LoadingScene { fn draw(&self, state: &mut SharedGameState, ctx: &mut Context) -> GameResult { match state.texture_set.get_or_load_batch(ctx, &state.constants, "Loading") { Ok(batch) => { - batch.add(((state.canvas_size.0 - batch.width() as f32) / 2.0).floor(), - ((state.canvas_size.1 - batch.height() as f32) / 2.0).floor()); + batch.add( + ((state.canvas_size.0 - batch.width() as f32) / 2.0).floor(), + ((state.canvas_size.1 - batch.height() as f32) / 2.0).floor(), + ); batch.draw(ctx)?; } Err(err) => { diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 146a489..2791db1 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -1,30 +1,48 @@ use crate::framework::context::Context; use crate::framework::error::GameResult; - -use crate::shared_game_state::SharedGameState; use crate::framework::ui::Components; +use crate::shared_game_state::SharedGameState; +#[cfg(feature = "editor")] +pub mod editor_scene; pub mod game_scene; pub mod loading_scene; pub mod no_data_scene; pub mod title_scene; -pub mod editor_scene; /// Implement this trait on any object that represents an interactive game screen. -pub trait Scene { +pub trait Scene: downcast::Any { /// Called when the scene is shown. - fn init(&mut self, _state: &mut SharedGameState, _ctx: &mut Context) -> GameResult { Ok(()) } + fn init(&mut self, _state: &mut SharedGameState, _ctx: &mut Context) -> GameResult { + Ok(()) + } /// Called at game tick. Perform any game state updates there. - fn tick(&mut self, _state: &mut SharedGameState, _ctx: &mut Context) -> GameResult { Ok(()) } + fn tick(&mut self, _state: &mut SharedGameState, _ctx: &mut Context) -> GameResult { + Ok(()) + } /// Called before draws between two ticks to update previous positions used for interpolation. /// DO NOT perform updates of the game state there. - fn draw_tick(&mut self, _state: &mut SharedGameState) -> GameResult { Ok(()) } + fn draw_tick(&mut self, _state: &mut SharedGameState) -> GameResult { + Ok(()) + } /// Called during frame rendering operation. - fn draw(&self, _state: &mut SharedGameState, _ctx: &mut Context) -> GameResult { Ok(()) } + fn draw(&self, _state: &mut SharedGameState, _ctx: &mut Context) -> GameResult { + Ok(()) + } /// Independent draw meant for debug overlay, that lets you mutate the game state. - fn imgui_draw(&mut self, _game_ui: &mut Components, _state: &mut SharedGameState, _ctx: &mut Context, _frame: &mut imgui::Ui) -> GameResult { Ok(()) } + fn imgui_draw( + &mut self, + _game_ui: &mut Components, + _state: &mut SharedGameState, + _ctx: &mut Context, + _frame: &mut imgui::Ui, + ) -> GameResult { + Ok(()) + } } + +downcast::impl_downcast!(dyn Scene); diff --git a/src/scene/no_data_scene.rs b/src/scene/no_data_scene.rs index 0b83090..3662265 100644 --- a/src/scene/no_data_scene.rs +++ b/src/scene/no_data_scene.rs @@ -1,6 +1,5 @@ use crate::framework::context::Context; -use crate::framework::error::{GameResult, GameError}; - +use crate::framework::error::{GameError, GameResult}; use crate::scene::Scene; use crate::shared_game_state::SharedGameState; @@ -27,23 +26,23 @@ impl Scene for NoDataScene { #[allow(unused)] fn tick(&mut self, state: &mut SharedGameState, ctx: &mut Context) -> GameResult { #[cfg(target_os = "android")] - { - use crate::common::Rect; + { + use crate::common::Rect; - if !self.flag { - self.flag = true; - let _ = std::fs::create_dir("/sdcard/doukutsu/"); - let _ = std::fs::write("/sdcard/doukutsu/extract game data here.txt", REL_URL); - let _ = std::fs::write("/sdcard/doukutsu/.nomedia", b""); - } + if !self.flag { + self.flag = true; + let _ = std::fs::create_dir("/sdcard/doukutsu/"); + let _ = std::fs::write("/sdcard/doukutsu/extract game data here.txt", REL_URL); + let _ = std::fs::write("/sdcard/doukutsu/.nomedia", b""); + } - let screen = Rect::new(0, 0, state.canvas_size.0 as isize, state.canvas_size.1 as isize); - if state.touch_controls.consume_click_in(screen) { - if let Err(err) = webbrowser::open(REL_URL) { - self.err = err.to_string(); - } + let screen = Rect::new(0, 0, state.canvas_size.0 as isize, state.canvas_size.1 as isize); + if state.touch_controls.consume_click_in(screen) { + if let Err(err) = webbrowser::open(REL_URL) { + self.err = err.to_string(); } } + } Ok(()) } @@ -51,43 +50,82 @@ impl Scene for NoDataScene { { let die = "doukutsu-rs internal error"; let die_width = state.font.text_width(die.chars().clone(), &state.constants); - state.font.draw_colored_text(die.chars(), (state.canvas_size.0 - die_width) / 2.0, 10.0, - (255, 100, 100, 255), &state.constants, &mut state.texture_set, ctx)?; + state.font.draw_colored_text( + die.chars(), + (state.canvas_size.0 - die_width) / 2.0, + 10.0, + (255, 100, 100, 255), + &state.constants, + &mut state.texture_set, + ctx, + )?; } { let ftl = "Failed to load game data."; let ftl_width = state.font.text_width(ftl.chars().clone(), &state.constants); - state.font.draw_colored_text(ftl.chars(), (state.canvas_size.0 - ftl_width) / 2.0, 30.0, - (255, 100, 100, 255), &state.constants, &mut state.texture_set, ctx)?; + state.font.draw_colored_text( + ftl.chars(), + (state.canvas_size.0 - ftl_width) / 2.0, + 30.0, + (255, 100, 100, 255), + &state.constants, + &mut state.texture_set, + ctx, + )?; } #[cfg(target_os = "android")] - { - let ftl = "It's likely that you haven't extracted the game data properly."; - let ftl2 = "Click here to open the guide."; - let ftl_width = state.font.text_width(ftl.chars().clone(), &state.constants); - let ftl2_width = state.font.text_width(ftl2.chars().clone(), &state.constants); - let ftl3_width = state.font.text_width(REL_URL.chars().clone(), &state.constants); + { + let ftl = "It's likely that you haven't extracted the game data properly."; + let ftl2 = "Click here to open the guide."; + let ftl_width = state.font.text_width(ftl.chars().clone(), &state.constants); + let ftl2_width = state.font.text_width(ftl2.chars().clone(), &state.constants); + let ftl3_width = state.font.text_width(REL_URL.chars().clone(), &state.constants); - state.font.draw_colored_text(ftl.chars(), (state.canvas_size.0 - ftl_width) / 2.0, 60.0, - (255, 255, 0, 255), &state.constants, &mut state.texture_set, ctx)?; + state.font.draw_colored_text( + ftl.chars(), + (state.canvas_size.0 - ftl_width) / 2.0, + 60.0, + (255, 255, 0, 255), + &state.constants, + &mut state.texture_set, + ctx, + )?; + state.font.draw_colored_text( + ftl2.chars(), + (state.canvas_size.0 - ftl2_width) / 2.0, + 80.0, + (255, 255, 0, 255), + &state.constants, + &mut state.texture_set, + ctx, + )?; - state.font.draw_colored_text(ftl2.chars(), (state.canvas_size.0 - ftl2_width) / 2.0, 80.0, - (255, 255, 0, 255), &state.constants, &mut state.texture_set, ctx)?; - - state.font.draw_colored_text(REL_URL.chars(), (state.canvas_size.0 - ftl3_width) / 2.0, 100.0, - (255, 255, 0, 255), &state.constants, &mut state.texture_set, ctx)?; - } + state.font.draw_colored_text( + REL_URL.chars(), + (state.canvas_size.0 - ftl3_width) / 2.0, + 100.0, + (255, 255, 0, 255), + &state.constants, + &mut state.texture_set, + ctx, + )?; + } { let err_width = state.font.text_width(self.err.chars().clone(), &state.constants); - state.font.draw_text(self.err.chars(), (state.canvas_size.0 - err_width) / 2.0, 140.0, - &state.constants, &mut state.texture_set, ctx)?; + state.font.draw_text( + self.err.chars(), + (state.canvas_size.0 - err_width) / 2.0, + 140.0, + &state.constants, + &mut state.texture_set, + ctx, + )?; } - Ok(()) } } diff --git a/src/scene/title_scene.rs b/src/scene/title_scene.rs index 82977d3..cccca09 100644 --- a/src/scene/title_scene.rs +++ b/src/scene/title_scene.rs @@ -4,8 +4,8 @@ use crate::framework::error::GameResult; use crate::framework::graphics; use crate::input::combined_menu_controller::CombinedMenuController; use crate::input::touch_controls::TouchControlType; -use crate::menu::settings_menu::SettingsMenu; use crate::menu::{Menu, MenuEntry, MenuSelectionResult}; +use crate::menu::settings_menu::SettingsMenu; use crate::scene::Scene; use crate::shared_game_state::SharedGameState; @@ -144,10 +144,10 @@ impl Scene for TitleScene { } MenuSelectionResult::Selected(3, _) => { #[cfg(feature = "editor")] - { - use crate::scene::editor_scene::EditorScene; - state.next_scene = Some(Box::new(EditorScene::new())); - } + { + use crate::scene::editor_scene::EditorScene; + state.next_scene = Some(Box::new(EditorScene::new())); + } } MenuSelectionResult::Selected(4, _) => { state.shutdown(); diff --git a/src/stage.rs b/src/stage.rs index 5ba216f..9174cd4 100644 --- a/src/stage.rs +++ b/src/stage.rs @@ -1,8 +1,8 @@ use std::io::{Cursor, Read}; use std::str::from_utf8; -use byteorder::LE; use byteorder::ReadBytesExt; +use byteorder::LE; use log::info; use crate::common::Color; @@ -511,6 +511,7 @@ impl StageData { } } +#[derive(Clone)] pub struct Stage { pub map: Map, pub data: StageData, @@ -609,4 +610,25 @@ impl StageTexturePaths { npc2: "Npc/Npc0".to_owned(), } } + + pub fn update(&mut self, stage: &Stage) { + self.background = stage.data.background.filename(); + let (tileset_fg, tileset_mg, tileset_bg) = if let Some(pxpack_data) = stage.data.pxpack_data.as_ref() { + let t_fg = ["Stage/", &pxpack_data.tileset_fg].join(""); + let t_mg = ["Stage/", &pxpack_data.tileset_mg].join(""); + let t_bg = ["Stage/", &pxpack_data.tileset_bg].join(""); + + (t_fg, t_mg, t_bg) + } else { + let tex_tileset_name = ["Stage/", &stage.data.tileset.filename()].join(""); + + (tex_tileset_name.clone(), tex_tileset_name.clone(), tex_tileset_name) + }; + self.tileset_fg = tileset_fg; + self.tileset_mg = tileset_mg; + self.tileset_bg = tileset_bg; + + self.npc1 = ["Npc/", &stage.data.npc1.filename()].join(""); + self.npc2 = ["Npc/", &stage.data.npc2.filename()].join(""); + } } diff --git a/src/texture_set.rs b/src/texture_set.rs index 9420f95..47b7d18 100644 --- a/src/texture_set.rs +++ b/src/texture_set.rs @@ -69,6 +69,8 @@ pub trait SpriteBatch { fn draw(&mut self, ctx: &mut Context) -> GameResult; fn draw_filtered(&mut self, _filter: FilterMode, _ctx: &mut Context) -> GameResult; + + fn get_texture(&self) -> Option<&Box>; } pub struct DummyBatch; @@ -136,6 +138,10 @@ impl SpriteBatch for DummyBatch { fn draw_filtered(&mut self, _filter: FilterMode, _ctx: &mut Context) -> GameResult { Ok(()) } + + fn get_texture(&self) -> Option<&Box> { + None + } } pub struct SubBatch { @@ -309,6 +315,10 @@ impl SpriteBatch for SubBatch { self.batch.clear(); Ok(()) } + + fn get_texture(&self) -> Option<&Box> { + Some(&self.batch) + } } impl SpriteBatch for CombinedBatch { @@ -395,6 +405,10 @@ impl SpriteBatch for CombinedBatch { fn draw_filtered(&mut self, filter: FilterMode, ctx: &mut Context) -> GameResult { self.main_batch.draw_filtered(filter, ctx) } + + fn get_texture(&self) -> Option<&Box> { + self.main_batch.get_texture() + } } pub struct TextureSet {