A repair the purse cannot cover is refused with the not-enough-money error, as in 1.12.1 (#443)
- A repair the purse cannot cover is refused as in 1.12.1: the not-enough-money error and its voice line, no repair sound, nothing sent; Repair All tests the worn gear alone - Tidy three comments: the gift-wrap getter's reader, the repair-reset test's claim, the merchant module doc's width Fixes #412 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
7c967eaf8d
commit
b17be2710f
@@ -1918,7 +1918,7 @@ mod tests {
|
||||
);
|
||||
assert!(
|
||||
press(false, true),
|
||||
"a press that drops a held payload is consumed before the hook"
|
||||
"a press while a payload is held skips the repair reset"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -219,25 +219,24 @@ pub(super) fn drain_container_uses(
|
||||
mut ladder: crate::spell::CastLadder,
|
||||
mut ui_errors: ResMut<crate::ui_action::UiErrorKeys>,
|
||||
mut gate: crate::ui_bind_confirm::BindGate,
|
||||
mut purse: crate::ui_merchant::RepairPurse,
|
||||
) {
|
||||
let Some(mut script) = script else {
|
||||
return;
|
||||
};
|
||||
// Repair-mode clicks (the reference's `0x4f9c7b` route) repair the one item, sounding
|
||||
// `ITEM_REPAIR` as it goes out (`0x4f9ce4`). The reference's affordability check before that
|
||||
// (error 0x25, silent) is not built: our costs lack its reputation discount, so the server
|
||||
// refuses instead.
|
||||
let me = self_q.iter().next();
|
||||
// Repair-mode clicks (the reference's `0x4f9c7b` route) repair the one item: the purse test
|
||||
// first (`0x4f9cc8`), then `ITEM_REPAIR` as it goes out (`0x4f9ce4`).
|
||||
for (bag, slot) in script.take_container_repairs() {
|
||||
let Some(vendor) = merchant.vendor else {
|
||||
let (Some(vendor), Some(me)) = (merchant.vendor, me) else {
|
||||
continue;
|
||||
};
|
||||
let slot0 = u8::try_from(slot.saturating_sub(1)).unwrap_or(0);
|
||||
let item_guid = self_q
|
||||
.iter()
|
||||
.next()
|
||||
.and_then(|store| slot_guid(&store.0, bag, slot0, &ladder.objects));
|
||||
match item_guid {
|
||||
match slot_guid(&me.0, bag, slot0, &ladder.objects) {
|
||||
Some(guid) => {
|
||||
if purse.refuses(&mut script, &merchant, me, [guid]) {
|
||||
continue;
|
||||
}
|
||||
debug!("ui_items: repair lua bag {bag} slot {slot} (item {guid:#x})");
|
||||
script.queue_sound_kit("ITEM_REPAIR");
|
||||
let _ = ladder.commands.0.send(ClientCommand::RepairItem {
|
||||
@@ -250,17 +249,17 @@ pub(super) fn drain_container_uses(
|
||||
}
|
||||
// The paper doll's repair clicks (`0x4c7714`, `0x4c79c4`), the same arm on a worn item.
|
||||
for id in script.take_inventory_repairs() {
|
||||
let Some(vendor) = merchant.vendor else {
|
||||
let (Some(vendor), Some(me)) = (merchant.vendor, me) else {
|
||||
continue;
|
||||
};
|
||||
let item_guid = u8::try_from(id.wrapping_sub(1)).ok().and_then(|slot0| {
|
||||
self_q
|
||||
.iter()
|
||||
.next()
|
||||
.and_then(|store| slot_guid(&store.0, EQUIPMENT_BAG, slot0, &ladder.objects))
|
||||
});
|
||||
let item_guid = u8::try_from(id.wrapping_sub(1))
|
||||
.ok()
|
||||
.and_then(|slot0| slot_guid(&me.0, EQUIPMENT_BAG, slot0, &ladder.objects));
|
||||
match item_guid {
|
||||
Some(guid) if (1..=19).contains(&id) => {
|
||||
if purse.refuses(&mut script, &merchant, me, [guid]) {
|
||||
continue;
|
||||
}
|
||||
debug!("ui_items: repair worn lua slot {id} (item {guid:#x})");
|
||||
script.queue_sound_kit("ITEM_REPAIR");
|
||||
let _ = ladder.commands.0.send(ClientCommand::RepairItem {
|
||||
@@ -756,6 +755,8 @@ mod tests {
|
||||
.init_resource::<crate::spell::SpellModifiers>()
|
||||
.init_resource::<crate::ui_action::CastErrors>()
|
||||
.init_resource::<crate::ui_action::UiErrorKeys>()
|
||||
.init_resource::<crate::ui_chat::ChatLog>()
|
||||
.init_resource::<crate::sound::MessageSounds>()
|
||||
.init_resource::<crate::spell::AutoRepeatActive>()
|
||||
.init_resource::<crate::ui_tradeskill::TradeSkillOpens>()
|
||||
.init_resource::<crate::spell::targeting::SpellTargeting>()
|
||||
@@ -937,6 +938,136 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// What one repair click did: the packets, the VM's sounds, the `UI_ERROR_MESSAGE` lines and
|
||||
/// the message rows queued for their sound.
|
||||
struct RepairClick {
|
||||
sent: Vec<ClientCommand>,
|
||||
sounds: Vec<benilla_ui::script::SoundRequest>,
|
||||
shown: Vec<String>,
|
||||
sounded: Vec<&'static str>,
|
||||
}
|
||||
|
||||
/// A repair-mode click on [`crate::ui_merchant::purse_fixture`]'s sword with `money` in the
|
||||
/// purse: in backpack slot 1 through `UseContainerItem`, or `worn` in the main hand (slot 15)
|
||||
/// through `PickupInventoryItem`.
|
||||
fn click_to_repair_the_sword(money: u32, worn: bool) -> RepairClick {
|
||||
use crate::ui_merchant::purse_fixture::{player, record_errors, seat, SWORD_ENTRY};
|
||||
use benilla_protocol::field::FIELD_PLAYER_INV_SLOT_HEAD;
|
||||
let (mut app, rx) = open_the_clam();
|
||||
while rx.try_recv().is_ok() {} // drain the clam's own send
|
||||
seat(app.world_mut());
|
||||
let slot_field = if worn {
|
||||
FIELD_PLAYER_INV_SLOT_HEAD + 2 * 15
|
||||
} else {
|
||||
F_PACK_SLOT_1
|
||||
};
|
||||
let me = app
|
||||
.world_mut()
|
||||
.query_filtered::<Entity, With<SelfPlayer>>()
|
||||
.single(app.world())
|
||||
.unwrap();
|
||||
app.world_mut()
|
||||
.entity_mut(me)
|
||||
.insert(player(slot_field, money));
|
||||
{
|
||||
let mut script = app.world_mut().non_send_resource_mut::<UiScript>();
|
||||
script.take_sounds();
|
||||
record_errors(&script);
|
||||
script.set_merchant(Some(benilla_ui::script::MerchantState {
|
||||
can_repair: true,
|
||||
..Default::default()
|
||||
}));
|
||||
let click = if worn {
|
||||
let mut slots: benilla_ui::script::InventorySlots = Default::default();
|
||||
slots[16] = Some(benilla_ui::script::InvSlotView {
|
||||
item_id: SWORD_ENTRY,
|
||||
..Default::default()
|
||||
});
|
||||
script.set_inventory_slots(slots);
|
||||
"ShowRepairCursor() PickupInventoryItem(16)"
|
||||
} else {
|
||||
let mut bag = benilla_ui::script::ContainerState {
|
||||
num_slots: 16,
|
||||
..Default::default()
|
||||
};
|
||||
bag.slots.insert(
|
||||
1,
|
||||
benilla_ui::script::ContainerSlot {
|
||||
item_id: SWORD_ENTRY,
|
||||
count: 1,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
script.set_container(0, Some(bag));
|
||||
"ShowRepairCursor() UseContainerItem(0, 1)"
|
||||
};
|
||||
script.run(click).unwrap();
|
||||
}
|
||||
app.world_mut()
|
||||
.run_system_once(drain_container_uses)
|
||||
.unwrap();
|
||||
let sounded = crate::ui_merchant::purse_fixture::sounded(app.world());
|
||||
let mut script = app.world_mut().non_send_resource_mut::<UiScript>();
|
||||
RepairClick {
|
||||
sent: rx.try_iter().collect(),
|
||||
sounds: script.take_sounds(),
|
||||
shown: crate::ui_merchant::purse_fixture::shown(&script),
|
||||
sounded,
|
||||
}
|
||||
}
|
||||
|
||||
/// The repair arm's purse test, unsigned `cost > money` on the discounted cost: one copper
|
||||
/// short shows `ERR_NOT_ENOUGH_MONEY` with its speech row, plays no `ITEM_REPAIR` and sends
|
||||
/// nothing; exactly the cost, under the full price, sounds and sends.
|
||||
fn the_purse_test_refuses_one_copper_short_and_pays_the_exact_cost(worn: bool) {
|
||||
use crate::ui_merchant::purse_fixture::{sword_prices, NOT_ENOUGH_MONEY, SWORD, VENDOR};
|
||||
let (full, discounted) = sword_prices();
|
||||
|
||||
let short = click_to_repair_the_sword(discounted - 1, worn);
|
||||
assert!(
|
||||
short.sent.is_empty(),
|
||||
"short of {discounted}: nothing goes out"
|
||||
);
|
||||
assert!(short.sounds.is_empty(), "…no ITEM_REPAIR");
|
||||
assert_eq!(short.shown, [NOT_ENOUGH_MONEY], "…the red line");
|
||||
assert_eq!(
|
||||
short.sounded,
|
||||
["ERR_NOT_ENOUGH_MONEY"],
|
||||
"…and its row, which speaks line 0x28"
|
||||
);
|
||||
|
||||
let exact = click_to_repair_the_sword(discounted, worn);
|
||||
assert!(
|
||||
matches!(
|
||||
exact.sent.as_slice(),
|
||||
[ClientCommand::RepairItem {
|
||||
vendor: VENDOR,
|
||||
item_guid: SWORD
|
||||
}]
|
||||
),
|
||||
"{discounted} in the purse pays the discounted {discounted}, short of the full {full}"
|
||||
);
|
||||
assert_eq!(
|
||||
exact.sounds,
|
||||
vec![benilla_ui::script::SoundRequest::KitName(
|
||||
"ITEM_REPAIR".into()
|
||||
)]
|
||||
);
|
||||
assert!(exact.shown.is_empty() && exact.sounded.is_empty());
|
||||
}
|
||||
|
||||
/// `UseContainerItem`'s repair arm (`0x4fa22e`–`0x4fa236`, error `0x4fa23a`).
|
||||
#[test]
|
||||
fn a_bag_repair_the_purse_cannot_cover_is_refused_and_one_it_covers_is_sent() {
|
||||
the_purse_test_refuses_one_copper_short_and_pays_the_exact_cost(false);
|
||||
}
|
||||
|
||||
/// `PickupInventoryItem`'s repair arm (`0x4c775f`–`0x4c7766`, error `0x4c776a`).
|
||||
#[test]
|
||||
fn a_worn_repair_the_purse_cannot_cover_is_refused_and_one_it_covers_is_sent() {
|
||||
the_purse_test_refuses_one_copper_short_and_pays_the_exact_cost(true);
|
||||
}
|
||||
|
||||
/// The unwrap arm's other side: wrapping paper arms the wrap cursor and sends nothing.
|
||||
#[test]
|
||||
fn a_wrapper_right_click_arms_the_cursor_and_ships_no_packet() {
|
||||
@@ -1578,6 +1709,8 @@ mod bind_confirm_tests {
|
||||
.init_resource::<crate::spell::SpellModifiers>()
|
||||
.init_resource::<crate::ui_action::CastErrors>()
|
||||
.init_resource::<crate::ui_action::UiErrorKeys>()
|
||||
.init_resource::<crate::ui_chat::ChatLog>()
|
||||
.init_resource::<crate::sound::MessageSounds>()
|
||||
.init_resource::<crate::spell::AutoRepeatActive>()
|
||||
.init_resource::<crate::ui_tradeskill::TradeSkillOpens>()
|
||||
.init_resource::<crate::spell::targeting::SpellTargeting>()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//! The merchant window's app side: [`MerchantOpen`] holds the `SMSG_LIST_INVENTORY` rows,
|
||||
//! [`feed_merchant`] pushes them with the buyback and repair rows, the purse and the refusals,
|
||||
//! [`feed_repair_all_cost`] the repair-all total, and [`drain_merchant`] sends the Lua intents. A bag click sells through [`crate::ui_items`].
|
||||
//! [`feed_repair_all_cost`] the repair-all total, and [`drain_merchant`] sends the Lua intents. A
|
||||
//! bag click sells through [`crate::ui_items`].
|
||||
|
||||
use benilla_protocol::messages::{buy_result, sell_result, VendorItem};
|
||||
use benilla_world::interact::WorldRightPress;
|
||||
@@ -361,6 +362,58 @@ fn vendor_npc_flags(
|
||||
vendor_store(open, units).map_or(0, |store| store.0.unit_npc_flags())
|
||||
}
|
||||
|
||||
/// What the purse test before a repair's send reads: the items' costs, the open vendor for its
|
||||
/// price discount, and the message path a refusal takes.
|
||||
#[derive(bevy::ecs::system::SystemParam)]
|
||||
pub(crate) struct RepairPurse<'w, 's> {
|
||||
objects: Objects<'w, 's>,
|
||||
items: Res<'w, Items>,
|
||||
commands: Res<'w, NetCommands>,
|
||||
tables: Option<Res<'w, RepairTables>>,
|
||||
units: Query<'w, 's, (&'static Guid, &'static ObjectStore), Without<SelfPlayer>>,
|
||||
reactions: crate::target::ReactionInputs<'w>,
|
||||
sink: crate::ui_action::MessageSink<'w>,
|
||||
}
|
||||
|
||||
impl RepairPurse<'_, '_> {
|
||||
/// The purse test of the four item-click repair arms (`0x4f9cc8`–`0x4f9ccf`) and of
|
||||
/// `RepairAllItems` (`0x4fc08e`): the `0x4faf30` costs of `guids`, summed in a u32, above
|
||||
/// `PLAYER_FIELD_COINAGE` (unsigned, so an equal cost passes) raise `ERR_NOT_ENOUGH_MONEY`
|
||||
/// (`0x496720(0x25)`, speech line 0x28) and answer `true`, and the caller sends nothing.
|
||||
pub(crate) fn refuses(
|
||||
&mut self,
|
||||
script: &mut UiScript,
|
||||
open: &MerchantOpen,
|
||||
player: &ObjectStore,
|
||||
guids: impl IntoIterator<Item = u64>,
|
||||
) -> bool {
|
||||
// `0x4faf30` keeps the full price unless it resolves the vendor's unit.
|
||||
let discount = vendor_store(open, &self.units).map_or(0.0, |vendor| {
|
||||
crate::target::vendor_price_discount(
|
||||
self.reactions.factions.as_deref(),
|
||||
&self.reactions.reputations,
|
||||
vendor,
|
||||
player,
|
||||
)
|
||||
});
|
||||
let cost = guids.into_iter().filter(|&g| g != 0).fold(0u32, |sum, g| {
|
||||
let item = self.tables.as_deref().map_or(0, |t| {
|
||||
item_repair_cost(g, &self.objects, &self.items, t, &self.commands, discount)
|
||||
});
|
||||
sum.wrapping_add(item)
|
||||
});
|
||||
let money = player.0.player_money().unwrap_or(0);
|
||||
if cost <= money {
|
||||
return false;
|
||||
}
|
||||
debug!("ui_merchant: repair of {cost} copper refused with {money} in the purse");
|
||||
if let Some(line) = crate::ui_action::keyed_line(script, "ERR_NOT_ENOUGH_MONEY") {
|
||||
crate::ui_action::show_messages(script, &mut self.sink, "ui_merchant", [line]);
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/// Push `GetRepairAllCost`'s total, which the reference sweeps at the call (`0x4fbd60`): every
|
||||
/// frame a repairing vendor is open, 0 otherwise.
|
||||
fn feed_repair_all_cost(
|
||||
@@ -541,13 +594,14 @@ impl NpcSession for MerchantOpen {
|
||||
|
||||
/// Drain the Lua intents: a buy sends `CMSG_BUY_ITEM` by item entry, a buyback
|
||||
/// `CMSG_BUYBACK_ITEM` with the absolute slot (69-80), a repair-all `CMSG_REPAIR_ITEM` with guid
|
||||
/// 0, and a close clears locally, as there is no close packet.
|
||||
/// 0 when the purse covers the worn gear, and a close clears locally, as there is no close packet.
|
||||
fn drain_merchant(
|
||||
script: Option<NonSendMut<UiScript>>,
|
||||
mut open: ResMut<MerchantOpen>,
|
||||
commands: Res<NetCommands>,
|
||||
self_q: Query<(&ObjectStore, &Guid), With<SelfPlayer>>,
|
||||
objects: Objects,
|
||||
mut purse: RepairPurse,
|
||||
) {
|
||||
let Some(mut script) = script else {
|
||||
return;
|
||||
@@ -574,12 +628,17 @@ fn drain_merchant(
|
||||
}
|
||||
}
|
||||
if script.take_repair_all() {
|
||||
if let Some(vendor) = open.vendor {
|
||||
debug!("ui_merchant: repair all");
|
||||
let _ = commands.0.send(ClientCommand::RepairItem {
|
||||
vendor,
|
||||
item_guid: 0,
|
||||
});
|
||||
if let (Some(vendor), Some(me)) = (open.vendor, self_store) {
|
||||
// `RepairAllItems` (`0x4fbfe0`) prices only the equipped slots 0-18 for its purse test
|
||||
// (`0x4fc026`–`0x4fc063`), not the bags `GetRepairAllCost` also sweeps.
|
||||
let worn = (0..19u8).filter_map(|i| me.0.player_inv_slot(i));
|
||||
if !purse.refuses(&mut script, &open, me, worn) {
|
||||
debug!("ui_merchant: repair all");
|
||||
let _ = commands.0.send(ClientCommand::RepairItem {
|
||||
vendor,
|
||||
item_guid: 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
for (index, quantity) in script.take_merchant_buys() {
|
||||
@@ -670,6 +729,122 @@ pub(crate) fn end_repair_mode_on_right_press(
|
||||
}
|
||||
}
|
||||
|
||||
/// A level-20 sword 40 points down at a repair vendor flagged `UNIT_FLAG_PVP`, for the purse tests
|
||||
/// here and in [`crate::ui_items`].
|
||||
#[cfg(test)]
|
||||
pub(crate) mod purse_fixture {
|
||||
use super::*;
|
||||
use benilla_protocol::field::{
|
||||
FIELD_PLAYER_FIELD_COINAGE, FIELD_UNIT_FLAGS, FIELD_UNIT_NPC_FLAGS,
|
||||
};
|
||||
use benilla_protocol::ObjectFields;
|
||||
|
||||
pub(crate) const VENDOR: u64 = 0xF130_0000_0000_0043;
|
||||
pub(crate) const SWORD: u64 = 0x4000_0000_0000_0707;
|
||||
pub(crate) const SWORD_ENTRY: u32 = 2488;
|
||||
/// Absolute descriptor indices.
|
||||
const OBJECT_ENTRY: u16 = 3;
|
||||
const ITEM_DURABILITY: u16 = 46;
|
||||
const ITEM_MAXDURABILITY: u16 = 47;
|
||||
const PLAYER_BYTES_3: u16 = 195;
|
||||
/// The string `ERR_NOT_ENOUGH_MONEY` holds in 1.12's `GlobalStrings.lua`.
|
||||
pub(crate) const NOT_ENOUGH_MONEY: &str = "You don't have enough money.";
|
||||
|
||||
/// One level-20 row at 1 copper a point for a sword (weapon subclass 7), and quality 1's
|
||||
/// multiplier (row `2·1 + 1`) at 1.
|
||||
fn tables() -> benilla_formats::DurabilityTables {
|
||||
let mut row = vec![0u32; 29];
|
||||
row[7] = 1;
|
||||
benilla_formats::DurabilityTables::from_rows([(20, row)], [(3, 1.0)])
|
||||
}
|
||||
|
||||
/// The sword's repair in full, and after the two honor steps (`0x612bcf`, `0x612bd7`) an honor
|
||||
/// rank byte of 8 earns at this vendor; a test at the discounted price discriminates the
|
||||
/// discount only while the two differ.
|
||||
pub(crate) fn sword_prices() -> (u32, u32) {
|
||||
let t = tables();
|
||||
let steps = f64::from(0.05f32) + f64::from(0.05f32);
|
||||
let full = t.repair_cost(40, 20, 1, 2, 7, 0.0);
|
||||
let discounted = t.repair_cost(40, 20, 1, 2, 7, steps);
|
||||
assert!(discounted < full, "{discounted} of {full}");
|
||||
(full, discounted)
|
||||
}
|
||||
|
||||
/// The merchant open on the vendor, whose unit is streamed, the tables, and the sword's
|
||||
/// template and object.
|
||||
pub(crate) fn seat(world: &mut World) {
|
||||
world.insert_resource(RepairTables(tables()));
|
||||
world
|
||||
.resource_mut::<MerchantOpen>()
|
||||
.open(VENDOR, Vec::new());
|
||||
world.spawn((
|
||||
Guid(VENDOR),
|
||||
ObjectStore(ObjectFields::from_pairs(&[
|
||||
(FIELD_UNIT_NPC_FLAGS, NPC_FLAG_REPAIR),
|
||||
(FIELD_UNIT_FLAGS, 0x1000),
|
||||
])),
|
||||
));
|
||||
let mut sword = crate::items::test_template("Sword");
|
||||
(sword.class, sword.subclass, sword.quality, sword.item_level) = (2, 7, 1, 20);
|
||||
world
|
||||
.resource_mut::<Items>()
|
||||
.insert_template(SWORD_ENTRY, Some(sword));
|
||||
crate::items::test_spawn_item(
|
||||
world,
|
||||
SWORD,
|
||||
ObjectFields::from_pairs(&[
|
||||
(OBJECT_ENTRY, SWORD_ENTRY),
|
||||
(ITEM_DURABILITY, 35),
|
||||
(ITEM_MAXDURABILITY, 75),
|
||||
]),
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
/// The player's fields: the sword in the guid pair at `slot_field`, `money` copper, and the
|
||||
/// honor rank byte at 8.
|
||||
pub(crate) fn player(slot_field: u16, money: u32) -> ObjectStore {
|
||||
ObjectStore(ObjectFields::from_pairs(&[
|
||||
(slot_field, SWORD as u32),
|
||||
(slot_field + 1, (SWORD >> 32) as u32),
|
||||
(FIELD_PLAYER_FIELD_COINAGE, money),
|
||||
(PLAYER_BYTES_3, 8 << 24),
|
||||
]))
|
||||
}
|
||||
|
||||
/// The message's string, and a recorder of the `UI_ERROR_MESSAGE` lines [`shown`] reads.
|
||||
pub(crate) fn record_errors(script: &UiScript) {
|
||||
script
|
||||
.run(&format!(
|
||||
r#"
|
||||
ERR_NOT_ENOUGH_MONEY = "{NOT_ENOUGH_MONEY}"
|
||||
BENILLA_TEST_LINES = {{}}
|
||||
local f = CreateFrame("Frame")
|
||||
f:RegisterEvent("UI_ERROR_MESSAGE")
|
||||
f:SetScript("OnEvent", function() table.insert(BENILLA_TEST_LINES, arg1) end)
|
||||
"#
|
||||
))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// The `UI_ERROR_MESSAGE` lines since [`record_errors`].
|
||||
pub(crate) fn shown(script: &UiScript) -> Vec<String> {
|
||||
script
|
||||
.eval::<Vec<String>>("return BENILLA_TEST_LINES")
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// The message rows queued for their sound.
|
||||
pub(crate) fn sounded(world: &World) -> Vec<&'static str> {
|
||||
world
|
||||
.resource::<crate::sound::MessageSounds>()
|
||||
.queued()
|
||||
.iter()
|
||||
.map(|r| r.key)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -781,6 +956,65 @@ mod tests {
|
||||
assert_eq!(got, i64::from(want), "the undiscounted total is {full}");
|
||||
}
|
||||
|
||||
/// `RepairAllItems` prices only the equipped slots for its purse test (`0x4fc026`–`0x4fc063`,
|
||||
/// `0x4fc08e`): the sword worn and over the purse is refused with `ERR_NOT_ENOUGH_MONEY` and
|
||||
/// no send; the same sword in the backpack, which `GetRepairAllCost` would count, leaves a worn
|
||||
/// total of 0 and the repair-all goes out.
|
||||
#[test]
|
||||
fn repair_all_tests_the_purse_against_the_worn_gear_alone() {
|
||||
use benilla_protocol::field::FIELD_PLAYER_INV_SLOT_HEAD;
|
||||
use bevy::ecs::system::RunSystemOnce;
|
||||
use purse_fixture::*;
|
||||
|
||||
let (_, discounted) = sword_prices();
|
||||
let repair_all = |slot_field: u16| {
|
||||
let (tx, rx) = crossbeam_channel::unbounded();
|
||||
let mut app = App::new();
|
||||
app.init_resource::<Items>()
|
||||
.init_resource::<crate::net::GuidIndex>()
|
||||
.init_resource::<MerchantOpen>()
|
||||
.init_resource::<crate::net::Reputations>()
|
||||
.init_resource::<crate::ui_chat::ChatLog>()
|
||||
.init_resource::<crate::sound::MessageSounds>()
|
||||
.insert_resource(NetCommands(tx));
|
||||
seat(app.world_mut());
|
||||
app.world_mut()
|
||||
.spawn((SelfPlayer, Guid(0x5e1f), player(slot_field, discounted - 1)));
|
||||
let script = UiScript::new().unwrap();
|
||||
record_errors(&script);
|
||||
script.run("RepairAllItems()").unwrap();
|
||||
app.insert_non_send_resource(script);
|
||||
app.world_mut().run_system_once(drain_merchant).unwrap();
|
||||
let sent: Vec<ClientCommand> = rx.try_iter().collect();
|
||||
let lines = shown(app.world().non_send_resource::<UiScript>());
|
||||
(sent, lines, sounded(app.world()))
|
||||
};
|
||||
|
||||
// The main hand (slot 15).
|
||||
let (sent, lines, rows) = repair_all(FIELD_PLAYER_INV_SLOT_HEAD + 2 * 15);
|
||||
assert!(sent.is_empty(), "worn, one copper short: nothing goes out");
|
||||
assert_eq!(lines, [NOT_ENOUGH_MONEY]);
|
||||
assert_eq!(
|
||||
rows,
|
||||
["ERR_NOT_ENOUGH_MONEY"],
|
||||
"the row that speaks line 0x28"
|
||||
);
|
||||
|
||||
// Backpack slot 1, inventory slot 23.
|
||||
let (sent, lines, rows) = repair_all(FIELD_PLAYER_INV_SLOT_HEAD + 2 * 23);
|
||||
assert!(
|
||||
matches!(
|
||||
sent.as_slice(),
|
||||
[ClientCommand::RepairItem {
|
||||
vendor: VENDOR,
|
||||
item_guid: 0
|
||||
}]
|
||||
),
|
||||
"the bag's {discounted} is not in the purse test"
|
||||
);
|
||||
assert!(lines.is_empty() && rows.is_empty());
|
||||
}
|
||||
|
||||
fn row(entry: u32, slot: u32, count: u32) -> VendorItem {
|
||||
VendorItem {
|
||||
slot,
|
||||
|
||||
@@ -25,6 +25,18 @@ pub struct DurabilityTables {
|
||||
}
|
||||
|
||||
impl DurabilityTables {
|
||||
/// Tables from explicit rows, for tests and tools: item level → the 29 cost columns, and
|
||||
/// quality row id → multiplier.
|
||||
pub fn from_rows(
|
||||
costs: impl IntoIterator<Item = (u32, Vec<u32>)>,
|
||||
qualities: impl IntoIterator<Item = (u32, f32)>,
|
||||
) -> Self {
|
||||
Self {
|
||||
costs: costs.into_iter().collect(),
|
||||
qualities: qualities.into_iter().collect(),
|
||||
}
|
||||
}
|
||||
|
||||
/// The per-point cost for a class and subclass at `item_level`, `None` when unrepairable.
|
||||
fn column(&self, item_level: u32, class: u32, subclass: u32) -> Option<u32> {
|
||||
let row = self.costs.get(&item_level)?;
|
||||
|
||||
@@ -199,7 +199,7 @@ impl super::UiScript {
|
||||
cursor::queue_lock_changed(&mut model, bag, slot);
|
||||
}
|
||||
|
||||
/// The armed gift wrap, if any; the app reads it to tell whether a right-click cancels one.
|
||||
/// The armed gift wrap, if any; the app reads it to hold the cursor's base at Cast.
|
||||
pub fn gift_wrap_armed(&self) -> Option<PendingWrap> {
|
||||
self.model_ref().pending_wrap
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user