1
0
Fork 0
mirror of https://github.com/doukutsu-rs/doukutsu-rs synced 2025-04-01 07:16:57 +00:00

add CS+ game difficulties

This commit is contained in:
Sallai József 2022-02-27 12:21:43 +02:00 committed by alula
parent a6272476ba
commit 6b7b6b7032
9 changed files with 241 additions and 30 deletions

View file

@ -1,10 +1,11 @@
use std::fmt;
use std::time::{SystemTime, UNIX_EPOCH};
use lazy_static::lazy_static;
use num_traits::{abs, Num};
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
use serde::de::{SeqAccess, Visitor};
use serde::ser::SerializeTupleStruct;
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
use crate::bitfield;
use crate::texture_set::G_MAG;
@ -392,6 +393,11 @@ pub fn interpolate_fix9_scale(old_val: i32, val: i32, frame_delta: f64) -> f32 {
}
}
pub fn get_timestamp() -> u64 {
let now = SystemTime::now();
now.duration_since(UNIX_EPOCH).unwrap().as_secs() as u64
}
/// A RGBA color in the `sRGB` color space represented as `f32`'s in the range `[0.0-1.0]`
///
/// For convenience, [`WHITE`](constant.WHITE.html) and [`BLACK`](constant.BLACK.html) are provided.

View file

@ -7,7 +7,7 @@ use crate::framework::error::GameResult;
use crate::framework::graphics;
use crate::input::combined_menu_controller::CombinedMenuController;
use crate::menu::save_select_menu::MenuSaveInfo;
use crate::shared_game_state::{MenuCharacter, SharedGameState};
use crate::shared_game_state::{GameDifficulty, MenuCharacter, SharedGameState};
pub mod pause_menu;
pub mod save_select_menu;
@ -435,6 +435,42 @@ impl Menu {
ctx,
)?;
// Difficulty
if state.constants.is_cs_plus && !state.settings.original_textures {
let difficulty = GameDifficulty::from_save_value(save.difficulty);
let batch = state.texture_set.get_or_load_batch(ctx, &state.constants, "MyChar")?;
batch.add_rect(
self.x as f32 + 20.0,
y + 10.0,
&Rect::new_size(
0,
GameDifficulty::get_skinsheet_offset(difficulty).saturating_mul(4 * 16),
16,
16,
),
);
batch.draw(ctx)?;
} else {
let mut difficulty_name: String = "Difficulty: ".to_owned();
match save.difficulty {
0 => difficulty_name.push_str("Normal"),
2 => difficulty_name.push_str("Easy"),
4 => difficulty_name.push_str("Hard"),
_ => difficulty_name.push_str("(unknown)"),
}
state.font.draw_text(
difficulty_name.chars(),
self.x as f32 + 20.0,
y + 10.0,
&state.constants,
&mut state.texture_set,
ctx,
)?;
}
// Weapons
let batch = state.texture_set.get_or_load_batch(ctx, &state.constants, "ArmsImage")?;

View file

@ -5,7 +5,7 @@ use crate::input::combined_menu_controller::CombinedMenuController;
use crate::menu::MenuEntry;
use crate::menu::{Menu, MenuSelectionResult};
use crate::profile::GameProfile;
use crate::shared_game_state::SharedGameState;
use crate::shared_game_state::{GameDifficulty, SharedGameState};
#[derive(Clone, Copy)]
pub struct MenuSaveInfo {
@ -14,21 +14,24 @@ pub struct MenuSaveInfo {
pub life: u16,
pub weapon_count: usize,
pub weapon_id: [u32; 8],
pub difficulty: u8,
}
impl Default for MenuSaveInfo {
fn default() -> Self {
MenuSaveInfo { current_map: 0, max_life: 0, life: 0, weapon_count: 0, weapon_id: [0; 8] }
MenuSaveInfo { current_map: 0, max_life: 0, life: 0, weapon_count: 0, weapon_id: [0; 8], difficulty: 0 }
}
}
pub enum CurrentMenu {
SaveMenu,
DifficultyMenu,
DeleteConfirm,
}
pub struct SaveSelectMenu {
pub saves: [MenuSaveInfo; 3],
current_menu: CurrentMenu,
save_menu: Menu,
difficulty_menu: Menu,
delete_confirm: Menu,
}
@ -37,13 +40,15 @@ impl SaveSelectMenu {
SaveSelectMenu {
saves: [MenuSaveInfo::default(); 3],
current_menu: CurrentMenu::SaveMenu,
save_menu: Menu::new(0, 0, 200, 0),
save_menu: Menu::new(0, 0, 230, 0),
difficulty_menu: Menu::new(0, 0, 130, 0),
delete_confirm: Menu::new(0, 0, 75, 0),
}
}
pub fn init(&mut self, state: &mut SharedGameState, ctx: &Context) -> GameResult {
self.save_menu = Menu::new(0, 0, 200, 0);
self.save_menu = Menu::new(0, 0, 230, 0);
self.difficulty_menu = Menu::new(0, 0, 130, 0);
self.delete_confirm = Menu::new(0, 0, 75, 0);
for (iter, save) in self.saves.iter_mut().enumerate() {
@ -55,6 +60,7 @@ impl SaveSelectMenu {
save.life = loaded_save.life;
save.weapon_count = loaded_save.weapon_data.iter().filter(|weapon| weapon.weapon_id != 0).count();
save.weapon_id = loaded_save.weapon_data.map(|weapon| weapon.weapon_id);
save.difficulty = loaded_save.difficulty;
self.save_menu.push_entry(MenuEntry::SaveData(*save));
} else {
@ -65,6 +71,14 @@ impl SaveSelectMenu {
self.save_menu.push_entry(MenuEntry::Active("< Back".to_owned()));
self.save_menu.push_entry(MenuEntry::Disabled("Press Right to Delete".to_owned()));
self.difficulty_menu.push_entry(MenuEntry::Disabled("Select Difficulty".to_owned()));
self.difficulty_menu.push_entry(MenuEntry::Active("Easy".to_owned()));
self.difficulty_menu.push_entry(MenuEntry::Active("Normal".to_owned()));
self.difficulty_menu.push_entry(MenuEntry::Active("Hard".to_owned()));
self.difficulty_menu.push_entry(MenuEntry::Active("< Back".to_owned()));
self.difficulty_menu.selected = 2;
self.delete_confirm.push_entry(MenuEntry::Disabled("Delete?".to_owned()));
self.delete_confirm.push_entry(MenuEntry::Active("Yes".to_owned()));
self.delete_confirm.push_entry(MenuEntry::Active("No".to_owned()));
@ -80,6 +94,10 @@ impl SaveSelectMenu {
self.save_menu.update_height();
self.save_menu.x = ((state.canvas_size.0 - self.save_menu.width as f32) / 2.0).floor() as isize;
self.save_menu.y = 30 + ((state.canvas_size.1 - self.save_menu.height as f32) / 2.0).floor() as isize;
self.difficulty_menu.update_height();
self.difficulty_menu.x = ((state.canvas_size.0 - self.difficulty_menu.width as f32) / 2.0).floor() as isize;
self.difficulty_menu.y =
30 + ((state.canvas_size.1 - self.difficulty_menu.height as f32) / 2.0).floor() as isize;
self.delete_confirm.update_height();
self.delete_confirm.x = ((state.canvas_size.0 - self.delete_confirm.width as f32) / 2.0).floor() as isize;
self.delete_confirm.y = 30 + ((state.canvas_size.1 - self.delete_confirm.height as f32) / 2.0).floor() as isize
@ -95,22 +113,20 @@ impl SaveSelectMenu {
self.update_sizes(state);
match self.current_menu {
CurrentMenu::SaveMenu => match self.save_menu.tick(controller, state) {
MenuSelectionResult::Selected(0, _) => {
state.save_slot = 1;
state.reload_resources(ctx)?;
state.load_or_start_game(ctx)?;
}
MenuSelectionResult::Selected(1, _) => {
state.save_slot = 2;
state.reload_resources(ctx)?;
state.load_or_start_game(ctx)?;
}
MenuSelectionResult::Selected(2, _) => {
state.save_slot = 3;
state.reload_resources(ctx)?;
state.load_or_start_game(ctx)?;
}
MenuSelectionResult::Selected(3, _) | MenuSelectionResult::Canceled => exit_action(),
MenuSelectionResult::Selected(slot, _) => {
state.save_slot = slot + 1;
if let Ok(_) =
filesystem::user_open(ctx, state.get_save_filename(state.save_slot).unwrap_or("".to_string()))
{
state.reload_resources(ctx)?;
state.load_or_start_game(ctx)?;
} else {
self.difficulty_menu.selected = 2;
self.current_menu = CurrentMenu::DifficultyMenu;
}
}
MenuSelectionResult::Right(slot, _, _) => {
if slot <= 2 {
self.current_menu = CurrentMenu::DeleteConfirm;
@ -119,6 +135,17 @@ impl SaveSelectMenu {
}
_ => (),
},
CurrentMenu::DifficultyMenu => match self.difficulty_menu.tick(controller, state) {
MenuSelectionResult::Selected(4, _) | MenuSelectionResult::Canceled => {
self.current_menu = CurrentMenu::SaveMenu;
}
MenuSelectionResult::Selected(item, _) => {
state.difficulty = GameDifficulty::from_index(item - 1);
state.reload_resources(ctx)?;
state.load_or_start_game(ctx)?;
}
_ => (),
},
CurrentMenu::DeleteConfirm => match self.delete_confirm.tick(controller, state) {
MenuSelectionResult::Selected(1, _) => {
state.sound_manager.play_sfx(17); // Player Death sfx
@ -144,6 +171,9 @@ impl SaveSelectMenu {
CurrentMenu::SaveMenu => {
self.save_menu.draw(state, ctx)?;
}
CurrentMenu::DifficultyMenu => {
self.difficulty_menu.draw(state, ctx)?;
}
CurrentMenu::DeleteConfirm => {
self.delete_confirm.draw(state, ctx)?;
}

View file

@ -9,7 +9,7 @@ use crate::npc::list::NPCList;
use crate::npc::{NPCLayer, NPC};
use crate::player::Player;
use crate::rng::RNG;
use crate::shared_game_state::SharedGameState;
use crate::shared_game_state::{GameDifficulty, SharedGameState};
use crate::stage::Stage;
impl NPC {
@ -137,6 +137,11 @@ impl NPC {
}
pub(crate) fn tick_n015_chest_closed(&mut self, state: &mut SharedGameState, npc_list: &NPCList) -> GameResult {
if state.difficulty == GameDifficulty::Hard && self.chest_has_missile_flag() {
self.cond.set_alive(false);
return Ok(());
}
match self.action_num {
0 | 1 => {
if self.action_num == 0 {
@ -332,6 +337,11 @@ impl NPC {
}
pub(crate) fn tick_n021_chest_open(&mut self, state: &mut SharedGameState) -> GameResult {
if state.difficulty == GameDifficulty::Hard && self.chest_has_missile_flag() {
self.cond.set_alive(false);
return Ok(());
}
if self.action_num == 0 {
self.action_num = 1;
@ -429,6 +439,11 @@ impl NPC {
}
pub(crate) fn tick_n032_life_capsule(&mut self, state: &mut SharedGameState) -> GameResult {
if state.difficulty == GameDifficulty::Hard {
self.cond.set_alive(false);
return Ok(());
}
self.anim_counter = (self.anim_counter + 1) % 4;
self.anim_num = self.anim_counter / 2;
self.anim_rect = state.constants.npc.n032_life_capsule[self.anim_num as usize];
@ -2609,4 +2624,9 @@ impl NPC {
Ok(())
}
fn chest_has_missile_flag(&self) -> bool {
let missile_flags: [u16; 9] = [200, 201, 202, 218, 550, 766, 880, 920, 1551];
missile_flags.contains(&self.flag_num)
}
}

View file

@ -839,13 +839,15 @@ impl Player {
self.vel_y = -0x400; // -2.0fix9
}
self.life = self.life.saturating_sub(hp as u16);
let final_hp = state.get_damage(hp);
self.life = self.life.saturating_sub(final_hp as u16);
if self.equip.has_whimsical_star() && self.stars > 0 {
self.stars -= 1;
}
self.damage = self.damage.saturating_add(hp as u16);
self.damage = self.damage.saturating_add(final_hp as u16);
if self.popup.value > 0 {
self.popup.set_value(-(self.damage as i16));
} else {

View file

@ -81,6 +81,7 @@ pub struct BasicPlayerSkin {
direction: Direction,
metadata: SkinMeta,
tick: u16,
skinsheet_offset: u16,
}
impl BasicPlayerSkin {
@ -111,8 +112,16 @@ impl BasicPlayerSkin {
direction: Direction::Left,
metadata,
tick: 0,
skinsheet_offset: state.get_skinsheet_offset(),
}
}
fn get_y_offset_by(&self, y: u16) -> u16 {
return self
.skinsheet_offset
.saturating_mul(self.metadata.frame_size_height.saturating_mul(4))
.saturating_add(y);
}
}
impl PlayerSkin for BasicPlayerSkin {
@ -143,9 +152,13 @@ impl PlayerSkin for BasicPlayerSkin {
let y_offset = if direction == Direction::Left { 0 } else { self.metadata.frame_size_height }
+ match self.appearance {
PlayerAppearanceState::Default => 0,
PlayerAppearanceState::MimigaMask => self.metadata.frame_size_height.saturating_mul(2),
PlayerAppearanceState::Custom(i) => (i as u16).saturating_mul(self.metadata.frame_size_height),
PlayerAppearanceState::Default => self.get_y_offset_by(0),
PlayerAppearanceState::MimigaMask => {
self.get_y_offset_by(self.metadata.frame_size_height.saturating_mul(2))
}
PlayerAppearanceState::Custom(i) => {
self.get_y_offset_by((i as u16).saturating_mul(self.metadata.frame_size_height))
}
};
Rect::new_size(
@ -241,4 +254,8 @@ impl PlayerSkin for BasicPlayerSkin {
rect.bottom = rect.top + 8;
return rect;
}
fn apply_gamestate(&mut self, state: &SharedGameState) {
self.skinsheet_offset = state.get_skinsheet_offset();
}
}

View file

@ -1,5 +1,6 @@
use crate::bitfield;
use crate::common::{Color, Direction, Rect};
use crate::shared_game_state::SharedGameState;
pub mod basic;
@ -92,6 +93,9 @@ pub trait PlayerSkin: PlayerSkinClone {
/// Returns Whimsical Star rect location
fn get_whimsical_star_rect(&self, index: usize) -> Rect<u16>;
/// Applies modifications on the skin based on current state
fn apply_gamestate(&mut self, state: &SharedGameState);
}
pub trait PlayerSkinClone {

View file

@ -3,13 +3,13 @@ use std::io;
use byteorder::{ReadBytesExt, WriteBytesExt, BE, LE};
use num_traits::{clamp, FromPrimitive};
use crate::common::{Direction, FadeState};
use crate::common::{get_timestamp, Direction, FadeState};
use crate::framework::context::Context;
use crate::framework::error::GameError::ResourceLoadError;
use crate::framework::error::GameResult;
use crate::player::ControlMode;
use crate::scene::game_scene::GameScene;
use crate::shared_game_state::SharedGameState;
use crate::shared_game_state::{GameDifficulty, SharedGameState};
use crate::weapon::{WeaponLevel, WeaponType};
pub struct WeaponData {
@ -44,6 +44,8 @@ pub struct GameProfile {
pub teleporter_slots: [TeleporterSlotData; 8],
pub map_flags: [u8; 128],
pub flags: [u8; 1000],
pub timestamp: u64,
pub difficulty: u8,
}
impl GameProfile {
@ -144,6 +146,11 @@ impl GameProfile {
game_scene.inventory_player2 = game_scene.inventory_player1.clone();
game_scene.player1.cond.0 = 0x80;
state.difficulty = GameDifficulty::from_save_value(self.difficulty);
game_scene.player1.skin.apply_gamestate(state);
game_scene.player2.skin.apply_gamestate(state);
}
pub fn dump(state: &mut SharedGameState, game_scene: &mut GameScene) -> GameProfile {
@ -228,6 +235,9 @@ impl GameProfile {
}
}
let timestamp = get_timestamp();
let difficulty = state.difficulty.to_save_value();
GameProfile {
current_map,
current_song,
@ -247,6 +257,8 @@ impl GameProfile {
teleporter_slots,
map_flags,
flags,
timestamp,
difficulty,
}
}
@ -291,6 +303,11 @@ impl GameProfile {
data.write_u32::<BE>(0x464c4147)?;
data.write(&self.flags)?;
data.write_u32::<LE>(0)?; // unused(?) CS+ space
data.write_u64::<LE>(self.timestamp)?;
data.write_u8(self.difficulty)?;
Ok(())
}
@ -363,6 +380,11 @@ impl GameProfile {
let mut flags = [0u8; 1000];
data.read_exact(&mut flags)?;
data.read_u32::<LE>().unwrap_or(0); // unused(?) CS+ space
let timestamp = data.read_u64::<LE>().unwrap_or(0);
let difficulty = data.read_u8().unwrap_or(0);
Ok(GameProfile {
current_map,
current_song,
@ -382,6 +404,8 @@ impl GameProfile {
teleporter_slots,
map_flags,
flags,
timestamp,
difficulty,
})
}
}

View file

@ -1,4 +1,4 @@
use std::ops::Div;
use std::{cmp, ops::Div};
use bitvec::vec::BitVec;
use chrono::{Datelike, Local};
@ -67,6 +67,49 @@ impl TimingMode {
}
}
#[derive(PartialEq, Eq, Copy, Clone)]
pub enum GameDifficulty {
Easy,
Normal,
Hard,
}
impl GameDifficulty {
pub fn from_index(index: usize) -> GameDifficulty {
match index {
0 => GameDifficulty::Easy,
1 => GameDifficulty::Normal,
2 => GameDifficulty::Hard,
_ => unreachable!(),
}
}
pub fn from_save_value(val: u8) -> GameDifficulty {
match val {
0 => GameDifficulty::Normal,
2 => GameDifficulty::Easy,
4 => GameDifficulty::Hard,
_ => unreachable!(),
}
}
pub fn to_save_value(self) -> u8 {
match self {
GameDifficulty::Normal => 0,
GameDifficulty::Easy => 2,
GameDifficulty::Hard => 4,
}
}
pub fn get_skinsheet_offset(difficulty: GameDifficulty) -> u16 {
match difficulty {
GameDifficulty::Easy => 1, // Yellow Quote
GameDifficulty::Normal => 0, // Good Quote
GameDifficulty::Hard => 2, // Human Quote
}
}
}
pub struct Fps {
pub frame_count: u32,
pub fps: u32,
@ -187,6 +230,7 @@ pub struct SharedGameState {
pub sound_manager: SoundManager,
pub settings: Settings,
pub save_slot: usize,
pub difficulty: GameDifficulty,
pub shutdown: bool,
}
@ -283,6 +327,7 @@ impl SharedGameState {
sound_manager,
settings,
save_slot: 1,
difficulty: GameDifficulty::Normal,
shutdown: false,
})
}
@ -584,4 +629,31 @@ impl SharedGameState {
return "/290.rec".to_string();
}
}
pub fn get_damage(&self, hp: i32) -> i32 {
match self.difficulty {
GameDifficulty::Easy => cmp::max(hp / 2, 1),
GameDifficulty::Normal | GameDifficulty::Hard => hp,
}
}
pub fn get_skinsheet_offset(&self) -> u16 {
if !self.constants.is_cs_plus || self.settings.original_textures {
return 0;
}
if self.settings.seasonal_textures {
let season = Season::current();
if season == Season::Halloween {
return 3; // Edgy Quote
}
if season == Season::Christmas {
return 4; // Furry Quote
}
}
GameDifficulty::get_skinsheet_offset(self.difficulty)
}
}