diff --git a/data/battle_param/ep1_multi_hard.toml b/data/battle_param/ep1_multi_hard.toml index 97461cc..4dbc97d 100644 --- a/data/battle_param/ep1_multi_hard.toml +++ b/data/battle_param/ep1_multi_hard.toml @@ -229,7 +229,7 @@ lck = 20 esp = 15 exp = 40 -[EasterRappy] +[AlRappy] atp = 527 mst = 0 evp = 90 diff --git a/data/battle_param/ep1_multi_normal.toml b/data/battle_param/ep1_multi_normal.toml index db12b5a..4a529e1 100644 --- a/data/battle_param/ep1_multi_normal.toml +++ b/data/battle_param/ep1_multi_normal.toml @@ -229,7 +229,7 @@ lck = 10 esp = 0 exp = 4 -[EasterRappy] +[AlRappy] atp = 184 mst = 0 evp = 45 diff --git a/data/battle_param/ep1_multi_ultimate.toml b/data/battle_param/ep1_multi_ultimate.toml index ce9100c..c30e089 100644 --- a/data/battle_param/ep1_multi_ultimate.toml +++ b/data/battle_param/ep1_multi_ultimate.toml @@ -229,7 +229,7 @@ lck = 30 esp = 35 exp = 256 -[EasterRappy] +[AlRappy] atp = 2100 mst = 0 evp = 419 diff --git a/data/battle_param/ep1_multi_veryhard.toml b/data/battle_param/ep1_multi_veryhard.toml index e070e92..566334d 100644 --- a/data/battle_param/ep1_multi_veryhard.toml +++ b/data/battle_param/ep1_multi_veryhard.toml @@ -229,7 +229,7 @@ lck = 35 esp = 30 exp = 88 -[EasterRappy] +[AlRappy] atp = 913 mst = 0 evp = 194 diff --git a/data/battle_param/ep1_rare_monster.toml b/data/battle_param/ep1_rare_monster.toml new file mode 100644 index 0000000..14a1d4a --- /dev/null +++ b/data/battle_param/ep1_rare_monster.toml @@ -0,0 +1,9 @@ +# 1/10 = 0.1 +# 1/100 = 0.01 +# 1/256 = 0.00390625 +# 1/512 = 0.001953125 + +Hildebear = 0.001953125 +RagRappy = 0.001953125 +PoisonLily = 0.001953125 +PofuillySlime = 0.001953125 diff --git a/data/battle_param/ep1_solo_hard.toml b/data/battle_param/ep1_solo_hard.toml index be12b97..469f988 100644 --- a/data/battle_param/ep1_solo_hard.toml +++ b/data/battle_param/ep1_solo_hard.toml @@ -229,7 +229,7 @@ lck = 20 esp = 15 exp = 40 -[EasterRappy] +[AlRappy] atp = 443 mst = 0 evp = 28 diff --git a/data/battle_param/ep1_solo_normal.toml b/data/battle_param/ep1_solo_normal.toml index 6a409c3..8012947 100644 --- a/data/battle_param/ep1_solo_normal.toml +++ b/data/battle_param/ep1_solo_normal.toml @@ -229,7 +229,7 @@ lck = 10 esp = 0 exp = 4 -[EasterRappy] +[AlRappy] atp = 150 mst = 0 evp = 5 diff --git a/data/battle_param/ep1_solo_ultimate.toml b/data/battle_param/ep1_solo_ultimate.toml index 3d3e0ef..2e076fb 100644 --- a/data/battle_param/ep1_solo_ultimate.toml +++ b/data/battle_param/ep1_solo_ultimate.toml @@ -229,7 +229,7 @@ lck = 30 esp = 30 exp = 256 -[EasterRappy] +[AlRappy] atp = 1800 mst = 0 evp = 276 diff --git a/data/battle_param/ep1_solo_veryhard.toml b/data/battle_param/ep1_solo_veryhard.toml index b6ecd53..306604c 100644 --- a/data/battle_param/ep1_solo_veryhard.toml +++ b/data/battle_param/ep1_solo_veryhard.toml @@ -229,7 +229,7 @@ lck = 35 esp = 30 exp = 88 -[EasterRappy] +[AlRappy] atp = 707 mst = 0 evp = 55 diff --git a/data/battle_param/ep2_rare_monster.toml b/data/battle_param/ep2_rare_monster.toml new file mode 100644 index 0000000..b56bee9 --- /dev/null +++ b/data/battle_param/ep2_rare_monster.toml @@ -0,0 +1,9 @@ +# 1/10 = 0.1 +# 1/100 = 0.01 +# 1/256 = 0.00390625 +# 1/512 = 0.001953125 + +Hildebear = 0.001953125 +RagRappy = 0.001953125 +PoisonLily = 0.001953125 + diff --git a/data/battle_param/ep4_rare_monster.toml b/data/battle_param/ep4_rare_monster.toml new file mode 100644 index 0000000..d5b0fb2 --- /dev/null +++ b/data/battle_param/ep4_rare_monster.toml @@ -0,0 +1,13 @@ +# 1/10 = 0.1 +# 1/100 = 0.01 +# 1/256 = 0.00390625 +# 1/512 = 0.001953125 + +SandRappyCrater = 0.001953125 +ZuCrater = 0.001953125 +Dorphon = 0.001953125 +SandRappyDesert = 0.001953125 +ZuDesert = 0.001953125 +MerissaA = 0.001953125 +Shambertin = 0.1 +SaintMillion = 0.1 \ No newline at end of file diff --git a/data/battle_param/global_rare_monster.toml b/data/battle_param/global_rare_monster.toml new file mode 100644 index 0000000..8a3ca93 --- /dev/null +++ b/data/battle_param/global_rare_monster.toml @@ -0,0 +1,11 @@ +# 1/100 = 0.01 +# 1/256 = 0.00390625 +# 1/512 = 0.001953125 + +# Everything that isn't Kondrieu +[[Grunt]] +rate = 0.01 + +# Kondrieu +[[Boss]] +rate = 0.1 \ No newline at end of file diff --git a/data/quests.toml b/data/quests.toml index a316475..d1d3298 100644 --- a/data/quests.toml +++ b/data/quests.toml @@ -32,4 +32,8 @@ dat = "q233-ext-bb.dat" [[Retrieval.quests]] bin = "q236-ext-bb.bin" dat = "q236-ext-bb.dat" -#drop_table = "q102-drops" \ No newline at end of file +#drop_table = "q102-drops" + +[[Retrieval.quests]] +bin = "q118-vr-bb.bin" +dat = "q118-vr-bb.dat" diff --git a/src/ship/drops/mod.rs b/src/ship/drops/mod.rs index 8de0408..e5037af 100644 --- a/src/ship/drops/mod.rs +++ b/src/ship/drops/mod.rs @@ -55,6 +55,17 @@ pub fn load_data_file(episode: Episode, difficul toml::from_str::(s.as_str()).unwrap() } +// this is just copypaste +pub fn load_rare_monster_file(episode: Episode) -> T { + // TODO: where does the rare monster toml file actually live + let mut path = PathBuf::from("data/battle_param/"); + path.push(episode.to_string().to_lowercase() + "_rare_monster.toml"); + + let mut f = File::open(path).unwrap(); + let mut s = String::new(); + f.read_to_string(&mut s); + toml::from_str::(s.as_str()).unwrap() +} #[derive(Debug, Serialize, Deserialize, Copy, Clone)] pub enum MonsterDropType { diff --git a/src/ship/map/area.rs b/src/ship/map/area.rs index 344c8a6..33b51aa 100644 --- a/src/ship/map/area.rs +++ b/src/ship/map/area.rs @@ -208,6 +208,54 @@ impl MapArea { // MapArea::TestMapEp4 => 10, } } + + pub fn to_episode(self) -> Episode { + match self { + MapArea::Pioneer2Ep1 => Episode::One, + MapArea::Forest1 => Episode::One, + MapArea::Forest2 => Episode::One, + MapArea::Caves1 => Episode::One, + MapArea::Caves2 => Episode::One, + MapArea::Caves3 => Episode::One, + MapArea::Mines1 => Episode::One, + MapArea::Mines2 => Episode::One, + MapArea::Ruins1 => Episode::One, + MapArea::Ruins2 => Episode::One, + MapArea::Ruins3 => Episode::One, + MapArea::Dragon => Episode::One, + MapArea::DeRolLe => Episode::One, + MapArea::VolOpt => Episode::One, + MapArea::DarkFalz => Episode::One, + MapArea::Pioneer2Ep2 => Episode::Two, + MapArea::VrTempleAlpha => Episode::Two, + MapArea::VrTempleBeta => Episode::Two, + MapArea::VrSpaceshipAlpha => Episode::Two, + MapArea::VrSpaceshipBeta => Episode::Two, + MapArea::Cca => Episode::Two, + MapArea::JungleAreaNorth => Episode::Two, + MapArea::JungleAreaEast => Episode::Two, + MapArea::Mountain => Episode::Two, + MapArea::Seaside => Episode::Two, + MapArea::SeabedUpper => Episode::Two, + MapArea::SeabedLower => Episode::Two, + MapArea::GalGryphon => Episode::Two, + MapArea::OlgaFlow => Episode::Two, + MapArea::BarbaRay => Episode::Two, + MapArea::GolDragon => Episode::Two, + MapArea::SeasideNight => Episode::Two, + MapArea::Tower => Episode::Two, + MapArea::Pioneer2Ep4 => Episode::Four, + MapArea::CraterEast => Episode::Four, + MapArea::CraterWest => Episode::Four, + MapArea::CraterSouth => Episode::Four, + MapArea::CraterNorth => Episode::Four, + MapArea::CraterInterior => Episode::Four, + MapArea::SubDesert1 => Episode::Four, + MapArea::SubDesert2 => Episode::Four, + MapArea::SubDesert3 => Episode::Four, + MapArea::SaintMillion => Episode::Four, + } + } } diff --git a/src/ship/map/enemy.rs b/src/ship/map/enemy.rs index f10bc69..6cbe07e 100644 --- a/src/ship/map/enemy.rs +++ b/src/ship/map/enemy.rs @@ -1,5 +1,6 @@ // TOOD: `pub(super) for most of these?` use std::io::{Read}; +use std::collections::HashMap; use byteorder::{LittleEndian, ReadBytesExt}; use thiserror::Error; @@ -9,6 +10,10 @@ use crate::ship::room::Episode; use crate::ship::map::*; +use rand::{Rng, SeedableRng}; +use serde::{Serialize, Deserialize}; +use crate::ship::drops::{load_rare_monster_file}; + #[derive(Debug, Copy, Clone)] pub struct RawMapEnemy { id: u32, @@ -37,7 +42,7 @@ pub struct RawMapEnemy { impl RawMapEnemy { pub fn from_byte_stream(cursor: &mut R) -> Result { Ok(RawMapEnemy { - id: cursor.read_u32::()?, + id: cursor.read_u32::()?, // TODO: is this really u32? shiny monsters are referred to by u16 in the client _unknown1: cursor.read_u16::()?, children: cursor.read_u16::()?, _map_area: cursor.read_u16::()?, @@ -70,6 +75,38 @@ pub enum MapEnemyError { MapAreaError(#[from] MapAreaError), } +// making this `pub type` doesn't allow `impl`s to be defined? +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct RareMonsterAppearTable { + pub appear_rate: HashMap, +} + +impl RareMonsterAppearTable { + pub fn new(episode: Episode) -> RareMonsterAppearTable { + let cfg: HashMap = load_rare_monster_file(episode); + + let appear_rates: HashMap = cfg + .into_iter() + .map(|(monster, rate)| { + let monster: MonsterType = monster.parse().unwrap(); // TODO: don't unwrap! + let appear_rate = rate; + (monster, appear_rate) + }) + .collect(); + + RareMonsterAppearTable { + appear_rate: appear_rates, + } + } + + pub fn roll_appearance(&self, monster: &MonsterType) -> bool { + if rand_chacha::ChaChaRng::from_entropy().gen::() < *self.appear_rate.get(monster).unwrap_or(&0.0f32) { + return true + } + false + } +} + #[derive(Debug, Copy, Clone)] pub struct MapEnemy { @@ -80,21 +117,23 @@ pub struct MapEnemy { pub player_hit: [bool; 4], pub dropped_item: bool, pub gave_exp: bool, + pub shiny: bool, } impl MapEnemy { pub fn from_raw(enemy: RawMapEnemy, episode: &Episode, map_area: &MapArea /*, battleparam */) -> Result { // TODO: rare enemies ep1-4, tower lilys, event rappies, ult variants? + // TODO: check what "skin" actually does. some unexpected enemies have many (panarms, slimes, lilys) let monster = match map_area { MapArea::Forest1 | MapArea::Forest2 | MapArea::Dragon | MapArea::Caves1 | MapArea::Caves2 | MapArea::Caves3 | MapArea::DeRolLe | MapArea::Mines1 | MapArea::Mines2 | MapArea::VolOpt | MapArea::Ruins1 | MapArea::Ruins2 | MapArea::Ruins3 | MapArea::DarkFalz => { match (enemy, episode) { - (RawMapEnemy {id: 64, ..}, _) => MonsterType::Hildebear, - // (RawMapEnemy {id: 64, ..}, _) => MonsterType::Hildeblue, - (RawMapEnemy {id: 65, ..}, _) => MonsterType::RagRappy, - // (RawMapEnemy {id: 65, ..}, _) => MonsterType::AlRappy, + (RawMapEnemy {id: 64, skin: 0, ..}, _) => MonsterType::Hildebear, + (RawMapEnemy {id: 64, skin: 1, ..}, _) => MonsterType::Hildeblue, + (RawMapEnemy {id: 65, skin: 0, ..}, _) => MonsterType::RagRappy, + (RawMapEnemy {id: 65, skin: 1, ..}, _) => MonsterType::AlRappy, (RawMapEnemy {id: 66, ..}, _) => MonsterType::Monest, (RawMapEnemy {id: 67, field2: 0, ..}, _) => MonsterType::SavageWolf, (RawMapEnemy {id: 67, ..}, _) => MonsterType::BarbarousWolf, @@ -103,13 +142,16 @@ impl MapEnemy { (RawMapEnemy {id: 68, skin: 2, ..}, _) => MonsterType::Gigobooma, (RawMapEnemy {id: 96, ..}, _) => MonsterType::GrassAssassin, (RawMapEnemy {id: 97, ..}, _) => MonsterType::PoisonLily, - // (RawMapEnemy {id: 97, ..}, _) => MonsterType::NarLily, + // (RawMapEnemy {id: 97, skin: 0, ..}, _) => MonsterType::PoisonLily, + // (RawMapEnemy {id: 97, skin: 1, ..}, _) => MonsterType::NarLily, (RawMapEnemy {id: 98, ..}, _) => MonsterType::NanoDragon, (RawMapEnemy {id: 99, skin: 0, ..}, _) => MonsterType::EvilShark, (RawMapEnemy {id: 99, skin: 1, ..}, _) => MonsterType::PalShark, (RawMapEnemy {id: 99, skin: 2, ..}, _) => MonsterType::GuilShark, (RawMapEnemy {id: 100, ..}, _) => MonsterType::PofuillySlime, - // (RawMapEnemy {id: 100, ..}, _) => MonsterType::PouillySlime, + // (RawMapEnemy {id: 100, skin: 0, ..}, _) => MonsterType::PofuillySlime, + // (RawMapEnemy {id: 100, skin: 1, ..}, _) => MonsterType::PouillySlime, + // (RawMapEnemy {id: 100, skin: 2, ..}, _) => MonsterType::PofuillySlime, (RawMapEnemy {id: 101, ..}, _) => MonsterType::PanArms, (RawMapEnemy {id: 128, skin: 0, ..}, _) => MonsterType::Dubchic, (RawMapEnemy {id: 128, skin: 1, ..}, _) => MonsterType::Gillchic, @@ -122,6 +164,7 @@ impl MapEnemy { (RawMapEnemy {id: 160, ..}, _) => MonsterType::Delsaber, (RawMapEnemy {id: 161, ..}, _) => MonsterType::ChaosSorcerer, (RawMapEnemy {id: 162, ..}, _) => MonsterType::DarkGunner, + (RawMapEnemy {id: 163, ..}, _) => MonsterType::DeathGunner, (RawMapEnemy {id: 164, ..}, _) => MonsterType::ChaosBringer, (RawMapEnemy {id: 165, ..}, _) => MonsterType::DarkBelra, (RawMapEnemy {id: 166, skin: 0, ..}, _) => MonsterType::Dimenian, @@ -143,17 +186,16 @@ impl MapEnemy { MapArea::JungleAreaNorth | MapArea::JungleAreaEast | MapArea::Mountain | MapArea::Seaside | MapArea::SeasideNight | MapArea::Cca | MapArea::GalGryphon | MapArea::SeabedUpper | MapArea::SeabedLower | MapArea::OlgaFlow => { match (enemy, episode) { - (RawMapEnemy {id: 64, ..}, _) => MonsterType::Hildebear, - // (RawMapEnemy {id: 64, ..}, _) => MonsterType::Hildeblue, - (RawMapEnemy {id: 65, ..}, _) => MonsterType::RagRappy, - // (RawMapEnemy {id: 65, ..}, _) => MonsterType::EventRappy, + (RawMapEnemy {id: 64, skin: 0, ..}, _) => MonsterType::Hildebear, + (RawMapEnemy {id: 64, skin: 1, ..}, _) => MonsterType::Hildeblue, + (RawMapEnemy {id: 65, skin: 0, ..}, _) => MonsterType::RagRappy, + (RawMapEnemy {id: 65, skin: 1, ..}, _) => MonsterType::EventRappy, (RawMapEnemy {id: 66, ..}, _) => MonsterType::Monest, (RawMapEnemy {id: 67, field2: 0, ..}, _) => MonsterType::SavageWolf, (RawMapEnemy {id: 67, ..}, _) => MonsterType::BarbarousWolf, (RawMapEnemy {id: 96, ..}, _) => MonsterType::GrassAssassin, - (RawMapEnemy {id: 97, ..}, _) => MonsterType::PoisonLily, - // (RawMapEnemy {id: 97, ..}, _) => MonsterType::NarLily, - // (RawMapEnemy {id: 97, ..}, _) => MonsterType::DelLily, + (RawMapEnemy {id: 97, skin: 0, ..}, _) => MonsterType::PoisonLily, + (RawMapEnemy {id: 97, skin: 1, ..}, _) => MonsterType::NarLily, (RawMapEnemy {id: 101, ..}, _) => MonsterType::PanArms, (RawMapEnemy {id: 128, skin: 0, ..}, _) => MonsterType::Dubchic, (RawMapEnemy {id: 128, skin: 1, ..}, _) => MonsterType::Gillchic, @@ -213,38 +255,38 @@ impl MapEnemy { MapArea::CraterEast | MapArea::CraterWest | MapArea::CraterSouth | MapArea::CraterNorth | MapArea::CraterInterior => { match (enemy, episode) { - (RawMapEnemy {id: 65, ..}, Episode::Four) => MonsterType::SandRappyCrater, - // (RawMapEnemy {id: 65, ..}, Episode::Four) => MonsterType::DelRappyCrater, + (RawMapEnemy {id: 65, skin: 0, ..}, Episode::Four) => MonsterType::SandRappyCrater, + (RawMapEnemy {id: 65, skin: 1, ..}, Episode::Four) => MonsterType::DelRappyCrater, (RawMapEnemy {id: 272, ..}, _) => MonsterType::Astark, (RawMapEnemy {id: 273, field2: 0, ..}, _) => MonsterType::SatelliteLizardCrater, (RawMapEnemy {id: 273, ..}, _) => MonsterType::YowieCrater, - (RawMapEnemy {id: 276, ..}, _) => MonsterType::ZuCrater, - // (RawMapEnemy {id: 276, ..}, _) => MonsterType::PazuzuCrater, + (RawMapEnemy {id: 276, skin: 0, ..}, _) => MonsterType::ZuCrater, + (RawMapEnemy {id: 276, skin: 1, ..}, _) => MonsterType::PazuzuCrater, (RawMapEnemy {id: 277, skin: 0, ..}, _) => MonsterType::Boota, (RawMapEnemy {id: 277, skin: 1, ..}, _) => MonsterType::ZeBoota, (RawMapEnemy {id: 277, skin: 2, ..}, _) => MonsterType::BaBoota, - (RawMapEnemy {id: 278, ..}, _) => MonsterType::Dorphon, - // (RawMapEnemy {id: 278, ..}, _) => MonsterType::DorphonEclair, + (RawMapEnemy {id: 278, skin: 0, ..}, _) => MonsterType::Dorphon, + (RawMapEnemy {id: 278, skin: 1, ..}, _) => MonsterType::DorphonEclair, _ => return Err(MapEnemyError::UnknownEnemyId(enemy.id)) } }, MapArea::SubDesert1 | MapArea::SubDesert2 | MapArea::SubDesert3 | MapArea::SaintMillion => { match (enemy, episode) { - (RawMapEnemy {id: 65, ..}, Episode::Four) => MonsterType::SandRappyDesert, - // (RawMapEnemy {id: 65, ..}, Episode::Four) => MonsterType::DelRappyDesert, + (RawMapEnemy {id: 65, skin: 0, ..}, Episode::Four) => MonsterType::SandRappyDesert, + (RawMapEnemy {id: 65, skin: 1, ..}, Episode::Four) => MonsterType::DelRappyDesert, (RawMapEnemy {id: 273, field2: 0, ..}, _) => MonsterType::SatelliteLizardDesert, (RawMapEnemy {id: 273, ..}, _) => MonsterType::YowieDesert, - (RawMapEnemy {id: 274, ..}, _) => MonsterType::MerissaA, - // (RawMapEnemy {id: 274, ..}, _) => MonsterType::MerissaAA, + (RawMapEnemy {id: 274, skin: 0, ..}, _) => MonsterType::MerissaA, + (RawMapEnemy {id: 274, skin: 1, ..}, _) => MonsterType::MerissaAA, (RawMapEnemy {id: 275, ..}, _) => MonsterType::Girtablulu, - (RawMapEnemy {id: 276, ..}, _) => MonsterType::ZuDesert, - // (RawMapEnemy {id: 276, ..}, _) => MonsterType::PazuzuDesert, + (RawMapEnemy {id: 276, skin: 0, ..}, _) => MonsterType::ZuDesert, + (RawMapEnemy {id: 276, skin: 1, ..}, _) => MonsterType::PazuzuDesert, (RawMapEnemy {id: 279, skin: 0, ..}, _) => MonsterType::Goran, (RawMapEnemy {id: 279, skin: 1, ..}, _) => MonsterType::PyroGoran, (RawMapEnemy {id: 279, skin: 2, ..}, _) => MonsterType::GoranDetonator, (RawMapEnemy {id: 281, skin: 0, ..}, _) => MonsterType::SaintMillion, - (RawMapEnemy {id: 281, skin: 1, ..}, _) => MonsterType::Shambertin, - // (RawMapEnemy {id: 281, skin: 1, ..}, _) => MonsterType::Kondrieu, + (RawMapEnemy {id: 281, skin: 1, ..}, _) => MonsterType::Shambertin, // TODO: don't guess the skin + (RawMapEnemy {id: 281, skin: 2, ..}, _) => MonsterType::Kondrieu, // TODO: don't guess the skin _ => return Err(MapEnemyError::UnknownEnemyId(enemy.id)) } }, @@ -258,6 +300,7 @@ impl MapEnemy { dropped_item: false, gave_exp: false, player_hit: [false; 4], + shiny: false, }) } @@ -269,7 +312,56 @@ impl MapEnemy { dropped_item: false, gave_exp: false, player_hit: [false; 4], + shiny: false, + } + } + + pub fn set_shiny(self) -> MapEnemy { + MapEnemy { + shiny: true, + ..self + } + } + + pub fn has_rare_appearance(self) -> bool { + matches!(self.monster, + MonsterType::RagRappy | MonsterType::Hildebear | + MonsterType::PoisonLily | MonsterType::PofuillySlime | + MonsterType::SandRappyCrater | MonsterType::ZuCrater | MonsterType::Dorphon | + MonsterType::SandRappyDesert | MonsterType::ZuDesert | MonsterType::MerissaA | + MonsterType::SaintMillion | MonsterType::Shambertin + ) + } + + /* + TODO: distinguish between a `random` rare monster and a `set/guaranteed` rare monster? (does any acceptable quest even have this?) + guaranteed rare monsters don't count towards the limit + */ + pub fn set_rare_appearance(self) -> MapEnemy { + match (self.monster, self.map_area.to_episode()) { + (MonsterType::RagRappy, Episode::One) => {MapEnemy {monster: MonsterType::AlRappy, shiny:true, ..self}}, + (MonsterType::RagRappy, Episode::Two) => {MapEnemy {monster: MonsterType::EventRappy, shiny:true, ..self}}, + (MonsterType::Hildebear, _) => {MapEnemy {monster: MonsterType::Hildeblue, shiny:true, ..self}}, + (MonsterType::PoisonLily, _) => {MapEnemy {monster: MonsterType::NarLily, shiny:true, ..self}}, + (MonsterType::PofuillySlime, _) => {MapEnemy {monster: MonsterType::PouillySlime, shiny:true, ..self}}, + (MonsterType::SandRappyCrater, _) => {MapEnemy {monster: MonsterType::DelRappyCrater, shiny:true, ..self}}, + (MonsterType::ZuCrater, _) => {MapEnemy {monster: MonsterType::PazuzuCrater, shiny:true, ..self}}, + (MonsterType::Dorphon, _) => {MapEnemy {monster: MonsterType::DorphonEclair, shiny:true, ..self}}, + (MonsterType::SandRappyDesert, _) => {MapEnemy {monster: MonsterType::DelRappyDesert, shiny:true, ..self}}, + (MonsterType::ZuDesert, _) => {MapEnemy {monster: MonsterType::PazuzuDesert, shiny:true, ..self}}, + (MonsterType::MerissaA, _) => {MapEnemy {monster: MonsterType::MerissaAA, shiny:true, ..self}}, + (MonsterType::SaintMillion, _) => {MapEnemy {monster: MonsterType::Kondrieu, shiny:true, ..self}}, + (MonsterType::Shambertin, _) => {MapEnemy {monster: MonsterType::Kondrieu, shiny:true, ..self}}, + _ => {self}, + } + } + + // in theory this should only be called on monsters we know can have rare types + pub fn roll_appearance_for_mission(self, rare_monster_table: &RareMonsterAppearTable) -> MapEnemy { + if rare_monster_table.roll_appearance(&self.monster) { + return self.set_rare_appearance() } + self } } diff --git a/src/ship/map/maps.rs b/src/ship/map/maps.rs index aaa63b9..0743c01 100644 --- a/src/ship/map/maps.rs +++ b/src/ship/map/maps.rs @@ -30,24 +30,30 @@ fn objects_from_map_data(path: PathBuf, episode: &Episode, map_area: &MapArea) - fn parse_enemy(episode: &Episode, map_area: &MapArea, raw_enemy: RawMapEnemy) -> Vec> { let enemy = MapEnemy::from_raw(raw_enemy, episode, map_area); + enemy .map_or(vec![None], |monster| { let mut monsters = vec![Some(monster)]; - + match monster.monster { MonsterType::Monest => { for _ in 0..30 { monsters.push(Some(MapEnemy::new(MonsterType::Mothmant, monster.map_area))); } }, + MonsterType::PanArms => { + monsters.push(Some(MapEnemy::new(MonsterType::Hidoom, monster.map_area))); + monsters.push(Some(MapEnemy::new(MonsterType::Migium, monster.map_area))); + }, MonsterType::PofuillySlime => { - for _ in 0..4 { + for _ in 0..5 { monsters.push(Some(MapEnemy::new(MonsterType::PofuillySlime, monster.map_area))); } }, - MonsterType::PanArms => { - monsters.push(Some(MapEnemy::new(MonsterType::Hidoom, monster.map_area))); - monsters.push(Some(MapEnemy::new(MonsterType::Migium, monster.map_area))); + MonsterType::PouillySlime => { + for _ in 0..5 { + monsters.push(Some(MapEnemy::new(MonsterType::PofuillySlime, monster.map_area))); + } }, MonsterType::SinowBeat => { for _ in 0..4 { @@ -180,7 +186,7 @@ pub struct Maps { } impl Maps { - pub fn new(room_mode: RoomMode) -> Maps { + pub fn new(room_mode: RoomMode, rare_monster_table: &enemy::RareMonsterAppearTable) -> Maps { let map_variants = match (room_mode.episode(), room_mode.single_player()) { (Episode::One, 0) => { vec![MapVariant::new(MapArea::Pioneer2Ep1, MapVariantMode::Online), @@ -272,16 +278,19 @@ impl Maps { _ => unreachable!() }; - let maps = Maps { - enemy_data: map_variants.iter().fold(Vec::new(), |mut enemy_data, map_variant| { - enemy_data.append(&mut enemy_data_from_map_data(map_variant, &room_mode.episode())); - enemy_data - }), - object_data: map_variants.iter().flat_map(|map_variant| { - objects_from_map_data(map_variant.obj_file().into(), &room_mode.episode(), &map_variant.map) - }).collect(), - map_variants, + let mut maps = Maps { + enemy_data: map_variants.iter() + .fold(Vec::new(), |mut enemy_data, map_variant| { + enemy_data.append(&mut enemy_data_from_map_data(map_variant, &room_mode.episode())); + enemy_data + }), + object_data: map_variants.iter() + .flat_map(|map_variant| { + objects_from_map_data(map_variant.obj_file().into(), &room_mode.episode(), &map_variant.map) + }).collect(), + map_variants, }; + maps.roll_monster_appearance(rare_monster_table); maps } @@ -305,9 +314,49 @@ impl Maps { }) } - pub fn set_quest_data(&mut self, enemies: Vec>, objects: Vec>) { + pub fn set_quest_data(&mut self, enemies: Vec>, objects: Vec>, rare_monster_appear_table: &RareMonsterAppearTable) { self.enemy_data = enemies; + self.roll_monster_appearance(rare_monster_appear_table); self.object_data = objects; } -} + pub fn get_rare_monster_list(&self) -> Vec { + let mut rare_monsters = vec![0xFFFF; 16]; + let shiny: Vec<(usize, &Option)> = self.enemy_data.iter() + .enumerate() + .filter(|(_,m)| { + if m.is_some() { + m.unwrap().shiny + } else { + false + } + }) + .collect(); + for monster in &shiny { + if let Some(j) = rare_monsters.iter().position(|&x| x == 0xFFFF) { + rare_monsters[j] = monster.0 as u16; + } else { + break + } + } + rare_monsters + } + + pub fn roll_monster_appearance(&mut self, rare_monster_table: &RareMonsterAppearTable) { + self.enemy_data = self.enemy_data + .iter() + // .map(|&x| if x.is_some() && x.unwrap().has_rare_appearance() { + .map(|&x| + if let Some(monster) = x { + if monster.has_rare_appearance() { + Some(monster.roll_appearance_for_mission(rare_monster_table)) + } else { + Some(monster) + } + } else { + x + } + ) + .collect(); + } +} diff --git a/src/ship/map/mod.rs b/src/ship/map/mod.rs index f57c80c..6c54343 100644 --- a/src/ship/map/mod.rs +++ b/src/ship/map/mod.rs @@ -1,5 +1,5 @@ pub mod area; -mod enemy; +pub mod enemy; mod object; mod variant; mod maps; diff --git a/src/ship/packet/builder/room.rs b/src/ship/packet/builder/room.rs index 1effe5d..c5bd11c 100644 --- a/src/ship/packet/builder/room.rs +++ b/src/ship/packet/builder/room.rs @@ -6,6 +6,7 @@ use crate::ship::location::{ClientLocation, RoomId, AreaClient, ClientLocationEr use crate::ship::room::RoomState; use crate::ship::items::ItemManager; use crate::ship::packet::builder::{player_header, player_info}; +use std::convert::TryInto; pub fn join_room(id: ClientId, clients: &Clients, @@ -71,3 +72,8 @@ pub fn add_to_room(_id: ClientId, }) } +pub fn build_rare_monster_list(rare_monster_vec: Vec) -> RareMonsterList { + RareMonsterList { + ids: rare_monster_vec.try_into().unwrap_or([0xFFFFu16; 16]), + } +} \ No newline at end of file diff --git a/src/ship/packet/handler/quest.rs b/src/ship/packet/handler/quest.rs index 1e2e569..550df18 100644 --- a/src/ship/packet/handler/quest.rs +++ b/src/ship/packet/handler/quest.rs @@ -68,7 +68,7 @@ pub fn quest_detail(id: ClientId, questdetailrequest: &QuestDetailRequest, quest Ok(Box::new(vec![(id, SendShipPacket::QuestDetail(qd))].into_iter())) } -pub fn load_quest(id: ClientId, questmenuselect: &QuestMenuSelect, quests: &QuestList, clients: &mut Clients, client_location: &ClientLocation, rooms: &mut Rooms) +pub fn player_chose_quest(id: ClientId, questmenuselect: &QuestMenuSelect, quests: &QuestList, clients: &mut Clients, client_location: &ClientLocation, rooms: &mut Rooms) -> Result + Send>, ShipError> { let (_, category_quests) = quests.iter() .nth(questmenuselect.category as usize) @@ -83,7 +83,7 @@ pub fn load_quest(id: ClientId, questmenuselect: &QuestMenuSelect, quests: &Ques let room = rooms.get_mut(room_id.0) .ok_or(ShipError::InvalidRoom(room_id.0 as u32))?.as_mut() .ok_or(ShipError::InvalidRoom(room_id.0 as u32))?; - room.maps.set_quest_data(quest.enemies.clone(), quest.objects.clone()); + room.maps.set_quest_data(quest.enemies.clone(), quest.objects.clone(), &room.rare_monster_table); room.map_areas = quest.map_areas.clone(); let bin = quest::quest_header(questmenuselect, &quest.bin_blob, "bin"); diff --git a/src/ship/packet/handler/room.rs b/src/ship/packet/handler/room.rs index 33d4753..ba82b2b 100644 --- a/src/ship/packet/handler/room.rs +++ b/src/ship/packet/handler/room.rs @@ -149,13 +149,16 @@ pub fn done_bursting(id: ClientId, rooms: &mut Rooms) -> Box + Send> { let area = client_location.get_area(id).unwrap(); + let mut rare_monster_list: Option> = None; if let RoomLobby::Room(room_id) = area { if let Some(room) = rooms.get_mut(room_id.0).unwrap().as_mut() { room.bursting = false; - } + rare_monster_list = Some(room.maps.get_rare_monster_list()); + }; } let area_client = client_location.get_local_client(id).unwrap(); // TODO: unwrap - Box::new(client_location.get_client_neighbors(id).unwrap().into_iter() // TODO: unwrap + let mut result: Box + Send> = Box::new( + client_location.get_client_neighbors(id).unwrap().into_iter() // TODO: unwrap .flat_map(move |client| { vec![ (client.client, SendShipPacket::Message(Message::new(GameMessage::BurstDone(BurstDone { @@ -163,7 +166,16 @@ pub fn done_bursting(id: ClientId, target: 0 })))), ] - })) + }) + ); + + // TODO: check how often `done_bursting` is called. ie: make sure it's only used when joining a room and not each time a player warps in a pipe + if let Some(rare_list) = rare_monster_list { + let rare_monster_packet = SendShipPacket::RareMonsterList(builder::room::build_rare_monster_list(rare_list)); + result = Box::new(result.chain(vec![(id, rare_monster_packet)])); // TODO: make sure we arent clobbering `result` here + } + + result } pub fn request_room_list(id: ClientId, diff --git a/src/ship/room.rs b/src/ship/room.rs index 991de54..bd142b7 100644 --- a/src/ship/room.rs +++ b/src/ship/room.rs @@ -1,13 +1,15 @@ use std::collections::HashMap; use std::convert::{From, Into, TryFrom, TryInto}; +use thiserror::Error; + use rand::Rng; use crate::ship::map::Maps; use crate::ship::drops::DropTable; use crate::entity::character::SectionID; use crate::ship::monster::{load_monster_stats_table, MonsterType, MonsterStats}; use crate::ship::map::area::MapAreaLookup; -use thiserror::Error; +use crate::ship::map::enemy::RareMonsterAppearTable; #[derive(Debug, Error)] #[error("")] @@ -168,6 +170,7 @@ pub struct RoomState { pub bursting: bool, pub monster_stats: Box>, pub map_areas: MapAreaLookup, + pub rare_monster_table: Box, // items on ground // enemy info } @@ -232,13 +235,16 @@ impl RoomState { } }; + let rare_monster_table = RareMonsterAppearTable::new(room_mode.episode()); + Ok(RoomState { monster_stats: Box::new(load_monster_stats_table(&room_mode).map_err(|_| RoomCreationError::CouldNotLoadMonsterStats(room_mode))?), mode: room_mode, random_seed: rand::thread_rng().gen(), + rare_monster_table: Box::new(rare_monster_table.clone()), name: String::from_utf16_lossy(&create_room.name).trim_matches(char::from(0)).into(), password: create_room.password, - maps: Maps::new(room_mode), + maps: Maps::new(room_mode, &rare_monster_table), // TODO: rare_monster_table here feels janky. is there some way to call the the RoomState.rare_monster_table we already created? section_id, drop_table: Box::new(DropTable::new(room_mode.episode(), room_mode.difficulty(), section_id)), bursting: false, diff --git a/src/ship/ship.rs b/src/ship/ship.rs index 8efac0f..d784677 100644 --- a/src/ship/ship.rs +++ b/src/ship/ship.rs @@ -195,6 +195,7 @@ pub enum SendShipPacket { DoneLoadingQuest(DoneLoadingQuest), BankItemList(BankItemList), RedirectClient(RedirectClient), + RareMonsterList(RareMonsterList), AcknowledgeTrade(AcknowledgeTrade), CancelTrade(CancelTrade), TradeSuccessful(TradeSuccessful), @@ -235,6 +236,7 @@ impl SendServerPacket for SendShipPacket { SendShipPacket::DoneLoadingQuest(pkt) => pkt.as_bytes(), SendShipPacket::BankItemList(pkt) => pkt.as_bytes(), SendShipPacket::RedirectClient(pkt) => pkt.as_bytes(), + SendShipPacket::RareMonsterList(pkt) => pkt.as_bytes(), SendShipPacket::AcknowledgeTrade(pkt) => pkt.as_bytes(), SendShipPacket::CancelTrade(pkt) => pkt.as_bytes(), SendShipPacket::TradeSuccessful(pkt) => pkt.as_bytes(), @@ -403,7 +405,6 @@ impl ShipServerStateBuilder { ip: self.ip.unwrap_or_else(|| Ipv4Addr::new(127,0,0,1)), port: self.port.unwrap_or(SHIP_PORT), shops: Box::new(ItemShops::default()), - blocks: Blocks(blocks), auth_token: self.auth_token.unwrap_or_else(|| AuthToken("".into())), @@ -414,7 +415,6 @@ impl ShipServerStateBuilder { } } - pub struct Block { client_location: Box, pub rooms: Box, @@ -632,7 +632,7 @@ impl ServerState for ShipServerState { }, RecvShipPacket::QuestMenuSelect(questmenuselect) => { let block = self.blocks.with_client(id, &self.clients)?; - handler::quest::load_quest(id, questmenuselect, &self.quests, &mut self.clients, &block.client_location, &mut block.rooms)? + handler::quest::player_chose_quest(id, questmenuselect, &self.quests, &mut self.clients, &block.client_location, &mut block.rooms)? }, RecvShipPacket::MenuDetail(_menudetail) => { //unreachable!(); diff --git a/tests/test_rooms.rs b/tests/test_rooms.rs index 91e8963..70dbdf4 100644 --- a/tests/test_rooms.rs +++ b/tests/test_rooms.rs @@ -95,3 +95,23 @@ async fn test_item_ids_reset_when_rejoining_rooms() { _ => panic!(), } } + +#[async_std::test] +async fn test_load_rare_monster_default_appear_rates() { + let mut entity_gateway = InMemoryGateway::default(); + let (_user1, _char1) = new_user_character(&mut entity_gateway, "a1", "a").await; + let mut ship = Box::new(ShipServerState::builder() + .gateway(entity_gateway.clone()) + .build()); + log_in_char(&mut ship, ClientId(1), "a1", "a").await; + join_lobby(&mut ship, ClientId(1)).await; + create_room(&mut ship, ClientId(1), "room", "").await; + + // assume episode 1 + let room = ship.blocks.0[0].rooms[0].as_ref().unwrap(); + println!("rare monster table: {:?}", room.rare_monster_table); + let rates = &*room.rare_monster_table; + for (_monster, rate) in rates.clone().appear_rate { + assert_eq!(rate, 0.001953125f32); // 1/512 = 0.001953125 + } +} \ No newline at end of file