diff --git a/core/src/Trade.cpp b/core/src/Trade.cpp index ca0c054..9238cc3 100644 --- a/core/src/Trade.cpp +++ b/core/src/Trade.cpp @@ -305,3 +305,170 @@ TradeOfferList buildTradeOffers(VillagerProfession profession, uint32_t seed) { 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