diff --git a/docs/item-layer.md b/docs/item-layer.md index a246fbf..7dc8953 100644 --- a/docs/item-layer.md +++ b/docs/item-layer.md @@ -416,15 +416,16 @@ bundle, and only the third and fourth encode pricing judgement. on the same reading a chart's and an ultimatum's get; **what is revealed** on a blueprint, as a floor, with the **total** beside it exact where the site indexes one, because a blueprint's wing count varies per copy and a four-wing Tunnels is a different item rather than a better one; the - **objective's value** on a contract, which is the parenthetical after the target and the only - thing about that target the site takes; and the **enchant**, which on a blueprint is what the + **job levels**, as floors at the level the item demands, because a requirement is what the run + costs to open — a rogue short of it cannot run this copy at all, and a copy asking for less is a + cheaper product rather than a better one; and the **enchant**, which on a blueprint is what the whole run is for and is something somebody paid to put there. - Offered and left unticked: the **job levels**, seeded as ceilings, because a requirement is a - demand on the *buyer's* rogue rather than a property of the thing being bought, and a copy - asking less is strictly more usable; and the **heist modifiers**, which are the danger the run - will hold — rolled, re-rollable, the map argument exactly, except that here the row stays and - only the tick goes, because a contract carries seven and ticking all seven asks for one - particular copy in the world. + Offered and left unticked: the **objective's value** on a contract, the parenthetical after the + target and the only thing about that target the site takes, which follows from whatever target + this copy rolled rather than from what the area is; and the **heist modifiers**, which are the + danger the run will hold — rolled, re-rollable, the map argument exactly, except that here the + row stays and only the tick goes, because a contract carries seven and ticking all seven asks + for one particular copy in the world. Two things are left out entirely. **Item quantity, item rarity, alert level reduction, time before lockdown and maximum alive reinforcements** have no `heist_` filter at all, so there is nowhere to put them. And **Total Escape Routes** has one, `heist_max_escape_routes`, which the diff --git a/src/item/plan.cpp b/src/item/plan.cpp index 681ae1c..b3e8785 100644 --- a/src/item/plan.cpp +++ b/src/item/plan.cpp @@ -984,14 +984,16 @@ std::string_view objective_value_of(const Property& p) { /// of what a buyer is paying for. The total beside each count is exact where the site indexes /// one — a blueprint's wing count varies per copy, so it is part of which item this is rather /// than an amount of anything — and Total Escape Routes is left out entirely. See below. -/// - **The objective's value** on a contract, which is the parenthetical after the target. +/// - **The job levels**, as a floor, at the level the item demands. A requirement is what the run +/// costs to open: a rogue short of it cannot run this copy at all, and a copy asking for less +/// is a cheaper product rather than a better copy of the same one. /// - **The enchant**, on a blueprint that has one: "Heist Targets are always Enchanted /// Armaments" is what the whole run is for, and somebody paid to put it there. /// /// Offered and left unticked, because they are the roll rather than the item: -/// - **The job levels.** A requirement is a demand on the *buyer's* rogue, not a property of the -/// thing being bought, so it is seeded as a ceiling — copies asking less are strictly more -/// usable — and left off, because a buyer whose rogue is levelled does not care. +/// - **The objective's value** on a contract, the parenthetical after the target. It follows from +/// the target the copy happens to have rolled, and a buyer opening the area for its level and +/// its jobs is not picking what sits at the end of it. /// - **The heist modifiers.** They are the danger the run will hold: rolled, re-rollable, and /// the map argument exactly, except that the row stays and only the tick goes. A contract /// carries seven of them and ticking all seven asks for one particular copy in the world. @@ -1065,10 +1067,11 @@ void plan_heist(const data::GameData& gd, const Item& it, SearchPlan& p) { const int i = value.empty() ? -1 : lex.index_of(data::TermList::HeistObjectiveValues, value); - if (i >= 0 && static_cast(i) < std::size(kHeistObjectiveIds)) + if (i >= 0 && static_cast(i) < std::size(kHeistObjectiveIds)) { add_option(p, "heist_objective_value", "Objective Value", std::string(kHeistObjectiveIds[i]), std::string(value), true); - else if (!value.empty()) + p.options.back().enabled = false; + } else if (!value.empty()) p.notes.push_back("\"" + std::string(value) + "\" is not an objective value the trade site knows, so the search " "does not ask what the target is worth"); @@ -1076,15 +1079,14 @@ void plan_heist(const data::GameData& gd, const Item& it, SearchPlan& p) { // Darnaw"), not a gap: there is no value to ask about, so nothing is said. } - // One row per job the item demands, seeded as a ceiling and left off. See the note above: - // a job level is what the run asks of the buyer, and a copy asking less still answers. + // One row per job the item demands, at the level it demands, ticked. See the note above: the + // requirement is what the run costs to open, so it is a floor and not a ceiling. for (const Property& prop : it.properties) { if (prop.key != data::PropertyKey::HeistJob || !prop.num) continue; const std::vector& jobs = lex.list(data::TermList::HeistJobs); for (size_t i = 0; i < jobs.size() && i < std::size(kHeistJobKeys); ++i) { if (jobs[i].empty() || prop.value.find(jobs[i]) == std::string::npos) continue; - add_numeric(p, std::string(kHeistJobKeys[i]), jobs[i] + " Level", std::nullopt, false, - 0, {}, *prop.num); + add_numeric(p, std::string(kHeistJobKeys[i]), jobs[i] + " Level", *prop.num, true); break; } } diff --git a/tests/item_pricing_test.cpp b/tests/item_pricing_test.cpp index cf35337..1021ac1 100644 --- a/tests/item_pricing_test.cpp +++ b/tests/item_pricing_test.cpp @@ -2093,11 +2093,6 @@ TEST_CASE("an ultimatum is searched on the deal it offers, not on the danger it TEST_CASE("a heist item is searched on the run it opens, not on the danger it rolled") { auto gd = fixture(); - const auto asked = [](const SearchPlan& p, std::string_view key) { - const OptionFilter* f = p.option(key); - return f && f->enabled ? f->option : std::string(); - }; - SUBCASE("a magic or rare heist item gets its own strategy; a unique one does not") { // The rarity switch would plan a rare contract as a rare and search its seven hazards // as if somebody were buying them, and ask for none of the filters the site indexes it @@ -2169,34 +2164,40 @@ TEST_CASE("a heist item is searched on the run it opens, not on the danger it ro CHECK(numeric_for(p, "heist_escape_routes")->min == 8); } - SUBCASE("the objective's value is the parenthetical, and a boss contract has none") { - const auto value_of = [&](const char* file) { + SUBCASE("the objective's value is offered as the parenthetical, and a boss contract has none") { + // The value follows from whatever target the copy rolled, so it is a row rather than a + // demand: it is drawn with what the item says, and left unticked. + const auto offered = [&](const char* file) { const Item it = resolved(*gd, capture(file)); - return asked(build_plan(*gd, it, derive(gd.get(), it)), "heist_objective_value"); + const SearchPlan p = build_plan(*gd, it, derive(gd.get(), it)); + const OptionFilter* f = p.option("heist_objective_value"); + if (!f) return std::string(); + CHECK(f->shown); + CHECK(!f->enabled); + return f->option; }; - CHECK(value_of("heist-contract-rare-tunnels.txt") == "high"); - CHECK(value_of("heist-contract-rare-laboratory.txt") == "priceless"); - CHECK(value_of("heist-contract-rare-underbelly.txt") == "precious"); + CHECK(offered("heist-contract-rare-tunnels.txt") == "high"); + CHECK(offered("heist-contract-rare-laboratory.txt") == "priceless"); + CHECK(offered("heist-contract-rare-underbelly.txt") == "precious"); // A blueprint sends the crew after a wing rather than after a thing, so it prints no // target line at all and there is nothing to ask. - CHECK(value_of("heist-blueprint-rare-tunnels-full.txt").empty()); + CHECK(offered("heist-blueprint-rare-tunnels-full.txt").empty()); } - SUBCASE("a job level is a ceiling and is offered rather than asked") { - // What the run demands of the *buyer's* rogue, not a property of the thing being - // bought — so a copy asking less is strictly more usable, and a buyer whose rogue is - // levelled does not care at all. + SUBCASE("a job level is a floor and is asked for at the level the item demands") { + // What the run costs to open: a rogue short of the requirement cannot run this copy at + // all, and a copy asking for less is a cheaper product rather than a better one. const Item it = resolved(*gd, capture("heist-blueprint-rare-underbelly.txt")); const SearchPlan p = build_plan(*gd, it, derive(gd.get(), it)); for (const char* key : {"heist_brute_force", "heist_agility", "heist_deception"}) { const NumericFilter* f = numeric_for(p, key); REQUIRE_MESSAGE(f != nullptr, key); - CHECK_MESSAGE(!f->enabled, key); - CHECK_MESSAGE(!f->min.has_value(), key); + CHECK_MESSAGE(f->enabled, key); + CHECK_MESSAGE(!f->max.has_value(), key); } - CHECK(numeric_for(p, "heist_brute_force")->max == 4); - CHECK(numeric_for(p, "heist_agility")->max == 3); - CHECK(numeric_for(p, "heist_deception")->max == 1); + CHECK(numeric_for(p, "heist_brute_force")->min == 4); + CHECK(numeric_for(p, "heist_agility")->min == 3); + CHECK(numeric_for(p, "heist_deception")->min == 1); // The six it does not demand are not rows at all. CHECK(numeric_for(p, "heist_engineering") == nullptr); CHECK(numeric_for(p, "heist_lockpicking") == nullptr); @@ -2226,7 +2227,8 @@ TEST_CASE("a heist item is searched on the run it opens, not on the danger it ro const SearchPlan p = build_plan(*gd, it, derive(gd.get(), it)); CHECK(p.type.empty()); CHECK(p.category == "heistmission.contract"); - CHECK(asked(p, "heist_objective_value") == "precious"); + REQUIRE(p.option("heist_objective_value") != nullptr); + CHECK(p.option("heist_objective_value")->option == "precious"); CHECK(p.notes.front().find("is not a heist base in this data bundle") != std::string::npos); }