feat: add restockOffers and tickVillagerRestock to Trade

This commit is contained in:
dualfroz committed 2026-09-09 21:01:32 +02:00
1 parent e8d9fe3a04
commit 1c229a18f2
1 file changed
+167
+167
View File
@@ -305,3 +305,170 @@ TradeOfferList buildTradeOffers(VillagerProfession profession, uint32_t seed) {
const OfferSpec* specs = specsForProfession(profession, specCount); const OfferSpec* specs = specsForProfession(profession, specCount);
if (specs == nullptr) return list; 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