From 716e5fa143dcbd9304379c68b62a8751dce70993 Mon Sep 17 00:00:00 2001 From: David Goldstein Date: Thu, 22 Oct 2026 02:17:08 -0400 Subject: [PATCH] HC: Legendary Spirit window (CUIEnchantSkill): an equip from the Equip tab, a scroll from the Use tab sent as 0x56 with the White Scroll box; effect, sounds and chat line on the answer; -1 refused --- HeavenClient/CMakeLists.txt | 2 + .../Character/Inventory/ScrollRules.h | 19 ++ HeavenClient/Gameplay/Combat/Combat.cpp | 14 + HeavenClient/IO/Components/EquipTooltip.cpp | 1 + HeavenClient/IO/Components/Tooltip.h | 1 + HeavenClient/IO/UI.cpp | 5 +- HeavenClient/IO/UIElement.h | 1 + HeavenClient/IO/UIStateGame.cpp | 2 + HeavenClient/IO/UITypes/UIEnchantSkill.cpp | 319 ++++++++++++++++++ HeavenClient/IO/UITypes/UIEnchantSkill.h | 79 +++++ .../Net/Handlers/MessagingHandlers.cpp | 12 +- HeavenClient/Net/Packets/InventoryPackets.h | 17 + HeavenClient/tests/scroll_rules.cpp | 7 + agent_docs/inventory.md | 13 +- 14 files changed, 486 insertions(+), 6 deletions(-) create mode 100644 HeavenClient/IO/UITypes/UIEnchantSkill.cpp create mode 100644 HeavenClient/IO/UITypes/UIEnchantSkill.h diff --git a/HeavenClient/CMakeLists.txt b/HeavenClient/CMakeLists.txt index 4d3e16c8..7b9e5d79 100644 --- a/HeavenClient/CMakeLists.txt +++ b/HeavenClient/CMakeLists.txt @@ -877,6 +877,8 @@ add_executable(HeavenClient IO/UITypes/UIKarmaScissors.h IO/UITypes/UIVegaSpell.cpp IO/UITypes/UIVegaSpell.h + IO/UITypes/UIEnchantSkill.cpp + IO/UITypes/UIEnchantSkill.h IO/UITypes/UIInviteCard.cpp IO/UITypes/UIInviteCard.h IO/UITypes/UIGuildBBS.cpp diff --git a/HeavenClient/Character/Inventory/ScrollRules.h b/HeavenClient/Character/Inventory/ScrollRules.h index 9ce0b30d..4409b176 100644 --- a/HeavenClient/Character/Inventory/ScrollRules.h +++ b/HeavenClient/Character/Inventory/ScrollRules.h @@ -15,11 +15,30 @@ namespace ms constexpr const char *SEALED_TARGET = "Due to the potency of the scroll,\\nthe sealed item might be destroyed." "\\nAre you sure you still want to use the scroll?"; + // Legendary Spirit's window (FUN_007f63fa, FUN_007f651c): StringPool + // 0x1063 for a cash equip dropped in, and the literal at 0xb388cc for + // the server's -1. + constexpr const char *CASH_REFUSED = "You may not use scrolls on this item."; + constexpr const char *ENCHANT_REFUSED = "You cannot use a Scroll with this item."; + inline bool is_scroll(int32_t item_id) { return item_id / 10000 == 204; } + // Legendary Spirit: the Beginner's, the Noblesse's and the Legend's. + inline bool is_legendary_spirit(int32_t skill_id) + { + return skill_id == 1003 || skill_id == 10001003 || skill_id == 20001003; + } + + // Whether Legendary Spirit's window takes the equip (FUN_007f63fa): + // no cash equip but pet equipment. + inline bool may_enchant(int32_t equip_id, bool cash) + { + return !cash || equip_id / 10000 == 180; + } + // FUN_004f5497. Clean Slate (20490xx) and Chaos (20491xx) fit any // equipment but pet equipment; 20492xx fits rings, pendants and belts; // any other scroll fits the category its id names (2043001: 130xxxx). diff --git a/HeavenClient/Gameplay/Combat/Combat.cpp b/HeavenClient/Gameplay/Combat/Combat.cpp index 109ca3c7..13842666 100644 --- a/HeavenClient/Gameplay/Combat/Combat.cpp +++ b/HeavenClient/Gameplay/Combat/Combat.cpp @@ -52,6 +52,8 @@ #include "../../IO/Messages.h" #include "../../IO/UI.h" #include "../../IO/UITypes/UIMaker.h" +#include "../../IO/UITypes/UIEnchantSkill.h" +#include "../../Character/Inventory/ScrollRules.h" #include "../../Util/Misc.h" #include "../../Util/Randomizer.h" @@ -251,6 +253,18 @@ namespace ms return false; } + // So does Legendary Spirit (FUN_0096d085). + if (scroll_rules::is_legendary_spirit(move_id)) + { + auto window = UI::get().get_element(); + bool open = window && window->is_active(); + + if (!open && player.get_skills().get_level(move_id) > 0 && player.can_request(200)) + UI::get().emplace(); + + return false; + } + if (player.is_barrel_busy()) { InChatMessage(Messages::Type::MORPHED).drop(); diff --git a/HeavenClient/IO/Components/EquipTooltip.cpp b/HeavenClient/IO/Components/EquipTooltip.cpp index bfdba2b2..d7c89209 100644 --- a/HeavenClient/IO/Components/EquipTooltip.cpp +++ b/HeavenClient/IO/Components/EquipTooltip.cpp @@ -192,6 +192,7 @@ namespace ms case Tooltip::Parent::ITEMPROTECTOR: case Tooltip::Parent::KARMASCISSORS: case Tooltip::Parent::VEGARESULT: + case Tooltip::Parent::ENCHANTSKILL: case Tooltip::Parent::DUEY: invtype = InventoryType::Id::EQUIP; break; diff --git a/HeavenClient/IO/Components/Tooltip.h b/HeavenClient/IO/Components/Tooltip.h index a3c7ae94..d9960f20 100644 --- a/HeavenClient/IO/Components/Tooltip.h +++ b/HeavenClient/IO/Components/Tooltip.h @@ -54,6 +54,7 @@ namespace ms ITEMPROTECTOR, KARMASCISSORS, VEGARESULT, + ENCHANTSKILL, STATUSBAR, // The status bar's quick slots, whose tooltips go above the cursor. QUICKSLOT, diff --git a/HeavenClient/IO/UI.cpp b/HeavenClient/IO/UI.cpp index 819e4b09..08575a55 100644 --- a/HeavenClient/IO/UI.cpp +++ b/HeavenClient/IO/UI.cpp @@ -47,6 +47,7 @@ #include "UITypes/UISystemOption.h" #include "UITypes/UITradingRoom.h" #include "UITypes/UIVegaSpell.h" +#include "UITypes/UIEnchantSkill.h" #include "UITypes/UIViciousHammer.h" #include "UITypes/UIWorldMap.h" @@ -136,7 +137,8 @@ namespace ms || open(get_element()) || open(get_element()) || open(get_element()) || open(get_element()) || open(get_element()) || open(get_element()) || open(get_element()) - || open(get_element()) || open(get_element()); + || open(get_element()) || open(get_element()) + || open(get_element()); } void UI::send_cursor(Point cursorpos, Cursor::State cursorstate) @@ -429,6 +431,7 @@ namespace ms types.emplace_back(UIElement::Type::ITEMPROTECTOR); types.emplace_back(UIElement::Type::KARMASCISSORS); types.emplace_back(UIElement::Type::VEGARESULT); + types.emplace_back(UIElement::Type::ENCHANTSKILL); types.emplace_back(UIElement::Type::PETCHOOSER); types.emplace_back(UIElement::Type::APRESET); types.emplace_back(UIElement::Type::SPRESET); diff --git a/HeavenClient/IO/UIElement.h b/HeavenClient/IO/UIElement.h index 2147dcd5..4ce4e9a0 100644 --- a/HeavenClient/IO/UIElement.h +++ b/HeavenClient/IO/UIElement.h @@ -126,6 +126,7 @@ namespace ms SCRIPTMESSAGE, FAMILY, FAMILYTREE, + ENCHANTSKILL, NUM_TYPES }; diff --git a/HeavenClient/IO/UIStateGame.cpp b/HeavenClient/IO/UIStateGame.cpp index af1d360d..9c6ff5e7 100644 --- a/HeavenClient/IO/UIStateGame.cpp +++ b/HeavenClient/IO/UIStateGame.cpp @@ -102,6 +102,8 @@ namespace ms return Tooltip::Parent::KARMASCISSORS; case UIElement::Type::VEGARESULT: return Tooltip::Parent::VEGARESULT; + case UIElement::Type::ENCHANTSKILL: + return Tooltip::Parent::ENCHANTSKILL; case UIElement::Type::EVENT: return Tooltip::Parent::EVENT; case UIElement::Type::KEYCONFIG: diff --git a/HeavenClient/IO/UITypes/UIEnchantSkill.cpp b/HeavenClient/IO/UITypes/UIEnchantSkill.cpp new file mode 100644 index 00000000..4a7fba9e --- /dev/null +++ b/HeavenClient/IO/UITypes/UIEnchantSkill.cpp @@ -0,0 +1,319 @@ +#include "UIEnchantSkill.h" + +#include "UIChatBar.h" +#include "UINotice.h" + +#include "../UI.h" + +#include "../Components/AreaButton.h" +#include "../Components/MapleButton.h" + +#include "../../Constants.h" +#include "../../Audio/Audio.h" +#include "../../Character/Inventory/ItemDrop.h" +#include "../../Character/Inventory/ScrollRules.h" +#include "../../Data/ItemData.h" +#include "../../Gameplay/Stage.h" +#include "../../Net/Packets/InventoryPackets.h" +#include "../../Util/NxFiles.h" + +#include + +namespace ms +{ + namespace + { + // From the window's top left (OnCreate 0x7f5c9b, draw FUN_007f6196). + // The equip's icon has its origin at the bottom left, and its tooltip + // shows over the box around it (0x7f637f). + constexpr Point SAY = Point(46, 26); + constexpr Point WHITE_LABEL = Point(30, 150); + constexpr Point WHITE_BOX = Point(10, 153); + constexpr Point WHITE_AREA = Point(150, 15); + constexpr Point ICON = Point(74, 126); + constexpr Rectangle ICON_HOVER = Rectangle(75, 102, 100, 127); + // The effect covers the magic circle (I: its place isn't traced). + constexpr Point EFFECT = Point(3, 62); + // The effect's frames have no delay; v83 plays them 60 ms apart (I). + constexpr uint16_t EFFECT_DELAY = 60; + + // As for a scroll dropped on a worn equip (FUN_004f4bb7's prologue). + constexpr const char *CATCH_BREATH = "You can't do it right now.\\nCatch your breath and then try again."; + constexpr const char *DEAD = "You can't do it once you're dead."; + + void notice(const std::string &text) + { + UI::get().emplace(text, [](bool) {}); + } + + int32_t item_at(InventoryType::Id tab, int16_t slot) + { + return slot ? Stage::get().get_player().get_inventory().get_item_id(tab, slot) : 0; + } + } + + UIEnchantSkill::UIEnchantSkill() + : UIElement(Point(), Point()), equip_slot(0), locked(false), white(false), white_sent(false), + success(false), cursed(false), playing(false) + { + nl::node basic = NxFiles::ui_classic["Basic.img"]; + nl::node src = NxFiles::ui_classic["UIWindow.img"]["EnchantSkill"]; + + Texture background = src["backgrnd"]; + sprites.emplace_back(src["backgrnd"]); + + for (size_t i = 0; i < say.size(); i++) + say[i] = src["say" + std::to_string(i)]; + + box[false] = basic["CheckBox"]["0"]; + box[true] = basic["CheckBox"]["1"]; + + std::vector frames; + + for (int32_t i = 0; src["Effect"][std::to_string(i)]; i++) + frames.emplace_back(src["Effect"][std::to_string(i)], EFFECT_DELAY, 255, 255); + + effect = Animation(frames); + + buttons[Buttons::CANCEL] = std::make_unique(basic["BtCancel2"], Point(65, 180)); + buttons[Buttons::CLOSE] = std::make_unique(basic["BtClose"], Point(158, 8)); + buttons[Buttons::WHITE] = std::make_unique(WHITE_BOX, WHITE_AREA); + buttons[Buttons::WHITE]->set_active(holds_white_scroll()); + + dimension = background.get_dimensions(); + position = Point((Constants::Constants::get().get_viewwidth() - dimension.x()) / 2, + (Constants::Constants::get().get_viewheight() - dimension.y()) / 2); + } + + void UIEnchantSkill::draw(float alpha) const + { + UIElement::draw(alpha); + + int32_t equip = item_at(InventoryType::Id::EQUIP, equip_slot); + + say[equip ? 1 : 0].draw(position + SAY); + + // The box is there only for a player with a White Scroll (FUN_007f6f0a). + if (buttons.at(Buttons::WHITE)->is_active()) + { + say[2].draw(position + WHITE_LABEL); + box[white].draw(position + WHITE_BOX); + } + + if (equip) + ItemData::get(equip).get_icon(false).draw(position + ICON); + + if (playing) + effect.draw(position + EFFECT, alpha); + } + + void UIEnchantSkill::update() + { + UIElement::update(); + + // v83 sets the box again once the effect is over; until then it stays. + if (!playing) + buttons[Buttons::WHITE]->set_active(holds_white_scroll()); + + if (playing && effect.update()) + { + playing = false; + finish(); + } + } + + void UIEnchantSkill::remove_cursor() + { + UIElement::remove_cursor(); + + UI::get().clear_tooltip(Tooltip::Parent::ENCHANTSKILL); + } + + Cursor::State UIEnchantSkill::send_cursor(bool clicked, Point cursorpos) + { + if (item_at(InventoryType::Id::EQUIP, equip_slot) && ICON_HOVER.contains(cursorpos - position)) + UI::get().show_equip(Tooltip::Parent::ENCHANTSKILL, equip_slot); + else + UI::get().clear_tooltip(Tooltip::Parent::ENCHANTSKILL); + + return UIElement::send_cursor(clicked, cursorpos); + } + + void UIEnchantSkill::send_key(int32_t, bool pressed, bool escape) + { + // v83 adds no keys of its own; Esc closes it as Cancel would. + if (pressed && escape && !locked) + close(); + } + + bool UIEnchantSkill::send_icon(const Icon &icon, Point) + { + if (locked) + return true; + + auto [tab, slot] = icon.get_source(); + int32_t id = tab == InventoryType::Id::NONE ? 0 : item_at(tab, slot); + + // FUN_004ef140: an equip from the Equip tab is the one to scroll + // (SetItem, FUN_007f63fa), a scroll from the Use tab goes on it. + if (tab == InventoryType::Id::EQUIP && id / 1000000 == 1) + { + if (!scroll_rules::may_enchant(id, ItemData::get(id).is_cashitem())) + { + notice(scroll_rules::CASH_REFUSED); + + return true; + } + + Sound(Sound::Name::DRAGEND).play(); + equip_slot = slot; + } + else if (tab == InventoryType::Id::USE) + { + scroll(slot); + } + + return true; + } + + UIElement::Type UIEnchantSkill::get_type() const + { + return TYPE; + } + + void UIEnchantSkill::receive(int8_t result, bool c) + { + if (!locked || playing) + return; + + if (result == -1) + { + notice(scroll_rules::ENCHANT_REFUSED); + lock(false); + + return; + } + + success = result != 0; + cursed = c; + playing = true; + effect.play(Animation::End::FIRST); + Sound(nl::nx::sound["UI.img"]["Enchant"]).play(); + } + + Button::State UIEnchantSkill::button_pressed(uint16_t buttonid) + { + if (buttonid == Buttons::WHITE) + white = !white; + else if (!locked) + close(); + + return Button::State::NORMAL; + } + + void UIEnchantSkill::scroll(int16_t slot) + { + // FUN_004f4bb7 with legendary 1. + Player &player = Stage::get().get_player(); + const Inventory &inventory = player.get_inventory(); + int32_t scroll_id = inventory.get_item_id(InventoryType::Id::USE, slot); + int32_t equip = item_at(InventoryType::Id::EQUIP, equip_slot); + + if (player.get_look().get_alerted()) + { + notice(CATCH_BREATH); + + return; + } + + if (player.is_dead()) + { + notice(DEAD); + + return; + } + + if (inventory.is_sealed(InventoryType::Id::USE, slot)) + { + notice(item_drop::SEALED); + + return; + } + + // Without an equip, or with one the scroll doesn't fit, nothing is + // said; the server checks the slots left. + if (!equip || !scroll_rules::fits(scroll_id, equip) || !player.can_request(ExclRequest::WAIT)) + return; + + // The box answers v83's question about the White Scroll, which some + // scrolls never use. + bool whitescroll = white && holds_white_scroll() && scroll_rules::takes_white_scroll(scroll_id); + int16_t target = equip_slot; + + auto send = [slot, target, whitescroll]() + { + auto window = UI::get().get_element(); + + if (!window || !window->is_active() || window->locked || window->equip_slot != target) + return; + + if (Stage::get().get_player().request(LegendaryScrollPacket(slot, target, whitescroll), ExclRequest::WAIT)) + { + window->white_sent = whitescroll; + window->lock(true); + } + }; + + // A sealed equip asks first (0x4f4e41); No stops there. + if (inventory.is_sealed(InventoryType::Id::EQUIP, equip_slot)) + UI::get().emplace(scroll_rules::SEALED_TARGET, [send](bool yes) + { + if (yes) + send(); + }); + else + send(); + } + + void UIEnchantSkill::finish() + { + // The line goes by the box as it is now, not by the answer's byte, but never tells of a White Scroll + // that didn't go (the last one, gone with the box, or one the scroll doesn't use). + if (auto chatbar = UI::get().get_element()) + chatbar->send_chatline(scroll_rules::result_text(success, cursed, white && white_sent), + UIChatBar::LineType::SKILL); + + Sound(success ? Sound::Name::ENCHANTSUCCESS : Sound::Name::ENCHANTFAILURE).play(); + Stage::get().get_player().show_effect_id(success ? CharEffect::Id::SCROLL_SUCCESS + : CharEffect::Id::SCROLL_FAILURE); + + // A destroyed equip closes the window; otherwise it stays for another scroll. + if (cursed) + close(); + else + lock(false); + } + + void UIEnchantSkill::lock(bool l) + { + // FUN_007f64ec: Cancel and Close wait for the answer. + locked = l; + Button::State state = l ? Button::State::DISABLED : Button::State::NORMAL; + buttons[Buttons::CANCEL]->set_state(state); + buttons[Buttons::CLOSE]->set_state(state); + } + + void UIEnchantSkill::close() + { + lock(false); + playing = false; + UI::get().clear_tooltip(Tooltip::Parent::ENCHANTSKILL); + deactivate(); + } + + bool UIEnchantSkill::holds_white_scroll() const + { + return Stage::get().get_player().get_inventory().find_item(InventoryType::Id::USE, scroll_rules::WHITE_SCROLL) + != 0; + } +} diff --git a/HeavenClient/IO/UITypes/UIEnchantSkill.h b/HeavenClient/IO/UITypes/UIEnchantSkill.h new file mode 100644 index 00000000..c5af66bf --- /dev/null +++ b/HeavenClient/IO/UITypes/UIEnchantSkill.h @@ -0,0 +1,79 @@ +#pragma once + +#include "../UIElement.h" + +#include "../../Graphics/Animation.h" + +#include + +namespace ms +{ + // Legendary Spirit's window (v83 CUIEnchantSkill, 0x7f5abb on UIWindow.img/EnchantSkill): an equip dragged in + // from the Equip tab, then a scroll from the Use tab, which is sent at once with a White Scroll if the box is + // ticked. It has no other button than Cancel and Close, and stays open with the equip for another scroll. + class UIEnchantSkill : public UIElement + { + public: + static constexpr Type TYPE = UIElement::Type::ENCHANTSKILL; + static constexpr bool FOCUSED = false; + static constexpr bool TOGGLED = false; + + UIEnchantSkill(); + + void draw(float alpha) const override; + + void update() override; + + void remove_cursor() override; + + Cursor::State send_cursor(bool clicked, Point cursorpos) override; + + void send_key(int32_t keycode, bool pressed, bool escape) override; + + bool send_icon(const Icon &icon, Point cursorpos) override; + + UIElement::Type get_type() const override; + + // SHOW_SCROLL_EFFECT's answer to the player's own request (FUN_007f651c): -1 if refused, else whether the + // scroll worked. + void receive(int8_t result, bool cursed); + + protected: + Button::State button_pressed(uint16_t buttonid) override; + + private: + // The scroll in USE `slot` goes on the equip. + void scroll(int16_t slot); + + // Once the effect has played (FUN_007f675d). + void finish(); + + void lock(bool locked); + + void close(); + + bool holds_white_scroll() const; + + enum Buttons : uint16_t + { + CANCEL, + CLOSE, + WHITE + }; + + // Its slot in the Equip tab, 0 until one is dropped in. + int16_t equip_slot; + bool locked; + // The White Scroll box is ticked. + bool white; + // A White Scroll went with the scroll waiting for its answer. + bool white_sent; + bool success; + bool cursed; + bool playing; + std::array say; + // The box: clear, ticked. + std::array box; + Animation effect; + }; +} diff --git a/HeavenClient/Net/Handlers/MessagingHandlers.cpp b/HeavenClient/Net/Handlers/MessagingHandlers.cpp index e6a37573..2d79e8c4 100644 --- a/HeavenClient/Net/Handlers/MessagingHandlers.cpp +++ b/HeavenClient/Net/Handlers/MessagingHandlers.cpp @@ -34,6 +34,7 @@ #include "StatusMessages.h" #include "../../IO/UITypes/UIAvatarMegaphone.h" +#include "../../IO/UITypes/UIEnchantSkill.h" #include "../../IO/UITypes/UIQuestComplete.h" #include "../../IO/UITypes/UIScriptMessage.h" #include "../../IO/UITypes/UIChatBar.h" @@ -807,19 +808,26 @@ namespace ms // v83 FUN_0093354d. Everyone sees the glow and hears the sound; the // scroller also reads the outcome in the chat log. The Legendary Spirit's - // result (legendary = 1) belongs to its dialog, which HeavenClient lacks. + // own result (legendary = 1) belongs to its window, UIEnchantSkill. // The inventory operation, not this, answers the scroller's request. void ScrollResultHandler::handle(InPacket& recv) const { int32_t cid = recv.read_int(); - bool success = recv.read_byte() != 0; + int8_t result = recv.read_byte(); + bool success = result > 0; bool cursed = recv.read_bool(); bool legendary = recv.read_bool(); bool whitescroll = recv.read_bool(); bool own = Stage::get().is_player(cid); + // Legendary Spirit's window shows the player's own result (FUN_0093354d); no window, nothing. if (own && legendary) + { + if (auto window = UI::get().get_element(); window && window->is_active()) + window->receive(result, cursed); + return; + } Sound(success ? Sound::Name::ENCHANTSUCCESS : Sound::Name::ENCHANTFAILURE).play(); Stage::get().show_character_effect(cid, success ? CharEffect::Id::SCROLL_SUCCESS : CharEffect::Id::SCROLL_FAILURE); diff --git a/HeavenClient/Net/Packets/InventoryPackets.h b/HeavenClient/Net/Packets/InventoryPackets.h index ed266a2b..5ee779e8 100644 --- a/HeavenClient/Net/Packets/InventoryPackets.h +++ b/HeavenClient/Net/Packets/InventoryPackets.h @@ -476,6 +476,23 @@ namespace ms } }; + // Uses the scroll in USE `scroll` on the equip in Equip-tab `equip`, from Legendary Spirit's window + // (FUN_00a09221 with legendary 1). + // Opcode: SCROLL_EQUIP(86) + class LegendaryScrollPacket : public OutPacket + { + public: + LegendaryScrollPacket(int16_t scroll, int16_t equip, bool whitescroll) + : OutPacket(OutPacket::Opcode::SCROLL_EQUIP) + { + write_time(); + write_short(scroll); + write_short(equip); + write_short(whitescroll ? 2 : 1); + write_byte(1); + } + }; + // Uses the cash item in Cash `slot` on the item in `target_slot` of `tab`: // an Item Guard (v83 CUIItemProtector's Yes, 0x82a2ee) or the Scissors // of Karma (0x830fb5). diff --git a/HeavenClient/tests/scroll_rules.cpp b/HeavenClient/tests/scroll_rules.cpp index effa5cf3..2fe823d5 100644 --- a/HeavenClient/tests/scroll_rules.cpp +++ b/HeavenClient/tests/scroll_rules.cpp @@ -15,6 +15,13 @@ int main() using namespace ms::scroll_rules; require(fits(2043001, 1302000), "a one-handed sword scroll fits a sword"); + + require(is_legendary_spirit(1003) && is_legendary_spirit(10001003) && is_legendary_spirit(20001003), + "the three Legendary Spirits"); + require(!is_legendary_spirit(1004), "Monster Rider isn't one"); + require(may_enchant(1302000, false), "the window takes a sword"); + require(!may_enchant(1702000, true), "but no cash weapon"); + require(may_enchant(1802000, true), "and pet equipment, cash or not"); require(!fits(2043001, 1402000), "a one-handed sword scroll doesn't fit a two-handed one"); require(fits(2040000, 1002067), "a helmet scroll fits a hat"); require(!fits(2040000, 1302000), "a helmet scroll doesn't fit a sword"); diff --git a/agent_docs/inventory.md b/agent_docs/inventory.md index 5551b5f9..bf6479b4 100644 --- a/agent_docs/inventory.md +++ b/agent_docs/inventory.md @@ -264,9 +264,16 @@ same rules (pet equipment excepted from the cash refusal), only from a player who has one of the three skills, and answers with Legendary Spirit set in SHOW_SCROLL_EFFECT and no look refresh. Any refused dialog request gets SHOW_SCROLL_EFFECT -1 to the scroller alone, which the dialog shows as -"You cannot use a Scroll with this item."; HeavenClient ignores both, as it -has no Legendary Spirit window yet. HeavenClient plays the Enchant sound and -the scroll glow, and gives the scroller the v83 chat line. A sealed scroll +"You cannot use a Scroll with this item." HeavenClient's window is +`UIEnchantSkill` (v83 CUIEnchantSkill, UIWindow.img/EnchantSkill), opened by +the skill's key: an equip dropped in from the Equip tab (a cash one refused +unless it is a pet's), then a scroll from the Use tab, sent at once with a +White Scroll if the box (shown only to a player holding one) is ticked. Its +own answer goes to the window alone: -1 shows the refusal; otherwise it plays +the Enchant sound and its effect, then the chat line, the scroll sound and +glow, and closes on a cursed equip or waits for the next scroll. Cancel, +Close and Esc wait for the answer. For other scrolls, HeavenClient plays +the scroll sound and glow, and gives the scroller the v83 chat line. A sealed scroll goes on nothing, and a sealed equip asks first whether to risk it (v83 0x114f, "Due to the potency of the scroll..."; No keeps the scroll).