From 0953fbfa3f06436dbc148e86744223410d814234 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Fri, 4 Sep 2026 14:04:40 +0200 Subject: [PATCH] feat(vf): add Raider and Stats --- core/include/vf/Raid.h | 200 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 200 insertions(+) create mode 100644 core/include/vf/Raid.h diff --git a/core/include/vf/Raid.h b/core/include/vf/Raid.h new file mode 100644 index 0000000..74ceb52 --- /dev/null +++ b/core/include/vf/Raid.h @@ -0,0 +1,200 @@ +#pragma once + +#include +#include +#include "Effect.h" +#include "Gossip.h" +#include "Math.h" +#include "Pathfind.h" +#include "TimeOfDay.h" +#include "Villager.h" +#include "World.h" + +namespace vf { + +enum class RaiderKind : uint8_t { + Raider = 0, + Vindicator, + Witch, + Ravager, + Count, +}; + +const char* raiderKindName(RaiderKind kind); + +// One attacker in a raid wave. Movement reuses the shared A* from Pathfind.h: +// the raider walks its route to the village centre and fights whatever stands +// between it and the bell. +struct Raider { + struct Stats { + float maxHealth = 24.0f; + float damage = 3.0f; + float speed = 2.6f; + float attackRange = 1.8f; + int attackIntervalTicks = 20; + }; + + int id = 0; + RaiderKind kind = RaiderKind::Raider; + int wave = 0; + Vec3 position; + float health = 24.0f; + std::vector path; + int pathCursor = 0; + int attackCooldown = 0; + bool alive = true; + + Vec3i cell() const; +}; + +const Raider::Stats& raiderStats(RaiderKind kind); + +// The composition of a single wave. Every count grows with the wave index, the +// difficulty and the strength of the omen that triggered the raid. +struct RaidWave { + int index = 0; + int raiders = 0; + int vindicators = 0; + int witches = 0; + int ravagers = 0; + + int countOf(RaiderKind kind) const; + int total() const { return raiders + vindicators + witches + ravagers; } +}; + +constexpr int raidDifficultyEasy = 1; +constexpr int raidDifficultyNormal = 2; +constexpr int raidDifficultyHard = 3; + +int raidWaveCount(int difficulty); +RaidWave planRaidWave(int waveIndex, int badOmenLevel, int difficulty); + +constexpr int badOmenDurationTicks = 100 * ticksPerSecond; +constexpr int heroDurationTicks = 40 * 60 * ticksPerSecond; +constexpr int badOmenMaxLevel = 5; + +// The two player-side effects this module owns: the bad omen that arms a raid +// and the hero of the village discount handed out for winning one. +struct OmenState { + int level = 0; + int ticksRemaining = 0; + int heroLevel = 0; + int heroTicksRemaining = 0; + + bool hasBadOmen() const { return level > 0 && ticksRemaining > 0; } + bool isHero() const { return heroLevel > 0 && heroTicksRemaining > 0; } +}; + +void applyBadOmen(OmenState& omen, int level, int durationTicks = badOmenDurationTicks); +void grantHeroOfTheVillage(OmenState& omen, int level, int durationTicks = heroDurationTicks); +void clearBadOmen(OmenState& omen); +void tickOmen(OmenState& omen, int ticks); + +enum class RaidState : uint8_t { + Inactive = 0, + Spawning, + Fighting, + Victory, + Defeat, + Count, +}; + +const char* raidStateName(RaidState state); + +// Iron golem production during a raid. The village fields more golems the more +// villagers it still has, and turns them on the player once the village-wide +// reputation drops far enough. +struct GolemSpawner { + int intervalTicks = 120; + int cooldownTicks = 0; + int golemsAlive = 0; + int spawnedTotal = 0; + int attackIntervalTicks = 20; + int attackCooldown = 0; + float damagePerAttack = 7.0f; + float maxHealth = 100.0f; + bool hostileToPlayer = false; +}; + +// A hamlet too small to be worth defending fields no golems at all. +constexpr int golemMinimumVillagers = 3; + +int golemCapFor(int villagerCount); + +// Counts the cooldown down and reports the ticks on which a golem is due. +bool tickGolemSpawner(GolemSpawner& spawner, int villagerCount, int ticks); +void updateGolemHostility(GolemSpawner& spawner, const ReputationView& reputation); + +constexpr int raidWaveDelayTicks = 100; +constexpr int raidWaveTimeoutTicks = 2400; +constexpr float raidSpawnRadius = 24.0f; +constexpr float raidGolemEngageRadius = 7.0f; + +// Drives a whole raid: wave planning, deterministic raider spawning, the walk +// to the village centre, golem production, villager panic and the gossip the +// outcome leaves behind. Every decision is a function of the seed. +class RaidManager { +public: + explicit RaidManager(const Vec3i& center = {}, uint32_t seed = 1u, + int difficulty = raidDifficultyNormal); + + // Arms the raid when the player carries a bad omen into the village. The + // omen is consumed. + bool tryStart(OmenState& omen, const Vec3& playerPos, float villageRadius); + + // Advances `ticks` whole game ticks: raiders, golems, villagers and wave + // progression. + void tick(const World& world, std::vector& villagers, PoiRegistry& pois, + OmenState& omen, const VillagerSchedule& schedule, const TimeOfDay& clock, int ticks, + const PathConfig& config = {}); + + RaidState state() const { return raidState; } + bool active() const { return raidState == RaidState::Spawning || raidState == RaidState::Fighting; } + bool villageThreatened() const { return active(); } + + int waveIndex() const { return waveIdx; } + int waveCount() const { return waves; } + const RaidWave& wave() const { return currentWave; } + + const std::vector& raiders() const { return attackers; } + int livingRaiders() const; + int totalSpawned() const { return spawned; } + int villagersLost() const { return lostVillagers; } + int badOmenLevel() const { return omenLevel; } + int elapsedTicks() const { return elapsed; } + const GolemSpawner& golems() const { return spawner; } + const Vec3i& center() const { return villageCenter; } + + // Returns true when the hit killed the raider. + bool damageRaider(int index, float amount); + +private: + void spawnWave(const World& world, const PathConfig& config); + void moveRaiders(const std::vector& villagers); + void resolveRaiderAttacks(std::vector& villagers, PoiRegistry& pois); + void resolveGolems(int livingVillagers); + void finishVictory(std::vector& villagers, OmenState& omen); + void stepOnce(const World& world, std::vector& villagers, PoiRegistry& pois, + OmenState& omen, const VillagerSchedule& schedule, const TimeOfDay& clock, + const PathConfig& config); + + Vec3i villageCenter; + Vec3i centerStand; + uint32_t seed = 1u; + int difficulty = raidDifficultyNormal; + RaidState raidState = RaidState::Inactive; + RaidWave currentWave; + std::vector attackers; + std::vector golemHealth; + GolemSpawner spawner; + int waves = 0; + int waveIdx = -1; + int waveTicks = 0; + int clearedTicks = 0; + int elapsed = 0; + int spawned = 0; + int lostVillagers = 0; + int omenLevel = 0; +}; + +} // namespace vf