Files
voxelforge/core/src/Trade.cpp
T

475 lines
20 KiB
C++

#include "vf/Trade.h"
#include <cmath>
namespace vf {
namespace {
uint32_t nextRandom(uint32_t& state) {
state += 0x9e3779b9u;
uint32_t z = state;
z = (z ^ (z >> 16)) * 0x85ebca6bu;
z = (z ^ (z >> 13)) * 0xc2b2ae35u;
return z ^ (z >> 16);
}
int clampInt(int value, int low, int high) {
if (value < low) return low;
if (value > high) return high;
return value;
}
struct OfferSpec {
uint8_t tier;
ItemId inputA;
int inputACount;
ItemId inputB;
int inputBCount;
ItemId output;
int outputCount;
int maxUses;
int experience;
};
const OfferSpec farmerSpecs[] = {
{0, ItemId::Grass, 20, ItemId::None, 0, ItemId::EmeraldGem, 1, 16, 2},
{0, ItemId::EmeraldGem, 1, ItemId::None, 0, ItemId::Bread, 6, 16, 1},
{1, ItemId::Dirt, 26, ItemId::None, 0, ItemId::EmeraldGem, 1, 16, 5},
{1, ItemId::EmeraldGem, 1, ItemId::None, 0, ItemId::Apple, 4, 16, 5},
{2, ItemId::Leaves, 32, ItemId::None, 0, ItemId::EmeraldGem, 1, 12, 10},
{2, ItemId::EmeraldGem, 3, ItemId::None, 0, ItemId::CookedBeef, 4, 12, 10},
{3, ItemId::EmeraldGem, 4, ItemId::None, 0, ItemId::CookedPorkchop, 5, 12, 15},
{4, ItemId::EmeraldGem, 6, ItemId::None, 0, ItemId::CookedMutton, 6, 12, 30},
};
const OfferSpec fishermanSpecs[] = {
{0, ItemId::Stick, 32, ItemId::None, 0, ItemId::EmeraldGem, 1, 16, 2},
{0, ItemId::EmeraldGem, 1, ItemId::None, 0, ItemId::CoalLump, 3, 16, 1},
{1, ItemId::CoalLump, 15, ItemId::None, 0, ItemId::EmeraldGem, 1, 16, 5},
{1, ItemId::EmeraldGem, 2, ItemId::None, 0, ItemId::CookedBeef, 3, 16, 5},
{2, ItemId::Gravel, 20, ItemId::None, 0, ItemId::EmeraldGem, 1, 12, 10},
{2, ItemId::EmeraldGem, 3, ItemId::None, 0, ItemId::Ice, 4, 12, 10},
{3, ItemId::EmeraldGem, 5, ItemId::None, 0, ItemId::IronIngot, 2, 12, 15},
{4, ItemId::EmeraldGem, 8, ItemId::None, 0, ItemId::DiamondGem, 1, 3, 30},
};
const OfferSpec shepherdSpecs[] = {
{0, ItemId::Leaves, 18, ItemId::None, 0, ItemId::EmeraldGem, 1, 16, 2},
{0, ItemId::EmeraldGem, 1, ItemId::None, 0, ItemId::Planks, 6, 16, 1},
{1, ItemId::Snow, 16, ItemId::None, 0, ItemId::EmeraldGem, 1, 16, 5},
{1, ItemId::EmeraldGem, 2, ItemId::None, 0, ItemId::Ladder, 3, 16, 5},
{2, ItemId::LapisLazuli, 12, ItemId::None, 0, ItemId::EmeraldGem, 1, 12, 10},
{2, ItemId::EmeraldGem, 3, ItemId::None, 0, ItemId::Torch, 8, 12, 10},
{3, ItemId::EmeraldGem, 4, ItemId::None, 0, ItemId::Glowstone, 2, 12, 15},
{4, ItemId::EmeraldGem, 6, ItemId::None, 0, ItemId::RedstoneLamp, 1, 9, 30},
};
const OfferSpec fletcherSpecs[] = {
{0, ItemId::Stick, 32, ItemId::None, 0, ItemId::EmeraldGem, 1, 16, 2},
{0, ItemId::EmeraldGem, 1, ItemId::None, 0, ItemId::Gunpowder, 2, 16, 1},
{1, ItemId::Gravel, 10, ItemId::None, 0, ItemId::EmeraldGem, 1, 16, 5},
{1, ItemId::EmeraldGem, 2, ItemId::None, 0, ItemId::Tnt, 1, 12, 5},
{2, ItemId::Wood, 16, ItemId::None, 0, ItemId::EmeraldGem, 1, 12, 10},
{2, ItemId::EmeraldGem, 3, ItemId::None, 0, ItemId::WoodSword, 1, 9, 10},
{3, ItemId::EmeraldGem, 5, ItemId::None, 0, ItemId::StoneSword, 1, 9, 15},
{4, ItemId::EmeraldGem, 8, ItemId::None, 0, ItemId::IronSword, 1, 6, 30},
};
const OfferSpec librarianSpecs[] = {
{0, ItemId::Planks, 24, ItemId::None, 0, ItemId::EmeraldGem, 1, 16, 2},
{0, ItemId::EmeraldGem, 1, ItemId::None, 0, ItemId::LapisLazuli, 4, 16, 1},
{1, ItemId::Wood, 20, ItemId::None, 0, ItemId::EmeraldGem, 1, 16, 5},
{1, ItemId::EmeraldGem, 3, ItemId::None, 0, ItemId::RedstoneDust, 6, 16, 5},
{2, ItemId::LapisLazuli, 9, ItemId::None, 0, ItemId::EmeraldGem, 1, 12, 10},
{2, ItemId::EmeraldGem, 4, ItemId::None, 0, ItemId::Comparator, 1, 12, 10},
{3, ItemId::EmeraldGem, 6, ItemId::None, 0, ItemId::Repeater, 2, 12, 15},
{4, ItemId::EmeraldGem, 12, ItemId::None, 0, ItemId::DiamondGem, 1, 3, 30},
};
const OfferSpec cartographerSpecs[] = {
{0, ItemId::Planks, 24, ItemId::None, 0, ItemId::EmeraldGem, 1, 16, 2},
{0, ItemId::EmeraldGem, 1, ItemId::None, 0, ItemId::Torch, 4, 16, 1},
{1, ItemId::Sand, 22, ItemId::None, 0, ItemId::EmeraldGem, 1, 16, 5},
{1, ItemId::EmeraldGem, 5, ItemId::GoldIngot, 1, ItemId::Chest, 1, 12, 5},
{2, ItemId::Sandstone, 16, ItemId::None, 0, ItemId::EmeraldGem, 1, 12, 10},
{2, ItemId::EmeraldGem, 4, ItemId::None, 0, ItemId::Ladder, 6, 12, 10},
{3, ItemId::EmeraldGem, 7, ItemId::None, 0, ItemId::Obsidian, 2, 9, 15},
{4, ItemId::EmeraldGem, 10, ItemId::None, 0, ItemId::RedstoneBlock, 2, 6, 30},
};
const OfferSpec clericSpecs[] = {
{0, ItemId::RottenFlesh, 32, ItemId::None, 0, ItemId::EmeraldGem, 1, 16, 2},
{0, ItemId::EmeraldGem, 1, ItemId::None, 0, ItemId::RedstoneDust, 2, 16, 1},
{1, ItemId::GoldIngot, 3, ItemId::None, 0, ItemId::EmeraldGem, 1, 16, 5},
{1, ItemId::EmeraldGem, 1, ItemId::None, 0, ItemId::LapisLazuli, 1, 16, 5},
{2, ItemId::CoalLump, 22, ItemId::None, 0, ItemId::EmeraldGem, 1, 12, 10},
{2, ItemId::EmeraldGem, 3, ItemId::None, 0, ItemId::Glowstone, 1, 12, 10},
{3, ItemId::EmeraldGem, 4, ItemId::None, 0, ItemId::Gunpowder, 3, 12, 15},
{4, ItemId::EmeraldGem, 8, ItemId::LapisLazuli, 4, ItemId::DiamondGem, 1, 3, 30},
};
const OfferSpec armorerSpecs[] = {
{0, ItemId::CoalLump, 15, ItemId::None, 0, ItemId::EmeraldGem, 1, 16, 2},
{0, ItemId::EmeraldGem, 1, ItemId::None, 0, ItemId::IronIngot, 1, 12, 1},
{1, ItemId::IronIngot, 4, ItemId::None, 0, ItemId::EmeraldGem, 1, 16, 5},
{1, ItemId::EmeraldGem, 3, ItemId::None, 0, ItemId::IronShovel, 1, 12, 5},
{2, ItemId::LapisLazuli, 10, ItemId::None, 0, ItemId::EmeraldGem, 1, 12, 10},
{2, ItemId::EmeraldGem, 6, ItemId::None, 0, ItemId::IronAxe, 1, 9, 10},
{3, ItemId::EmeraldGem, 9, ItemId::None, 0, ItemId::DiamondShovel, 1, 6, 15},
{4, ItemId::EmeraldGem, 14, ItemId::DiamondGem, 1, ItemId::DiamondAxe, 1, 3, 30},
};
const OfferSpec weaponsmithSpecs[] = {
{0, ItemId::CoalLump, 15, ItemId::None, 0, ItemId::EmeraldGem, 1, 16, 2},
{0, ItemId::EmeraldGem, 3, ItemId::None, 0, ItemId::IronSword, 1, 12, 1},
{1, ItemId::IronIngot, 4, ItemId::None, 0, ItemId::EmeraldGem, 1, 16, 5},
{1, ItemId::EmeraldGem, 2, ItemId::None, 0, ItemId::StoneSword, 1, 12, 5},
{2, ItemId::Gravel, 24, ItemId::None, 0, ItemId::EmeraldGem, 1, 12, 10},
{2, ItemId::EmeraldGem, 6, ItemId::None, 0, ItemId::IronHoe, 1, 9, 10},
{3, ItemId::EmeraldGem, 10, ItemId::None, 0, ItemId::GoldSword, 1, 6, 15},
{4, ItemId::EmeraldGem, 13, ItemId::DiamondGem, 1, ItemId::DiamondSword, 1, 3, 30},
};
const OfferSpec toolsmithSpecs[] = {
{0, ItemId::CoalLump, 15, ItemId::None, 0, ItemId::EmeraldGem, 1, 16, 2},
{0, ItemId::EmeraldGem, 1, ItemId::None, 0, ItemId::StoneAxe, 1, 12, 1},
{1, ItemId::IronIngot, 4, ItemId::None, 0, ItemId::EmeraldGem, 1, 16, 5},
{1, ItemId::EmeraldGem, 3, ItemId::None, 0, ItemId::IronPickaxe, 1, 12, 5},
{2, ItemId::Stone, 26, ItemId::None, 0, ItemId::EmeraldGem, 1, 12, 10},
{2, ItemId::EmeraldGem, 5, ItemId::None, 0, ItemId::IronAxe, 1, 9, 10},
{3, ItemId::EmeraldGem, 8, ItemId::None, 0, ItemId::GoldPickaxe, 1, 6, 15},
{4, ItemId::EmeraldGem, 12, ItemId::DiamondGem, 1, ItemId::DiamondPickaxe, 1, 3, 30},
};
const OfferSpec butcherSpecs[] = {
{0, ItemId::RawPorkchop, 7, ItemId::None, 0, ItemId::EmeraldGem, 1, 16, 2},
{0, ItemId::EmeraldGem, 1, ItemId::None, 0, ItemId::CookedPorkchop, 4, 16, 1},
{1, ItemId::RawBeef, 10, ItemId::None, 0, ItemId::EmeraldGem, 1, 16, 5},
{1, ItemId::EmeraldGem, 1, ItemId::None, 0, ItemId::CookedBeef, 3, 16, 5},
{2, ItemId::RawMutton, 10, ItemId::None, 0, ItemId::EmeraldGem, 1, 12, 10},
{2, ItemId::EmeraldGem, 1, ItemId::None, 0, ItemId::CookedMutton, 3, 12, 10},
{3, ItemId::CoalLump, 15, ItemId::None, 0, ItemId::EmeraldGem, 1, 12, 15},
{4, ItemId::EmeraldGem, 4, ItemId::None, 0, ItemId::CookedBeef, 8, 12, 30},
};
const OfferSpec leatherworkerSpecs[] = {
{0, ItemId::Leaves, 26, ItemId::None, 0, ItemId::EmeraldGem, 1, 16, 2},
{0, ItemId::EmeraldGem, 2, ItemId::None, 0, ItemId::Planks, 8, 16, 1},
{1, ItemId::RottenFlesh, 26, ItemId::None, 0, ItemId::EmeraldGem, 1, 16, 5},
{1, ItemId::EmeraldGem, 3, ItemId::None, 0, ItemId::Ladder, 4, 16, 5},
{2, ItemId::Snow, 24, ItemId::None, 0, ItemId::EmeraldGem, 1, 12, 10},
{2, ItemId::EmeraldGem, 4, ItemId::None, 0, ItemId::Ice, 6, 12, 10},
{3, ItemId::EmeraldGem, 5, ItemId::None, 0, ItemId::Chest, 2, 12, 15},
{4, ItemId::EmeraldGem, 6, ItemId::IronIngot, 2, ItemId::Furnace, 2, 9, 30},
};
const OfferSpec masonSpecs[] = {
{0, ItemId::Stone, 20, ItemId::None, 0, ItemId::EmeraldGem, 1, 16, 2},
{0, ItemId::EmeraldGem, 1, ItemId::None, 0, ItemId::Cobblestone, 10, 16, 1},
{1, ItemId::Sand, 20, ItemId::None, 0, ItemId::EmeraldGem, 1, 16, 5},
{1, ItemId::EmeraldGem, 1, ItemId::None, 0, ItemId::Sandstone, 6, 16, 5},
{2, ItemId::Gravel, 20, ItemId::None, 0, ItemId::EmeraldGem, 1, 12, 10},
{2, ItemId::EmeraldGem, 2, ItemId::None, 0, ItemId::Obsidian, 1, 12, 10},
{3, ItemId::EmeraldGem, 3, ItemId::None, 0, ItemId::Glowstone, 2, 12, 15},
{4, ItemId::EmeraldGem, 5, ItemId::None, 0, ItemId::Sandstone, 12, 9, 30},
};
const OfferSpec* specsForProfession(VillagerProfession profession, int& outCount) {
switch (profession) {
case VillagerProfession::Farmer: outCount = 8; return farmerSpecs;
case VillagerProfession::Fisherman: outCount = 8; return fishermanSpecs;
case VillagerProfession::Shepherd: outCount = 8; return shepherdSpecs;
case VillagerProfession::Fletcher: outCount = 8; return fletcherSpecs;
case VillagerProfession::Librarian: outCount = 8; return librarianSpecs;
case VillagerProfession::Cartographer: outCount = 8; return cartographerSpecs;
case VillagerProfession::Cleric: outCount = 8; return clericSpecs;
case VillagerProfession::Armorer: outCount = 8; return armorerSpecs;
case VillagerProfession::Weaponsmith: outCount = 8; return weaponsmithSpecs;
case VillagerProfession::Toolsmith: outCount = 8; return toolsmithSpecs;
case VillagerProfession::Butcher: outCount = 8; return butcherSpecs;
case VillagerProfession::Leatherworker: outCount = 8; return leatherworkerSpecs;
case VillagerProfession::Mason: outCount = 8; return masonSpecs;
default: break;
}
outCount = 0;
return nullptr;
}
int containerCount(const Container& container, ItemId item) {
int total = 0;
for (int i = 0; i < container.size(); ++i) {
const ItemSlot& slot = container.slot(i);
if (!slot.empty() && slot.item == item) total += slot.count;
}
return total;
}
bool containerHasRoom(const Container& container, ItemId item, int count) {
int capacity = 0;
int limit = maxStackSize(item);
for (int i = 0; i < container.size(); ++i) {
const ItemSlot& slot = container.slot(i);
if (slot.empty()) capacity += limit;
else if (slot.item == item && slot.count < limit) capacity += limit - slot.count;
if (capacity >= count) return true;
}
return capacity >= count;
}
} // namespace
const char* tradeTierName(TradeTier tier) {
switch (tier) {
case TradeTier::Novice: return "novice";
case TradeTier::Apprentice: return "apprentice";
case TradeTier::Journeyman: return "journeyman";
case TradeTier::Expert: return "expert";
case TradeTier::Master: return "master";
case TradeTier::Count: break;
}
return "unknown";
}
TradeTier tierForLevel(int level) {
int clamped = clampInt(level, 1, villagerMaxLevel);
return static_cast<TradeTier>(clamped - 1);
}
int levelForTier(TradeTier tier) { return static_cast<int>(tier) + 1; }
int PriceModifier::totalPercent() const {
int percent = reputationPercent + heroLevel * heroDiscountPercentPerLevel -
hitCount * hitSurchargePercent;
return clampInt(percent, -300, 90);
}
PriceModifier priceModifierFor(const ReputationView& reputation, int heroLevel, int hitCount) {
PriceModifier modifier;
modifier.reputationPercent = reputation.discountPercent();
modifier.heroLevel = heroLevel < 0 ? 0 : heroLevel;
modifier.hitCount = hitCount < 0 ? 0 : hitCount;
return modifier;
}
int villagerHitCount(const GossipLedger& gossip) {
return gossip.valueOf(GossipKind::MinorNegative) / gossipMinorNegativeGain;
}
int offerPrice(const TradeOffer& offer, const PriceModifier& modifier) {
int base = offer.basePrice();
int demandDelta = static_cast<int>(
std::lround(static_cast<double>(offer.demand) * offer.priceMultiplier * base));
if (demandDelta < 0) demandDelta = 0;
int raw = base + demandDelta;
int shift = static_cast<int>(
std::lround(static_cast<double>(raw) * modifier.totalPercent() / 100.0));
int adjusted = raw - shift;
return adjusted < 1 ? 1 : adjusted;
}
bool TradeOfferList::unlocked(int index, int level) const {
if (index < 0 || index >= size()) return false;
return static_cast<int>(offers[index].tier) < clampInt(level, 1, villagerMaxLevel);
}
int TradeOfferList::unlockedCount(int level) const {
int count = 0;
for (int i = 0; i < size(); ++i)
if (unlocked(i, level)) ++count;
return count;
}
int TradeOfferList::countForTier(TradeTier tier) const {
int count = 0;
for (const TradeOffer& offer : offers)
if (offer.tier == tier) ++count;
return count;
}
bool TradeOfferList::allExhausted(int level) const {
int reachable = 0;
for (int i = 0; i < size(); ++i) {
if (!unlocked(i, level)) continue;
++reachable;
if (!offers[i].exhausted()) return false;
}
return reachable > 0;
}
TradeOfferList buildTradeOffers(VillagerProfession profession, uint32_t seed) {
TradeOfferList list;
list.profession = profession;
int specCount = 0;
const OfferSpec* specs = specsForProfession(profession, specCount);
if (specs == nullptr) return list;
uint32_t rng = seed ^ (static_cast<uint32_t>(profession) * 0x9e3779b9u);
for (int i = 0; i < specCount; ++i) {
const OfferSpec& spec = specs[i];
TradeOffer offer;
offer.tier = static_cast<TradeTier>(spec.tier);
offer.inputA = spec.inputA;
offer.inputACount = spec.inputACount;
offer.inputB = spec.inputB;
offer.inputBCount = spec.inputBCount;
offer.output = spec.output;
offer.outputCount = spec.outputCount;
offer.maxUses = spec.maxUses;
offer.experienceReward = spec.experience;
offer.priceMultiplier = spec.inputA == ItemId::EmeraldGem ? 0.05f : 0.02f;
uint32_t draw = nextRandom(rng);
if (offer.inputACount >= 6) {
offer.inputACount += static_cast<int>(draw % 5u) - 2;
} else if (offer.outputCount >= 3) {
offer.outputCount += static_cast<int>(draw % 3u) - 1;
}
list.offers.push_back(offer);
}
return list;
}
bool restockOffers(TradeOfferList& offers) {
bool changed = false;
for (TradeOffer& offer : offers.offers) {
if (offer.uses > 0) {
offer.uses = 0;
changed = true;
}
int faded = offer.demand - tradeDemandDecayPerRestock;
if (faded < 0) faded = 0;
if (faded != offer.demand) {
offer.demand = faded;
changed = true;
}
}
return changed;
}
bool tickVillagerRestock(Villager& villager, TradeOfferList& offers, const TimeOfDay& clock) {
long long day = clock.dayCount();
if (villager.lastRestockDay != day) {
villager.lastRestockDay = day;
villager.restocksToday = 0;
}
if (!villager.alive || villager.restocksToday >= tradeRestocksPerDay) return false;
if (villager.activity != VillagerActivity::Work ||
villager.state != VillagerState::AtTarget) return false;
bool anyUsed = false;
for (const TradeOffer& offer : offers.offers)
if (offer.uses > 0) anyUsed = true;
if (!anyUsed) return false;
restockOffers(offers);
++villager.restocksToday;
return true;
}
const char* tradeResultReasonName(TradeResult::Reason reason) {
switch (reason) {
case TradeResult::Reason::Ok: return "ok";
case TradeResult::Reason::NoSuchOffer: return "noSuchOffer";
case TradeResult::Reason::VillagerUnavailable: return "villagerUnavailable";
case TradeResult::Reason::LevelLocked: return "levelLocked";
case TradeResult::Reason::OfferExhausted: return "offerExhausted";
case TradeResult::Reason::MissingInputA: return "missingInputA";
case TradeResult::Reason::MissingInputB: return "missingInputB";
case TradeResult::Reason::NoRoom: return "noRoom";
case TradeResult::Reason::Count: break;
}
return "unknown";
}
TradeSession::TradeSession(Villager& villager, TradeOfferList& offers, Container& customerInventory)
: villager(villager), offers(offers), inventory(customerInventory) {}
void TradeSession::setPriceModifier(const PriceModifier& newModifier) { modifier = newModifier; }
void TradeSession::refreshPrices(const ReputationView& reputation, int heroLevel) {
modifier = priceModifierFor(reputation, heroLevel, villagerHitCount(villager.gossip));
}
int TradeSession::priceOf(int index) const {
if (index < 0 || index >= offers.size()) return 0;
return offerPrice(offers.at(index), modifier);
}
bool TradeSession::available(int index) const {
if (index < 0 || index >= offers.size()) return false;
if (!villager.alive || !villager.employed()) return false;
if (!offers.unlocked(index, villager.level)) return false;
return !offers.at(index).exhausted();
}
bool TradeSession::canAfford(int index) const {
if (!available(index)) return false;
const TradeOffer& offer = offers.at(index);
if (containerCount(inventory, offer.inputA) < priceOf(index)) return false;
if (offer.hasSecondaryInput() && containerCount(inventory, offer.inputB) < offer.inputBCount)
return false;
return true;
}
TradeResult TradeSession::trade(int index) {
TradeResult outcome;
if (index < 0 || index >= offers.size()) {
outcome.reason = TradeResult::Reason::NoSuchOffer;
return outcome;
}
if (!villager.alive || !villager.employed()) {
outcome.reason = TradeResult::Reason::VillagerUnavailable;
return outcome;
}
if (!offers.unlocked(index, villager.level)) {
outcome.reason = TradeResult::Reason::LevelLocked;
return outcome;
}
TradeOffer& offer = offers.at(index);
if (offer.exhausted()) {
outcome.reason = TradeResult::Reason::OfferExhausted;
return outcome;
}
int price = offerPrice(offer, modifier);
if (containerCount(inventory, offer.inputA) < price) {
outcome.reason = TradeResult::Reason::MissingInputA;
return outcome;
}
if (offer.hasSecondaryInput() && containerCount(inventory, offer.inputB) < offer.inputBCount) {
outcome.reason = TradeResult::Reason::MissingInputB;
return outcome;
}
if (!containerHasRoom(inventory, offer.output, offer.outputCount)) {
outcome.reason = TradeResult::Reason::NoRoom;
return outcome;
}
inventory.extract(offer.inputA, price);
if (offer.hasSecondaryInput()) inventory.extract(offer.inputB, offer.inputBCount);
inventory.insert(offer.output, offer.outputCount);
++offer.uses;
offer.demand = clampInt(offer.demand + tradeDemandPerUse, 0, tradeDemandCap);
outcome.success = true;
outcome.reason = TradeResult::Reason::Ok;
outcome.pricePaid = price;
outcome.secondaryPaid = offer.hasSecondaryInput() ? offer.inputBCount : 0;
outcome.received = offer.output;
outcome.receivedCount = offer.outputCount;
outcome.experienceGained = offer.experienceReward;
outcome.leveledUp = grantVillagerExperience(villager, offer.experienceReward);
villager.gossip.add(GossipKind::Trading, gossipTradingGain);
++completed;
if (offer.inputA == ItemId::EmeraldGem) spent += price;
return outcome;
}
} // namespace vf