#include "vf/Trade.h" #include 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(clamped - 1); } int levelForTier(TradeTier tier) { return static_cast(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( std::lround(static_cast(offer.demand) * offer.priceMultiplier * base)); if (demandDelta < 0) demandDelta = 0; int raw = base + demandDelta; int shift = static_cast( std::lround(static_cast(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(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(profession) * 0x9e3779b9u); for (int i = 0; i < specCount; ++i) { const OfferSpec& spec = specs[i]; TradeOffer offer; offer.tier = static_cast(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(draw % 5u) - 2; } else if (offer.outputCount >= 3) { offer.outputCount += static_cast(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