Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion agent_docs/map-travel.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
21 changes: 20 additions & 1 deletion agent_docs/party-quests.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 |
Expand Down
3 changes: 3 additions & 0 deletions game-data/placeholder_text.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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."
Expand Down
3 changes: 3 additions & 0 deletions game-data/server_content.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
12 changes: 12 additions & 0 deletions game-data/src/server_content.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
160 changes: 160 additions & 0 deletions integration-harness/tests/king_pepe.rs
Original file line number Diff line number Diff line change
@@ -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::<usize>()
== 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;
}
56 changes: 55 additions & 1 deletion net/src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -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<Item = EventKind> {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -2557,6 +2597,7 @@ impl EventKind {
EventKind::Magatia(MagatiaSide::Zenumist),
EventKind::Magatia(MagatiaSide::Alcadno),
EventKind::Tristan,
EventKind::KingPepe,
];

pub fn event(self) -> &'static Event {
Expand Down Expand Up @@ -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,
}
}

Expand Down Expand Up @@ -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));
Expand Down
Loading
Loading