From e2d45f159959ed17ce89d85f37737d7fe0decd15 Mon Sep 17 00:00:00 2001
From: andy <andynewjack@protonmail.com>
Date: Sat, 20 Feb 2021 22:29:40 +0000
Subject: [PATCH 01/15] rare monsters get

---
 src/ship/map/enemy.rs           |  71 +++++++++++++--------
 src/ship/map/maps.rs            | 109 +++++++++++++++++++++++++++++---
 src/ship/packet/builder/room.rs |   6 ++
 src/ship/packet/handler/room.rs |  19 +++++-
 src/ship/ship.rs                |   3 +-
 5 files changed, 168 insertions(+), 40 deletions(-)

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<R: Read>(cursor: &mut R) -> Result<RawMapEnemy, std::io::Error> {
         Ok(RawMapEnemy {
-            id: cursor.read_u32::<LittleEndian>()?,
+            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>()?,
@@ -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<MapEnemy, MapEnemyError> {
+        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<Option<MapObject>> {
     let mut object_data = Vec::new();
@@ -30,21 +32,91 @@ fn objects_from_map_data(path: PathBuf, episode: &Episode, map_area: &MapArea) -
 
 fn parse_enemy(episode: &Episode, map_area: &MapArea, raw_enemy: RawMapEnemy) -> Vec<Option<MapEnemy>> {
     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 {
+                // 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 {
+                        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::PofuillySlime => {
-                    for _ in 0..4 {
-                        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)));
@@ -309,5 +381,26 @@ impl Maps {
         self.enemy_data = enemies;
         self.object_data = objects;
     }
-}
 
+    pub fn get_rare_monster_list(&self) -> Vec<u16> {
+        let mut rare_monsters = vec![0xFFFF; 16];
+        let shiny: Vec<(usize, &Option<MapEnemy>)> = 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<u16>) -> Result<RareMonsterList, ShipError> {
+    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<dyn Iterator<Item = (ClientId, SendShipPacket)> + Send> {
     let area = client_location.get_area(id).unwrap();
+    let mut rare_monster_list: Option<Vec<u16>> = 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<dyn Iterator<Item=(ClientId, SendShipPacket)> + 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<EG: EntityGateway> ShipServerStateBuilder<EG> {
     }
 }
 
-
 pub struct Block {
     client_location: Box<ClientLocation>,
     pub rooms: Box<Rooms>,
-- 
2.36.0


From 2a7e793b23ba452cc550bf86a0c67c766dac2d14 Mon Sep 17 00:00:00 2001
From: andy <andynewjack@protonmail.com>
Date: Fri, 25 Jun 2021 23:00:57 +0000
Subject: [PATCH 02/15] ep1 doesn't have easter rappies

---
 data/battle_param/ep1_multi_hard.toml     | 2 +-
 data/battle_param/ep1_multi_normal.toml   | 2 +-
 data/battle_param/ep1_multi_ultimate.toml | 2 +-
 data/battle_param/ep1_multi_veryhard.toml | 2 +-
 data/battle_param/ep1_solo_hard.toml      | 2 +-
 data/battle_param/ep1_solo_normal.toml    | 2 +-
 data/battle_param/ep1_solo_ultimate.toml  | 2 +-
 data/battle_param/ep1_solo_veryhard.toml  | 2 +-
 src/ship/packet/handler/room.rs           | 2 +-
 src/ship/ship.rs                          | 1 -
 10 files changed, 9 insertions(+), 10 deletions(-)

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_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/src/ship/packet/handler/room.rs b/src/ship/packet/handler/room.rs
index 9a85def..283c2e4 100644
--- a/src/ship/packet/handler/room.rs
+++ b/src/ship/packet/handler/room.rs
@@ -113,7 +113,7 @@ pub fn done_bursting(id: ClientId,
         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
 
diff --git a/src/ship/ship.rs b/src/ship/ship.rs
index 250f2c0..09d89bb 100644
--- a/src/ship/ship.rs
+++ b/src/ship/ship.rs
@@ -376,7 +376,6 @@ impl<EG: EntityGateway> ShipServerStateBuilder<EG> {
             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())),
-- 
2.36.0


From 7c7cf95942ddf0ff803f2ecbe882a6c2e0e5bae5 Mon Sep 17 00:00:00 2001
From: andy <andynewjack@protonmail.com>
Date: Mon, 28 Jun 2021 17:42:50 +0000
Subject: [PATCH 03/15] various config files

---
 data/battle_param/ep1_rare_monster.toml    | 15 ++++++++++++
 data/battle_param/ep2_rare_monster.toml    | 13 +++++++++++
 data/battle_param/ep4_rare_monster.toml    | 27 ++++++++++++++++++++++
 data/battle_param/global_rare_monster.toml | 11 +++++++++
 src/ship/map/maps.rs                       |  2 +-
 5 files changed, 67 insertions(+), 1 deletion(-)
 create mode 100644 data/battle_param/ep1_rare_monster.toml
 create mode 100644 data/battle_param/ep2_rare_monster.toml
 create mode 100644 data/battle_param/ep4_rare_monster.toml
 create mode 100644 data/battle_param/global_rare_monster.toml

diff --git a/data/battle_param/ep1_rare_monster.toml b/data/battle_param/ep1_rare_monster.toml
new file mode 100644
index 0000000..b7e62c4
--- /dev/null
+++ b/data/battle_param/ep1_rare_monster.toml
@@ -0,0 +1,15 @@
+# 1/100 = 0.01
+# 1/256 = 0.00390625
+# 1/512 = 0.001953125
+
+[[Hildebear]]
+rate = 0.01
+
+[[RagRappy]]
+rate = 0.01
+
+[[PoisonLily]]
+rate = 0.01
+
+[[PofuillySlime]]
+rate = 0.01
\ No newline at end of file
diff --git a/data/battle_param/ep2_rare_monster.toml b/data/battle_param/ep2_rare_monster.toml
new file mode 100644
index 0000000..2b6f916
--- /dev/null
+++ b/data/battle_param/ep2_rare_monster.toml
@@ -0,0 +1,13 @@
+# 1/100 = 0.01
+# 1/256 = 0.00390625
+# 1/512 = 0.001953125
+
+[[Hildebear]]
+rate = 0.01
+
+[[RagRappy]]
+rate = 0.01
+
+[[PoisonLily]]
+rate = 0.01
+
diff --git a/data/battle_param/ep4_rare_monster.toml b/data/battle_param/ep4_rare_monster.toml
new file mode 100644
index 0000000..9a4b244
--- /dev/null
+++ b/data/battle_param/ep4_rare_monster.toml
@@ -0,0 +1,27 @@
+# 1/100 = 0.01
+# 1/256 = 0.00390625
+# 1/512 = 0.001953125
+
+[[SandRappyCrater]]
+rate = 0.01
+
+[[ZuCrater]]
+rate = 0.01
+
+[[Dorphon]]
+rate = 0.01
+
+[[SandRappyDesert]]
+rate = 0.01
+
+[[ZuDesert]]
+rate = 0.01
+
+[[MerissaA]]
+rate = 0.01
+
+[[Shambertin]]
+rate = 0.1
+
+[[SaintMillion]]
+rate = 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/src/ship/map/maps.rs b/src/ship/map/maps.rs
index 7f18e50..866ae12 100644
--- a/src/ship/map/maps.rs
+++ b/src/ship/map/maps.rs
@@ -73,7 +73,7 @@ fn parse_enemy(episode: &Episode, map_area: &MapArea, raw_enemy: RawMapEnemy) ->
                 },
                 MonsterType::PouillySlime => {
                     // guaranteed rare slime already pushed
-                    // roll for the other 3 copies
+                    // roll for the other 3 (4?) copies
                     for _ in 0..4 {
                         if rand::thread_rng().gen_range(0, 100) < 11 {
                             monsters.push(Some(MapEnemy::new(MonsterType::PouillySlime, monster.map_area).set_shiny()))
-- 
2.36.0


From f5946013a142330a2466572dbea439ad87df9d79 Mon Sep 17 00:00:00 2001
From: andy <andynewjack@protonmail.com>
Date: Mon, 12 Jul 2021 02:24:31 +0000
Subject: [PATCH 04/15] rare monster appear config files. init rare monster
 appear table stuff

---
 data/battle_param/ep1_rare_monster.toml |  8 ++---
 data/battle_param/ep2_rare_monster.toml |  6 ++--
 data/battle_param/ep4_rare_monster.toml | 16 ++++-----
 src/ship/drops/mod.rs                   | 11 ++++++
 src/ship/map/enemy.rs                   | 45 +++++++++++++++++++++++++
 5 files changed, 71 insertions(+), 15 deletions(-)

diff --git a/data/battle_param/ep1_rare_monster.toml b/data/battle_param/ep1_rare_monster.toml
index b7e62c4..b806f9a 100644
--- a/data/battle_param/ep1_rare_monster.toml
+++ b/data/battle_param/ep1_rare_monster.toml
@@ -3,13 +3,13 @@
 # 1/512 = 0.001953125
 
 [[Hildebear]]
-rate = 0.01
+appear_rate = 0.01
 
 [[RagRappy]]
-rate = 0.01
+appear_rate = 0.01
 
 [[PoisonLily]]
-rate = 0.01
+appear_rate = 0.01
 
 [[PofuillySlime]]
-rate = 0.01
\ No newline at end of file
+appear_rate = 0.01
\ No newline at end of file
diff --git a/data/battle_param/ep2_rare_monster.toml b/data/battle_param/ep2_rare_monster.toml
index 2b6f916..948b189 100644
--- a/data/battle_param/ep2_rare_monster.toml
+++ b/data/battle_param/ep2_rare_monster.toml
@@ -3,11 +3,11 @@
 # 1/512 = 0.001953125
 
 [[Hildebear]]
-rate = 0.01
+appear_rate = 0.01
 
 [[RagRappy]]
-rate = 0.01
+appear_rate = 0.01
 
 [[PoisonLily]]
-rate = 0.01
+appear_rate = 0.01
 
diff --git a/data/battle_param/ep4_rare_monster.toml b/data/battle_param/ep4_rare_monster.toml
index 9a4b244..9f807c2 100644
--- a/data/battle_param/ep4_rare_monster.toml
+++ b/data/battle_param/ep4_rare_monster.toml
@@ -3,25 +3,25 @@
 # 1/512 = 0.001953125
 
 [[SandRappyCrater]]
-rate = 0.01
+appear_rate = 0.01
 
 [[ZuCrater]]
-rate = 0.01
+appear_rate = 0.01
 
 [[Dorphon]]
-rate = 0.01
+appear_rate = 0.01
 
 [[SandRappyDesert]]
-rate = 0.01
+appear_rate = 0.01
 
 [[ZuDesert]]
-rate = 0.01
+appear_rate = 0.01
 
 [[MerissaA]]
-rate = 0.01
+appear_rate = 0.01
 
 [[Shambertin]]
-rate = 0.1
+appear_rate = 0.1
 
 [[SaintMillion]]
-rate = 0.1
\ No newline at end of file
+appear_rate = 0.1
\ No newline at end of file
diff --git a/src/ship/drops/mod.rs b/src/ship/drops/mod.rs
index 8de0408..4ada11e 100644
--- a/src/ship/drops/mod.rs
+++ b/src/ship/drops/mod.rs
@@ -55,6 +55,17 @@ pub fn load_data_file<T: serde::de::DeserializeOwned>(episode: Episode, difficul
     toml::from_str::<T>(s.as_str()).unwrap()
 }
 
+// this is just copypaste
+pub fn load_rare_monster_file<T: serde::de::DeserializeOwned>(episode: Episode) -> T {
+    let mut path = PathBuf::from("data/battle_param/");
+    path.push(episode.to_string().to_lowercase());
+    path.push("_rare_monster.toml");
+    
+    let mut f = File::open(path).unwrap();
+    let mut s = String::new();
+    f.read_to_string(&mut s);
+    toml::from_str::<T>(s.as_str()).unwrap()
+}
 
 #[derive(Debug, Serialize, Deserialize, Copy, Clone)]
 pub enum MonsterDropType {
diff --git a/src/ship/map/enemy.rs b/src/ship/map/enemy.rs
index 5f9a60e..62c2155 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,
@@ -70,6 +75,46 @@ pub enum MapEnemyError {
     MapAreaError(#[from] MapAreaError),
 }
 
+#[derive(Debug, Serialize, Deserialize)]
+pub struct RareMonsterAppearRate(pub f32);
+
+// making this `pub type` doesn't allow `impl`s to be defined?
+#[derive(Debug, Serialize, Deserialize)]
+pub struct RareMonsterAppearTable {
+    appear_rate: HashMap<MonsterType, RareMonsterAppearRate>,
+    seed: u32,
+}
+
+impl RareMonsterAppearTable {
+    pub fn new(episode: Episode, room_seed: u32) -> RareMonsterAppearTable {
+        let cfg: HashMap<String, f32> = load_rare_monster_file(episode);
+
+        let appear_rates: HashMap<MonsterType, RareMonsterAppearRate> = cfg
+            .into_iter()
+            .map(|(monster, appear_rate)| {
+                let monster: MonsterType = monster.parse().unwrap();
+                let appear_rate = RareMonsterAppearRate(appear_rate);
+                (monster, appear_rate)
+            })
+            .collect();
+
+        RareMonsterAppearTable {
+            appear_rate: appear_rates,
+            seed: room_seed,
+        }
+    }
+
+    pub fn roll_appearance(&self, monster: &MonsterType) -> bool {
+        let mut rng = rand_chacha::ChaChaRng::seed_from_u64(self.seed as u64);
+        if rng.gen::<f32>() < self.appear_rate.get(monster).unwrap_or(&RareMonsterAppearRate(0.0f32)).0 {
+            true
+        }
+        else {
+            false
+        }
+    }
+}
+
 
 #[derive(Debug, Copy, Clone)]
 pub struct MapEnemy {
-- 
2.36.0


From 080e939e569b8c9978bd461dd003d0f7d7d723f4 Mon Sep 17 00:00:00 2001
From: andy <andynewjack@protonmail.com>
Date: Mon, 12 Jul 2021 19:32:16 +0000
Subject: [PATCH 05/15] differentiate between player loading quest and server
 preloading all quests

---
 src/ship/packet/handler/quest.rs | 2 +-
 src/ship/ship.rs                 | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/ship/packet/handler/quest.rs b/src/ship/packet/handler/quest.rs
index 41412ac..90359c3 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<Box<dyn Iterator<Item = (ClientId, SendShipPacket)> + Send>, ShipError> {
     let (_, category_quests) = quests.iter()
         .nth(questmenuselect.category as usize)
diff --git a/src/ship/ship.rs b/src/ship/ship.rs
index 09d89bb..09e0afa 100644
--- a/src/ship/ship.rs
+++ b/src/ship/ship.rs
@@ -595,7 +595,7 @@ impl<EG: EntityGateway> ServerState for ShipServerState<EG> {
             },
             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!();
-- 
2.36.0


From 39660f0cb8cf0c7616e21fbae77e08c7819b9b1f Mon Sep 17 00:00:00 2001
From: andy <andynewjack@protonmail.com>
Date: Sun, 18 Jul 2021 18:40:10 +0000
Subject: [PATCH 06/15] load rare monster config file each time a room is
 created

---
 data/battle_param/ep1_rare_monster.toml | 16 ++--
 data/battle_param/ep2_rare_monster.toml | 12 +--
 data/battle_param/ep4_rare_monster.toml | 32 +++-----
 src/ship/drops/mod.rs                   |  4 +-
 src/ship/map/area.rs                    | 48 ++++++++++++
 src/ship/map/enemy.rs                   | 81 ++++++++++++++++++---
 src/ship/map/maps.rs                    | 97 ++++++++++++++++++-------
 src/ship/map/mod.rs                     |  2 +-
 src/ship/packet/handler/room.rs         |  3 +-
 src/ship/room.rs                        | 12 ++-
 10 files changed, 221 insertions(+), 86 deletions(-)

diff --git a/data/battle_param/ep1_rare_monster.toml b/data/battle_param/ep1_rare_monster.toml
index b806f9a..1e1f3e0 100644
--- a/data/battle_param/ep1_rare_monster.toml
+++ b/data/battle_param/ep1_rare_monster.toml
@@ -1,15 +1,9 @@
+# 1/10 = 0.1
 # 1/100 = 0.01
 # 1/256 = 0.00390625
 # 1/512 = 0.001953125
 
-[[Hildebear]]
-appear_rate = 0.01
-
-[[RagRappy]]
-appear_rate = 0.01
-
-[[PoisonLily]]
-appear_rate = 0.01
-
-[[PofuillySlime]]
-appear_rate = 0.01
\ No newline at end of file
+Hildebear = 1.0
+RagRappy = 0.1
+PoisonLily = 0.1
+PofuillySlime = 0.1
\ No newline at end of file
diff --git a/data/battle_param/ep2_rare_monster.toml b/data/battle_param/ep2_rare_monster.toml
index 948b189..290f727 100644
--- a/data/battle_param/ep2_rare_monster.toml
+++ b/data/battle_param/ep2_rare_monster.toml
@@ -1,13 +1,9 @@
+# 1/10 = 0.1
 # 1/100 = 0.01
 # 1/256 = 0.00390625
 # 1/512 = 0.001953125
 
-[[Hildebear]]
-appear_rate = 0.01
-
-[[RagRappy]]
-appear_rate = 0.01
-
-[[PoisonLily]]
-appear_rate = 0.01
+Hildebear = 0.01
+RagRappy = 0.01
+PoisonLily = 0.01
 
diff --git a/data/battle_param/ep4_rare_monster.toml b/data/battle_param/ep4_rare_monster.toml
index 9f807c2..0ddddbf 100644
--- a/data/battle_param/ep4_rare_monster.toml
+++ b/data/battle_param/ep4_rare_monster.toml
@@ -1,27 +1,13 @@
+# 1/10 = 0.1
 # 1/100 = 0.01
 # 1/256 = 0.00390625
 # 1/512 = 0.001953125
 
-[[SandRappyCrater]]
-appear_rate = 0.01
-
-[[ZuCrater]]
-appear_rate = 0.01
-
-[[Dorphon]]
-appear_rate = 0.01
-
-[[SandRappyDesert]]
-appear_rate = 0.01
-
-[[ZuDesert]]
-appear_rate = 0.01
-
-[[MerissaA]]
-appear_rate = 0.01
-
-[[Shambertin]]
-appear_rate = 0.1
-
-[[SaintMillion]]
-appear_rate = 0.1
\ No newline at end of file
+SandRappyCrater = 0.01
+ZuCrater = 0.01
+Dorphon = 0.01
+SandRappyDesert = 0.01
+ZuDesert = 0.01
+MerissaA = 0.01
+Shambertin = 0.1
+SaintMillion = 0.1
\ No newline at end of file
diff --git a/src/ship/drops/mod.rs b/src/ship/drops/mod.rs
index 4ada11e..427da74 100644
--- a/src/ship/drops/mod.rs
+++ b/src/ship/drops/mod.rs
@@ -58,8 +58,8 @@ pub fn load_data_file<T: serde::de::DeserializeOwned>(episode: Episode, difficul
 // this is just copypaste
 pub fn load_rare_monster_file<T: serde::de::DeserializeOwned>(episode: Episode) -> T {
     let mut path = PathBuf::from("data/battle_param/");
-    path.push(episode.to_string().to_lowercase());
-    path.push("_rare_monster.toml");
+    path.push(episode.to_string().to_lowercase() + "_rare_monster.toml");
+    println!("rare monster file path: {:?}", path);
     
     let mut f = File::open(path).unwrap();
     let mut s = String::new();
diff --git a/src/ship/map/area.rs b/src/ship/map/area.rs
index a26aa19..9da3374 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 {
+            Pioneer2Ep1 => Episode::One,
+            Forest1 => Episode::One,
+            Forest2 => Episode::One,
+            Caves1 => Episode::One,
+            Caves2 => Episode::One,
+            Caves3 => Episode::One,
+            Mines1 => Episode::One,
+            Mines2 => Episode::One,
+            Ruins1 => Episode::One,
+            Ruins2 => Episode::One,
+            Ruins3 => Episode::One,
+            Dragon => Episode::One,
+            DeRolLe => Episode::One,
+            VolOpt => Episode::One,
+            DarkFalz => Episode::One,
+            Pioneer2Ep2 => Episode::Two,
+            VrTempleAlpha => Episode::Two,
+            VrTempleBeta => Episode::Two,
+            VrSpaceshipAlpha => Episode::Two,
+            VrSpaceshipBeta => Episode::Two,
+            Cca => Episode::Two,
+            JungleAreaNorth => Episode::Two,
+            JungleAreaEast => Episode::Two,
+            Mountain => Episode::Two,
+            Seaside => Episode::Two,
+            SeabedUpper => Episode::Two,
+            SeabedLower => Episode::Two,
+            GalGryphon => Episode::Two,
+            OlgaFlow => Episode::Two,
+            BarbaRay => Episode::Two,
+            GolDragon => Episode::Two,
+            SeasideNight => Episode::Two,
+            Tower => Episode::Two,
+            Pioneer2Ep4 => Episode::Four,
+            CraterEast => Episode::Four,
+            CraterWest => Episode::Four,
+            CraterSouth => Episode::Four,
+            CraterNorth => Episode::Four,
+            CraterInterior => Episode::Four,
+            SubDesert1 => Episode::Four,
+            SubDesert2 => Episode::Four,
+            SubDesert3 => Episode::Four,
+            SaintMillion => Episode::Four,
+        }
+    }
 }
 
 
diff --git a/src/ship/map/enemy.rs b/src/ship/map/enemy.rs
index 62c2155..6d4d7c7 100644
--- a/src/ship/map/enemy.rs
+++ b/src/ship/map/enemy.rs
@@ -75,25 +75,27 @@ pub enum MapEnemyError {
     MapAreaError(#[from] MapAreaError),
 }
 
-#[derive(Debug, Serialize, Deserialize)]
-pub struct RareMonsterAppearRate(pub f32);
+// TODO: is this even needed/used?
+// #[derive(Clone, Debug, Serialize, Deserialize)]
+// pub type RareMonsterAppearRate: HashMap<MonsterType, f32>,
 
 // making this `pub type` doesn't allow `impl`s to be defined?
-#[derive(Debug, Serialize, Deserialize)]
+#[derive(Clone, Debug, Serialize, Deserialize)]
 pub struct RareMonsterAppearTable {
-    appear_rate: HashMap<MonsterType, RareMonsterAppearRate>,
-    seed: u32,
+    appear_rate: HashMap<MonsterType, f32>,
+    seed: u32
 }
 
 impl RareMonsterAppearTable {
     pub fn new(episode: Episode, room_seed: u32) -> RareMonsterAppearTable {
         let cfg: HashMap<String, f32> = load_rare_monster_file(episode);
+        println!("got cfg: {:?}", cfg);
 
-        let appear_rates: HashMap<MonsterType, RareMonsterAppearRate> = cfg
+        let appear_rates: HashMap<MonsterType, f32> = cfg
             .into_iter()
-            .map(|(monster, appear_rate)| {
-                let monster: MonsterType = monster.parse().unwrap();
-                let appear_rate = RareMonsterAppearRate(appear_rate);
+            .map(|(monster, rate)| {
+                let monster: MonsterType = monster.parse().unwrap(); // TODO: don't unwrap!
+                let appear_rate = rate;
                 (monster, appear_rate)
             })
             .collect();
@@ -105,11 +107,14 @@ impl RareMonsterAppearTable {
     }
 
     pub fn roll_appearance(&self, monster: &MonsterType) -> bool {
-        let mut rng = rand_chacha::ChaChaRng::seed_from_u64(self.seed as u64);
-        if rng.gen::<f32>() < self.appear_rate.get(monster).unwrap_or(&RareMonsterAppearRate(0.0f32)).0 {
+        let mut rng = rand_chacha::ChaChaRng::seed_from_u64(self.seed as u64); // TODO: figure out how to seed with a u32
+        println!("rolling for {:?} appearance", monster);
+        if rng.gen::<f32>() < *self.appear_rate.get(monster).unwrap_or(&0.0f32) {
+            println!("its a rare!");
             true
         }
         else {
+            println!("lol sucker");
             false
         }
     }
@@ -331,5 +336,59 @@ impl MapEnemy {
             ..self
         }
     }
+
+    // TODO: does this actually do anything useful?
+    pub fn has_rare_appearance(self) -> bool {
+        match 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 => true,
+            _ => false
+        }
+    }
+
+    // TODO: does `shiny` need to be set here?
+    // TODO: distinguish between a `random` rare monster and a `set/guaranteed` rare monster? (does any acceptable quest even have this?)
+    pub fn set_rare_appearance(self) -> MapEnemy {
+        match 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 => self.set_shiny(),
+            _ => self,
+        }
+    }
+    
+    // // TODO: does `shiny` need to be set here?
+    // // TODO: distinguish between a `random` rare monster and a `set/guaranteed` rare monster? (does any acceptable quest even have this?)
+    // pub fn set_rare_appearance(self) -> MapEnemy {
+    //     match (self.monster, self.map_area.to_episode()) {
+    //         (MonsterType::RagRappy, Episode::One) => {MapEnemy {monster: MonsterType::AlRappy, ..self}},
+    //         (MonsterType::RagRappy, Episode::Two) => {MapEnemy {monster: MonsterType::EventRappy, ..self}},
+    //         (MonsterType::Hildebear, _) => {MapEnemy {monster: MonsterType::Hildeblue, ..self}},
+    //         (MonsterType::PoisonLily, _) => {MapEnemy {monster: MonsterType::NarLily, ..self}},
+    //         (MonsterType::PofuillySlime, _) => {MapEnemy {monster: MonsterType::PouillySlime, ..self}},
+    //         (MonsterType::SandRappyCrater, _) => {MapEnemy {monster: MonsterType::DelRappyCrater, ..self}},
+    //         (MonsterType::ZuCrater, _) => {MapEnemy {monster: MonsterType::PazuzuCrater, ..self}},
+    //         (MonsterType::Dorphon, _) => {MapEnemy {monster: MonsterType::DorphonEclair, ..self}},
+    //         (MonsterType::SandRappyDesert, _) => {MapEnemy {monster: MonsterType::DelRappyDesert, ..self}},
+    //         (MonsterType::ZuDesert, _) => {MapEnemy {monster: MonsterType::PazuzuDesert, ..self}},
+    //         (MonsterType::MerissaA, _) => {MapEnemy {monster: MonsterType::MerissaAA, ..self}},
+    //         (MonsterType::SaintMillion, _) => {MapEnemy {monster: MonsterType::Kondrieu, ..self}},
+    //         (MonsterType::Shambertin, _) => {MapEnemy {monster: MonsterType::Kondrieu, ..self}},
+    //         _ => {self},
+    //     }
+    // }
+    
+    // in theory this should only be called on monsters we know can have rare types
+    pub fn roll_appearance_for_quest(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 866ae12..71ef38d 100644
--- a/src/ship/map/maps.rs
+++ b/src/ship/map/maps.rs
@@ -12,7 +12,7 @@ use crate::ship::room::{Episode, RoomMode};
 // TODO: don't use *
 use crate::ship::map::*;
 
-use rand::{Rng};
+// use rand::{Rng};
 
 
 pub fn objects_from_stream(cursor: &mut impl Read, episode: &Episode, map_area: &MapArea) -> Vec<Option<MapObject>> {
@@ -31,10 +31,10 @@ fn objects_from_map_data(path: PathBuf, episode: &Episode, map_area: &MapArea) -
 }
 
 fn parse_enemy(episode: &Episode, map_area: &MapArea, raw_enemy: RawMapEnemy) -> Vec<Option<MapEnemy>> {
+// fn parse_enemy(episode: &Episode, map_area: &MapArea, raw_enemy: RawMapEnemy, rare_monster_table: RareMonsterAppearTable) -> Vec<Option<MapEnemy>> {
     let enemy = MapEnemy::from_raw(raw_enemy, episode, map_area);
-    /*
-    TODO: load rare monster rates config
-    */
+
+    // TODO: load rare monster rates config
     enemy
         .map_or(vec![None], |monster| {
             let mut monsters = Vec::new();
@@ -43,7 +43,8 @@ fn parse_enemy(episode: &Episode, map_area: &MapArea, raw_enemy: RawMapEnemy) ->
             match monster.monster {
                 // TODO: make real spawn rates
                 // TODO: specific ep 2 event rappies
-                MonsterType::RagRappy => {if rand::thread_rng().gen_range(0, 100) < 11 {
+                // MonsterType::RagRappy => {if rand::thread_rng().gen_range(0, 100) < 11 {
+/*                MonsterType::RagRappy => {if rare_monster_table.roll_appearance(&monster.monster) {
                     monsters.pop();
                     match episode {
                         Episode::One => {monsters.push(Some(MapEnemy::new(MonsterType::AlRappy, monster.map_area).set_shiny()))},
@@ -52,11 +53,13 @@ fn parse_enemy(episode: &Episode, map_area: &MapArea, raw_enemy: RawMapEnemy) ->
                     }
                     
                 }},
-                MonsterType::Hildebear => {if rand::thread_rng().gen_range(0, 100) < 11 {
+                // MonsterType::Hildebear => {if rand::thread_rng().gen_range(0, 100) < 11 {
+                MonsterType::Hildebear => {if rare_monster_table.roll_appearance(&monster.monster) {
                     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 {
+                // MonsterType::PoisonLily => {if rand::thread_rng().gen_range(0, 100) < 11 {
+                MonsterType::PoisonLily => {if rare_monster_table.roll_appearance(&monster.monster) {
                     monsters.pop();
                     monsters.push(Some(MapEnemy::new(MonsterType::NarLily, monster.map_area).set_shiny()))
                 }},
@@ -64,7 +67,8 @@ fn parse_enemy(episode: &Episode, map_area: &MapArea, raw_enemy: RawMapEnemy) ->
                 MonsterType::PofuillySlime => {
                     monsters.pop();
                     for _ in 0..5 {
-                        if rand::thread_rng().gen_range(0, 100) < 11 {
+                        // if rand::thread_rng().gen_range(0, 100) < 11 {
+                        if rare_monster_table.roll_appearance(&monster.monster) {
                             monsters.push(Some(MapEnemy::new(MonsterType::PouillySlime, monster.map_area).set_shiny()))
                         } else {
                             monsters.push(Some(MapEnemy::new(MonsterType::PofuillySlime, monster.map_area)))
@@ -75,42 +79,50 @@ fn parse_enemy(episode: &Episode, map_area: &MapArea, raw_enemy: RawMapEnemy) ->
                     // guaranteed rare slime already pushed
                     // roll for the other 3 (4?) copies
                     for _ in 0..4 {
-                        if rand::thread_rng().gen_range(0, 100) < 11 {
+                        // if rand::thread_rng().gen_range(0, 100) < 11 {
+                        if rare_monster_table.roll_appearance(&monster.monster) {
                             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 {
+                // MonsterType::SandRappyCrater => {if rand::thread_rng().gen_range(0, 100) < 11 {
+                MonsterType::SandRappyCrater => {if rare_monster_table.roll_appearance(&monster.monster) {
                     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 {
+                // MonsterType::ZuCrater => {if rand::thread_rng().gen_range(0, 100) < 11 {
+                MonsterType::ZuCrater => {if rare_monster_table.roll_appearance(&monster.monster) {
                     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 {
+                // MonsterType::Dorphon => {if rand::thread_rng().gen_range(0, 100) < 11 {
+                MonsterType::Dorphon => {if rare_monster_table.roll_appearance(&monster.monster) {
                     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 {
+                // MonsterType::SandRappyDesert => {if rand::thread_rng().gen_range(0, 100) < 11 {
+                MonsterType::SandRappyDesert => {if rare_monster_table.roll_appearance(&monster.monster) {
                     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 {
+                // MonsterType::ZuDesert => {if rand::thread_rng().gen_range(0, 100) < 11 {
+                MonsterType::ZuDesert => {if rare_monster_table.roll_appearance(&monster.monster) {
                     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 {
+                // MonsterType::MerissaA => {if rand::thread_rng().gen_range(0, 100) < 11 {
+                MonsterType::MerissaA => {if rare_monster_table.roll_appearance(&monster.monster) {
                     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 {
+                // MonsterType::SaintMillion | MonsterType::Shambertin => { if rand::thread_rng().gen_range(0, 100) < 11 {
+                MonsterType::SaintMillion | MonsterType::Shambertin => { if rare_monster_table.roll_appearance(&monster.monster) {
                     monsters.pop();
                     monsters.push(Some(MapEnemy::new(MonsterType::Kondrieu, monster.map_area).set_shiny()))
                 }},
-
+*/
 
                 MonsterType::Monest => {
                     for _ in 0..30 {
@@ -223,17 +235,21 @@ fn parse_enemy(episode: &Episode, map_area: &MapArea, raw_enemy: RawMapEnemy) ->
 
 
 pub fn enemy_data_from_stream(cursor: &mut impl Read, map_area: &MapArea, episode: &Episode) -> Vec<Option<MapEnemy>> {
+// pub fn enemy_data_from_stream(cursor: &mut impl Read, map_area: &MapArea, episode: &Episode, rare_monster_table: enemy::RareMonsterAppearTable) -> Vec<Option<MapEnemy>> {
     let mut enemy_data = Vec::new();
     while let Ok(enemy) = RawMapEnemy::from_byte_stream(cursor) {
         enemy_data.append(&mut parse_enemy(episode, map_area, enemy));
+        // enemy_data.append(&mut parse_enemy(episode, map_area, enemy, rare_monster_table));
     }
     enemy_data
 }
 
 fn enemy_data_from_map_data(map_variant: &MapVariant, episode: &Episode) -> Vec<Option<MapEnemy>> {
+// fn enemy_data_from_map_data(map_variant: &MapVariant, episode: &Episode, rare_monster_table: enemy::RareMonsterAppearTable) -> Vec<Option<MapEnemy>> {
     let path = map_variant.dat_file();
     let mut cursor = File::open(path).unwrap();
     enemy_data_from_stream(&mut cursor, &map_variant.map, episode)
+    // enemy_data_from_stream(&mut cursor, &map_variant.map, episode, rare_monster_table)
 }
 
 
@@ -252,7 +268,8 @@ pub struct Maps {
 }
 
 impl Maps {
-    pub fn new(room_mode: RoomMode) -> 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),
@@ -344,16 +361,20 @@ 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().map(|map_variant| {
-                objects_from_map_data(map_variant.obj_file().into(), &room_mode.episode(), &map_variant.map)
-            }).flatten().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.append(&mut enemy_data_from_map_data(map_variant, &room_mode.episode(), rare_monster_table));
+                            enemy_data
+                        }),
+            object_data: map_variants.iter()
+                        .map(|map_variant| {
+                            objects_from_map_data(map_variant.obj_file().into(), &room_mode.episode(), &map_variant.map)
+                        }).flatten().collect(),
+                        map_variants,
         };
+        maps.roll_monster_appearance(rare_monster_table);
         maps
     }
 
@@ -378,7 +399,18 @@ impl Maps {
     }
 
     pub fn set_quest_data(&mut self, enemies: Vec<Option<MapEnemy>>, objects: Vec<Option<MapObject>>) {
+    // pub fn set_quest_data(&mut self, enemies: Vec<Option<MapEnemy>>, objects: Vec<Option<MapObject>>, rare_monster_table: RareMonsterAppearTable) {
         self.enemy_data = enemies;
+        
+        // self.enemy_data = enemies
+        //                     .iter()
+        //                     .map(|&x| if x.is_some() {
+        //                         Some(x.unwrap().roll_appearance_for_quest(rare_monster_table))
+        //                     } else {
+        //                         x
+        //                     })
+        //                     .collect();
+        
         self.object_data = objects;
     }
 
@@ -403,4 +435,15 @@ impl Maps {
         }
         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() {
+                                Some(x.unwrap().roll_appearance_for_quest(&rare_monster_table))
+                            } 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/handler/room.rs b/src/ship/packet/handler/room.rs
index 283c2e4..8c2ba8c 100644
--- a/src/ship/packet/handler/room.rs
+++ b/src/ship/packet/handler/room.rs
@@ -130,9 +130,10 @@ pub fn done_bursting(id: ClientId,
              .flatten()
     );
 
+    // 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 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 = Box::new(result.chain(vec![(id, rare_monster_packet)])); // TODO: make sure we arent clobbering `result` here
     }
 
     result
diff --git a/src/ship/room.rs b/src/ship/room.rs
index 3aae5da..3f0918a 100644
--- a/src/ship/room.rs
+++ b/src/ship/room.rs
@@ -7,6 +7,7 @@ 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 crate::ship::map::enemy::RareMonsterAppearTable;
 
 #[derive(Debug)]
 pub enum RoomCreationError {
@@ -167,6 +168,7 @@ pub struct RoomState {
     pub bursting: bool,
     pub monster_stats: Box<HashMap<MonsterType, MonsterStats>>,
     pub map_areas: MapAreaLookup,
+    pub rare_monster_table: Box<RareMonsterAppearTable>,
     // items on ground
     // enemy info
 }
@@ -231,13 +233,19 @@ impl RoomState {
             }
         };
 
+        let random_seed = rand::thread_rng().gen();
+        let rare_monster_table = RareMonsterAppearTable::new(room_mode.episode(), random_seed);
+
         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(),
+            // random_seed: rand::thread_rng().gen(),
+            random_seed: random_seed,
+            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),
+            maps: Maps::new(room_mode, &rare_monster_table),
             section_id,
             drop_table: Box::new(DropTable::new(room_mode.episode(), room_mode.difficulty(), section_id)),
             bursting: false,
-- 
2.36.0


From 14e14639c8daa37e7f3ebf94aa2fdfae375cef01 Mon Sep 17 00:00:00 2001
From: andy <andynewjack@protonmail.com>
Date: Mon, 19 Jul 2021 03:49:37 +0000
Subject: [PATCH 07/15] more testing/debugging. align monster appearance
 between client and server.

---
 data/battle_param/ep1_rare_monster.toml |  8 +--
 src/ship/map/enemy.rs                   | 68 +++++++++++++------------
 src/ship/map/maps.rs                    |  4 +-
 3 files changed, 42 insertions(+), 38 deletions(-)

diff --git a/data/battle_param/ep1_rare_monster.toml b/data/battle_param/ep1_rare_monster.toml
index 1e1f3e0..9de3518 100644
--- a/data/battle_param/ep1_rare_monster.toml
+++ b/data/battle_param/ep1_rare_monster.toml
@@ -3,7 +3,7 @@
 # 1/256 = 0.00390625
 # 1/512 = 0.001953125
 
-Hildebear = 1.0
-RagRappy = 0.1
-PoisonLily = 0.1
-PofuillySlime = 0.1
\ No newline at end of file
+Hildebear = 0.1
+RagRappy = 0.2
+PoisonLily = 0.3
+PofuillySlime = 0.4
\ No newline at end of file
diff --git a/src/ship/map/enemy.rs b/src/ship/map/enemy.rs
index 6d4d7c7..6ed160b 100644
--- a/src/ship/map/enemy.rs
+++ b/src/ship/map/enemy.rs
@@ -107,9 +107,13 @@ impl RareMonsterAppearTable {
     }
 
     pub fn roll_appearance(&self, monster: &MonsterType) -> bool {
-        let mut rng = rand_chacha::ChaChaRng::seed_from_u64(self.seed as u64); // TODO: figure out how to seed with a u32
-        println!("rolling for {:?} appearance", monster);
-        if rng.gen::<f32>() < *self.appear_rate.get(monster).unwrap_or(&0.0f32) {
+        println!("rolling appearance for {:?} with seed {:?}", monster, self.seed);
+        let mut rng = rand_chacha::ChaChaRng::from_entropy();
+        let roll: f32 = rng.gen();
+        let monster_rate = self.appear_rate.get(monster).unwrap_or(&0.0f32);
+        println!("rolled {:?} and {:?} has appear rate {:?}", roll, monster, monster_rate);
+        if roll < *monster_rate {
+        // if rng.gen::<f32>() < *self.appear_rate.get(monster).unwrap_or(&0.0f32) {
             println!("its a rare!");
             true
         }
@@ -349,42 +353,42 @@ impl MapEnemy {
         }
     }
 
-    // TODO: does `shiny` need to be set here?
-    // TODO: distinguish between a `random` rare monster and a `set/guaranteed` rare monster? (does any acceptable quest even have this?)
-    pub fn set_rare_appearance(self) -> MapEnemy {
-        match 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 => self.set_shiny(),
-            _ => self,
-        }
-    }
-    
     // // TODO: does `shiny` need to be set here?
     // // TODO: distinguish between a `random` rare monster and a `set/guaranteed` rare monster? (does any acceptable quest even have this?)
     // pub fn set_rare_appearance(self) -> MapEnemy {
-    //     match (self.monster, self.map_area.to_episode()) {
-    //         (MonsterType::RagRappy, Episode::One) => {MapEnemy {monster: MonsterType::AlRappy, ..self}},
-    //         (MonsterType::RagRappy, Episode::Two) => {MapEnemy {monster: MonsterType::EventRappy, ..self}},
-    //         (MonsterType::Hildebear, _) => {MapEnemy {monster: MonsterType::Hildeblue, ..self}},
-    //         (MonsterType::PoisonLily, _) => {MapEnemy {monster: MonsterType::NarLily, ..self}},
-    //         (MonsterType::PofuillySlime, _) => {MapEnemy {monster: MonsterType::PouillySlime, ..self}},
-    //         (MonsterType::SandRappyCrater, _) => {MapEnemy {monster: MonsterType::DelRappyCrater, ..self}},
-    //         (MonsterType::ZuCrater, _) => {MapEnemy {monster: MonsterType::PazuzuCrater, ..self}},
-    //         (MonsterType::Dorphon, _) => {MapEnemy {monster: MonsterType::DorphonEclair, ..self}},
-    //         (MonsterType::SandRappyDesert, _) => {MapEnemy {monster: MonsterType::DelRappyDesert, ..self}},
-    //         (MonsterType::ZuDesert, _) => {MapEnemy {monster: MonsterType::PazuzuDesert, ..self}},
-    //         (MonsterType::MerissaA, _) => {MapEnemy {monster: MonsterType::MerissaAA, ..self}},
-    //         (MonsterType::SaintMillion, _) => {MapEnemy {monster: MonsterType::Kondrieu, ..self}},
-    //         (MonsterType::Shambertin, _) => {MapEnemy {monster: MonsterType::Kondrieu, ..self}},
-    //         _ => {self},
+    //     match 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 => self.set_shiny(),
+    //         _ => self,
     //     }
     // }
     
+    // TODO: does `shiny` need to be set here?
+    // TODO: distinguish between a `random` rare monster and a `set/guaranteed` rare monster? (does any acceptable quest even have this?)
+    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_quest(self, rare_monster_table: &RareMonsterAppearTable) -> MapEnemy {
+    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()
         }
diff --git a/src/ship/map/maps.rs b/src/ship/map/maps.rs
index 71ef38d..0bd34b7 100644
--- a/src/ship/map/maps.rs
+++ b/src/ship/map/maps.rs
@@ -439,8 +439,8 @@ impl Maps {
     pub fn roll_monster_appearance(&mut self, rare_monster_table: &RareMonsterAppearTable) {
         self.enemy_data = self.enemy_data
                             .iter()
-                            .map(|&x| if x.is_some() {
-                                Some(x.unwrap().roll_appearance_for_quest(&rare_monster_table))
+                            .map(|&x| if x.is_some() && x.unwrap().has_rare_appearance() {
+                                Some(x.unwrap().roll_appearance_for_mission(&rare_monster_table))
                             } else {
                                 x
                             })
-- 
2.36.0


From 5c49ded823165dd8f9bd23a1f268c935c8ee3407 Mon Sep 17 00:00:00 2001
From: andy <andynewjack@protonmail.com>
Date: Tue, 20 Jul 2021 00:37:54 +0000
Subject: [PATCH 08/15] add best quest. add rare monsters to quests. dont use
 the same seed for random numbers if you want different results

---
 data/quests.toml                 | 6 +++++-
 src/ship/map/enemy.rs            | 8 ++------
 src/ship/map/maps.rs             | 5 +++--
 src/ship/packet/handler/quest.rs | 3 ++-
 4 files changed, 12 insertions(+), 10 deletions(-)

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/map/enemy.rs b/src/ship/map/enemy.rs
index 6ed160b..e66cca4 100644
--- a/src/ship/map/enemy.rs
+++ b/src/ship/map/enemy.rs
@@ -108,12 +108,8 @@ impl RareMonsterAppearTable {
 
     pub fn roll_appearance(&self, monster: &MonsterType) -> bool {
         println!("rolling appearance for {:?} with seed {:?}", monster, self.seed);
-        let mut rng = rand_chacha::ChaChaRng::from_entropy();
-        let roll: f32 = rng.gen();
-        let monster_rate = self.appear_rate.get(monster).unwrap_or(&0.0f32);
-        println!("rolled {:?} and {:?} has appear rate {:?}", roll, monster, monster_rate);
-        if roll < *monster_rate {
-        // if rng.gen::<f32>() < *self.appear_rate.get(monster).unwrap_or(&0.0f32) {
+        // let mut rng = rand_chacha::ChaChaRng::from_entropy();
+        if rand_chacha::ChaChaRng::from_entropy().gen::<f32>() < *self.appear_rate.get(monster).unwrap_or(&0.0f32) {
             println!("its a rare!");
             true
         }
diff --git a/src/ship/map/maps.rs b/src/ship/map/maps.rs
index 0bd34b7..ceebd65 100644
--- a/src/ship/map/maps.rs
+++ b/src/ship/map/maps.rs
@@ -398,9 +398,10 @@ impl Maps {
             })
     }
 
-    pub fn set_quest_data(&mut self, enemies: Vec<Option<MapEnemy>>, objects: Vec<Option<MapObject>>) {
-    // pub fn set_quest_data(&mut self, enemies: Vec<Option<MapEnemy>>, objects: Vec<Option<MapObject>>, rare_monster_table: RareMonsterAppearTable) {
+    // pub fn set_quest_data(&mut self, enemies: Vec<Option<MapEnemy>>, objects: Vec<Option<MapObject>>) {
+    pub fn set_quest_data(&mut self, enemies: Vec<Option<MapEnemy>>, objects: Vec<Option<MapObject>>, rare_monster_appear_table: &RareMonsterAppearTable) {
         self.enemy_data = enemies;
+        self.roll_monster_appearance(rare_monster_appear_table);
         
         // self.enemy_data = enemies
         //                     .iter()
diff --git a/src/ship/packet/handler/quest.rs b/src/ship/packet/handler/quest.rs
index 90359c3..c993579 100644
--- a/src/ship/packet/handler/quest.rs
+++ b/src/ship/packet/handler/quest.rs
@@ -83,7 +83,8 @@ pub fn player_chose_quest(id: ClientId, questmenuselect: &QuestMenuSelect, quest
     let room = rooms.get_mut(room_id.0)
         .ok_or_else(|| ShipError::InvalidRoom(room_id.0 as u32))?.as_mut()
         .ok_or_else(|| 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.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");
-- 
2.36.0


From 309b97cbc27f61fbd7a1d7dbf598efee813918d4 Mon Sep 17 00:00:00 2001
From: andy <andynewjack@protonmail.com>
Date: Fri, 23 Jul 2021 01:45:02 +0000
Subject: [PATCH 09/15] clean up and set rare monster appear rate to 1/512

---
 data/battle_param/ep1_rare_monster.toml |   8 +-
 data/battle_param/ep2_rare_monster.toml |   6 +-
 data/battle_param/ep4_rare_monster.toml |  12 +--
 src/ship/drops/mod.rs                   |   2 +-
 src/ship/map/enemy.rs                   |  40 ++-------
 src/ship/map/maps.rs                    | 104 ------------------------
 src/ship/packet/handler/quest.rs        |   1 -
 src/ship/room.rs                        |   9 +-
 8 files changed, 24 insertions(+), 158 deletions(-)

diff --git a/data/battle_param/ep1_rare_monster.toml b/data/battle_param/ep1_rare_monster.toml
index 9de3518..14a1d4a 100644
--- a/data/battle_param/ep1_rare_monster.toml
+++ b/data/battle_param/ep1_rare_monster.toml
@@ -3,7 +3,7 @@
 # 1/256 = 0.00390625
 # 1/512 = 0.001953125
 
-Hildebear = 0.1
-RagRappy = 0.2
-PoisonLily = 0.3
-PofuillySlime = 0.4
\ No newline at end of file
+Hildebear = 0.001953125
+RagRappy = 0.001953125
+PoisonLily = 0.001953125
+PofuillySlime = 0.001953125
diff --git a/data/battle_param/ep2_rare_monster.toml b/data/battle_param/ep2_rare_monster.toml
index 290f727..b56bee9 100644
--- a/data/battle_param/ep2_rare_monster.toml
+++ b/data/battle_param/ep2_rare_monster.toml
@@ -3,7 +3,7 @@
 # 1/256 = 0.00390625
 # 1/512 = 0.001953125
 
-Hildebear = 0.01
-RagRappy = 0.01
-PoisonLily = 0.01
+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
index 0ddddbf..d5b0fb2 100644
--- a/data/battle_param/ep4_rare_monster.toml
+++ b/data/battle_param/ep4_rare_monster.toml
@@ -3,11 +3,11 @@
 # 1/256 = 0.00390625
 # 1/512 = 0.001953125
 
-SandRappyCrater = 0.01
-ZuCrater = 0.01
-Dorphon = 0.01
-SandRappyDesert = 0.01
-ZuDesert = 0.01
-MerissaA = 0.01
+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/src/ship/drops/mod.rs b/src/ship/drops/mod.rs
index 427da74..e5037af 100644
--- a/src/ship/drops/mod.rs
+++ b/src/ship/drops/mod.rs
@@ -57,9 +57,9 @@ pub fn load_data_file<T: serde::de::DeserializeOwned>(episode: Episode, difficul
 
 // this is just copypaste
 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");
-    println!("rare monster file path: {:?}", path);
     
     let mut f = File::open(path).unwrap();
     let mut s = String::new();
diff --git a/src/ship/map/enemy.rs b/src/ship/map/enemy.rs
index e66cca4..2555024 100644
--- a/src/ship/map/enemy.rs
+++ b/src/ship/map/enemy.rs
@@ -75,21 +75,15 @@ pub enum MapEnemyError {
     MapAreaError(#[from] MapAreaError),
 }
 
-// TODO: is this even needed/used?
-// #[derive(Clone, Debug, Serialize, Deserialize)]
-// pub type RareMonsterAppearRate: HashMap<MonsterType, f32>,
-
 // making this `pub type` doesn't allow `impl`s to be defined?
 #[derive(Clone, Debug, Serialize, Deserialize)]
 pub struct RareMonsterAppearTable {
     appear_rate: HashMap<MonsterType, f32>,
-    seed: u32
 }
 
 impl RareMonsterAppearTable {
-    pub fn new(episode: Episode, room_seed: u32) -> RareMonsterAppearTable {
+    pub fn new(episode: Episode) -> RareMonsterAppearTable {
         let cfg: HashMap<String, f32> = load_rare_monster_file(episode);
-        println!("got cfg: {:?}", cfg);
 
         let appear_rates: HashMap<MonsterType, f32> = cfg
             .into_iter()
@@ -102,21 +96,14 @@ impl RareMonsterAppearTable {
 
         RareMonsterAppearTable {
             appear_rate: appear_rates,
-            seed: room_seed,
         }
     }
 
     pub fn roll_appearance(&self, monster: &MonsterType) -> bool {
-        println!("rolling appearance for {:?} with seed {:?}", monster, self.seed);
-        // let mut rng = rand_chacha::ChaChaRng::from_entropy();
         if rand_chacha::ChaChaRng::from_entropy().gen::<f32>() < *self.appear_rate.get(monster).unwrap_or(&0.0f32) {
-            println!("its a rare!");
-            true
-        }
-        else {
-            println!("lol sucker");
-            false
+            return true
         }
+        false
     }
 }
 
@@ -135,7 +122,6 @@ pub struct MapEnemy {
 
 impl MapEnemy {
     pub fn from_raw(enemy: RawMapEnemy, episode: &Episode, map_area: &MapArea /*, battleparam */) -> Result<MapEnemy, MapEnemyError> {
-        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 {
@@ -337,7 +323,6 @@ impl MapEnemy {
         }
     }
 
-    // TODO: does this actually do anything useful?
     pub fn has_rare_appearance(self) -> bool {
         match self.monster {
             MonsterType::RagRappy | MonsterType::Hildebear |
@@ -348,22 +333,11 @@ impl MapEnemy {
             _ => false
         }
     }
-
-    // // TODO: does `shiny` need to be set here?
-    // // TODO: distinguish between a `random` rare monster and a `set/guaranteed` rare monster? (does any acceptable quest even have this?)
-    // pub fn set_rare_appearance(self) -> MapEnemy {
-    //     match 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 => self.set_shiny(),
-    //         _ => self,
-    //     }
-    // }
     
-    // TODO: does `shiny` need to be set here?
-    // TODO: distinguish between a `random` rare monster and a `set/guaranteed` rare monster? (does any acceptable quest even have this?)
+    /*
+    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}},
diff --git a/src/ship/map/maps.rs b/src/ship/map/maps.rs
index ceebd65..aca75b7 100644
--- a/src/ship/map/maps.rs
+++ b/src/ship/map/maps.rs
@@ -12,8 +12,6 @@ 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<Option<MapObject>> {
     let mut object_data = Vec::new();
@@ -31,99 +29,14 @@ fn objects_from_map_data(path: PathBuf, episode: &Episode, map_area: &MapArea) -
 }
 
 fn parse_enemy(episode: &Episode, map_area: &MapArea, raw_enemy: RawMapEnemy) -> Vec<Option<MapEnemy>> {
-// fn parse_enemy(episode: &Episode, map_area: &MapArea, raw_enemy: RawMapEnemy, rare_monster_table: RareMonsterAppearTable) -> Vec<Option<MapEnemy>> {
     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::new();
             monsters.push(Some(monster));
             
             match monster.monster {
-                // TODO: make real spawn rates
-                // TODO: specific ep 2 event rappies
-                // MonsterType::RagRappy => {if rand::thread_rng().gen_range(0, 100) < 11 {
-/*                MonsterType::RagRappy => {if rare_monster_table.roll_appearance(&monster.monster) {
-                    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 {
-                MonsterType::Hildebear => {if rare_monster_table.roll_appearance(&monster.monster) {
-                    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 {
-                MonsterType::PoisonLily => {if rare_monster_table.roll_appearance(&monster.monster) {
-                    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 {
-                        if rare_monster_table.roll_appearance(&monster.monster) {
-                            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 (4?) copies
-                    for _ in 0..4 {
-                        // if rand::thread_rng().gen_range(0, 100) < 11 {
-                        if rare_monster_table.roll_appearance(&monster.monster) {
-                            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 {
-                MonsterType::SandRappyCrater => {if rare_monster_table.roll_appearance(&monster.monster) {
-                    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 {
-                MonsterType::ZuCrater => {if rare_monster_table.roll_appearance(&monster.monster) {
-                    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 {
-                MonsterType::Dorphon => {if rare_monster_table.roll_appearance(&monster.monster) {
-                    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 {
-                MonsterType::SandRappyDesert => {if rare_monster_table.roll_appearance(&monster.monster) {
-                    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 {
-                MonsterType::ZuDesert => {if rare_monster_table.roll_appearance(&monster.monster) {
-                    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 {
-                MonsterType::MerissaA => {if rare_monster_table.roll_appearance(&monster.monster) {
-                    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 {
-                MonsterType::SaintMillion | MonsterType::Shambertin => { if rare_monster_table.roll_appearance(&monster.monster) {
-                    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)));
@@ -235,21 +148,17 @@ fn parse_enemy(episode: &Episode, map_area: &MapArea, raw_enemy: RawMapEnemy) ->
 
 
 pub fn enemy_data_from_stream(cursor: &mut impl Read, map_area: &MapArea, episode: &Episode) -> Vec<Option<MapEnemy>> {
-// pub fn enemy_data_from_stream(cursor: &mut impl Read, map_area: &MapArea, episode: &Episode, rare_monster_table: enemy::RareMonsterAppearTable) -> Vec<Option<MapEnemy>> {
     let mut enemy_data = Vec::new();
     while let Ok(enemy) = RawMapEnemy::from_byte_stream(cursor) {
         enemy_data.append(&mut parse_enemy(episode, map_area, enemy));
-        // enemy_data.append(&mut parse_enemy(episode, map_area, enemy, rare_monster_table));
     }
     enemy_data
 }
 
 fn enemy_data_from_map_data(map_variant: &MapVariant, episode: &Episode) -> Vec<Option<MapEnemy>> {
-// fn enemy_data_from_map_data(map_variant: &MapVariant, episode: &Episode, rare_monster_table: enemy::RareMonsterAppearTable) -> Vec<Option<MapEnemy>> {
     let path = map_variant.dat_file();
     let mut cursor = File::open(path).unwrap();
     enemy_data_from_stream(&mut cursor, &map_variant.map, episode)
-    // enemy_data_from_stream(&mut cursor, &map_variant.map, episode, rare_monster_table)
 }
 
 
@@ -268,7 +177,6 @@ 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) => {
@@ -365,7 +273,6 @@ impl 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.append(&mut enemy_data_from_map_data(map_variant, &room_mode.episode(), rare_monster_table));
                             enemy_data
                         }),
             object_data: map_variants.iter()
@@ -398,20 +305,9 @@ impl Maps {
             })
     }
 
-    // pub fn set_quest_data(&mut self, enemies: Vec<Option<MapEnemy>>, objects: Vec<Option<MapObject>>) {
     pub fn set_quest_data(&mut self, enemies: Vec<Option<MapEnemy>>, objects: Vec<Option<MapObject>>, rare_monster_appear_table: &RareMonsterAppearTable) {
         self.enemy_data = enemies;
         self.roll_monster_appearance(rare_monster_appear_table);
-        
-        // self.enemy_data = enemies
-        //                     .iter()
-        //                     .map(|&x| if x.is_some() {
-        //                         Some(x.unwrap().roll_appearance_for_quest(rare_monster_table))
-        //                     } else {
-        //                         x
-        //                     })
-        //                     .collect();
-        
         self.object_data = objects;
     }
 
diff --git a/src/ship/packet/handler/quest.rs b/src/ship/packet/handler/quest.rs
index c993579..3c4e7b4 100644
--- a/src/ship/packet/handler/quest.rs
+++ b/src/ship/packet/handler/quest.rs
@@ -83,7 +83,6 @@ pub fn player_chose_quest(id: ClientId, questmenuselect: &QuestMenuSelect, quest
     let room = rooms.get_mut(room_id.0)
         .ok_or_else(|| ShipError::InvalidRoom(room_id.0 as u32))?.as_mut()
         .ok_or_else(|| 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();
 
diff --git a/src/ship/room.rs b/src/ship/room.rs
index 3f0918a..cfee400 100644
--- a/src/ship/room.rs
+++ b/src/ship/room.rs
@@ -233,19 +233,16 @@ impl RoomState {
             }
         };
 
-        let random_seed = rand::thread_rng().gen();
-        let rare_monster_table = RareMonsterAppearTable::new(room_mode.episode(), random_seed);
+        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(),
-            random_seed: random_seed,
+            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),
+            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,
-- 
2.36.0


From b8534eb520ff0fca4014d66716572d0d3ce88c60 Mon Sep 17 00:00:00 2001
From: andy <andynewjack@protonmail.com>
Date: Fri, 23 Jul 2021 01:48:20 +0000
Subject: [PATCH 10/15] push slime clones

---
 src/ship/map/maps.rs | 10 ++++++++++
 1 file changed, 10 insertions(+)

diff --git a/src/ship/map/maps.rs b/src/ship/map/maps.rs
index aca75b7..8c11d21 100644
--- a/src/ship/map/maps.rs
+++ b/src/ship/map/maps.rs
@@ -46,6 +46,16 @@ fn parse_enemy(episode: &Episode, map_area: &MapArea, raw_enemy: RawMapEnemy) ->
                     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..5 {
+                        monsters.push(Some(MapEnemy::new(MonsterType::PofuillySlime, 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 {
                         monsters.push(Some(MapEnemy::new(MonsterType::SinowBeat, monster.map_area)));
-- 
2.36.0


From e578d9bad5b2cbaa253b24ffdca3b4cd3d5e02a4 Mon Sep 17 00:00:00 2001
From: andy <andynewjack@protonmail.com>
Date: Fri, 23 Jul 2021 02:57:53 +0000
Subject: [PATCH 11/15] imagine adding useless code

---
 src/ship/map/area.rs | 48 --------------------------------------------
 1 file changed, 48 deletions(-)

diff --git a/src/ship/map/area.rs b/src/ship/map/area.rs
index 9da3374..a26aa19 100644
--- a/src/ship/map/area.rs
+++ b/src/ship/map/area.rs
@@ -208,54 +208,6 @@ impl MapArea {
             // MapArea::TestMapEp4 => 10,
         }
     }
-
-    pub fn to_episode(self) -> Episode {
-        match self {
-            Pioneer2Ep1 => Episode::One,
-            Forest1 => Episode::One,
-            Forest2 => Episode::One,
-            Caves1 => Episode::One,
-            Caves2 => Episode::One,
-            Caves3 => Episode::One,
-            Mines1 => Episode::One,
-            Mines2 => Episode::One,
-            Ruins1 => Episode::One,
-            Ruins2 => Episode::One,
-            Ruins3 => Episode::One,
-            Dragon => Episode::One,
-            DeRolLe => Episode::One,
-            VolOpt => Episode::One,
-            DarkFalz => Episode::One,
-            Pioneer2Ep2 => Episode::Two,
-            VrTempleAlpha => Episode::Two,
-            VrTempleBeta => Episode::Two,
-            VrSpaceshipAlpha => Episode::Two,
-            VrSpaceshipBeta => Episode::Two,
-            Cca => Episode::Two,
-            JungleAreaNorth => Episode::Two,
-            JungleAreaEast => Episode::Two,
-            Mountain => Episode::Two,
-            Seaside => Episode::Two,
-            SeabedUpper => Episode::Two,
-            SeabedLower => Episode::Two,
-            GalGryphon => Episode::Two,
-            OlgaFlow => Episode::Two,
-            BarbaRay => Episode::Two,
-            GolDragon => Episode::Two,
-            SeasideNight => Episode::Two,
-            Tower => Episode::Two,
-            Pioneer2Ep4 => Episode::Four,
-            CraterEast => Episode::Four,
-            CraterWest => Episode::Four,
-            CraterSouth => Episode::Four,
-            CraterNorth => Episode::Four,
-            CraterInterior => Episode::Four,
-            SubDesert1 => Episode::Four,
-            SubDesert2 => Episode::Four,
-            SubDesert3 => Episode::Four,
-            SaintMillion => Episode::Four,
-        }
-    }
 }
 
 
-- 
2.36.0


From 7a1a20db532f6946427cf4adbab5ee31a9787ead Mon Sep 17 00:00:00 2001
From: andy <andynewjack@protonmail.com>
Date: Sat, 24 Jul 2021 00:42:14 +0000
Subject: [PATCH 12/15] imagine reading the warning message wrongly. not me

---
 src/ship/map/area.rs | 48 ++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 48 insertions(+)

diff --git a/src/ship/map/area.rs b/src/ship/map/area.rs
index a26aa19..2b686ce 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,
+        }
+    }
 }
 
 
-- 
2.36.0


From 3faca14883455915abc4a718d5f2645b7ac487b8 Mon Sep 17 00:00:00 2001
From: andy <andynewjack@protonmail.com>
Date: Sat, 24 Jul 2021 01:32:58 +0000
Subject: [PATCH 13/15] andy vs. clippy round 4 and some cleanup

---
 src/ship/map/enemy.rs           |  7 +++----
 src/ship/map/maps.rs            | 24 +++++++++++++++---------
 src/ship/packet/builder/room.rs |  6 +++---
 src/ship/packet/handler/room.rs | 10 ++++++++--
 4 files changed, 29 insertions(+), 18 deletions(-)

diff --git a/src/ship/map/enemy.rs b/src/ship/map/enemy.rs
index 2555024..6aed225 100644
--- a/src/ship/map/enemy.rs
+++ b/src/ship/map/enemy.rs
@@ -324,14 +324,13 @@ impl MapEnemy {
     }
 
     pub fn has_rare_appearance(self) -> bool {
-        match self.monster {
+        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 => true,
-            _ => false
-        }
+            MonsterType::SaintMillion | MonsterType::Shambertin
+        )
     }
     
     /*
diff --git a/src/ship/map/maps.rs b/src/ship/map/maps.rs
index 8c11d21..1f85124 100644
--- a/src/ship/map/maps.rs
+++ b/src/ship/map/maps.rs
@@ -33,8 +33,7 @@ fn parse_enemy(episode: &Episode, map_area: &MapArea, raw_enemy: RawMapEnemy) ->
 
     enemy
         .map_or(vec![None], |monster| {
-            let mut monsters = Vec::new();
-            monsters.push(Some(monster));
+            let mut monsters = vec![Some(monster)];
             
             match monster.monster {
                 MonsterType::Monest => {
@@ -333,9 +332,9 @@ impl Maps {
                 }
             })
             .collect();
-        for i in 0..shiny.len() {
+        for monster in &shiny {
             if let Some(j) = rare_monsters.iter().position(|&x| x == 0xFFFF) {
-                rare_monsters[j] = shiny[i].0 as u16;
+                rare_monsters[j] = monster.0 as u16;
             } else {
                 break
             }
@@ -346,11 +345,18 @@ impl Maps {
     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() {
-                                Some(x.unwrap().roll_appearance_for_mission(&rare_monster_table))
-                            } else {
-                                x
-                            })
+                            // .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/packet/builder/room.rs b/src/ship/packet/builder/room.rs
index 3fe196a..c5bd11c 100644
--- a/src/ship/packet/builder/room.rs
+++ b/src/ship/packet/builder/room.rs
@@ -72,8 +72,8 @@ pub fn add_to_room(_id: ClientId,
     })
 }
 
-pub fn build_rare_monster_list(rare_monster_vec: Vec<u16>) -> Result<RareMonsterList, ShipError> {
-    Ok(RareMonsterList {
+pub fn build_rare_monster_list(rare_monster_vec: Vec<u16>) -> 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/room.rs b/src/ship/packet/handler/room.rs
index 8c2ba8c..854d24f 100644
--- a/src/ship/packet/handler/room.rs
+++ b/src/ship/packet/handler/room.rs
@@ -130,9 +130,15 @@ pub fn done_bursting(id: ClientId,
              .flatten()
     );
 
+    // // 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 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)])); // TODO: make sure we arent clobbering `result` here
+    // }
+
     // 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 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
+    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
     }
 
-- 
2.36.0


From 00cff460774ca1c32bce5ef6b3e8438e36ce0d34 Mon Sep 17 00:00:00 2001
From: andy <andynewjack@protonmail.com>
Date: Sat, 24 Jul 2021 01:34:26 +0000
Subject: [PATCH 14/15] oops forgot to delete this

---
 src/ship/packet/handler/room.rs | 6 ------
 1 file changed, 6 deletions(-)

diff --git a/src/ship/packet/handler/room.rs b/src/ship/packet/handler/room.rs
index 854d24f..f912d18 100644
--- a/src/ship/packet/handler/room.rs
+++ b/src/ship/packet/handler/room.rs
@@ -130,12 +130,6 @@ pub fn done_bursting(id: ClientId,
              .flatten()
     );
 
-    // // 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 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)])); // TODO: make sure we arent clobbering `result` here
-    // }
-
     // 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));
-- 
2.36.0


From 899527d3e7e5e520ac8cf4f021d1aa6bb6888faf Mon Sep 17 00:00:00 2001
From: andy <andynewjack@protonmail.com>
Date: Mon, 26 Jul 2021 01:21:29 +0000
Subject: [PATCH 15/15] basic test

---
 src/ship/map/enemy.rs |  2 +-
 tests/test_rooms.rs   | 20 ++++++++++++++++++++
 2 files changed, 21 insertions(+), 1 deletion(-)

diff --git a/src/ship/map/enemy.rs b/src/ship/map/enemy.rs
index 6aed225..52092d4 100644
--- a/src/ship/map/enemy.rs
+++ b/src/ship/map/enemy.rs
@@ -78,7 +78,7 @@ pub enum MapEnemyError {
 // making this `pub type` doesn't allow `impl`s to be defined?
 #[derive(Clone, Debug, Serialize, Deserialize)]
 pub struct RareMonsterAppearTable {
-    appear_rate: HashMap<MonsterType, f32>,
+    pub appear_rate: HashMap<MonsterType, f32>,
 }
 
 impl RareMonsterAppearTable {
diff --git a/tests/test_rooms.rs b/tests/test_rooms.rs
index 45f8ace..61ec3c1 100644
--- a/tests/test_rooms.rs
+++ b/tests/test_rooms.rs
@@ -101,3 +101,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
-- 
2.36.0