// TOOD: `pub(super) for most of these?` use std::io::{Read}; use std::collections::HashMap; use byteorder::{LittleEndian, ReadBytesExt}; use thiserror::Error; use crate::ship::monster::MonsterType; 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, _unknown1: u16, pub children: u16, _map_area: u16, _unknown4: u16, _section: u16, _wave_idd: u16, _wave_id: u32, _x: f32, _y: f32, _z: f32, _xrot: u32, _yrot: u32, _zrot: u32, _field1: u32, field2: u32, _field3: u32, _field4: u32, _field5: u32, skin: u32, _field6: u32 } impl RawMapEnemy { pub fn from_byte_stream(cursor: &mut R) -> Result { Ok(RawMapEnemy { 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::()?, _unknown4: cursor.read_u16::()?, _section: cursor.read_u16::()?, _wave_idd: cursor.read_u16::()?, _wave_id: cursor.read_u32::()?, _x: cursor.read_f32::()?, _y: cursor.read_f32::()?, _z: cursor.read_f32::()?, _xrot: cursor.read_u32::()?, _yrot: cursor.read_u32::()?, _zrot: cursor.read_u32::()?, _field1: cursor.read_u32::()?, field2: cursor.read_u32::()?, _field3: cursor.read_u32::()?, _field4: cursor.read_u32::()?, _field5: cursor.read_u32::()?, skin: cursor.read_u32::()?, _field6: cursor.read_u32::()?, }) } } #[derive(Error, Debug)] #[error("")] pub enum MapEnemyError { UnknownEnemyId(u32), 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 { pub monster: MonsterType, pub map_area: MapArea, _hp: u32, // TODO: other stats from battleparam 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, 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, (RawMapEnemy {id: 68, skin: 0, ..}, _) => MonsterType::Booma, (RawMapEnemy {id: 68, skin: 1, ..}, _) => MonsterType::Gobooma, (RawMapEnemy {id: 68, skin: 2, ..}, _) => MonsterType::Gigobooma, (RawMapEnemy {id: 96, ..}, _) => MonsterType::GrassAssassin, (RawMapEnemy {id: 97, ..}, _) => MonsterType::PoisonLily, // (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, 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, (RawMapEnemy {id: 129, ..}, _) => MonsterType::Garanz, (RawMapEnemy {id: 130, field2: 0, ..}, _) => MonsterType::SinowBeat, (RawMapEnemy {id: 130, ..}, _) => MonsterType::SinowGold, (RawMapEnemy {id: 131, ..}, _) => MonsterType::Canadine, (RawMapEnemy {id: 132, ..}, _) => MonsterType::Canane, (RawMapEnemy {id: 133, ..}, _) => MonsterType::Dubwitch, (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, (RawMapEnemy {id: 166, skin: 1, ..}, _) => MonsterType::LaDimenian, (RawMapEnemy {id: 166, skin: 2, ..}, _) => MonsterType::SoDimenian, (RawMapEnemy {id: 167, ..}, _) => MonsterType::Bulclaw, (RawMapEnemy {id: 168, ..}, _) => MonsterType::Claw, (RawMapEnemy {id: 192, ..}, Episode::One) => MonsterType::Dragon, (RawMapEnemy {id: 193, ..}, _) => MonsterType::DeRolLe, (RawMapEnemy {id: 194, ..}, _) => MonsterType::VolOptPartA, (RawMapEnemy {id: 197, ..}, _) => MonsterType::VolOpt, (RawMapEnemy {id: 200, ..}, _) => MonsterType::DarkFalz, _ => return Err(MapEnemyError::UnknownEnemyId(enemy.id)) } }, MapArea::VrTempleAlpha | MapArea::VrTempleBeta | MapArea::BarbaRay | MapArea::VrSpaceshipAlpha | MapArea::VrSpaceshipBeta | MapArea::GolDragon | 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, 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, 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, (RawMapEnemy {id: 129, ..}, _) => MonsterType::Garanz, (RawMapEnemy {id: 133, ..}, _) => MonsterType::Dubwitch, (RawMapEnemy {id: 160, ..}, _) => MonsterType::Delsaber, (RawMapEnemy {id: 161, ..}, _) => MonsterType::ChaosSorcerer, (RawMapEnemy {id: 165, ..}, _) => MonsterType::DarkBelra, (RawMapEnemy {id: 166, skin: 0, ..}, _) => MonsterType::Dimenian, (RawMapEnemy {id: 166, skin: 1, ..}, _) => MonsterType::LaDimenian, (RawMapEnemy {id: 166, skin: 2, ..}, _) => MonsterType::SoDimenian, (RawMapEnemy {id: 192, ..}, Episode::Two) => MonsterType::GalGryphon, (RawMapEnemy {id: 202, ..}, _) => MonsterType::OlgaFlow, (RawMapEnemy {id: 203, ..}, _) => MonsterType::BarbaRay, (RawMapEnemy {id: 204, ..}, _) => MonsterType::GolDragon, (RawMapEnemy {id: 212, skin: 0, ..}, _) => MonsterType::SinowBerill, (RawMapEnemy {id: 212, skin: 1, ..}, _) => MonsterType::SinowSpigell, (RawMapEnemy {id: 213, skin: 0, ..}, _) => MonsterType::Merillia, (RawMapEnemy {id: 213, skin: 1, ..}, _) => MonsterType::Meriltas, (RawMapEnemy {id: 214, skin: 0, ..}, _) => MonsterType::Mericarol, (RawMapEnemy {id: 214, skin: 1, ..}, _) => MonsterType::Merikle, (RawMapEnemy {id: 214, skin: 2, ..}, _) => MonsterType::Mericus, (RawMapEnemy {id: 215, skin: 0, ..}, _) => MonsterType::UlGibbon, (RawMapEnemy {id: 215, skin: 1, ..}, _) => MonsterType::ZolGibbon, (RawMapEnemy {id: 216, ..}, _) => MonsterType::Gibbles, (RawMapEnemy {id: 217, ..}, _) => MonsterType::Gee, (RawMapEnemy {id: 218, ..}, _) => MonsterType::GiGue, (RawMapEnemy {id: 219, ..}, _) => MonsterType::Deldepth, (RawMapEnemy {id: 220, ..}, _) => MonsterType::Delbiter, (RawMapEnemy {id: 221, skin: 0, ..}, _) => MonsterType::Dolmolm, (RawMapEnemy {id: 221, skin: 1, ..}, _) => MonsterType::Dolmdarl, (RawMapEnemy {id: 222, ..}, _) => MonsterType::Morfos, (RawMapEnemy {id: 223, ..}, _) => MonsterType::Recobox, (RawMapEnemy {id: 224, skin: 0, ..}, _) => MonsterType::SinowZoa, (RawMapEnemy {id: 224, skin: 1, ..}, _) => MonsterType::SinowZele, (RawMapEnemy {id: 224, ..}, _) => MonsterType::Epsilon, (RawMapEnemy {id: 225, ..}, _) => MonsterType::IllGill, _ => return Err(MapEnemyError::UnknownEnemyId(enemy.id)) } }, MapArea::Tower => { match (enemy, episode) { (RawMapEnemy {id: 97, ..}, _) => MonsterType::DelLily, (RawMapEnemy {id: 214, skin: 0, ..}, _) => MonsterType::Mericarol, (RawMapEnemy {id: 214, skin: 1, ..}, _) => MonsterType::Merikle, (RawMapEnemy {id: 214, skin: 2, ..}, _) => MonsterType::Mericus, (RawMapEnemy {id: 216, ..}, _) => MonsterType::Gibbles, (RawMapEnemy {id: 218, ..}, _) => MonsterType::GiGue, (RawMapEnemy {id: 220, ..}, _) => MonsterType::Delbiter, (RawMapEnemy {id: 223, ..}, _) => MonsterType::Recobox, (RawMapEnemy {id: 224, ..}, _) => MonsterType::Epsilon, (RawMapEnemy {id: 225, ..}, _) => MonsterType::IllGill, _ => return Err(MapEnemyError::UnknownEnemyId(enemy.id)) } }, MapArea::CraterEast | MapArea::CraterWest | MapArea::CraterSouth | MapArea::CraterNorth | MapArea::CraterInterior => { match (enemy, episode) { (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, 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, 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, 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, skin: 0, ..}, _) => MonsterType::MerissaA, (RawMapEnemy {id: 274, skin: 1, ..}, _) => MonsterType::MerissaAA, (RawMapEnemy {id: 275, ..}, _) => MonsterType::Girtablulu, (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, // 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)) } }, _ => return Err(MapEnemyError::UnknownEnemyId(enemy.id)) }; Ok(MapEnemy { monster, map_area: *map_area, _hp: 0, dropped_item: false, gave_exp: false, player_hit: [false; 4], shiny: false, }) } pub fn new(monster: MonsterType, map_area: MapArea) -> MapEnemy { MapEnemy { monster, map_area, _hp: 0, 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 } }