feat(vf): add PriceModifier and TradeOffer
This commit is contained in:
1 parent
e2042456b4
commit
937e85dee4
1 file changed
+155
@@ -0,0 +1,155 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
#include "Container.h"
|
||||
#include "Gossip.h"
|
||||
#include "Item.h"
|
||||
#include "TimeOfDay.h"
|
||||
#include "Villager.h"
|
||||
|
||||
namespace vf {
|
||||
|
||||
// Trade unlock band. A villager of level N has tiers 0..N-1 unlocked.
|
||||
enum class TradeTier : uint8_t {
|
||||
Novice = 0,
|
||||
Apprentice,
|
||||
Journeyman,
|
||||
Expert,
|
||||
Master,
|
||||
Count,
|
||||
};
|
||||
|
||||
const char* tradeTierName(TradeTier tier);
|
||||
TradeTier tierForLevel(int level);
|
||||
int levelForTier(TradeTier tier);
|
||||
|
||||
constexpr int heroDiscountPercentPerLevel = 30;
|
||||
constexpr int hitSurchargePercent = 20;
|
||||
|
||||
// Everything outside the offer itself that bends its price: how the villager
|
||||
// feels about the customer, the hero of the village effect, and how many
|
||||
// unforgiven hits the customer landed on villagers.
|
||||
struct PriceModifier {
|
||||
int reputationPercent = 0;
|
||||
int heroLevel = 0;
|
||||
int hitCount = 0;
|
||||
|
||||
// Net discount in percent. Positive makes goods cheaper.
|
||||
int totalPercent() const;
|
||||
};
|
||||
|
||||
PriceModifier priceModifierFor(const ReputationView& reputation, int heroLevel, int hitCount);
|
||||
int villagerHitCount(const GossipLedger& gossip);
|
||||
|
||||
constexpr int tradeDemandCap = 64;
|
||||
constexpr int tradeDemandPerUse = 1;
|
||||
constexpr int tradeDemandDecayPerRestock = 2;
|
||||
constexpr int tradeRestocksPerDay = 2;
|
||||
|
||||
// One entry in a villager's trade list: up to two inputs for one output, a use
|
||||
// budget that a restock refills, and a demand counter that raises the price the
|
||||
// more the offer is taken.
|
||||
struct TradeOffer {
|
||||
ItemId inputA = ItemId::None;
|
||||
int inputACount = 1;
|
||||
ItemId inputB = ItemId::None;
|
||||
int inputBCount = 0;
|
||||
ItemId output = ItemId::None;
|
||||
int outputCount = 1;
|
||||
int maxUses = 12;
|
||||
int uses = 0;
|
||||
int demand = 0;
|
||||
int experienceReward = villagerExperiencePerTrade;
|
||||
float priceMultiplier = 0.05f;
|
||||
TradeTier tier = TradeTier::Novice;
|
||||
|
||||
bool exhausted() const { return uses >= maxUses; }
|
||||
int basePrice() const { return inputACount; }
|
||||
bool hasSecondaryInput() const { return inputB != ItemId::None && inputBCount > 0; }
|
||||
};
|
||||
|
||||
// Base price plus demand, then the modifier percentage. Never drops below one.
|
||||
int offerPrice(const TradeOffer& offer, const PriceModifier& modifier);
|
||||
|
||||
// A villager's whole trade table across all five tiers. Which entries are
|
||||
// reachable depends on the villager's current level.
|
||||
struct TradeOfferList {
|
||||
std::vector<TradeOffer> offers;
|
||||
VillagerProfession profession = VillagerProfession::Unemployed;
|
||||
|
||||
int size() const { return static_cast<int>(offers.size()); }
|
||||
const TradeOffer& at(int index) const { return offers[index]; }
|
||||
TradeOffer& at(int index) { return offers[index]; }
|
||||
bool unlocked(int index, int level) const;
|
||||
int unlockedCount(int level) const;
|
||||
int countForTier(TradeTier tier) const;
|
||||
bool allExhausted(int level) const;
|
||||
};
|
||||
|
||||
// Deterministic per profession and seed: the same pair always yields the same
|
||||
// table, with per-offer count jitter drawn from the seed.
|
||||
TradeOfferList buildTradeOffers(VillagerProfession profession, uint32_t seed);
|
||||
|
||||
// Refills use budgets and lets demand fade. Returns true when anything changed.
|
||||
bool restockOffers(TradeOfferList& offers);
|
||||
|
||||
// Restocks at most twice per day, and only while the villager is standing at
|
||||
// its own workstation. Returns true on the tick a restock fires.
|
||||
bool tickVillagerRestock(Villager& villager, TradeOfferList& offers, const TimeOfDay& clock);
|
||||
|
||||
struct TradeResult {
|
||||
enum class Reason : uint8_t {
|
||||
Ok = 0,
|
||||
NoSuchOffer,
|
||||
VillagerUnavailable,
|
||||
LevelLocked,
|
||||
OfferExhausted,
|
||||
MissingInputA,
|
||||
MissingInputB,
|
||||
NoRoom,
|
||||
Count,
|
||||
};
|
||||
|
||||
Reason reason = Reason::Ok;
|
||||
bool success = false;
|
||||
int pricePaid = 0;
|
||||
int secondaryPaid = 0;
|
||||
ItemId received = ItemId::None;
|
||||
int receivedCount = 0;
|
||||
int experienceGained = 0;
|
||||
bool leveledUp = false;
|
||||
};
|
||||
|
||||
const char* tradeResultReasonName(TradeResult::Reason reason);
|
||||
|
||||
// Binds a villager, its offer table and the customer's inventory for the
|
||||
// duration of a trading screen. Every completed trade moves items, raises
|
||||
// demand, feeds the villager experience and leaves a Trading gossip entry.
|
||||
class TradeSession {
|
||||
public:
|
||||
TradeSession(Villager& villager, TradeOfferList& offers, Container& customerInventory);
|
||||
|
||||
void setPriceModifier(const PriceModifier& modifier);
|
||||
void refreshPrices(const ReputationView& reputation, int heroLevel);
|
||||
const PriceModifier& priceModifier() const { return modifier; }
|
||||
|
||||
int priceOf(int index) const;
|
||||
bool available(int index) const;
|
||||
bool canAfford(int index) const;
|
||||
|
||||
TradeResult trade(int index);
|
||||
|
||||
int tradesCompleted() const { return completed; }
|
||||
int emeraldsSpent() const { return spent; }
|
||||
|
||||
private:
|
||||
Villager& villager;
|
||||
TradeOfferList& offers;
|
||||
Container& inventory;
|
||||
PriceModifier modifier;
|
||||
int completed = 0;
|
||||
int spent = 0;
|
||||
};
|
||||
|
||||
} // namespace vf
|
||||
Reference in new issue
Block a user