From 8d72b5e88a859f367653412ef673369c75d0cee8 Mon Sep 17 00:00:00 2001 From: David Goldstein Date: Mon, 26 Oct 2026 17:50:03 -0400 Subject: [PATCH] Row 231: Tristan's Tomb (2238, 2245): Mu Young's silent start scripts start 2238, then take a player on it alone into the unknown Jr. Balrog's room for ten minutes (new solo EventKind::Tristan, one at a time on a channel), completing 2245 so interval 0 lets it repeat; Jr. Balrog's fall brings out Tristan's Spirit, where 2238 ends --- agent_docs/party-quests.md | 17 ++- agent_docs/quests.md | 11 +- game-data/placeholder_text.toml | 1 + game-data/server_content.toml | 1 + game-data/src/quests.rs | 12 +- game-data/src/server_content.rs | 6 + game-data/supported_quests.txt | 2 + integration-harness/tests/tristan_tomb.rs | 140 ++++++++++++++++++++++ net/src/events.rs | 55 ++++++++- net/src/npc_scripts.rs | 43 +++++++ runtime/src/actor/client/events.rs | 2 + runtime/src/actor/field.rs | 14 +++ 12 files changed, 295 insertions(+), 9 deletions(-) create mode 100644 integration-harness/tests/tristan_tomb.rs diff --git a/agent_docs/party-quests.md b/agent_docs/party-quests.md index 322b6a85..77dc4655 100644 --- a/agent_docs/party-quests.md +++ b/agent_docs/party-quests.md @@ -37,7 +37,8 @@ Haze is played through 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)). +([Romeo and Juliet](#romeo-and-juliet)). Jr. Balrog's room, for quest 2245, +is taken alone ([Jr. Balrog's room](#jr-balrogs-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 @@ -1369,6 +1370,20 @@ our Map.nx never names) leads back to the side's room. The event's items `onFirstUserEnter` scripts (`StageMsg_romio`, `StageMsg_juliet`) do nothing yet. Its lines are placeholders (`magatia.*`). +## Jr. Balrog's room + +For "To Tristan's Tomb" (2245; Cosmic `BalrogQuest`). Mu Young's start +script `q2245s` (see [quests](./quests.md)) takes a player on 2238, at +level 50 and up, alone into their own copy of 910520000 for ten minutes +(`events::TRISTAN`, `EventKind::Tristan`), party or not. Only one may be in +it on a channel at a time (`runs: 1`, Cosmic's "someone in this map" +refusal): another is told so (`tristan.room_taken`, a placeholder) and +nothing is applied. The room's `life` holds the unknown Jr. Balrog +(9300326), spawned once. Its fall brings out Tristan's Spirit (1061015) at +(0, 115) (`FieldActor::tristan_balrog_falls`), where 2238 ends. The event +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`. + ## NPCs | NPC | Where | Script | diff --git a/agent_docs/quests.md b/agent_docs/quests.md index dcf298fe..e8691ab5 100644 --- a/agent_docs/quests.md +++ b/agent_docs/quests.md @@ -177,7 +177,7 @@ itself ("Etc item inventory is full."). `startscript`/ `endscript` are allowed only for scripts the server implements (`game_data::quests::SCRIPTS`: `q1021s`, `q1021e`, Bartos's secret `q4647e`, Garnox's pet evolutions `q8185e`, `q8189e` and `q4659e` (see -[Pets](./pets.md)), Meren's Maker lessons, Dyle's informants, Reef, the baby dragon and Captain Al's family lessons below); those steps go through +[Pets](./pets.md)), Meren's Maker lessons, Dyle's informants, Reef, Mu Young, the baby dragon and Captain Al's family lessons below); those steps go through QUEST_ACTION 4/5 and the server drives the dialogue (see [NPC dialogue](./npc-dialogue.md)). `money` pays (or, if negative, takes) mesos on completion; a quest that takes mesos needs them held, or the @@ -266,6 +266,15 @@ Reef (1032108, in 910100001) thanks the player through a start script (`Act/1 pop`, through `Effect::GainFame`, since `FinishQuest` gives no `Act/1` rewards). Its line is a placeholder (`reef.thanks`). +Mu Young (1061014, in 105100100) has two start scripts, both silent as in +Cosmic. `q2238s` starts "Who is the Owner of the Mysterious Note?" (2238, +once 2237 is done). `q2245s` ("To Tristan's Tomb", for a player on 2238) +completes 2245 at once, so that its `interval 0` lets it be taken again, +and takes the player alone into the unknown Jr. Balrog's room +(`EventKind::Tristan`, see [party quests](./party-quests.md#jr-balrogs-room)). +Jr. Balrog's fall brings out Tristan's Spirit (1061015), where 2238 ends +for the data's 50,000 EXP; with it done, the `tristanEnter` portal opens. + "The Remnants of Horned Tail" (3714) works the same way. Nine Spirit's Baby Dragon (2081011), in the nest past Leafre's `dragonNest` portal (240040612, open once 3706 is done), has a start script (`q3714s`) and no end. It diff --git a/game-data/placeholder_text.toml b/game-data/placeholder_text.toml index cb8c77ff..f2b1ec40 100644 --- a/game-data/placeholder_text.toml +++ b/game-data/placeholder_text.toml @@ -772,6 +772,7 @@ "nine_spirit.baby.stone_held" = "[placeholder] The Dragon Stone in your bag glows brighter here, and the baby dragon glares at it." "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." "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 49e44bef..a941bc44 100644 --- a/game-data/server_content.toml +++ b/game-data/server_content.toml @@ -1445,6 +1445,7 @@ unsourced = [ "nine_spirit.baby.stone_held", "nine_spirit.baby.full", "reef.thanks", + "tristan.room_taken", "family.captain_al.junior.done", "family.captain_al.junior.not_yet", "family.captain_al.rep.done", diff --git a/game-data/src/quests.rs b/game-data/src/quests.rs index 753ec05f..3e0b43ec 100644 --- a/game-data/src/quests.rs +++ b/game-data/src/quests.rs @@ -64,16 +64,16 @@ pub const INERT_END_REQUIREMENTS: &[&str] = &["start", "end"]; /// Quest scripts (`startscript`/`endscript`) the server implements. A quest /// naming any other script is unsupported. pub const SCRIPTS: &[&str] = &[ - "q1021s", "q1021e", "q2214e", "q2215e", "q2228s", "q2232e", "q2233e", "q2234e", "q2216s", - "q2217s", "q2218s", "q2219s", "q3714s", "q4647e", "q4659e", "q6030e", "q6031e", "q6032e", - "q6033e", "q6036e", "q8185e", "q8189e", + "q1021s", "q1021e", "q2214e", "q2215e", "q2228s", "q2238s", "q2245s", "q2232e", "q2233e", + "q2234e", "q2216s", "q2217s", "q2218s", "q2219s", "q3714s", "q4647e", "q4659e", "q6030e", + "q6031e", "q6032e", "q6033e", "q6036e", "q8185e", "q8189e", ]; /// Quests with nothing to complete them by (an empty `Check/1`) whose start /// script completes them as it starts them (`Effect::FinishQuest`): JM's /// informants (2216-2219), each heard from once, Reef's thanks for Faust -/// (2228), and the Remnants of Horned Tail (3714), once for each Nine -/// Spirit's Egg. -pub const FINISHED_BY_START_SCRIPT: &[i16] = &[2216, 2217, 2218, 2219, 2228, 3714]; +/// (2228), Mu Young's way to Tristan's Tomb (2245), each time it opens, and +/// the Remnants of Horned Tail (3714), once for each Nine Spirit's Egg. +pub const FINISHED_BY_START_SCRIPT: &[i16] = &[2216, 2217, 2218, 2219, 2228, 2245, 3714]; /// Meren's Maker classes (quests 6030-6032) and the quest they make up /// (6029) share 6029's quest-info string, one digit per class: `Act/0 info` /// starts it at "000", a class starts only while its own digit is 0 diff --git a/game-data/src/server_content.rs b/game-data/src/server_content.rs index 5040b86d..44afb92d 100644 --- a/game-data/src/server_content.rs +++ b/game-data/src/server_content.rs @@ -786,6 +786,11 @@ pub mod keys { /// the player for beating Faust. No licensed script has the text. pub const REEF_THANKS: &str = "reef.thanks"; + /// To Tristan's Tomb (quest 2245, `q2245s`): Mu Young (NPC 1061014) + /// turns a player away while another is in the Jr. Balrog's room. No + /// licensed script has the text. + pub const TRISTAN_ROOM_TAKEN: &str = "tristan.room_taken"; + /// 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 @@ -3009,6 +3014,7 @@ mod tests { .chain(&keys::CAPTAIN_AL_ENTITLEMENT) .chain(&[ keys::REEF_THANKS, + keys::TRISTAN_ROOM_TAKEN, keys::NINE_SPIRIT_HATCHED, keys::NINE_SPIRIT_STONE_HELD, keys::NINE_SPIRIT_FULL, diff --git a/game-data/supported_quests.txt b/game-data/supported_quests.txt index 59b5ad13..6785fac7 100644 --- a/game-data/supported_quests.txt +++ b/game-data/supported_quests.txt @@ -264,11 +264,13 @@ 2234 2235 2237 +2238 2239 2240 2241 2242 2243 +2245 2246 2247 2248 diff --git a/integration-harness/tests/tristan_tomb.rs b/integration-harness/tests/tristan_tomb.rs new file mode 100644 index 00000000..d684d718 --- /dev/null +++ b/integration-harness/tests/tristan_tomb.rs @@ -0,0 +1,140 @@ +//! Who is the Owner of the Mysterious Note? (2238) and To Tristan's Tomb +//! (2245): Mu Young's start scripts start 2238, then send the player alone +//! into the unknown Jr. Balrog's room for ten minutes, completing 2245 so +//! it may be taken again. Its fall brings out Tristan's Spirit, where 2238 +//! ends. One player at a time may be in the room on a channel. The harness +//! world runs events 60 times as fast (`RUSTMS_EVENT_SCALE`), so the ten +//! minutes are ten seconds. + +use integration_harness::connection::MapleTestConnection; +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::quests::{build_quest_action, build_quest_completion, SCRIPT_START}; +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::{MU_YOUNG_MAP, TRISTANS_SPIRIT, TRISTAN_BALROG, TRISTAN_ROOM}; +use net::packet::op::RecvOpcode; +use packet::io::write::PktWrite; +use packet::Packet; +use std::time::Duration; + +const MU_YOUNG: i32 = 1_061_014; + +/// A level 55 fighter, armed to fell Jr. Balrog, with 2237 done, waiting +/// at Mu Young. +async fn at_mu_young(config: &HarnessConfig) -> 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, 55, 50).await.expect("a level 55 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, 2237, 2, "").await.expect("2237 done"); + let mut session = relogin(config).await; + let c = &mut session.connection; + let mut trace = Trace::default(); + assert_eq!(warp(c, MU_YOUNG_MAP, "", &mut trace).await, MU_YOUNG_MAP); + step(c, &config.character_name, walk_to(617, 246)).await; + session +} + +/// Ask Mu Young for 2245 and follow what comes of it for a moment. +async fn ask_to_go(c: &mut MapleTestConnection) -> Trace { + send(c, build_quest_action(SCRIPT_START, 2245, MU_YOUNG)).await; + let mut trace = Trace::default(); + read_for(c, &mut trace, 1_500).await; + trace +} + +#[tokio::test] +async fn jr_balrogs_fall_brings_out_tristans_spirit() { + let (config, other_config) = ( + load_harness_config_or_fail().await, + load_harness_config_or_fail().await, + ); + let m = config.character_name.clone(); + let mut session = at_mu_young(&config).await; + let mut other = at_mu_young(&other_config).await; + let c = &mut session.connection; + + // 2238 starts with no word, and 2245 takes the player in. + let t = step(c, &m, build_quest_action(SCRIPT_START, 2238, MU_YOUNG)).await; + assert!(t.prompts.is_empty(), "Mu Young says nothing"); + assert_eq!(t.progress, [(2238, 1)]); + // What came on entering the room, Jr. Balrog among it. + let mut trace = ask_to_go(c).await; + assert!(trace.prompts.is_empty(), "nor here"); + assert_eq!(trace.progress, [(2245, 2)], "2245 is done as it starts"); + assert_eq!(trace.warps, [TRISTAN_ROOM]); + assert_eq!(trace.countdowns.len(), 1, "the ten minutes run"); + + // Someone else on 2238 must wait while the room is taken. + let n = other_config.character_name.clone(); + let o = &mut other.connection; + step(o, &n, build_quest_action(SCRIPT_START, 2238, MU_YOUNG)).await; + let t = ask_to_go(o).await; + assert_eq!(t.prompts, [(MU_YOUNG, 0)], "told the room is taken"); + assert!(t.progress.is_empty() && t.warps.is_empty()); + + // Jr. Balrog falls, and Tristan's Spirit comes out, where 2238 ends. + until(c, &mut trace, |t| t.field.alive(TRISTAN_BALROG).len() == 1).await; + let balrog = trace.field.alive(TRISTAN_BALROG)[0]; + let spirit = |t: &Trace| { + t.field + .npcs + .iter() + .find(|&&(_, npc)| npc == TRISTANS_SPIRIT) + .map(|&(oid, _)| oid) + }; + for _ in 0..40 { + send(c, attack(balrog, 30_000)).await; + read_for(c, &mut trace, 100).await; + if trace.field.killed.contains(&balrog) { + break; + } + } + until(c, &mut trace, |t| { + t.field.killed.contains(&balrog) && spirit(t).is_some() + }) + .await; + let oid = spirit(&trace).unwrap(); + let (x, y) = trace.field.npc_positions[&oid]; + step(c, &m, walk_to(x, y)).await; + let t = step(c, &m, build_quest_completion(2238, TRISTANS_SPIRIT, -1)).await; + assert_eq!(t.progress, [(2238, 2)]); + assert_eq!(t.gains, [50_000]); + + // Out by its portal, back to Mu Young, and the room is free again. + send(c, walk_to(-343, 188)).await; + let mut p = Packet::new_empty(); + p.write_short(RecvOpcode::ChangeMapSpecial as i16).unwrap(); + p.write_byte(0).unwrap(); + p.write_str_with_length("out00").unwrap(); + p.write_short(0).unwrap(); + send(c, p).await; + let mut trace = Trace::default(); + until(c, &mut trace, |t| t.warps == [MU_YOUNG_MAP]).await; + drop(session); + + // The other goes in at last, and the time running out sends them back. + tokio::time::sleep(Duration::from_secs(3)).await; + let o = &mut other.connection; + let t = ask_to_go(o).await; + assert_eq!(t.progress, [(2245, 2)]); + assert_eq!(t.warps, [TRISTAN_ROOM]); + let mut trace = Trace::default(); + until_within(o, &mut trace, Duration::from_secs(20), |t| { + t.warps == [MU_YOUNG_MAP] + }) + .await; + drop(other); + + // 2238 is done for good, and 2245 stays done. + let restored = relogin(&config).await; + assert_eq!(restored.quests.get(&2238), Some(&2)); + assert_eq!(restored.quests.get(&2245), Some(&2)); +} diff --git a/net/src/events.rs b/net/src/events.rs index 7b0b155d..1471adc5 100644 --- a/net/src/events.rs +++ b/net/src/events.rs @@ -69,6 +69,9 @@ pub enum EventKind { /// with that mode's gem at Duarte in the lobby or straight from its /// room in the Shades after a run. PyramidTomb(PyramidMode), + /// Jr. Balrog's room before Tristan's Tomb (2245), taken alone from + /// Mu Young (1061014) in the Sleepywood dungeon. + Tristan, /// A Monster Carnival room (1 to 6), taken by a party from Spiegelmann /// (2042000) on the Carnival Field (`crate::carnival`). Carnival(u8), @@ -2396,6 +2399,39 @@ pub const PYRAMID_TOMB_RUNS: [Event; 4] = [ pyramid_tomb(PyramidMode::Hell, &[PYRAMID_SHADES + 3]), ]; +/// The room before Tristan's Tomb, 910520000, where the unknown Jr. Balrog +/// waits (Map.nx `life`, spawned once). +pub const TRISTAN_ROOM: i32 = 910520000; +/// The Sleepywood dungeon's map where Mu Young (1061014) stands, and the way +/// back from the room (its `forcedReturn` and its `out00`). +pub const MU_YOUNG_MAP: i32 = 105100100; +/// The unknown Jr. Balrog, whose fall brings out Tristan's Spirit. +pub const TRISTAN_BALROG: i32 = 9_300_326; +/// Tristan's Spirit (1061015), where 2238 ends, and the point above the +/// ground below which it comes out (Cosmic `BalrogQuest.monsterKilled`). +pub const TRISTANS_SPIRIT: i32 = 1_061_015; +pub const TRISTANS_SPIRIT_AT: (i16, i16) = (0, 115); + +/// To Tristan's Tomb (2245): Mu Young's start script takes a player on 2238 +/// alone into their own copy of the room for ten minutes, one at a time on +/// a channel (Cosmic `BalrogQuest`, whose script turns a second player +/// away). It is never won; it ends when they leave by `out00` or the time +/// runs out. +pub const TRISTAN: Event = Event { + lobby: MU_YOUNG_MAP, + entrances: &[], + maps: TRISTAN_ROOM..=TRISTAN_ROOM, + entry: TRISTAN_ROOM, + exit: MU_YOUNG_MAP, + levels: 50..=u8::MAX, + team: 1..=1, + minutes: 10, + 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 { @@ -2482,7 +2518,7 @@ pub fn protected(mob_id: i32) -> bool { } impl EventKind { - pub const ALL: [EventKind; 37] = [ + pub const ALL: [EventKind; 38] = [ EventKind::Kpq, EventKind::Hpq, EventKind::Lpq, @@ -2520,6 +2556,7 @@ impl EventKind { EventKind::Carnival(6), EventKind::Magatia(MagatiaSide::Zenumist), EventKind::Magatia(MagatiaSide::Alcadno), + EventKind::Tristan, ]; pub fn event(self) -> &'static Event { @@ -2546,6 +2583,7 @@ impl EventKind { EventKind::PyramidTomb(mode) => &PYRAMID_TOMB_RUNS[mode as usize], EventKind::Carnival(room) => &CARNIVAL[usize::from(room.clamp(1, CARNIVAL_ROOMS) - 1)], EventKind::Magatia(side) => &MAGATIA[side as usize], + EventKind::Tristan => &TRISTAN, } } @@ -3595,6 +3633,21 @@ mod tests { assert!(event_map(926010010) && !event_map(PYRAMID_LOBBY)); } + #[test] + fn jr_balrogs_room_is_taken_alone_from_mu_young_for_ten_minutes() { + let tristan = EventKind::Tristan.event(); + assert_eq!((tristan.entry, tristan.exit), (TRISTAN_ROOM, MU_YOUNG_MAP)); + assert!(tristan.solo() && tristan.lasts_until_empty()); + assert_eq!( + (tristan.minutes, tristan.last_stage(), tristan.runs), + (10, 0, 1) + ); + assert!(tristan.starts_on(MU_YOUNG_MAP)); + assert!(tristan.team(1, &[at(1, 50, MU_YOUNG_MAP)]).is_some()); + assert_eq!(tristan.team(1, &[at(1, 49, MU_YOUNG_MAP)]), None); + assert!(event_map(TRISTAN_ROOM) && !event_map(MU_YOUNG_MAP)); + } + #[test] fn dojo_sections_have_their_own_clocks() { let floor = |n: u8| clock_on(DOJO.stage_map(n)); diff --git a/net/src/npc_scripts.rs b/net/src/npc_scripts.rs index 52e820c2..46ed86a2 100644 --- a/net/src/npc_scripts.rs +++ b/net/src/npc_scripts.rs @@ -2342,6 +2342,11 @@ impl NpcScript { "q2218s" => informant(player, 2), "q2219s" => informant(player, 3), "q2228s" => Some(reef()), + "q2238s" => Some(NpcScript::new( + MU_YOUNG, + vec![Page::Done(vec![Effect::StartQuest(2238)])], + )), + "q2245s" => Some(tristan_tomb()), "q3714s" => Some(baby_dragon(player)), "q6030e" => Some(maker_class(CARSON, keys::MAKER_ALCHEMY, 6030)), "q6031e" => Some(maker_class(HUGHES, keys::MAKER_SCIENCE, 6031)), @@ -2800,6 +2805,23 @@ fn reef() -> NpcScript { /// Reef, in 910100001. const REEF: i32 = 1032108; +/// Mu Young, in the Sleepywood dungeon (105100100). +const MU_YOUNG: i32 = 1061014; + +/// `q2245s`, To Tristan's Tomb: Mu Young sends a player on 2238 into the +/// unknown Jr. Balrog's room at once, with no word (Cosmic `2245.js`), and +/// 2245 completes as it starts, so that `interval 0` lets them go again. +/// Only one may be in the room on a channel at a time; another is told so +/// and nothing is applied. +fn tristan_tomb() -> NpcScript { + let go = vec![ + Effect::FinishQuest(2245), + Effect::StartEvent(EventKind::Tristan), + ]; + let mut script = NpcScript::new(MU_YOUNG, vec![Page::Done(go)]); + script.refused = Some(text(keys::TRISTAN_ROOM_TAKEN)); + script +} /// Captain Al, in Lith Harbor (104000000) and Orbis Park (200000200). const CAPTAIN_AL: i32 = 1002103; /// Captain Al's end scripts ([`NpcScript::for_family_quest`]). @@ -10016,6 +10038,27 @@ mod tests { ); } + #[test] + fn mu_young_starts_2238_and_sends_the_player_to_jr_balrog() { + let player = with_quests(&[(2237, true)]); + let script = NpcScript::for_quest("q2238s", &player).unwrap(); + let step = script.begin(); + assert!(step.prompt.is_none()); + assert_eq!(step.effects, [Effect::StartQuest(2238)]); + let mut script = NpcScript::for_quest("q2245s", &player).unwrap(); + assert_eq!(script.npc_id(), MU_YOUNG); + let step = script.begin(); + assert!(step.prompt.is_none()); + assert_eq!( + step.effects, + [ + Effect::FinishQuest(2245), + Effect::StartEvent(EventKind::Tristan) + ] + ); + assert!(script.refused().is_some(), "another is in the room"); + } + #[test] fn the_baby_dragon_trades_each_egg_for_a_dragon_stone() { let mut player = with_quests(&[(3706, true)]); diff --git a/runtime/src/actor/client/events.rs b/runtime/src/actor/client/events.rs index 5f1f158c..67037406 100644 --- a/runtime/src/actor/client/events.rs +++ b/runtime/src/actor/client/events.rs @@ -94,6 +94,8 @@ impl ClientActor { EventKind::Pyramid(_) | EventKind::PyramidParty(_) => return Ok(()), // Duarte takes the gem, or sends the player in from the Shades. EventKind::PyramidTomb(_) => return Ok(()), + // Mu Young's start script for 2245 takes the player in. + EventKind::Tristan => 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 7f131ef0..9a3c2fd7 100644 --- a/runtime/src/actor/field.rs +++ b/runtime/src/actor/field.rs @@ -2066,6 +2066,11 @@ impl FieldActor { { self.lord_pirate_falls() } + (net::events::TRISTAN_BALROG, net::events::TRISTAN_ROOM) + if self.key.instance_id != 0 => + { + self.tristan_balrog_falls() + } _ => {} } // A trial's head clears its stage, which unseals its door; each @@ -2226,6 +2231,15 @@ impl FieldActor { self.appear(wu_yang, net::events::LORD_PIRATE_AT); } + /// The unknown Jr. Balrog is beaten: Tristan's Spirit comes out, once, + /// for 2238 to end at (Cosmic `BalrogQuest.monsterKilled`). + fn tristan_balrog_falls(&mut self) { + let spirit = net::events::TRISTANS_SPIRIT; + if !self.map_npcs.iter().any(|npc| npc.npc_id == spirit) { + self.appear(spirit, net::events::TRISTANS_SPIRIT_AT); + } + } + /// Whether every reactor here that must be set before the stage clears /// is (`net::events::seals`). fn sealed(&self) -> bool {