diff --git a/src/ship/map/enemy.rs b/src/ship/map/enemy.rs index 32e53df..5f9a60e 100644 --- a/src/ship/map/enemy.rs +++ b/src/ship/map/enemy.rs @@ -37,7 +37,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::()?, @@ -80,21 +80,24 @@ 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 { + println!("enemy.rs::from_raw - {:?}", enemy); // 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 +106,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 +128,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 +150,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 +219,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 +264,7 @@ impl MapEnemy { dropped_item: false, gave_exp: false, player_hit: [false; 4], + shiny: false, }) } @@ -269,6 +276,14 @@ impl MapEnemy { dropped_item: false, gave_exp: false, player_hit: [false; 4], + shiny: false, + } + } + + pub fn set_shiny(self) -> MapEnemy { + MapEnemy { + shiny: true, + ..self } } } diff --git a/src/ship/map/maps.rs b/src/ship/map/maps.rs index e1558d8..7f18e50 100644 --- a/src/ship/map/maps.rs +++ b/src/ship/map/maps.rs @@ -12,6 +12,8 @@ use crate::ship::room::{Episode, RoomMode}; // TODO: don't use * use crate::ship::map::*; +use rand::{Rng}; + pub fn objects_from_stream(cursor: &mut impl Read, episode: &Episode, map_area: &MapArea) -> Vec> { let mut object_data = Vec::new(); @@ -30,19 +32,89 @@ 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); + /* + TODO: load rare monster rates config + */ enemy .map_or(vec![None], |monster| { - let mut monsters = vec![Some(monster)]; - + let mut monsters = Vec::new(); + monsters.push(Some(monster)); + match monster.monster { - MonsterType::Monest => { - for _ in 0..30 { - monsters.push(Some(MapEnemy::new(MonsterType::Mothmant, monster.map_area))); + // TODO: make real spawn rates + // TODO: specific ep 2 event rappies + MonsterType::RagRappy => {if rand::thread_rng().gen_range(0, 100) < 11 { + monsters.pop(); + match episode { + Episode::One => {monsters.push(Some(MapEnemy::new(MonsterType::AlRappy, monster.map_area).set_shiny()))}, + Episode::Two => {monsters.push(Some(MapEnemy::new(MonsterType::EventRappy, monster.map_area).set_shiny()))}, + _ => {unreachable!()}, } - }, + + }}, + MonsterType::Hildebear => {if rand::thread_rng().gen_range(0, 100) < 11 { + monsters.pop(); + monsters.push(Some(MapEnemy::new(MonsterType::Hildeblue, monster.map_area).set_shiny())) + }}, + MonsterType::PoisonLily => {if rand::thread_rng().gen_range(0, 100) < 11 { + monsters.pop(); + monsters.push(Some(MapEnemy::new(MonsterType::NarLily, monster.map_area).set_shiny())) + }}, + // TODO: client increments by 5 for slimes instead of 4 segawhyyyyy????? MonsterType::PofuillySlime => { + monsters.pop(); + for _ in 0..5 { + if rand::thread_rng().gen_range(0, 100) < 11 { + monsters.push(Some(MapEnemy::new(MonsterType::PouillySlime, monster.map_area).set_shiny())) + } else { + monsters.push(Some(MapEnemy::new(MonsterType::PofuillySlime, monster.map_area))) + } + } + }, + MonsterType::PouillySlime => { + // guaranteed rare slime already pushed + // roll for the other 3 copies for _ in 0..4 { - monsters.push(Some(MapEnemy::new(MonsterType::PofuillySlime, monster.map_area))); + if rand::thread_rng().gen_range(0, 100) < 11 { + monsters.push(Some(MapEnemy::new(MonsterType::PouillySlime, monster.map_area).set_shiny())) + } else { + monsters.push(Some(MapEnemy::new(MonsterType::PofuillySlime, monster.map_area))) + } + } + }, + MonsterType::SandRappyCrater => {if rand::thread_rng().gen_range(0, 100) < 11 { + monsters.pop(); + monsters.push(Some(MapEnemy::new(MonsterType::DelRappyCrater, monster.map_area).set_shiny())) + }}, + MonsterType::ZuCrater => {if rand::thread_rng().gen_range(0, 100) < 11 { + monsters.pop(); + monsters.push(Some(MapEnemy::new(MonsterType::PazuzuCrater, monster.map_area).set_shiny())) + }}, + MonsterType::Dorphon => {if rand::thread_rng().gen_range(0, 100) < 11 { + monsters.pop(); + monsters.push(Some(MapEnemy::new(MonsterType::DorphonEclair, monster.map_area).set_shiny())) + }}, + MonsterType::SandRappyDesert => {if rand::thread_rng().gen_range(0, 100) < 11 { + monsters.pop(); + monsters.push(Some(MapEnemy::new(MonsterType::DelRappyDesert, monster.map_area).set_shiny())) + }}, + MonsterType::ZuDesert => {if rand::thread_rng().gen_range(0, 100) < 11 { + monsters.pop(); + monsters.push(Some(MapEnemy::new(MonsterType::PazuzuDesert, monster.map_area).set_shiny())) + }}, + MonsterType::MerissaA => {if rand::thread_rng().gen_range(0, 100) < 11 { + monsters.pop(); + monsters.push(Some(MapEnemy::new(MonsterType::MerissaAA, monster.map_area).set_shiny())) + }}, + MonsterType::SaintMillion | MonsterType::Shambertin => {if rand::thread_rng().gen_range(0, 100) < 11 { + monsters.pop(); + monsters.push(Some(MapEnemy::new(MonsterType::Kondrieu, monster.map_area).set_shiny())) + }}, + + + MonsterType::Monest => { + for _ in 0..30 { + monsters.push(Some(MapEnemy::new(MonsterType::Mothmant, monster.map_area))); } }, MonsterType::PanArms => { @@ -309,5 +381,26 @@ impl Maps { self.enemy_data = enemies; 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 i in 0..shiny.len() { + if let Some(j) = rare_monsters.iter().position(|&x| x == 0xFFFF) { + rare_monsters[j] = shiny[i].0 as u16; + } else { + break + } + } + rare_monsters + } +} diff --git a/src/ship/packet/builder/room.rs b/src/ship/packet/builder/room.rs index 1effe5d..3fe196a 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) -> Result { + Ok(RareMonsterList { + ids: rare_monster_vec.try_into().unwrap_or([0xFFFFu16; 16]), + }) +} \ No newline at end of file diff --git a/src/ship/packet/handler/room.rs b/src/ship/packet/handler/room.rs index c3be9dc..9a85def 100644 --- a/src/ship/packet/handler/room.rs +++ b/src/ship/packet/handler/room.rs @@ -108,13 +108,17 @@ 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 .map(move |client| { vec![ (client.client, SendShipPacket::Message(Message::new(GameMessage::BurstDone(BurstDone { @@ -122,7 +126,16 @@ pub fn done_bursting(id: ClientId, target: 0 })))), ] - }).flatten()) + }) + .flatten() + ); + + if rare_monster_list.is_some() { + let rare_monster_packet = SendShipPacket::RareMonsterList(builder::room::build_rare_monster_list(rare_monster_list.unwrap()).unwrap()); // TODO: don't double unwrap + result = Box::new(result.chain(vec![(id, rare_monster_packet)])); + } + + result } pub fn request_room_list(id: ClientId, diff --git a/src/ship/ship.rs b/src/ship/ship.rs index 8091647..250f2c0 100644 --- a/src/ship/ship.rs +++ b/src/ship/ship.rs @@ -184,6 +184,7 @@ pub enum SendShipPacket { DoneLoadingQuest(DoneLoadingQuest), BankItemList(BankItemList), RedirectClient(RedirectClient), + RareMonsterList(RareMonsterList), } impl SendServerPacket for SendShipPacket { @@ -221,6 +222,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(), } } } @@ -384,7 +386,6 @@ impl ShipServerStateBuilder { } } - pub struct Block { client_location: Box, pub rooms: Box,