From 6a1e0143212f4933af8c12361cfb4ddcdc68dabb Mon Sep 17 00:00:00 2001 From: David Goldstein Date: Mon, 26 Oct 2026 18:08:17 -0400 Subject: [PATCH] Row 231: King Pepe's room (2330): while Stop the Wedding still wants a Yeti brother, the East Castle Tower's TD_MC_enterboss1 takes the player alone into their own copy of King Pepe's room for twenty minutes with one of the three brothers (new solo EventKind::KingPepe, one at a time on a channel); its way out (out_pepeking) leads back up the tower, with the Wedding Hall key once all three are beaten --- agent_docs/map-travel.md | 3 +- agent_docs/party-quests.md | 21 +++- game-data/placeholder_text.toml | 3 + game-data/server_content.toml | 3 + game-data/src/server_content.rs | 12 ++ integration-harness/tests/king_pepe.rs | 160 +++++++++++++++++++++++++ net/src/events.rs | 56 ++++++++- net/src/scripts.rs | 127 ++++++++++++++++++-- runtime/src/actor/client/events.rs | 2 + runtime/src/actor/field.rs | 6 + 10 files changed, 383 insertions(+), 10 deletions(-) create mode 100644 integration-harness/tests/king_pepe.rs diff --git a/agent_docs/map-travel.md b/agent_docs/map-travel.md index f12c1eac..9111dc79 100644 --- a/agent_docs/map-travel.md +++ b/agent_docs/map-travel.md @@ -257,7 +257,8 @@ nothing. A script returns effects that the client actor sends: | `ariant_castle` | Ariant Castle's door: with an Entry Pass to the Palace (`4031582`, kept), portal sound, warp to `260000301` `out00`; else a pink line | | `enterDisguise0` | Ereve's Crossroads into the Training Forest (`130010000` `east00`): portal sound and warp, except while a Master of Disguise quest (20301-20305) is taken without an Ereve Investigation Permit (`4032179`): a pink line | | `enterWitch` | Leafre's nest into the Cave of Black Witches once 20404 is completed: `924010200` after 20407, `924010100` after 20406, else `924010000`, portal `out00`; else a pink line | -| `TD_MC_enterboss1` | The castle tower's way to the Wedding Hall entrance: portal sound, warp to `106021401` `out00`. Cosmic starts a fight with the Yeti brothers instead while 2330 is taken and they aren't all beaten; that event isn't in, so the way is open to everyone | +| `TD_MC_enterboss1` | The castle tower's way up: while 2330 is taken and a Yeti brother is still unbeaten, portal sound and into King Pepe's room (`EventKind::KingPepe`, see [party quests](./party-quests.md#king-pepes-room)), or a pink line while another player is in it; else portal sound, warp to the Wedding Hall entrance `106021401` `out00` | +| `out_pepeking` | King Pepe's room's way out: the Wedding Hall key (4032388) and a pink line once 2330's three brothers are beaten and none is held (shut with a pink line if there's no ETC slot), then portal sound, warp to `106021400` `TD_MC_enterboss1` | | `secretDoor` | Magatia's lab doors (`261010000`, `261020200` `secret00`) once 3360 is taken or done: portal sound, warp to `261030000` `sp_jenu` (from Zenumist's) or `sp_alca`; else a pink line. Cosmic's password prompt while 3360 is taken is left out: the quest's start script that sets the password isn't run | | `magatia_dark0` | The dark pipe on `261020600` (`up00`) into Yulete's lab: portal sound, warp to `926130000` `out00`, once Yulete's traces in the Romeo and Juliet party quest have shown the player the way (marker 32600, `party-quests.md`; Cosmic completes quest 7770, which isn't in `Quest.nx`); else a pink line. The lab's quest rooms (`q3366in`-`q3368in` on `926130100`) aren't in | | `curseforest` | The Forest of Evil I's hidden portal (`100040105` `secret00`) into the Cursed Forest, for players with 2224 or 2226 taken or 2227 done: portal sound, warp to `910100001` (after 2227) or `910100000`, portal `out00`, from 17:00 until 7:00 by the stations' clock (route time, UTC); by day, or off those quests, a pink line | diff --git a/agent_docs/party-quests.md b/agent_docs/party-quests.md index 77dc4655..8e45dc7c 100644 --- a/agent_docs/party-quests.md +++ b/agent_docs/party-quests.md @@ -38,7 +38,9 @@ Pirate Ship is played through ([the Pirate Ship](#the-pirate-ship)). Magatia's Romeo and Juliet party quest has its entry, its way out, its end rules and its first four stages; its other stages come in later slices ([Romeo and Juliet](#romeo-and-juliet)). Jr. Balrog's room, for quest 2245, -is taken alone ([Jr. Balrog's room](#jr-balrogs-room)). +is taken alone ([Jr. Balrog's room](#jr-balrogs-room)), and so is King +Pepe's room in the Mushroom Castle, for quest 2330 ([King Pepe's +room](#king-pepes-room)). No event NPC offers Cosmic's "enable/disable Party Search" line. That opt-out is Cosmic's own: in v83 the opt-out is the client's Party Search @@ -1384,6 +1386,23 @@ nothing is applied. The room's `life` holds the unknown Jr. Balrog is never won: it ends when the player leaves by `out00` or the time runs out, back to Mu Young's map 105100100, the room's `forcedReturn`. +## King Pepe's room + +For "Stop the Wedding" (2330), which wants one each of the three Yeti +brothers King Pepe rides (3300005-3300007; Cosmic `KingPepeAndYetis`). The +East Castle Tower's (106021400) `TD_MC_enterboss1` takes a player on 2330 +who has yet to beat one of them alone into their own copy of 106021500 for +twenty minutes (`events::KING_PEPE`, `EventKind::KingPepe`). Only one may be +in it on a channel at a time; another is told so in pink +(`king_pepe.busy`, a placeholder). Anyone else goes on to the Wedding Hall's +entrance (106021401). Each copy opens with one brother, drawn at random, at +(-28, -67) (Cosmic `pepeking_effect`), so a player may have to go in more +than three times. The event is never won: it ends when the player leaves by +`out01` (`out_pepeking`) or the time runs out, back to the tower. The way +out gives the Wedding Hall key (4032388), which "Where's Violetta?" (2332) +starts with, once all three are beaten and none is held, or stays shut with +no ETC slot for it (`king_pepe.key`, `king_pepe.key_no_room`). + ## NPCs | NPC | Where | Script | diff --git a/game-data/placeholder_text.toml b/game-data/placeholder_text.toml index f2b1ec40..a79da4d0 100644 --- a/game-data/placeholder_text.toml +++ b/game-data/placeholder_text.toml @@ -773,6 +773,9 @@ "nine_spirit.baby.full" = "[placeholder] You need a free USE slot for the Dragon Stone." "reef.thanks" = "[placeholder] Reef thanks you for beating Faust. His spirit can rest at last." "tristan.room_taken" = "[placeholder] Someone is already in that room. Come back later." +"king_pepe.busy" = "[placeholder] Someone is already fighting King Pepe. Try again later." +"king_pepe.key" = "[placeholder] King Pepe dropped a key to the Wedding Hall, and you picked it up." +"king_pepe.key_no_room" = "[placeholder] You need a free ETC slot for the key King Pepe dropped." "family.captain_al.junior.done" = "[placeholder] Captain Al is pleased you have taken a Junior, and teaches you how a Senior earns Rep." "family.captain_al.junior.not_yet" = "[placeholder] Captain Al says you have no Junior yet. Add one from the Family window." "family.captain_al.rep.done" = "[placeholder] Captain Al sees you have earned 1,000 Rep, and praises how you look after your Juniors." diff --git a/game-data/server_content.toml b/game-data/server_content.toml index a941bc44..dce2fb20 100644 --- a/game-data/server_content.toml +++ b/game-data/server_content.toml @@ -1446,6 +1446,9 @@ unsourced = [ "nine_spirit.baby.full", "reef.thanks", "tristan.room_taken", + "king_pepe.busy", + "king_pepe.key", + "king_pepe.key_no_room", "family.captain_al.junior.done", "family.captain_al.junior.not_yet", "family.captain_al.rep.done", diff --git a/game-data/src/server_content.rs b/game-data/src/server_content.rs index 44afb92d..8ed4981f 100644 --- a/game-data/src/server_content.rs +++ b/game-data/src/server_content.rs @@ -791,6 +791,15 @@ pub mod keys { /// licensed script has the text. pub const TRISTAN_ROOM_TAKEN: &str = "tristan.room_taken"; + /// King Pepe's room in the Mushroom Castle: the tower's way in + /// (`TD_MC_enterboss1`) while another player is in it, and its way out + /// (`out_pepeking`) giving the Wedding Hall key once the three Yeti + /// brothers are beaten, or shut with no room for it. No licensed script + /// has the text. + pub const KING_PEPE_BUSY: &str = "king_pepe.busy"; + pub const KING_PEPE_KEY: &str = "king_pepe.key"; + pub const KING_PEPE_KEY_NO_ROOM: &str = "king_pepe.key_no_room"; + /// Captain Al's lessons in leading a family (quests 2232-2234), each /// ended by his script (NPC 1002103, `q2232e`-`q2234e`): done, then not /// yet, for a junior taken, 1,000 Rep earned, and 2,000 earned with the @@ -3015,6 +3024,9 @@ mod tests { .chain(&[ keys::REEF_THANKS, keys::TRISTAN_ROOM_TAKEN, + keys::KING_PEPE_BUSY, + keys::KING_PEPE_KEY, + keys::KING_PEPE_KEY_NO_ROOM, keys::NINE_SPIRIT_HATCHED, keys::NINE_SPIRIT_STONE_HELD, keys::NINE_SPIRIT_FULL, diff --git a/integration-harness/tests/king_pepe.rs b/integration-harness/tests/king_pepe.rs new file mode 100644 index 00000000..547855eb --- /dev/null +++ b/integration-harness/tests/king_pepe.rs @@ -0,0 +1,160 @@ +//! Stop the Wedding (2330) and King Pepe's room: while the quest still wants +//! a Yeti brother, the East Castle Tower's `TD_MC_enterboss1` takes the +//! player alone into their own copy of the room for twenty minutes, where +//! one of the three brothers rides out with King Pepe. One player at a time +//! may be in it on a channel. Its way out (`out_pepeking`) leads back up +//! the tower, with the Wedding Hall key once all three are beaten. The +//! harness world runs events 60 times as fast (`RUSTMS_EVENT_SCALE`), so +//! the twenty minutes are twenty seconds. + +use integration_harness::database::{replace_item, set_job, set_quest, set_str, set_weapon_attack}; +use integration_harness::hunting::{attack, walk_to}; +use integration_harness::preconditions::load_harness_config_or_fail; +use integration_harness::trace::{read_for, send, step, until, until_within, warp, Trace}; +use integration_harness::{login_to_world_session, relogin, HarnessConfig, WorldSession}; +use net::events::{ + EAST_CASTLE_TOWER, KING_PEPE_ROOM, STOP_THE_WEDDING, WEDDING_HALL_KEY, YETI_BROTHERS, +}; +use net::packet::op::RecvOpcode; +use packet::io::write::PktWrite; +use packet::Packet; +use std::time::Duration; + +const WEDDING_HALL_ENTRANCE: i32 = 106_021_401; +const ETC: u8 = 4; + +/// A level 35 fighter, armed for the brothers, on Stop the Wedding with +/// `progress` (three kill counts), standing on `map_id` at `(x, y)`. +async fn on_quest( + config: &HarnessConfig, + progress: &str, + map_id: i32, + (x, y): (i16, i16), +) -> WorldSession { + let id = login_to_world_session(config) + .await + .expect("first login") + .character_id; + tokio::time::sleep(Duration::from_millis(500)).await; + set_job(id, 100, 35, 50).await.expect("a level 35 warrior"); + replace_item(id, -1, -11, 1302000).await.expect("a sword"); + set_weapon_attack(id, 500).await.expect("its attack"); + set_str(id, 999).await.expect("strength"); + set_quest(id, STOP_THE_WEDDING, 1, progress) + .await + .expect("2330 taken"); + let mut session = relogin(config).await; + let c = &mut session.connection; + let mut trace = Trace::default(); + assert_eq!(warp(c, map_id, "", &mut trace).await, map_id); + step(c, &config.character_name, walk_to(x, y)).await; + session +} + +/// Going through a scripted portal. +fn touch(portal: &str) -> Packet { + let mut p = Packet::new_empty(); + p.write_short(RecvOpcode::ChangeMapSpecial as i16).unwrap(); + p.write_byte(0).unwrap(); + p.write_str_with_length(portal).unwrap(); + p.write_short(0).unwrap(); + p +} + +const TOWER_DOOR: (i16, i16) = (268, -699); +const WAY_OUT: (i16, i16) = (-487, -66); + +#[tokio::test] +async fn a_yeti_brother_waits_in_king_pepes_room() { + let (config, other_config) = ( + load_harness_config_or_fail().await, + load_harness_config_or_fail().await, + ); + let none = "000000000"; + let mut session = on_quest(&config, none, EAST_CASTLE_TOWER, TOWER_DOOR).await; + let mut other = on_quest(&other_config, none, EAST_CASTLE_TOWER, TOWER_DOOR).await; + let c = &mut session.connection; + + // In, alone, with one of the brothers. + send(c, touch("TD_MC_enterboss1")).await; + let mut trace = Trace::default(); + read_for(c, &mut trace, 1_500).await; + assert_eq!(trace.warps, [KING_PEPE_ROOM]); + assert_eq!(trace.countdowns.len(), 1, "the twenty minutes run"); + until(c, &mut trace, |t| { + YETI_BROTHERS + .iter() + .map(|&id| t.field.alive(id).len()) + .sum::() + == 1 + }) + .await; + let (index, brother) = YETI_BROTHERS + .iter() + .enumerate() + .find_map(|(i, &id)| Some((i, *trace.field.alive(id).first()?))) + .unwrap(); + + // Someone else on the quest must wait while the room is taken. + let o = &mut other.connection; + send(o, touch("TD_MC_enterboss1")).await; + let mut t = Trace::default(); + read_for(o, &mut t, 1_500).await; + assert!(t.warps.is_empty()); + assert_eq!(t.broadcasts.len(), 1, "told in pink: {:?}", t.broadcasts); + + // The brother falls, and counts for 2330. + for _ in 0..40 { + send(c, attack(brother, 30_000)).await; + read_for(c, &mut trace, 100).await; + if trace.field.killed.contains(&brother) { + break; + } + } + until(c, &mut trace, |t| t.kills.contains_key(&STOP_THE_WEDDING)).await; + let mut kills = ["000"; 3]; + kills[index] = "001"; + assert_eq!(trace.kills[&STOP_THE_WEDDING], kills.concat()); + + // Out and up the tower, with no key for one brother. + step(c, &config.character_name, walk_to(WAY_OUT.0, WAY_OUT.1)).await; + send(c, touch("out01")).await; + let mut trace = Trace::default(); + until(c, &mut trace, |t| t.warps == [EAST_CASTLE_TOWER]).await; + assert!(trace.added.is_empty()); + drop(session); + + // The other goes in at last (the room is held two seconds after its + // player leaves), and the time running out sends them back. + tokio::time::sleep(Duration::from_secs(5)).await; + let o = &mut other.connection; + send(o, touch("TD_MC_enterboss1")).await; + let mut t = Trace::default(); + until(o, &mut t, |t| t.warps == [KING_PEPE_ROOM]).await; + let mut t = Trace::default(); + until_within(o, &mut t, Duration::from_secs(30), |t| { + t.warps == [EAST_CASTLE_TOWER] + }) + .await; +} + +#[tokio::test] +async fn king_pepe_drops_the_wedding_hall_key_once_all_three_are_beaten() { + let config = load_harness_config_or_fail().await; + let m = config.character_name.clone(); + // Straight onto the room's own map, out of any event. + let mut session = on_quest(&config, "001001001", KING_PEPE_ROOM, WAY_OUT).await; + let c = &mut session.connection; + send(c, touch("out01")).await; + let mut trace = Trace::default(); + until(c, &mut trace, |t| t.warps == [EAST_CASTLE_TOWER]).await; + assert_eq!(trace.added.len(), 1); + let (inventory, _, item) = trace.added[0]; + assert_eq!((inventory, item), (ETC, WEDDING_HALL_KEY)); + + // With all three beaten, the tower's door leads on to the Wedding Hall. + step(c, &m, walk_to(TOWER_DOOR.0, TOWER_DOOR.1)).await; + send(c, touch("TD_MC_enterboss1")).await; + let mut trace = Trace::default(); + until(c, &mut trace, |t| t.warps == [WEDDING_HALL_ENTRANCE]).await; +} diff --git a/net/src/events.rs b/net/src/events.rs index 1471adc5..7ae297d0 100644 --- a/net/src/events.rs +++ b/net/src/events.rs @@ -72,6 +72,10 @@ pub enum EventKind { /// Jr. Balrog's room before Tristan's Tomb (2245), taken alone from /// Mu Young (1061014) in the Sleepywood dungeon. Tristan, + /// King Pepe's room in the Mushroom Castle, taken alone through the + /// East Castle Tower's `TD_MC_enterboss1` while Stop the Wedding (2330) + /// still wants a Yeti brother. + KingPepe, /// A Monster Carnival room (1 to 6), taken by a party from Spiegelmann /// (2042000) on the Carnival Field (`crate::carnival`). Carnival(u8), @@ -2432,6 +2436,42 @@ pub const TRISTAN: Event = Event { ..DOJO }; +/// The Mushroom Castle's East Castle Tower, 106021400: the way up to King +/// Pepe's room and the Wedding Hall (`TD_MC_enterboss1`), and back. +pub const EAST_CASTLE_TOWER: i32 = 106021400; +/// King Pepe's room, 106021500, where a Yeti brother rides out with him. +pub const KING_PEPE_ROOM: i32 = 106021500; +/// Stop the Wedding (2330), which wants one of each Yeti brother, King +/// Pepe on his back (Quest.nx `Check.img/2330/1/mob`, in this order). +pub const STOP_THE_WEDDING: i16 = 2330; +pub const YETI_BROTHERS: [i32; 3] = [3_300_005, 3_300_006, 3_300_007]; +/// The point above the ground below which a brother comes out (Cosmic +/// `pepeking_effect`). +pub const YETI_BROTHER_AT: (i16, i16) = (-28, -67); +/// The key to the Wedding Hall, which King Pepe drops once all three are +/// beaten, and which Where's Violetta? (2332) starts with. +pub const WEDDING_HALL_KEY: i32 = 4_032_388; + +/// King Pepe's room: the tower's portal takes a player on Stop the Wedding +/// alone into their own copy for twenty minutes, one at a time on a channel +/// (Cosmic `KingPepeAndYetis`, one lobby). Each copy opens with one of the +/// brothers, drawn at random (Cosmic `pepeking_effect`). It is never won; +/// it ends when they leave by `out01` or the time runs out. +pub const KING_PEPE: Event = Event { + lobby: EAST_CASTLE_TOWER, + entrances: &[], + maps: KING_PEPE_ROOM..=KING_PEPE_ROOM, + entry: KING_PEPE_ROOM, + exit: EAST_CASTLE_TOWER, + levels: 30..=u8::MAX, + team: 1..=1, + minutes: 20, + exp_per_stage: &[], + member_clears: &[], + runs: 1, + ..DOJO +}; + /// Every run of Nett's Pyramid Duarte offers, alone first, then with a /// party, each by mode. pub fn pyramid_kinds() -> impl Iterator { @@ -2518,7 +2558,7 @@ pub fn protected(mob_id: i32) -> bool { } impl EventKind { - pub const ALL: [EventKind; 38] = [ + pub const ALL: [EventKind; 39] = [ EventKind::Kpq, EventKind::Hpq, EventKind::Lpq, @@ -2557,6 +2597,7 @@ impl EventKind { EventKind::Magatia(MagatiaSide::Zenumist), EventKind::Magatia(MagatiaSide::Alcadno), EventKind::Tristan, + EventKind::KingPepe, ]; pub fn event(self) -> &'static Event { @@ -2584,6 +2625,7 @@ impl EventKind { EventKind::Carnival(room) => &CARNIVAL[usize::from(room.clamp(1, CARNIVAL_ROOMS) - 1)], EventKind::Magatia(side) => &MAGATIA[side as usize], EventKind::Tristan => &TRISTAN, + EventKind::KingPepe => &KING_PEPE, } } @@ -3648,6 +3690,18 @@ mod tests { assert!(event_map(TRISTAN_ROOM) && !event_map(MU_YOUNG_MAP)); } + #[test] + fn king_pepes_room_is_taken_alone_from_the_east_castle_tower() { + let pepe = EventKind::KingPepe.event(); + assert_eq!((pepe.entry, pepe.exit), (KING_PEPE_ROOM, EAST_CASTLE_TOWER)); + assert!(pepe.solo() && pepe.lasts_until_empty()); + assert_eq!((pepe.minutes, pepe.last_stage(), pepe.runs), (20, 0, 1)); + assert!(pepe.starts_on(EAST_CASTLE_TOWER)); + assert!(pepe.team(1, &[at(1, 30, EAST_CASTLE_TOWER)]).is_some()); + assert_eq!(pepe.team(1, &[at(1, 29, EAST_CASTLE_TOWER)]), None); + assert!(event_map(KING_PEPE_ROOM) && !event_map(EAST_CASTLE_TOWER)); + } + #[test] fn dojo_sections_have_their_own_clocks() { let floor = |n: u8| clock_on(DOJO.stage_map(n)); diff --git a/net/src/scripts.rs b/net/src/scripts.rs index 73733a9e..7662f79d 100644 --- a/net/src/scripts.rs +++ b/net/src/scripts.rs @@ -5,6 +5,9 @@ use crate::content; use crate::conti::Portal; +use crate::events::{ + EventKind, EAST_CASTLE_TOWER, STOP_THE_WEDDING, WEDDING_HALL_KEY, YETI_BROTHERS, +}; use crate::npc_scripts; use game::character::{CARNIVAL_RETURN, DOJO_RETURN, FREE_MARKET_RETURN, PYRAMID_RETURN}; use game::{BuffStat, InventoryType, Player, QuestState}; @@ -1251,13 +1254,52 @@ pub fn portal(script: &str, player: &Player) -> Option { block: false, occupied: Some(content::text().get(keys::ZAKUM_GATE_BLOCKED, &[])), }, - // Cosmic starts the fight with the Yeti brothers here while Stop the - // Wedding (2330) is taken and they aren't all beaten; that isn't in, - // so the way is open to everyone rather than shut for good. + // Into King Pepe's room while Stop the Wedding (2330) still wants a + // Yeti brother, else on to the Wedding Hall (Cosmic + // `TD_MC_enterboss1`). + "TD_MC_enterboss1" + if quest(STOP_THE_WEDDING) == QuestState::Started && !yeti_brothers_beaten(player) => + { + ScriptOutcome { + effects: vec![Effect::PortalSound, Effect::StartEvent(EventKind::KingPepe)], + block: false, + occupied: Some(content::text().get(keys::KING_PEPE_BUSY, &[])), + } + } "TD_MC_enterboss1" => through(Effect::WarpToPortal( WEDDING_HALL_ENTRANCE, named(WEDDING_HALL_ENTRANCE, "out00"), )), + // Out of King Pepe's room, back up the tower, with the key he drops + // once all three brothers are beaten (Cosmic `out_pepeking`). + "out_pepeking" => { + let mut effects = Vec::new(); + let inventory = &player.inventory; + if yeti_brothers_beaten(player) && inventory.count(WEDDING_HALL_KEY) == 0 { + if !crate::items::fits_until(inventory, WEDDING_HALL_KEY, 1, None) { + return Some(refused(keys::KING_PEPE_KEY_NO_ROOM)); + } + effects.extend([ + Effect::GiveItem { + item_id: WEDDING_HALL_KEY, + quantity: 1, + }, + Effect::Pink(content::text().get(keys::KING_PEPE_KEY, &[])), + ]); + } + effects.extend([ + Effect::PortalSound, + Effect::WarpToPortal( + EAST_CASTLE_TOWER, + named(EAST_CASTLE_TOWER, "TD_MC_enterboss1"), + ), + ]); + ScriptOutcome { + effects, + block: false, + occupied: None, + } + } // Cosmic asks for the password while the quest is taken; its start // script, which sets one, isn't run, so the door just opens. "secretDoor" if quest(VERIFYING_THE_PASSWORD) != QuestState::NotStarted => { @@ -1793,6 +1835,12 @@ pub fn portal(script: &str, player: &Player) -> Option { Some(outcome) } +/// Whether `player`, on Stop the Wedding, has beaten each Yeti brother. +fn yeti_brothers_beaten(player: &Player) -> bool { + let kills = player.quests.kills(STOP_THE_WEDDING); + kills.len() == YETI_BROTHERS.len() && kills.iter().all(|&count| count > 0) +} + /// The Cursed Forest's way in at `hour` of the world's (UTC) day, the hour /// the stations' clocks show: shut from 7:00 until 17:00. fn cursed_forest(player: &Player, hour: u8) -> ScriptOutcome { @@ -2457,6 +2505,74 @@ mod tests { ); } + /// A player on Stop the Wedding with `kills` of each Yeti brother. + fn stopping_the_wedding(kills: [u16; 3]) -> Player { + let mut player = player_with(&[(STOP_THE_WEDDING, QuestState::Started)]); + *player.quests.kills_mut(STOP_THE_WEDDING).unwrap() = kills.to_vec(); + player + } + + #[test] + fn king_pepes_room_is_open_until_the_three_yeti_brothers_are_beaten() { + let to_hall = vec![ + Effect::PortalSound, + Effect::WarpToPortal(WEDDING_HALL_ENTRANCE, named(WEDDING_HALL_ENTRANCE, "out00")), + ]; + let tower = |player: &Player| portal("TD_MC_enterboss1", player).unwrap(); + for kills in [[0, 0, 0], [1, 0, 1], [1, 1, 0]] { + let way = tower(&stopping_the_wedding(kills)); + assert_eq!( + way.effects, + [Effect::PortalSound, Effect::StartEvent(EventKind::KingPepe)] + ); + assert_eq!( + way.occupied, + Some(content::text().get(keys::KING_PEPE_BUSY, &[])) + ); + } + assert_eq!(tower(&stopping_the_wedding([1, 1, 1])).effects, to_hall); + for state in [QuestState::NotStarted, QuestState::Completed] { + assert_eq!( + tower(&player_with(&[(STOP_THE_WEDDING, state)])).effects, + to_hall + ); + } + + // Out, with the key once all three are beaten. + let up = [ + Effect::PortalSound, + Effect::WarpToPortal( + EAST_CASTLE_TOWER, + named(EAST_CASTLE_TOWER, "TD_MC_enterboss1"), + ), + ]; + let out = |player: &Player| portal("out_pepeking", player).unwrap().effects; + assert_eq!(out(&stopping_the_wedding([1, 0, 1])), up); + let mut player = stopping_the_wedding([1, 1, 2]); + let key = Effect::GiveItem { + item_id: WEDDING_HALL_KEY, + quantity: 1, + }; + let found = Effect::Pink(content::text().get(keys::KING_PEPE_KEY, &[])); + assert_eq!(out(&player), [&[key, found][..], &up].concat()); + player.inventory.insert( + InventoryType::Etc, + 1, + game::Item::stack(WEDDING_HALL_KEY, 1), + ); + assert_eq!(out(&player), up, "only one key"); + let mut full = stopping_the_wedding([1, 1, 1]); + full.inventory.set_slot_limit(InventoryType::Etc, 1); + full.inventory + .insert(InventoryType::Etc, 1, game::Item::stack(4_000_000, 1)); + assert_eq!( + out(&full), + [Effect::Pink( + content::text().get(keys::KING_PEPE_KEY_NO_ROOM, &[]) + )] + ); + } + #[test] fn a_poison_spore_clears_the_way_through_the_mushroom_forest() { let mut player = player_with(&[]); @@ -3107,10 +3223,7 @@ mod tests { } assert_eq!( - go( - "TD_MC_enterboss1", - &player_with(&[(2330, QuestState::Started)]) - ), + go("TD_MC_enterboss1", &player_with(&[])), Some((WEDDING_HALL_ENTRANCE, 1)) ); diff --git a/runtime/src/actor/client/events.rs b/runtime/src/actor/client/events.rs index 67037406..3abc264e 100644 --- a/runtime/src/actor/client/events.rs +++ b/runtime/src/actor/client/events.rs @@ -96,6 +96,8 @@ impl ClientActor { EventKind::PyramidTomb(_) => return Ok(()), // Mu Young's start script for 2245 takes the player in. EventKind::Tristan => return Ok(()), + // The East Castle Tower's portal takes the player in. + EventKind::KingPepe => return Ok(()), // Spiegelmann offers every room at once (`talk_to_spiegelmann`). EventKind::Carnival(_) => return Ok(()), }; diff --git a/runtime/src/actor/field.rs b/runtime/src/actor/field.rs index 9a3c2fd7..d152e78b 100644 --- a/runtime/src/actor/field.rs +++ b/runtime/src/actor/field.rs @@ -425,6 +425,12 @@ impl FieldActor { field.bring_out_head(head); } } + // A copy of King Pepe's room with one of the Yeti brothers. + if key.map_id == net::events::KING_PEPE_ROOM && key.instance_id != 0 { + let brothers = net::events::YETI_BROTHERS; + let brother = brothers[rand::thread_rng().gen_range(0, brothers.len())]; + field.board(brother, 1, net::events::YETI_BROTHER_AT); + } // And a copy of one of the Pirate Ship's decks with its crew. if key.instance_id != 0 { for &(mob_id, at, count) in net::events::opening_mobs(key.map_id) {