1
0
Fork 0
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:
Alula 2020-09-20 03:05:41 +02:00
parent 39d0d2d6f4
commit ab7e8da162
No known key found for this signature in database
GPG key ID: 3E00485503A1D8BA
7 changed files with 409 additions and 11 deletions

View file

@ -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> {

View file

@ -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;

View file

@ -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
View 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
}
}

View file

@ -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;

View file

@ -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
View 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(())
}
}