diff --git a/core/include/vf/Behavior.h b/core/include/vf/Behavior.h new file mode 100644 index 0000000..f846de8 --- /dev/null +++ b/core/include/vf/Behavior.h @@ -0,0 +1,156 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "Item.h" +#include "Math.h" +#include "Mob.h" +#include "Pathfind.h" +#include "World.h" + +namespace vf { + +// Behaviour trees layered on top of the reactive creature AI in Mob.h. A node +// answers with one of three verdicts every tick and the tree remembers which +// node was left Running, so the next tick resumes there instead of restarting +// the whole traversal. +enum class BehaviorStatus : uint8_t { + Failure = 0, + Success = 1, + Running = 2, +}; + +const char* behaviorStatusName(BehaviorStatus status); + +// Traversal is recursive, so a malformed tree would blow the stack. Trees are +// measured when the root is installed and refuse to run past this. +constexpr int maxBehaviorDepth = 48; + +// Deterministic xorshift shared by every module built on this one. No clock, +// no global state: the caller owns the stream. +uint32_t behaviorNextRandom(uint32_t& state); +float behaviorUnitFloat(uint32_t& state); +int behaviorRandomRange(uint32_t& state, int minInclusive, int maxInclusive); + +Vec3i behaviorCellOf(const Vec3& position); +Vec3 behaviorCellCenter(const Vec3i& cell); +float behaviorHorizontalDistance(const Vec3& a, const Vec3& b); + +// Typed shared storage between nodes. One map per value type rather than a +// variant, so a key written as a route can never be read back as a number. +// Ordered maps keep iteration stable for anyone dumping the contents. +class Blackboard { +public: + void setNumber(const std::string& key, float value); + void setCount(const std::string& key, int value); + void setFlag(const std::string& key, bool value); + void setPoint(const std::string& key, const Vec3& value); + void setCell(const std::string& key, const Vec3i& value); + void setRoute(const std::string& key, std::vector cells); + + bool hasNumber(const std::string& key) const; + bool hasCount(const std::string& key) const; + bool hasFlag(const std::string& key) const; + bool hasPoint(const std::string& key) const; + bool hasCell(const std::string& key) const; + bool hasRoute(const std::string& key) const; + + float number(const std::string& key, float fallback = 0.0f) const; + int count(const std::string& key, int fallback = 0) const; + bool flag(const std::string& key, bool fallback = false) const; + Vec3 point(const std::string& key, const Vec3& fallback = {}) const; + Vec3i cell(const std::string& key, const Vec3i& fallback = {}) const; + const std::vector* route(const std::string& key) const; + + void eraseNumber(const std::string& key); + void eraseRoute(const std::string& key); + void clear(); + std::size_t entryCount() const; + +private: + std::map numbers; + std::map counts; + std::map flags; + std::map points; + std::map cells; + std::map> routes; +}; + +// Tunables the stock trees read. Kept out of the node code so a caller can +// retune a behaviour without rebuilding the tree. +struct BehaviorTuning { + float arriveEpsilon = 0.35f; + float fleeRadius = 8.0f; + float followFoodRadius = 8.0f; + float breedRadius = 3.0f; + float homeReturnRadius = 1.2f; + int breedCooldownTicks = 120; + int grazeDurationTicks = 40; + int attackCooldownTicks = 20; + int repathIntervalTicks = 10; + int shadeSearchRadius = 6; + int shadeRunTicks = 120; + int sunLightThreshold = 12; + int shelterHoldTicks = 60; +}; + +// The mob the stock trees steer. Movement is expressed in blocks per tick so +// the whole simulation stays integer-tick driven and reproducible. +struct BehaviorAgent { + Species species = Species::Zombie; + Vec3 position; + Vec3 homePosition; + Vec3 heading{1.0f, 0.0f, 0.0f}; + float health = 20.0f; + float maxHealth = 20.0f; + float moveStep = 0.25f; + int grazeTicks = 0; + int attacksLanded = 0; + int breedCount = 0; + int shelterTicks = 0; + bool sheltered = false; + std::vector route; + int routeCursor = 0; + long long routePlannedTick = -4096; + Vec3i routeGoal; + uint32_t rng = 0x9e3779b9u; + + Vec3i cell() const { return behaviorCellOf(position); } + bool routeFinished() const { return routeCursor >= static_cast(route.size()); } +}; + +// Everything a node may read or write. `agent` is null for trees that drive +// something other than a mob (the boss fights capture their own state in the +// node lambdas instead). +struct BehaviorContext { + const World* world = nullptr; + BehaviorAgent* agent = nullptr; + Blackboard blackboard; + BehaviorTuning tuning; + PathConfig path; + + long long tick = 0; + uint32_t rng = 0x2545f491u; + + Vec3 playerPosition; + bool playerPresent = false; + ItemId playerHeldItem = ItemId::None; + + Vec3 matePosition; + bool matePresent = false; + + int skyLight = 15; + bool daylight = true; + + bool traceEnabled = false; + std::vector trace; +}; + +using BehaviorPredicate = std::function; +using BehaviorAction = std::function; +