You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

387 lines
20 KiB

// TOOD: `pub(super) for most of these?`
use std::io::{Read};
use std::collections::HashMap;
use std::fs::File;
use std::path::PathBuf;
use byteorder::{LittleEndian, ReadBytesExt};
use thiserror::Error;
use rand::{Rng, SeedableRng};
use serde::{Serialize, Deserialize};
use crate::area::{MapArea, MapAreaError};
use crate::room::Episode;
use crate::monster::MonsterType;
#[derive(Clone, Copy)]
pub enum RareEnemyEvent {
Easter,
Halloween,
Christmas,
}
#[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<R: Read>(cursor: &mut R) -> Result<RawMapEnemy, std::io::Error> {
Ok(RawMapEnemy {
id: cursor.read_u32::<LittleEndian>()?, // TODO: is this really u32? shiny monsters are referred to by u16 in the client
_unknown1: cursor.read_u16::<LittleEndian>()?,
children: cursor.read_u16::<LittleEndian>()?,
_map_area: cursor.read_u16::<LittleEndian>()?,
_unknown4: cursor.read_u16::<LittleEndian>()?,
_section: cursor.read_u16::<LittleEndian>()?,
_wave_idd: cursor.read_u16::<LittleEndian>()?,
_wave_id: cursor.read_u32::<LittleEndian>()?,
_x: cursor.read_f32::<LittleEndian>()?,
_y: cursor.read_f32::<LittleEndian>()?,
_z: cursor.read_f32::<LittleEndian>()?,
_xrot: cursor.read_u32::<LittleEndian>()?,
_yrot: cursor.read_u32::<LittleEndian>()?,
_zrot: cursor.read_u32::<LittleEndian>()?,
_field1: cursor.read_u32::<LittleEndian>()?,
field2: cursor.read_u32::<LittleEndian>()?,
_field3: cursor.read_u32::<LittleEndian>()?,
_field4: cursor.read_u32::<LittleEndian>()?,
_field5: cursor.read_u32::<LittleEndian>()?,
skin: cursor.read_u32::<LittleEndian>()?,
_field6: cursor.read_u32::<LittleEndian>()?,
})
}
}
pub fn load_rare_monster_file<T: serde::de::DeserializeOwned>(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).unwrap();
toml::from_str::<T>(s.as_str()).unwrap()
}
#[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<MonsterType, f32>,
}
impl RareMonsterAppearTable {
pub fn new(episode: Episode) -> RareMonsterAppearTable {
let cfg: HashMap<String, f32> = load_rare_monster_file(episode);
let appear_rates: HashMap<MonsterType, f32> = cfg
.into_iter()
.filter_map(|(monster, rate)| {
let monster: MonsterType = monster.parse().ok()?;
Some((monster, rate))
})
.collect();
RareMonsterAppearTable {
appear_rate: appear_rates,
}
}
fn roll_is_rare(&self, monster: &MonsterType) -> bool {
rand_chacha::ChaChaRng::from_entropy().gen::<f32>() < *self.appear_rate.get(monster).unwrap_or(&0.0f32)
}
pub fn apply(&self, enemy: MapEnemy, event: Option<RareEnemyEvent>) -> MapEnemy {
if enemy.can_be_rare() && self.roll_is_rare(&enemy.monster) {
enemy.into_rare(event)
}
else {
enemy
}
}
}
#[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<MapEnemy, MapEnemyError> {
// 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,
}
}
#[must_use]
pub fn set_shiny(self) -> MapEnemy {
MapEnemy {
shiny: true,
..self
}
}
pub fn can_be_rare(&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
*/
#[must_use]
pub fn into_rare(self, event: Option<RareEnemyEvent>) -> MapEnemy {
match (self.monster, self.map_area.to_episode(), event) {
(MonsterType::RagRappy, Episode::One, _) => {MapEnemy {monster: MonsterType::AlRappy, shiny:true, ..self}},
(MonsterType::RagRappy, Episode::Two, Some(RareEnemyEvent::Easter)) => {MapEnemy {monster: MonsterType::EasterRappy, shiny:true, ..self}},
(MonsterType::RagRappy, Episode::Two, Some(RareEnemyEvent::Halloween)) => {MapEnemy {monster: MonsterType::HalloRappy, shiny:true, ..self}},
(MonsterType::RagRappy, Episode::Two, Some(RareEnemyEvent::Christmas)) => {MapEnemy {monster: MonsterType::StRappy, shiny:true, ..self}},
(MonsterType::RagRappy, Episode::Two, _) => {MapEnemy {monster: MonsterType::LoveRappy, 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},
}
}
}