mirror of
https://github.com/doukutsu-rs/doukutsu-rs
synced 2025-03-25 03:19:27 +00:00
add a cs+/nxengine inspired title screen
This commit is contained in:
parent
39d0d2d6f4
commit
ab7e8da162
|
@ -217,6 +217,14 @@ impl<T: Num + Copy> Rect<T> {
|
|||
bottom: (rect.y + rect.h),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn width(&self) -> T {
|
||||
self.right.sub(self.left)
|
||||
}
|
||||
|
||||
pub fn height(&self) -> T {
|
||||
self.bottom.sub(self.top)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Num + Copy + AsPrimitive<f32>> Into<crate::ggez::graphics::Rect> for Rect<T> {
|
||||
|
|
|
@ -210,6 +210,21 @@ pub struct TextScriptConsts {
|
|||
pub get_item_bottom_right: Rect<usize>,
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct TitleConsts {
|
||||
pub logo_rect: Rect<usize>,
|
||||
pub menu_left_top: Rect<usize>,
|
||||
pub menu_right_top: Rect<usize>,
|
||||
pub menu_left_bottom: Rect<usize>,
|
||||
pub menu_right_bottom: Rect<usize>,
|
||||
pub menu_top: Rect<usize>,
|
||||
pub menu_bottom: Rect<usize>,
|
||||
pub menu_middle: Rect<usize>,
|
||||
pub menu_left: Rect<usize>,
|
||||
pub menu_right: Rect<usize>
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct EngineConstants {
|
||||
pub is_cs_plus: bool,
|
||||
|
@ -221,6 +236,7 @@ pub struct EngineConstants {
|
|||
pub weapon: WeaponConsts,
|
||||
pub tex_sizes: CaseInsensitiveHashMap<(usize, usize)>,
|
||||
pub textscript: TextScriptConsts,
|
||||
pub title: TitleConsts,
|
||||
pub font_path: String,
|
||||
pub font_scale: f32,
|
||||
pub font_space_offset: f32,
|
||||
|
@ -239,6 +255,7 @@ impl Clone for EngineConstants {
|
|||
weapon: self.weapon.clone(),
|
||||
tex_sizes: self.tex_sizes.clone(),
|
||||
textscript: self.textscript.clone(),
|
||||
title: self.title,
|
||||
font_path: self.font_path.clone(),
|
||||
font_scale: self.font_scale,
|
||||
font_space_offset: self.font_space_offset,
|
||||
|
@ -1117,6 +1134,18 @@ impl EngineConstants {
|
|||
get_item_right: Rect { left: 240, top: 8, right: 244, bottom: 16 },
|
||||
get_item_bottom_right: Rect { left: 240, top: 16, right: 244, bottom: 24 },
|
||||
},
|
||||
title: TitleConsts {
|
||||
logo_rect: Rect { left: 0, top: 0, right: 144, bottom: 40 },
|
||||
menu_left_top: Rect { left: 0, top: 0, right: 8, bottom: 8 },
|
||||
menu_right_top: Rect { left: 236, top: 0, right: 244, bottom: 8 },
|
||||
menu_left_bottom: Rect { left: 0, top: 16, right: 8, bottom: 24 },
|
||||
menu_right_bottom: Rect { left: 236, top: 16, right: 244, bottom: 24 },
|
||||
menu_top: Rect { left: 8, top: 0, right: 236, bottom: 8 },
|
||||
menu_middle: Rect { left: 8, top: 8, right: 236, bottom: 16 },
|
||||
menu_bottom: Rect { left: 8, top: 16, right: 236, bottom: 24 },
|
||||
menu_left: Rect { left: 0, top: 8, right: 8, bottom: 16 },
|
||||
menu_right: Rect { left: 236, top: 8, right: 244, bottom: 16 },
|
||||
},
|
||||
font_path: str!("builtin/builtin_font.fnt"),
|
||||
font_scale: 1.0,
|
||||
font_space_offset: -3.0,
|
||||
|
@ -1135,6 +1164,7 @@ impl EngineConstants {
|
|||
self.tex_sizes.insert(str!("Caret"), (320, 320));
|
||||
self.tex_sizes.insert(str!("MyChar"), (200, 384));
|
||||
self.tex_sizes.insert(str!("Npc/NpcRegu"), (320, 410));
|
||||
self.title.logo_rect = Rect { left: 0, top: 0, right: 214, bottom: 50};
|
||||
self.font_path = str!("csfont.fnt");
|
||||
self.font_scale = 0.5;
|
||||
self.font_space_offset = 2.0;
|
||||
|
|
|
@ -59,6 +59,7 @@ mod ggez;
|
|||
mod live_debugger;
|
||||
mod macros;
|
||||
mod map;
|
||||
mod menu;
|
||||
mod npc;
|
||||
mod physics;
|
||||
mod player;
|
||||
|
|
231
src/menu.rs
Normal file
231
src/menu.rs
Normal file
|
@ -0,0 +1,231 @@
|
|||
use crate::common::Rect;
|
||||
use crate::ggez::{Context, GameResult};
|
||||
use crate::SharedGameState;
|
||||
|
||||
pub enum MenuEntry {
|
||||
Active(String),
|
||||
Disabled(String),
|
||||
}
|
||||
|
||||
pub enum MenuSelectionResult<'a> {
|
||||
None,
|
||||
Canceled,
|
||||
Selected(usize, &'a MenuEntry),
|
||||
}
|
||||
|
||||
pub struct Menu {
|
||||
pub x: isize,
|
||||
pub y: isize,
|
||||
pub width: usize,
|
||||
pub height: usize,
|
||||
selected: usize,
|
||||
anim_num: u16,
|
||||
anim_wait: u16,
|
||||
entries: Vec<MenuEntry>,
|
||||
}
|
||||
|
||||
static QUOTE_FRAMES: [usize; 4] = [0, 1, 0, 2];
|
||||
|
||||
impl Menu {
|
||||
pub fn new(x: isize, y: isize, width: usize, height: usize) -> Menu {
|
||||
Menu {
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
selected: 0,
|
||||
anim_num: 0,
|
||||
anim_wait: 0,
|
||||
entries: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push_entry(&mut self, entry: MenuEntry) {
|
||||
self.entries.push(entry);
|
||||
}
|
||||
|
||||
pub fn draw(&self, state: &mut SharedGameState, ctx: &mut Context) -> GameResult {
|
||||
let batch = state.texture_set.get_or_load_batch(ctx, &state.constants, "TextBox")?;
|
||||
|
||||
let mut rect = Rect::new(0, 0, 0, 0);
|
||||
let mut rect2 = Rect::new(0, 0, 0, 0);
|
||||
|
||||
rect = state.constants.title.menu_left_top;
|
||||
batch.add_rect(self.x as f32 - rect.width() as f32, self.y as f32 - rect.height() as f32, &rect);
|
||||
rect = state.constants.title.menu_right_top;
|
||||
batch.add_rect(self.x as f32 + self.width as f32, self.y as f32 - rect.height() as f32, &rect);
|
||||
rect = state.constants.title.menu_left_bottom;
|
||||
batch.add_rect(self.x as f32 - rect.width() as f32, self.y as f32 + self.height as f32, &rect);
|
||||
rect = state.constants.title.menu_right_bottom;
|
||||
batch.add_rect(self.x as f32 + self.width as f32, self.y as f32 + self.height as f32, &rect);
|
||||
|
||||
rect = state.constants.title.menu_top;
|
||||
rect2 = state.constants.title.menu_bottom;
|
||||
let mut x = self.x as f32;
|
||||
let mut y = self.y as f32;
|
||||
let mut width = self.width;
|
||||
let mut height = self.height;
|
||||
|
||||
while width > 0 {
|
||||
rect.right = if width >= rect.width() {
|
||||
width = width.saturating_sub(rect.width());
|
||||
rect.right
|
||||
} else {
|
||||
let old_width = width;
|
||||
width = 0;
|
||||
rect.left + old_width
|
||||
};
|
||||
rect2.right = rect.right;
|
||||
|
||||
batch.add_rect(x, y - rect.height() as f32, &rect);
|
||||
batch.add_rect(x, y + self.height as f32, &rect2);
|
||||
x += rect.width() as f32;
|
||||
}
|
||||
|
||||
x = self.x as f32;
|
||||
rect = state.constants.title.menu_left;
|
||||
rect2 = state.constants.title.menu_right;
|
||||
while height > 0 {
|
||||
rect.bottom = if height >= rect.height() {
|
||||
height = height.saturating_sub(rect.height());
|
||||
rect.bottom
|
||||
} else {
|
||||
let old_height = height;
|
||||
height = 0;
|
||||
rect.top + old_height
|
||||
};
|
||||
rect2.bottom = rect.bottom;
|
||||
|
||||
batch.add_rect(x - rect.width() as f32, y, &rect);
|
||||
batch.add_rect(x + self.width as f32, y, &rect2);
|
||||
y += rect.height() as f32;
|
||||
}
|
||||
|
||||
height = self.height;
|
||||
y = self.y as f32;
|
||||
|
||||
while height > 0 {
|
||||
rect = state.constants.title.menu_middle;
|
||||
width = self.width;
|
||||
x = self.x as f32;
|
||||
|
||||
rect.bottom = if height >= rect.height() {
|
||||
height = height.saturating_sub(rect.height());
|
||||
rect.bottom
|
||||
} else {
|
||||
let old_height = height;
|
||||
height = 0;
|
||||
rect.top + old_height
|
||||
};
|
||||
|
||||
while width > 0 {
|
||||
rect.right = if width >= rect.width() {
|
||||
width = width.saturating_sub(rect.width());
|
||||
rect.right
|
||||
} else {
|
||||
let old_width = width;
|
||||
width = 0;
|
||||
rect.left + old_width
|
||||
};
|
||||
|
||||
batch.add_rect(x, y, &rect);
|
||||
|
||||
x += rect.width() as f32;
|
||||
}
|
||||
|
||||
y += rect.height() as f32;
|
||||
}
|
||||
|
||||
batch.draw(ctx)?;
|
||||
|
||||
let batch = state.texture_set.get_or_load_batch(ctx, &state.constants, "MyChar")?;
|
||||
|
||||
rect.left = QUOTE_FRAMES[self.anim_num as usize] * 16;
|
||||
rect.top = 16;
|
||||
rect.right = rect.left + 16;
|
||||
rect.bottom = rect.top + 16;
|
||||
|
||||
batch.add_rect(self.x as f32,
|
||||
self.y as f32 + 2.0 + (self.selected as f32 * 14.0),
|
||||
&rect,
|
||||
);
|
||||
|
||||
batch.draw(ctx)?;
|
||||
|
||||
y = self.y as f32 + 6.0;
|
||||
for entry in self.entries.iter() {
|
||||
match entry {
|
||||
MenuEntry::Active(name) => {
|
||||
state.font.draw_text(name.chars(), self.x as f32 + 20.0, y, &state.constants, &mut state.texture_set, ctx)?;
|
||||
}
|
||||
MenuEntry::Disabled(name) => {
|
||||
state.font.draw_text(name.chars(), self.x as f32 + 20.0, y, &state.constants, &mut state.texture_set, ctx)?;
|
||||
}
|
||||
}
|
||||
|
||||
y += 14.0;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn tick(&mut self, state: &mut SharedGameState) -> MenuSelectionResult {
|
||||
state.update_key_trigger();
|
||||
|
||||
if state.key_trigger.fire() {
|
||||
state.sound_manager.play_sfx(5);
|
||||
return MenuSelectionResult::Canceled;
|
||||
}
|
||||
|
||||
if state.key_trigger.up() || state.key_trigger.down() && !self.entries.is_empty() {
|
||||
state.sound_manager.play_sfx(1);
|
||||
loop {
|
||||
if state.key_trigger.down() {
|
||||
self.selected += 1;
|
||||
if self.selected == self.entries.len() {
|
||||
self.selected = 0;
|
||||
}
|
||||
} else {
|
||||
if self.selected == 0 {
|
||||
self.selected = self.entries.len();
|
||||
}
|
||||
self.selected -= 1;
|
||||
}
|
||||
|
||||
if let Some(entry) = self.entries.get(self.selected) {
|
||||
match entry {
|
||||
MenuEntry::Active(_) => { break; }
|
||||
_ => {}
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if state.key_trigger.jump() && !self.entries.is_empty() {
|
||||
if let Some(entry) = self.entries.get(self.selected) {
|
||||
match entry {
|
||||
MenuEntry::Active(_) => {
|
||||
state.sound_manager.play_sfx(18);
|
||||
return MenuSelectionResult::Selected(self.selected, entry);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// todo nikumaru counter support
|
||||
self.anim_wait += 1;
|
||||
if self.anim_wait > 8 {
|
||||
self.anim_wait = 0;
|
||||
|
||||
self.anim_num += 1;
|
||||
if self.anim_num >= QUOTE_FRAMES.len() as u16 {
|
||||
self.anim_num = 0;
|
||||
}
|
||||
}
|
||||
|
||||
MenuSelectionResult::None
|
||||
}
|
||||
}
|
|
@ -1,11 +1,10 @@
|
|||
use crate::ggez::{Context, filesystem, GameResult};
|
||||
use crate::scene::game_scene::GameScene;
|
||||
use crate::npc::NPCTable;
|
||||
use crate::scene::Scene;
|
||||
use crate::scene::title_scene::TitleScene;
|
||||
use crate::SharedGameState;
|
||||
use crate::stage::StageData;
|
||||
use crate::text_script::{TextScript, TextScriptExecutionState};
|
||||
use crate::common::FadeState;
|
||||
use crate::npc::NPCTable;
|
||||
use crate::text_script::TextScript;
|
||||
|
||||
pub struct LoadingScene {
|
||||
tick: usize,
|
||||
|
@ -30,13 +29,7 @@ impl Scene for LoadingScene {
|
|||
let head_script = TextScript::load_from(filesystem::open(ctx, [&state.base_path, "/Head.tsc"].join(""))?)?;
|
||||
state.textscript_vm.set_global_script(head_script);
|
||||
|
||||
let mut next_scene = GameScene::new(state, ctx, 13)?;
|
||||
next_scene.player.x = 10 * 16 * 0x200;
|
||||
next_scene.player.y = 8 * 16 * 0x200;
|
||||
state.fade_state = FadeState::Hidden;
|
||||
state.textscript_vm.state = TextScriptExecutionState::Running(200, 0);
|
||||
|
||||
state.next_scene = Some(Box::new(next_scene));
|
||||
state.next_scene = Some(Box::new(TitleScene::new()));
|
||||
}
|
||||
|
||||
self.tick += 1;
|
||||
|
|
|
@ -5,6 +5,7 @@ use crate::ui::Components;
|
|||
|
||||
pub mod game_scene;
|
||||
pub mod loading_scene;
|
||||
pub mod title_scene;
|
||||
|
||||
pub trait Scene {
|
||||
fn init(&mut self, _state: &mut SharedGameState, _ctx: &mut Context) -> GameResult { Ok(()) }
|
||||
|
|
134
src/scene/title_scene.rs
Normal file
134
src/scene/title_scene.rs
Normal file
|
@ -0,0 +1,134 @@
|
|||
use crate::common::{FadeState, Rect};
|
||||
use crate::ggez::{Context, GameResult};
|
||||
use crate::menu::{Menu, MenuSelectionResult, MenuEntry};
|
||||
use crate::scene::game_scene::GameScene;
|
||||
use crate::scene::Scene;
|
||||
use crate::SharedGameState;
|
||||
use crate::text_script::TextScriptExecutionState;
|
||||
|
||||
pub struct TitleScene {
|
||||
tick: usize,
|
||||
title_menu: Menu,
|
||||
}
|
||||
|
||||
impl TitleScene {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
tick: 0,
|
||||
title_menu: Menu::new(0, 0, 100, 78),
|
||||
}
|
||||
}
|
||||
|
||||
fn start_game(&self, state: &mut SharedGameState, ctx: &mut Context) -> GameResult {
|
||||
let mut next_scene = GameScene::new(state, ctx, 13)?;
|
||||
next_scene.player.x = 10 * 16 * 0x200;
|
||||
next_scene.player.y = 8 * 16 * 0x200;
|
||||
state.fade_state = FadeState::Hidden;
|
||||
state.textscript_vm.state = TextScriptExecutionState::Running(200, 0);
|
||||
|
||||
state.next_scene = Some(Box::new(next_scene));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn draw_background(&self, state: &mut SharedGameState, ctx: &mut Context) -> GameResult {
|
||||
let batch = state.texture_set.get_or_load_batch(ctx, &state.constants, "bkMoon")?;
|
||||
let offset = (self.tick % 640) as isize;
|
||||
|
||||
batch.add_rect(((state.canvas_size.0 - 320.0) / 2.0).floor(), 0.0,
|
||||
&Rect::<usize>::new_size(0, 0, 320, 88));
|
||||
|
||||
for x in ((-offset / 2)..(state.canvas_size.0 as isize)).step_by(320) {
|
||||
batch.add_rect(x as f32, 88.0,
|
||||
&Rect::<usize>::new_size(0, 88, 320, 35));
|
||||
}
|
||||
|
||||
for x in ((-offset % 320)..(state.canvas_size.0 as isize)).step_by(320) {
|
||||
batch.add_rect(x as f32, 123.0,
|
||||
&Rect::<usize>::new_size(0, 123, 320, 23));
|
||||
}
|
||||
|
||||
for x in ((-offset * 2)..(state.canvas_size.0 as isize)).step_by(320) {
|
||||
batch.add_rect(x as f32, 146.0,
|
||||
&Rect::<usize>::new_size(0, 146, 320, 30));
|
||||
}
|
||||
|
||||
for x in ((-offset * 4)..(state.canvas_size.0 as isize)).step_by(320) {
|
||||
batch.add_rect(x as f32, 176.0,
|
||||
&Rect::<usize>::new_size(0, 176, 320, 64));
|
||||
}
|
||||
|
||||
batch.draw(ctx)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn draw_text_centered(&self, text: &str, y: f32, state: &mut SharedGameState, ctx: &mut Context) -> GameResult {
|
||||
let width = state.font.text_width(text.chars(), &state.constants);
|
||||
state.font.draw_text(text.chars(), ((state.canvas_size.0 - width) / 2.0).floor(), y, &state.constants, &mut state.texture_set, ctx)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
static ENGINE_VERSION: &str = "doukutsu-rs 0.1.0";
|
||||
// asset copyright for freeware version
|
||||
static COPYRIGHT_PIXEL: &str = "2004.12 Studio Pixel";
|
||||
// asset copyright for Nicalis, why they've even replaced © with @?
|
||||
static COPYRIGHT_NICALIS: &str = "@2011 NICALIS INC.";
|
||||
static COPYRIGHT_NICALIS_SWITCH: &str = "@2017 NICALIS INC."; // untested?
|
||||
|
||||
impl Scene for TitleScene {
|
||||
fn tick(&mut self, state: &mut SharedGameState, ctx: &mut Context) -> GameResult {
|
||||
if self.tick == 0 {
|
||||
state.sound_manager.play_song(24, &state.constants, ctx)?;
|
||||
self.title_menu.push_entry(MenuEntry::Active("Load game".to_string()));
|
||||
self.title_menu.push_entry(MenuEntry::Active("New game".to_string()));
|
||||
self.title_menu.push_entry(MenuEntry::Disabled("Options".to_string()));
|
||||
self.title_menu.push_entry(MenuEntry::Disabled("Editor".to_string()));
|
||||
self.title_menu.push_entry(MenuEntry::Active("Quit".to_string()));
|
||||
}
|
||||
|
||||
self.title_menu.x = ((state.canvas_size.0 - self.title_menu.width as f32) / 2.0).floor() as isize;
|
||||
self.title_menu.y = ((state.canvas_size.1 + 70.0 - self.title_menu.height as f32) / 2.0).floor() as isize;
|
||||
|
||||
match self.title_menu.tick(state) {
|
||||
MenuSelectionResult::Selected(0, _) => {
|
||||
self.start_game(state, ctx);
|
||||
}
|
||||
MenuSelectionResult::Selected(1, _) => {
|
||||
self.start_game(state, ctx);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
self.tick += 1;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn draw(&self, state: &mut SharedGameState, ctx: &mut Context) -> GameResult {
|
||||
self.draw_background(state, ctx)?;
|
||||
|
||||
{
|
||||
let batch = state.texture_set.get_or_load_batch(ctx, &state.constants, "Title")?;
|
||||
batch.add_rect(((state.canvas_size.0 - state.constants.title.logo_rect.width() as f32) / 2.0).floor(),
|
||||
40.0,
|
||||
&state.constants.title.logo_rect);
|
||||
|
||||
batch.draw(ctx)?;
|
||||
}
|
||||
|
||||
self.draw_text_centered(ENGINE_VERSION, state.canvas_size.1 - 15.0, state, ctx)?;
|
||||
|
||||
if state.constants.is_cs_plus {
|
||||
self.draw_text_centered(COPYRIGHT_NICALIS, state.canvas_size.1 - 30.0, state, ctx)?;
|
||||
} else {
|
||||
self.draw_text_centered(COPYRIGHT_PIXEL, state.canvas_size.1 - 30.0, state, ctx)?;
|
||||
}
|
||||
|
||||
self.title_menu.draw(state, ctx)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue