From 59af42843db422e70e00d2704f1b6a2786dc9024 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Wed, 2 Sep 2026 18:56:12 +0200 Subject: [PATCH] feat(vf): add GossipEntry and Tuning --- core/include/vf/Gossip.h | 104 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 core/include/vf/Gossip.h diff --git a/core/include/vf/Gossip.h b/core/include/vf/Gossip.h new file mode 100644 index 0000000..2db047e --- /dev/null +++ b/core/include/vf/Gossip.h @@ -0,0 +1,104 @@ +#pragma once + +#include +#include + +namespace vf { + +// What a village remembers about one player. Every kind accumulates its own +// saturating counter; the signed sum of those counters weighted per kind is the +// reputation that trade prices and iron golem hostility read. +enum class GossipKind : uint8_t { + MajorPositive = 0, // saved the village from a raid + MinorPositive = 1, // helped a villager + Trading = 2, // completed a trade + MinorNegative = 3, // hit a villager + MajorNegative = 4, // killed a villager + Count, +}; + +const char* gossipKindName(GossipKind kind); + +// Value granted by one occurrence of each kind. +constexpr int gossipMajorPositiveGain = 20; +constexpr int gossipMinorPositiveGain = 25; +constexpr int gossipTradingGain = 2; +constexpr int gossipMinorNegativeGain = 25; +constexpr int gossipMajorNegativeGain = 25; + +// Reputation at or below which iron golems turn on the player. +constexpr int golemHostileReputation = -100; + +struct GossipEntry { + // Per-kind rules: how much a point of this kind is worth, where it + // saturates, how fast it fades per in-game day, and how far it travels when + // two villagers meet. A zero sharePercent means the kind never spreads. + struct Tuning { + int weight = 1; + int maxValue = 100; + int decayPerDay = 10; + int shareCap = 0; + int sharePercent = 0; + + bool shareable() const { return sharePercent > 0 && shareCap > 0; } + }; + + GossipKind kind = GossipKind::Trading; + int value = 0; +}; + +const GossipEntry::Tuning& gossipTuning(GossipKind kind); + +// One villager's memory of the player. Values only ever saturate at their per +// kind cap, so no single event source can dominate the sum without bound. +class GossipLedger { +public: + // Adds `amount` points of `kind`, clamped into [0, maxValue]. Returns the + // amount actually stored. + int add(GossipKind kind, int amount); + + int valueOf(GossipKind kind) const; + + // Signed weighted sum across every kind. + int reputation() const; + + // Fades every kind by its own per-day rate. Returns true when anything moved. + bool decayDays(int days); + + // Spreads shareable kinds into `receiver`. The receiver can never be pushed + // past the kind's share cap by gossip alone, so repeated meetings converge + // instead of growing without limit. Returns true when the receiver changed. + bool shareInto(GossipLedger& receiver) const; + + // Non-zero entries in GossipKind order. + std::vector entries() const; + + int entryCount() const; + bool empty() const; + void forget(GossipKind kind); + void clear(); + +private: + int values[static_cast(GossipKind::Count)] = {}; +}; + +// Reputation resolved for one villager against the backdrop of its village: +// `personal` drives that villager's prices, `village` drives golem hostility. +struct ReputationView { + int personal = 0; + int village = 0; + int villagerCount = 0; + int majorNegative = 0; + + // Positive lowers prices, negative raises them. Clamped to keep a single + // extreme ledger from making goods free. + int discountPercent() const; + + bool golemHostile() const { return village <= golemHostileReputation; } + bool friendly() const { return personal > 0; } +}; + +ReputationView buildReputation(const GossipLedger& personal, + const std::vector& village); + +} // namespace vf