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
17 changes: 9 additions & 8 deletions docs/item-layer.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
22 changes: 12 additions & 10 deletions src/item/plan.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -1065,26 +1067,26 @@ 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<size_t>(i) < std::size(kHeistObjectiveIds))
if (i >= 0 && static_cast<size_t>(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");
// No parenthetical at all is the ordinary shape of a boss contract ("Kill Admiral
// 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<std::string>& 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;
}
}
Expand Down
46 changes: 24 additions & 22 deletions tests/item_pricing_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
}
Expand Down