diff --git a/Cargo.toml b/Cargo.toml index bc6f015..df980f5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,6 +44,7 @@ bitflags = "1" bitvec = "0.17.4" byteorder = "1.3" case_insensitive_hashmap = "1.0.0" +chrono = "0.4" cpal = {git = "https://github.com/doukutsu-rs/cpal.git", branch = "android-support"} directories = "2" gfx = "0.18" diff --git a/src/player.rs b/src/player.rs index 7cd48d6..b69fe8e 100644 --- a/src/player.rs +++ b/src/player.rs @@ -5,7 +5,7 @@ use num_traits::clamp; use num_traits::FromPrimitive; use crate::caret::CaretType; -use crate::common::{Condition, Direction, Equipment, fix9_scale, Flag, interpolate_fix9_scale, Rect}; +use crate::common::{Condition, Direction, Equipment, Flag, interpolate_fix9_scale, Rect}; use crate::entity::GameEntity; use crate::frame::Frame; use crate::ggez::{Context, GameResult}; @@ -19,6 +19,18 @@ pub enum ControlMode { IronHead, } +#[derive(Debug, Clone, Copy, PartialEq, Eq, FromPrimitive)] +#[repr(u8)] +/// Cave Story+ player skins +pub enum PlayerAppearance { + Quote = 0, + YellowQuote, + HumanQuote, + HalloweenQuote, + ReindeerQuote, + Curly, +} + #[derive(Clone)] pub struct Player { pub x: isize, @@ -48,6 +60,7 @@ pub struct Player { pub damage: u16, pub air_counter: u16, pub air: u16, + pub appearance: PlayerAppearance, weapon_offset_y: i8, index_x: isize, index_y: isize, @@ -99,6 +112,7 @@ impl Player { damage: 0, air_counter: 0, air: 0, + appearance: PlayerAppearance::Quote, bubble: 0, damage_counter: 0, damage_taken: 0, @@ -109,6 +123,10 @@ impl Player { } } + pub fn get_texture_offset(&self) -> usize { + self.appearance as usize * 64 + if self.equip.has_mimiga_mask() { 32 } else { 0 } + } + fn tick_normal(&mut self, state: &mut SharedGameState) -> GameResult { if !state.control_flags.interactions_disabled() && state.control_flags.control_enabled() { if self.equip.has_air_tank() { @@ -557,6 +575,10 @@ impl Player { if self.anim_num == 1 || self.anim_num == 3 || self.anim_num == 6 || self.anim_num == 8 { self.weapon_rect.top += 1; } + + let offset = self.get_texture_offset(); + self.anim_rect.top += offset; + self.anim_rect.bottom += offset; } pub fn damage(&mut self, hp: isize, state: &mut SharedGameState) { diff --git a/src/scene/game_scene.rs b/src/scene/game_scene.rs index 0fed6c2..a295b27 100644 --- a/src/scene/game_scene.rs +++ b/src/scene/game_scene.rs @@ -12,11 +12,11 @@ use crate::ggez::nalgebra::clamp; use crate::inventory::{Inventory, TakeExperienceResult}; use crate::npc::NPCMap; use crate::physics::PhysicalEntity; -use crate::player::Player; +use crate::player::{Player, PlayerAppearance}; use crate::rng::RNG; use crate::scene::Scene; use crate::scene::title_scene::TitleScene; -use crate::shared_game_state::SharedGameState; +use crate::shared_game_state::{SharedGameState, Season}; use crate::stage::{BackgroundType, Stage}; use crate::text_script::{ConfirmSelection, ScriptMode, TextScriptExecutionState, TextScriptVM}; use crate::texture_set::SizedBatch; @@ -1189,6 +1189,15 @@ impl Scene for GameScene { state.npc_table.tex_npc1_name = ["Npc/", &self.stage.data.npc1.filename()].join(""); state.npc_table.tex_npc2_name = ["Npc/", &self.stage.data.npc2.filename()].join(""); + if state.constants.is_cs_plus { + match state.season { + Season::Halloween => self.player.appearance = PlayerAppearance::HalloweenQuote, + Season::Christmas => self.player.appearance = PlayerAppearance::ReindeerQuote, + _ => {} + } + } + + self.npc_map.boss_map.boss_type = self.stage.data.boss_no as u16; self.frame.target_x = self.player.x; self.frame.target_y = self.player.y; self.frame.immediate_update(state, &self.stage); diff --git a/src/scene/title_scene.rs b/src/scene/title_scene.rs index b091941..ddc9330 100644 --- a/src/scene/title_scene.rs +++ b/src/scene/title_scene.rs @@ -97,8 +97,10 @@ impl Scene for TitleScene { self.option_menu.push_entry(MenuEntry::Toggle("Lighting effects".to_string(), state.settings.lighting_efects)); if state.constants.is_cs_plus { self.option_menu.push_entry(MenuEntry::Toggle("Freeware textures".to_string(), state.settings.original_textures)); + self.option_menu.push_entry(MenuEntry::Toggle("Seasonal textures".to_string(), state.settings.seasonal_textures)); } else { self.option_menu.push_entry(MenuEntry::Disabled("Freeware textures".to_string())); + self.option_menu.push_entry(MenuEntry::Disabled("Seasonal textures".to_string())); } self.option_menu.push_entry(MenuEntry::Active("Join our Discord".to_string())); self.option_menu.push_entry(MenuEntry::Disabled(DISCORD_LINK.to_owned())); @@ -168,23 +170,25 @@ impl Scene for TitleScene { MenuSelectionResult::Selected(3, toggle) => { if let MenuEntry::Toggle(_, value) = toggle { state.settings.original_textures = !state.settings.original_textures; - - let path = if state.settings.original_textures { - "/base/ogph/" - } else { - "/base/" - }; - state.texture_set = TextureSet::new(path); + state.reload_textures(); *value = state.settings.original_textures; } } - MenuSelectionResult::Selected(4, _) => { + MenuSelectionResult::Selected(4, toggle) => { + if let MenuEntry::Toggle(_, value) = toggle { + state.settings.seasonal_textures = !state.settings.seasonal_textures; + state.reload_textures(); + + *value = state.settings.seasonal_textures; + } + } + MenuSelectionResult::Selected(5, _) => { if let Err(e) = webbrowser::open(DISCORD_LINK) { log::warn!("Error opening web browser: {}", e); } } - MenuSelectionResult::Selected(6, _) | MenuSelectionResult::Canceled => { + MenuSelectionResult::Selected(7, _) | MenuSelectionResult::Canceled => { self.current_menu = CurrentMenu::MainMenu; } _ => {} diff --git a/src/shared_game_state.rs b/src/shared_game_state.rs index 4a9805a..6fafd67 100644 --- a/src/shared_game_state.rs +++ b/src/shared_game_state.rs @@ -1,13 +1,16 @@ +use std::io::Seek; use std::ops::Div; -use std::time::Instant; +use std::time::{Instant, SystemTime}; use bitvec::vec::BitVec; +use chrono::{Local, Datelike}; use crate::bmfont_renderer::BMFontRenderer; use crate::caret::{Caret, CaretType}; use crate::common::{ControlFlags, Direction, FadeState, KeyState}; use crate::engine_constants::EngineConstants; use crate::ggez::{Context, filesystem, GameResult, graphics}; +use crate::ggez::filesystem::OpenOptions; use crate::ggez::graphics::Canvas; use crate::npc::{NPC, NPCTable}; use crate::profile::GameProfile; @@ -20,8 +23,6 @@ use crate::str; use crate::text_script::{ScriptMode, TextScriptExecutionState, TextScriptVM}; use crate::texture_set::TextureSet; use crate::touch_controls::TouchControls; -use crate::ggez::filesystem::OpenOptions; -use std::io::Seek; #[derive(PartialEq, Eq, Copy, Clone)] pub enum TimingMode { @@ -48,10 +49,33 @@ impl TimingMode { } } + +#[derive(PartialEq, Eq, Copy, Clone)] +pub enum Season { + None, + Halloween, + Christmas, +} + +impl Season { + pub fn current() -> Season { + let now = Local::now(); + + if (now.month() == 10 && now.day() > 25) || (now.month() == 11 && now.day() < 3) { + Season::Halloween + } else if (now.month() == 12 && now.day() > 23) || (now.month() == 0 && now.day() < 7) { + Season::Christmas + } else { + Season::None + } + } +} + pub struct Settings { pub god_mode: bool, pub infinite_booster: bool, pub speed: f64, + pub seasonal_textures: bool, pub original_textures: bool, pub lighting_efects: bool, pub debug_outlines: bool, @@ -73,15 +97,10 @@ pub struct SharedGameState { pub key_state: KeyState, pub key_trigger: KeyState, pub touch_controls: TouchControls, - pub font: BMFontRenderer, - pub texture_set: TextureSet, pub base_path: String, pub npc_table: NPCTable, pub npc_super_pos: (isize, isize), pub stages: Vec, - pub sound_manager: SoundManager, - pub settings: Settings, - pub constants: EngineConstants, pub new_npcs: Vec, pub frame_time: f64, pub prev_frame_time: f64, @@ -91,6 +110,12 @@ pub struct SharedGameState { pub screen_size: (f32, f32), pub next_scene: Option>, pub textscript_vm: TextScriptVM, + pub season: Season, + pub constants: EngineConstants, + pub font: BMFontRenderer, + pub texture_set: TextureSet, + pub sound_manager: SoundManager, + pub settings: Settings, pub shutdown: bool, key_old: u16, } @@ -98,11 +123,12 @@ pub struct SharedGameState { impl SharedGameState { pub fn new(ctx: &mut Context) -> GameResult { let screen_size = graphics::drawable_size(ctx); - let scale = screen_size.1.div(240.0).floor().max(1.0); + let scale = screen_size.1.div(235.0).floor().max(1.0); let canvas_size = (screen_size.0 / scale, screen_size.1 / scale); let mut constants = EngineConstants::defaults(); let mut base_path = "/"; + let settings = SharedGameState::load_settings(ctx)?; if filesystem::exists(ctx, "/base/Nicalis.bmp") { info!("Cave Story+ (PC) data files detected."); @@ -121,6 +147,14 @@ impl SharedGameState { let font = BMFontRenderer::load(base_path, &constants.font_path, ctx) .or_else(|_| BMFontRenderer::load("/", "builtin/builtin_font.fnt", ctx))?; + let season = Season::current(); + let mut texture_set = TextureSet::new(base_path); + + if constants.is_cs_plus { + texture_set.apply_seasonal_content(season, &settings); + } + + println!("lookup path: {:#?}", texture_set.paths); Ok(SharedGameState { timing_mode: TimingMode::_50Hz, @@ -135,23 +169,10 @@ impl SharedGameState { key_state: KeyState(0), key_trigger: KeyState(0), touch_controls: TouchControls::new(), - font, - texture_set: TextureSet::new(base_path), base_path: str!(base_path), npc_table: NPCTable::new(), npc_super_pos: (0, 0), stages: Vec::with_capacity(96), - sound_manager: SoundManager::new(ctx)?, - settings: Settings { - god_mode: false, - infinite_booster: false, - speed: 1.0, - original_textures: false, - lighting_efects: true, - debug_outlines: false, - touch_controls: cfg!(target_os = "android"), - }, - constants, new_npcs: Vec::with_capacity(8), frame_time: 0.0, prev_frame_time: 0.0, @@ -161,11 +182,40 @@ impl SharedGameState { canvas_size, next_scene: None, textscript_vm: TextScriptVM::new(), - key_old: 0, + season, + constants, + font, + texture_set, + sound_manager: SoundManager::new(ctx)?, + settings, shutdown: false, + key_old: 0, }) } + fn load_settings(ctx: &mut Context) -> GameResult { + Ok(Settings { + god_mode: false, + infinite_booster: false, + speed: 1.0, + seasonal_textures: true, + original_textures: false, + lighting_efects: true, + debug_outlines: false, + touch_controls: cfg!(target_os = "android"), + }) + } + + pub fn reload_textures(&mut self) { + let mut texture_set = TextureSet::new(self.base_path.as_str()); + + if self.constants.is_cs_plus { + texture_set.apply_seasonal_content(self.season, &self.settings); + } + + self.texture_set = texture_set; + } + pub fn start_new_game(&mut self, ctx: &mut Context) -> GameResult { let mut next_scene = GameScene::new(self, ctx, 13)?; next_scene.player.x = 10 * 16 * 0x200; diff --git a/src/texture_set.rs b/src/texture_set.rs index 8baf389..5552d47 100644 --- a/src/texture_set.rs +++ b/src/texture_set.rs @@ -10,10 +10,11 @@ use crate::common::FILE_TYPES; use crate::engine_constants::EngineConstants; use crate::ggez::{Context, GameError, GameResult, graphics}; use crate::ggez::filesystem; -use crate::ggez::graphics::{Drawable, DrawMode, DrawParam, FilterMode, Image, Mesh, Rect, Color}; +use crate::ggez::graphics::{Color, Drawable, DrawMode, DrawParam, FilterMode, Image, Mesh, Rect}; use crate::ggez::graphics::spritebatch::SpriteBatch; use crate::ggez::nalgebra::{Point2, Vector2}; use crate::str; +use crate::shared_game_state::{Season, Settings}; pub struct SizedBatch { pub batch: SpriteBatch, @@ -90,7 +91,7 @@ impl SizedBatch { self.batch.add(param); } - pub fn add_rect_scaled_tinted(&mut self, x: f32, y: f32, color: (u8,u8,u8), scale_x: f32, scale_y: f32, rect: &common::Rect) { + pub fn add_rect_scaled_tinted(&mut self, x: f32, y: f32, color: (u8, u8, u8), scale_x: f32, scale_y: f32, rect: &common::Rect) { if (rect.right - rect.left) == 0 || (rect.bottom - rect.top) == 0 { return; } @@ -122,14 +123,26 @@ impl SizedBatch { pub struct TextureSet { pub tex_map: HashMap, - base_path: String, + pub paths: Vec, } impl TextureSet { pub fn new(base_path: &str) -> TextureSet { TextureSet { tex_map: HashMap::new(), - base_path: base_path.to_string(), + paths: vec![base_path.to_string(), "".to_string()], + } + } + + pub fn apply_seasonal_content(&mut self, season: Season, settings: &Settings) { + if settings.original_textures { + self.paths.insert(0, "/base/ogph/".to_string()) + } else if settings.seasonal_textures { + match season { + Season::Halloween => self.paths.insert(0, "/Halloween/season/".to_string()), + Season::Christmas => self.paths.insert(0, "/Christmas/season/".to_string()), + _ => {} + } } } @@ -175,15 +188,14 @@ impl TextureSet { } pub fn load_texture(&self, ctx: &mut Context, constants: &EngineConstants, name: &str) -> GameResult { - let path = FILE_TYPES + let path = self.paths.iter().find_map(|s| FILE_TYPES .iter() - .map(|ext| [&self.base_path, name, ext].join("")) - .find(|path| filesystem::exists(ctx, path)) - .or_else(|| FILE_TYPES - .iter() - .map(|ext| [name, ext].join("")) - .find(|path| filesystem::exists(ctx, path))) - .ok_or_else(|| GameError::ResourceLoadError(format!("Texture {:?} does not exist.", name)))?; + .map(|ext| [s, name, ext].join("")) + .find(|path| { + println!("{}", path); + filesystem::exists(ctx, path) + }) + ).ok_or_else(|| GameError::ResourceLoadError(format!("Texture {} does not exist.", name)))?; info!("Loading texture: {}", path);