637 lines
24 KiB
C++
637 lines
24 KiB
C++
#include "vf/Villager.h"
|
|
|
|
#include <cmath>
|
|
|
|
namespace vf {
|
|
|
|
namespace {
|
|
|
|
uint32_t nextRandom(uint32_t& state) {
|
|
state += 0x9e3779b9u;
|
|
uint32_t z = state;
|
|
z = (z ^ (z >> 16)) * 0x85ebca6bu;
|
|
z = (z ^ (z >> 13)) * 0xc2b2ae35u;
|
|
return z ^ (z >> 16);
|
|
}
|
|
|
|
Vec3 cellCentre(const Vec3i& cell) {
|
|
return {static_cast<float>(cell.x) + 0.5f, static_cast<float>(cell.y),
|
|
static_cast<float>(cell.z) + 0.5f};
|
|
}
|
|
|
|
int squaredDistance(const Vec3i& a, const Vec3i& b) {
|
|
int dx = a.x - b.x;
|
|
int dy = a.y - b.y;
|
|
int dz = a.z - b.z;
|
|
return dx * dx + dy * dy + dz * dz;
|
|
}
|
|
|
|
const int levelThresholds[villagerMaxLevel] = {0, 10, 70, 150, 250};
|
|
|
|
const int horizontalOffsets[4][2] = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
|
|
|
|
// A point of interest is worked from the cell on top of it when it is solid,
|
|
// from its own cell when it is not, or from any walkable neighbour otherwise.
|
|
bool resolveStandCell(const World& world, const Vec3i& pos, Block block, const PathConfig& config,
|
|
Vec3i& outStand) {
|
|
if (!isSolid(block) && isStandable(world, pos, config)) {
|
|
outStand = pos;
|
|
return true;
|
|
}
|
|
Vec3i above{pos.x, pos.y + 1, pos.z};
|
|
if (isStandable(world, above, config)) {
|
|
outStand = above;
|
|
return true;
|
|
}
|
|
for (int level = 0; level <= 1; ++level) {
|
|
for (const auto& offset : horizontalOffsets) {
|
|
Vec3i candidate{pos.x + offset[0], pos.y + level, pos.z + offset[1]};
|
|
if (isStandable(world, candidate, config)) {
|
|
outStand = candidate;
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
outStand = above;
|
|
return false;
|
|
}
|
|
|
|
bool poiBlock(Block block) {
|
|
return block == Block::Bed || block == Block::Bell || isWorkstationBlock(block);
|
|
}
|
|
|
|
// Nothing under the feet any more, usually because the workstation the villager
|
|
// was standing on got mined out: drop it onto the first solid ground below and
|
|
// throw away the route it was walking.
|
|
bool settleVillager(const World& world, Villager& villager, const PathConfig& config) {
|
|
Vec3i cell = villager.cell();
|
|
if (isStandable(world, cell, config)) return false;
|
|
for (int drop = 1; drop <= 8; ++drop) {
|
|
Vec3i below{cell.x, cell.y - drop, cell.z};
|
|
if (!isStandable(world, below, config)) continue;
|
|
villager.body.position = cellCentre(below);
|
|
villager.path.clear();
|
|
villager.pathCursor = 0;
|
|
villager.hasGoal = false;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool withinVillage(const Vec3i& cell, const Vec3i& center, int radius) {
|
|
return std::abs(cell.x - center.x) <= radius && std::abs(cell.z - center.z) <= radius &&
|
|
std::abs(cell.y - center.y) <= radius;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
const char* villagerProfessionName(VillagerProfession profession) {
|
|
switch (profession) {
|
|
case VillagerProfession::Unemployed: return "unemployed";
|
|
case VillagerProfession::Nitwit: return "nitwit";
|
|
case VillagerProfession::Farmer: return "farmer";
|
|
case VillagerProfession::Fisherman: return "fisherman";
|
|
case VillagerProfession::Shepherd: return "shepherd";
|
|
case VillagerProfession::Fletcher: return "fletcher";
|
|
case VillagerProfession::Librarian: return "librarian";
|
|
case VillagerProfession::Cartographer: return "cartographer";
|
|
case VillagerProfession::Cleric: return "cleric";
|
|
case VillagerProfession::Armorer: return "armorer";
|
|
case VillagerProfession::Weaponsmith: return "weaponsmith";
|
|
case VillagerProfession::Toolsmith: return "toolsmith";
|
|
case VillagerProfession::Butcher: return "butcher";
|
|
case VillagerProfession::Leatherworker: return "leatherworker";
|
|
case VillagerProfession::Mason: return "mason";
|
|
case VillagerProfession::Count: break;
|
|
}
|
|
return "unknown";
|
|
}
|
|
|
|
bool professionEmployable(VillagerProfession profession) {
|
|
return profession != VillagerProfession::Unemployed && profession != VillagerProfession::Nitwit;
|
|
}
|
|
|
|
Block workstationForProfession(VillagerProfession profession) {
|
|
switch (profession) {
|
|
case VillagerProfession::Farmer: return Block::Composter;
|
|
case VillagerProfession::Fisherman: return Block::Barrel;
|
|
case VillagerProfession::Shepherd: return Block::Loom;
|
|
case VillagerProfession::Fletcher: return Block::CraftingTable;
|
|
case VillagerProfession::Librarian: return Block::Lectern;
|
|
case VillagerProfession::Cartographer: return Block::CartographyTable;
|
|
case VillagerProfession::Cleric: return Block::BrewingStand;
|
|
case VillagerProfession::Armorer: return Block::Furnace;
|
|
case VillagerProfession::Weaponsmith: return Block::Grindstone;
|
|
case VillagerProfession::Toolsmith: return Block::SmithingTable;
|
|
case VillagerProfession::Butcher: return Block::FurnaceLit;
|
|
case VillagerProfession::Leatherworker: return Block::Cauldron;
|
|
case VillagerProfession::Mason: return Block::Stonecutter;
|
|
default: break;
|
|
}
|
|
return Block::Air;
|
|
}
|
|
|
|
VillagerProfession professionForWorkstation(Block block) {
|
|
switch (block) {
|
|
case Block::Composter: return VillagerProfession::Farmer;
|
|
case Block::Barrel: return VillagerProfession::Fisherman;
|
|
case Block::Loom: return VillagerProfession::Shepherd;
|
|
case Block::CraftingTable: return VillagerProfession::Fletcher;
|
|
case Block::Lectern: return VillagerProfession::Librarian;
|
|
case Block::CartographyTable: return VillagerProfession::Cartographer;
|
|
case Block::BrewingStand: return VillagerProfession::Cleric;
|
|
case Block::Furnace: return VillagerProfession::Armorer;
|
|
case Block::Grindstone: return VillagerProfession::Weaponsmith;
|
|
case Block::SmithingTable: return VillagerProfession::Toolsmith;
|
|
case Block::FurnaceLit: return VillagerProfession::Butcher;
|
|
case Block::Cauldron: return VillagerProfession::Leatherworker;
|
|
case Block::Stonecutter: return VillagerProfession::Mason;
|
|
default: break;
|
|
}
|
|
return VillagerProfession::Unemployed;
|
|
}
|
|
|
|
bool isWorkstationBlock(Block block) {
|
|
return professionForWorkstation(block) != VillagerProfession::Unemployed;
|
|
}
|
|
|
|
int villagerExperienceForLevel(int level) {
|
|
if (level <= 1) return 0;
|
|
if (level >= villagerMaxLevel) return levelThresholds[villagerMaxLevel - 1];
|
|
return levelThresholds[level - 1];
|
|
}
|
|
|
|
int villagerLevelForExperience(int experience) {
|
|
int level = 1;
|
|
for (int candidate = 2; candidate <= villagerMaxLevel; ++candidate)
|
|
if (experience >= levelThresholds[candidate - 1]) level = candidate;
|
|
return level;
|
|
}
|
|
|
|
const char* villagerActivityName(VillagerActivity activity) {
|
|
switch (activity) {
|
|
case VillagerActivity::Idle: return "idle";
|
|
case VillagerActivity::SeekWorkstation: return "seekWorkstation";
|
|
case VillagerActivity::Work: return "work";
|
|
case VillagerActivity::Meet: return "meet";
|
|
case VillagerActivity::Sleep: return "sleep";
|
|
case VillagerActivity::Panic: return "panic";
|
|
case VillagerActivity::Count: break;
|
|
}
|
|
return "unknown";
|
|
}
|
|
|
|
void PoiRegistry::scan(const World& world, const Vec3i& center, int radius, int verticalRadius,
|
|
const PathConfig& config) {
|
|
clear();
|
|
for (int x = center.x - radius; x <= center.x + radius; ++x) {
|
|
for (int y = center.y - verticalRadius; y <= center.y + verticalRadius; ++y) {
|
|
for (int z = center.z - radius; z <= center.z + radius; ++z) {
|
|
Block block = world.blockAt(x, y, z);
|
|
if (!poiBlock(block)) continue;
|
|
WorkstationPoi poi;
|
|
poi.pos = {x, y, z};
|
|
poi.block = block;
|
|
poi.profession = professionForWorkstation(block);
|
|
poi.valid = true;
|
|
poi.reachable = resolveStandCell(world, poi.pos, block, config, poi.standCell);
|
|
if (block == Block::Bell && bell < 0) bell = static_cast<int>(pois.size());
|
|
pois.push_back(poi);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
int PoiRegistry::indexAt(const Vec3i& pos) const {
|
|
for (std::size_t i = 0; i < pois.size(); ++i)
|
|
if (pois[i].valid && pois[i].pos == pos) return static_cast<int>(i);
|
|
return -1;
|
|
}
|
|
|
|
int PoiRegistry::workstationCount() const { return countPois(false, false); }
|
|
|
|
int PoiRegistry::bedCount() const { return countPois(true, false); }
|
|
|
|
int PoiRegistry::freeWorkstationCount() const { return countPois(false, true); }
|
|
|
|
int PoiRegistry::freeBedCount() const { return countPois(true, true); }
|
|
|
|
int PoiRegistry::countPois(bool beds, bool onlyFree) const {
|
|
int count = 0;
|
|
for (const WorkstationPoi& poi : pois) {
|
|
if (beds ? !poi.isBed() : !poi.isWorkstation()) continue;
|
|
if (onlyFree ? !poi.free() : !poi.valid) continue;
|
|
++count;
|
|
}
|
|
return count;
|
|
}
|
|
|
|
int PoiRegistry::nearestFree(const Vec3i& from, bool beds, VillagerProfession wanted) const {
|
|
int best = -1;
|
|
int bestDistance = 0;
|
|
for (std::size_t i = 0; i < pois.size(); ++i) {
|
|
const WorkstationPoi& poi = pois[i];
|
|
if (!poi.free() || !poi.reachable) continue;
|
|
if (beds ? !poi.isBed() : !poi.isWorkstation()) continue;
|
|
if (!beds && wanted != VillagerProfession::Unemployed && poi.profession != wanted) continue;
|
|
int distance = squaredDistance(poi.standCell, from);
|
|
if (best < 0 || distance < bestDistance) {
|
|
best = static_cast<int>(i);
|
|
bestDistance = distance;
|
|
}
|
|
}
|
|
return best;
|
|
}
|
|
|
|
int PoiRegistry::nearestFreeWorkstation(const Vec3i& from, VillagerProfession wanted) const {
|
|
return nearestFree(from, false, wanted);
|
|
}
|
|
|
|
int PoiRegistry::nearestFreeBed(const Vec3i& from) const {
|
|
return nearestFree(from, true, VillagerProfession::Unemployed);
|
|
}
|
|
|
|
bool PoiRegistry::claim(int index, int villagerId) {
|
|
if (index < 0 || index >= size()) return false;
|
|
WorkstationPoi& poi = pois[index];
|
|
if (!poi.valid || poi.ownerId >= 0) return false;
|
|
poi.ownerId = villagerId;
|
|
return true;
|
|
}
|
|
|
|
void PoiRegistry::release(int index) {
|
|
if (index < 0 || index >= size()) return;
|
|
pois[index].ownerId = -1;
|
|
}
|
|
|
|
void PoiRegistry::releaseOwner(int villagerId) {
|
|
for (WorkstationPoi& poi : pois)
|
|
if (poi.ownerId == villagerId) poi.ownerId = -1;
|
|
}
|
|
|
|
int PoiRegistry::revalidate(const World& world, const PathConfig& config) {
|
|
int lost = 0;
|
|
for (std::size_t i = 0; i < pois.size(); ++i) {
|
|
WorkstationPoi& poi = pois[i];
|
|
if (!poi.valid) continue;
|
|
if (world.blockAt(poi.pos.x, poi.pos.y, poi.pos.z) != poi.block) {
|
|
poi.valid = false;
|
|
poi.ownerId = -1;
|
|
if (bell == static_cast<int>(i)) bell = -1;
|
|
++lost;
|
|
continue;
|
|
}
|
|
poi.reachable = resolveStandCell(world, poi.pos, poi.block, config, poi.standCell);
|
|
}
|
|
return lost;
|
|
}
|
|
|
|
void PoiRegistry::clear() {
|
|
pois.clear();
|
|
bell = -1;
|
|
}
|
|
|
|
Vec3i Villager::cell() const {
|
|
return {static_cast<int>(std::floor(body.position.x)),
|
|
static_cast<int>(std::floor(body.position.y)),
|
|
static_cast<int>(std::floor(body.position.z))};
|
|
}
|
|
|
|
Villager makeVillager(int id, const Vec3& position, uint32_t seed) {
|
|
Villager villager;
|
|
villager.id = id;
|
|
villager.body.position = position;
|
|
villager.body.width = 0.6f;
|
|
villager.body.height = 1.95f;
|
|
villager.rng = seed ^ (static_cast<uint32_t>(id) * 0x27d4eb2du);
|
|
uint32_t draw = nextRandom(villager.rng);
|
|
if (draw % 8u == 0u) villager.profession = VillagerProfession::Nitwit;
|
|
return villager;
|
|
}
|
|
|
|
bool assignVillagerWorkstation(Villager& villager, PoiRegistry& pois) {
|
|
if (!villager.alive || villager.profession == VillagerProfession::Nitwit) return false;
|
|
if (villager.workstationIndex >= 0) return false;
|
|
int index = pois.nearestFreeWorkstation(villager.cell(), villager.profession);
|
|
if (index < 0) return false;
|
|
if (!pois.claim(index, villager.id)) return false;
|
|
villager.workstationIndex = index;
|
|
if (villager.profession == VillagerProfession::Unemployed)
|
|
villager.profession = pois.at(index).profession;
|
|
return true;
|
|
}
|
|
|
|
bool assignVillagerBed(Villager& villager, PoiRegistry& pois) {
|
|
if (!villager.alive || villager.bedIndex >= 0) return false;
|
|
int index = pois.nearestFreeBed(villager.cell());
|
|
if (index < 0) return false;
|
|
if (!pois.claim(index, villager.id)) return false;
|
|
villager.bedIndex = index;
|
|
return true;
|
|
}
|
|
|
|
void releaseVillagerPois(Villager& villager, PoiRegistry& pois) {
|
|
pois.release(villager.workstationIndex);
|
|
pois.release(villager.bedIndex);
|
|
villager.workstationIndex = -1;
|
|
villager.bedIndex = -1;
|
|
villager.targetPoiIndex = -1;
|
|
}
|
|
|
|
bool syncVillagerPois(Villager& villager, PoiRegistry& pois) {
|
|
bool professionChanged = false;
|
|
if (villager.workstationIndex >= 0) {
|
|
const WorkstationPoi& poi = pois.at(villager.workstationIndex);
|
|
if (!poi.valid || poi.ownerId != villager.id) {
|
|
villager.workstationIndex = -1;
|
|
if (villager.level <= 1) {
|
|
villager.profession = VillagerProfession::Unemployed;
|
|
villager.experience = 0;
|
|
professionChanged = true;
|
|
}
|
|
}
|
|
}
|
|
if (villager.bedIndex >= 0) {
|
|
const WorkstationPoi& bed = pois.at(villager.bedIndex);
|
|
if (!bed.valid || bed.ownerId != villager.id) villager.bedIndex = -1;
|
|
}
|
|
return professionChanged;
|
|
}
|
|
|
|
bool grantVillagerExperience(Villager& villager, int amount) {
|
|
if (amount <= 0) return false;
|
|
int before = villager.level;
|
|
villager.experience += amount;
|
|
villager.level = villagerLevelForExperience(villager.experience);
|
|
return villager.level > before;
|
|
}
|
|
|
|
void killVillager(Villager& villager, PoiRegistry& pois) {
|
|
villager.alive = false;
|
|
villager.health = 0.0f;
|
|
villager.state = VillagerState::Dead;
|
|
villager.activity = VillagerActivity::Idle;
|
|
villager.path.clear();
|
|
villager.pathCursor = 0;
|
|
villager.hasGoal = false;
|
|
releaseVillagerPois(villager, pois);
|
|
}
|
|
|
|
bool damageVillager(Villager& villager, PoiRegistry& pois, float amount) {
|
|
if (!villager.alive) return false;
|
|
villager.health -= amount;
|
|
if (villager.health > 0.0f) return false;
|
|
killVillager(villager, pois);
|
|
return true;
|
|
}
|
|
|
|
VillagerActivity VillagerSchedule::activityAt(double dayFraction) const {
|
|
if (dayFraction < wakeFraction) return VillagerActivity::Sleep;
|
|
if (dayFraction < workStart) return VillagerActivity::Idle;
|
|
if (dayFraction < meetStart) return VillagerActivity::Work;
|
|
if (dayFraction < meetEnd) return VillagerActivity::Meet;
|
|
if (dayFraction < workEnd) return VillagerActivity::Work;
|
|
if (dayFraction < sleepStart) return VillagerActivity::Idle;
|
|
return VillagerActivity::Sleep;
|
|
}
|
|
|
|
VillagerActivity chooseVillagerActivity(const Villager& villager, const VillagerSchedule& schedule,
|
|
const TimeOfDay& clock, bool villageThreatened) {
|
|
if (!villager.alive) return VillagerActivity::Idle;
|
|
if (villageThreatened) return VillagerActivity::Panic;
|
|
|
|
VillagerActivity planned = schedule.activityAt(clock.dayFraction());
|
|
switch (planned) {
|
|
case VillagerActivity::Work:
|
|
if (villager.profession == VillagerProfession::Nitwit) return VillagerActivity::Idle;
|
|
if (villager.workstationIndex < 0) return VillagerActivity::SeekWorkstation;
|
|
return VillagerActivity::Work;
|
|
case VillagerActivity::Sleep:
|
|
if (villager.bedIndex < 0) return VillagerActivity::Idle;
|
|
return VillagerActivity::Sleep;
|
|
default:
|
|
return planned;
|
|
}
|
|
}
|
|
|
|
bool villagerGoalFor(const Villager& villager, const PoiRegistry& pois, const Vec3i& villageCenter,
|
|
VillagerActivity activity, Vec3i& outGoal, int& outPoiIndex) {
|
|
outPoiIndex = -1;
|
|
switch (activity) {
|
|
case VillagerActivity::Work:
|
|
if (villager.workstationIndex < 0) return false;
|
|
outPoiIndex = villager.workstationIndex;
|
|
outGoal = pois.at(outPoiIndex).standCell;
|
|
return true;
|
|
case VillagerActivity::SeekWorkstation: {
|
|
int index = pois.nearestFreeWorkstation(villager.cell(), villager.profession);
|
|
if (index < 0) return false;
|
|
outPoiIndex = index;
|
|
outGoal = pois.at(index).standCell;
|
|
return true;
|
|
}
|
|
case VillagerActivity::Meet: {
|
|
int index = pois.bellIndex();
|
|
if (index >= 0 && pois.at(index).valid) {
|
|
outPoiIndex = index;
|
|
outGoal = pois.at(index).standCell;
|
|
} else {
|
|
outGoal = villageCenter;
|
|
}
|
|
return true;
|
|
}
|
|
case VillagerActivity::Sleep:
|
|
if (villager.bedIndex < 0) return false;
|
|
outPoiIndex = villager.bedIndex;
|
|
outGoal = pois.at(outPoiIndex).standCell;
|
|
return true;
|
|
case VillagerActivity::Panic:
|
|
if (villager.bedIndex >= 0 && pois.at(villager.bedIndex).valid) {
|
|
outPoiIndex = villager.bedIndex;
|
|
outGoal = pois.at(outPoiIndex).standCell;
|
|
} else {
|
|
outGoal = villageCenter;
|
|
}
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
bool repathVillager(const World& world, Villager& villager, const Vec3i& goal,
|
|
const PathConfig& config) {
|
|
std::vector<Vec3i> route = findPath(world, villager.cell(), goal, config);
|
|
if (route.empty()) {
|
|
villager.hasGoal = false;
|
|
return false;
|
|
}
|
|
villager.path = std::move(route);
|
|
villager.pathCursor = 0;
|
|
villager.goalCell = goal;
|
|
villager.hasGoal = true;
|
|
villager.state = villager.path.size() == 1 ? VillagerState::AtTarget : VillagerState::Travelling;
|
|
return true;
|
|
}
|
|
|
|
bool stepVillagerAlongPath(Villager& villager, float speed, float dt) {
|
|
if (villager.path.empty()) return false;
|
|
int last = static_cast<int>(villager.path.size()) - 1;
|
|
float budget = speed * dt;
|
|
while (budget > 0.0f) {
|
|
Vec3 target = cellCentre(villager.path[villager.pathCursor]);
|
|
Vec3 delta = target - villager.body.position;
|
|
float remaining = length(delta);
|
|
if (remaining <= budget) {
|
|
villager.body.position = target;
|
|
budget -= remaining;
|
|
if (villager.pathCursor >= last) {
|
|
villager.state = VillagerState::AtTarget;
|
|
return true;
|
|
}
|
|
++villager.pathCursor;
|
|
} else {
|
|
villager.body.position += normalize(delta) * budget;
|
|
villager.state = VillagerState::Travelling;
|
|
return false;
|
|
}
|
|
}
|
|
villager.state = VillagerState::Travelling;
|
|
return false;
|
|
}
|
|
|
|
void tickVillager(const World& world, Villager& villager, PoiRegistry& pois, const TimeOfDay& clock,
|
|
const VillagerSchedule& schedule, const Vec3i& villageCenter,
|
|
bool villageThreatened, float dt, const PathConfig& config) {
|
|
if (!villager.alive) {
|
|
villager.state = VillagerState::Dead;
|
|
return;
|
|
}
|
|
|
|
syncVillagerPois(villager, pois);
|
|
settleVillager(world, villager, config);
|
|
if (villager.bedIndex < 0) assignVillagerBed(villager, pois);
|
|
|
|
VillagerActivity next = chooseVillagerActivity(villager, schedule, clock, villageThreatened);
|
|
if (next != villager.activity) {
|
|
villager.activity = next;
|
|
villager.hasGoal = false;
|
|
villager.path.clear();
|
|
villager.pathCursor = 0;
|
|
villager.state = VillagerState::Idle;
|
|
}
|
|
|
|
Vec3i goal;
|
|
int poiIndex = -1;
|
|
if (!villagerGoalFor(villager, pois, villageCenter, next, goal, poiIndex)) {
|
|
villager.hasGoal = false;
|
|
villager.path.clear();
|
|
villager.pathCursor = 0;
|
|
villager.targetPoiIndex = -1;
|
|
villager.state = VillagerState::Idle;
|
|
return;
|
|
}
|
|
|
|
if (!villager.hasGoal || villager.goalCell != goal) {
|
|
villager.targetPoiIndex = poiIndex;
|
|
repathVillager(world, villager, goal, config);
|
|
}
|
|
|
|
bool arrived = stepVillagerAlongPath(villager, villagerWalkSpeed, dt);
|
|
if (!arrived && villager.state != VillagerState::AtTarget) return;
|
|
|
|
switch (next) {
|
|
case VillagerActivity::Work:
|
|
++villager.workTicks;
|
|
break;
|
|
case VillagerActivity::SeekWorkstation:
|
|
if (villager.targetPoiIndex >= 0 && pois.at(villager.targetPoiIndex).free()) {
|
|
pois.claim(villager.targetPoiIndex, villager.id);
|
|
villager.workstationIndex = villager.targetPoiIndex;
|
|
if (villager.profession == VillagerProfession::Unemployed)
|
|
villager.profession = pois.at(villager.targetPoiIndex).profession;
|
|
} else {
|
|
villager.hasGoal = false;
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
bool exchangeVillagerGossip(Villager& first, Villager& second) {
|
|
if (!first.alive || !second.alive || first.id == second.id) return false;
|
|
Vec3 delta = first.body.position - second.body.position;
|
|
if (length(delta) > villagerGossipRange) return false;
|
|
bool changed = first.gossip.shareInto(second.gossip);
|
|
changed = second.gossip.shareInto(first.gossip) || changed;
|
|
return changed;
|
|
}
|
|
|
|
int spreadVillageGossip(std::vector<Villager>& villagers) {
|
|
int exchanges = 0;
|
|
for (std::size_t i = 0; i < villagers.size(); ++i)
|
|
for (std::size_t j = i + 1; j < villagers.size(); ++j)
|
|
if (exchangeVillagerGossip(villagers[i], villagers[j])) ++exchanges;
|
|
return exchanges;
|
|
}
|
|
|
|
VillageCensus takeVillageCensus(const std::vector<Villager>& villagers, const PoiRegistry& pois,
|
|
const Vec3i& center, int radius) {
|
|
VillageCensus census;
|
|
for (const Villager& villager : villagers) {
|
|
if (!withinVillage(villager.cell(), center, radius)) continue;
|
|
++census.villagers;
|
|
if (!villager.alive) continue;
|
|
++census.living;
|
|
if (villager.employed()) ++census.employed;
|
|
census.foodStock += villager.food;
|
|
if (villager.food >= villagerBreedingFoodCost) ++census.willingBreeders;
|
|
}
|
|
for (int i = 0; i < pois.size(); ++i) {
|
|
const WorkstationPoi& poi = pois.at(i);
|
|
if (!poi.valid || !withinVillage(poi.pos, center, radius)) continue;
|
|
if (poi.isBed()) {
|
|
++census.beds;
|
|
if (poi.free()) ++census.freeBeds;
|
|
} else if (poi.isWorkstation()) {
|
|
++census.workstations;
|
|
if (poi.free()) ++census.freeWorkstations;
|
|
}
|
|
}
|
|
return census;
|
|
}
|
|
|
|
int breedVillagers(std::vector<Villager>& villagers, PoiRegistry& pois, const Vec3i& center,
|
|
int radius, uint32_t& rng) {
|
|
int born = 0;
|
|
while (takeVillageCensus(villagers, pois, center, radius).canGrow()) {
|
|
int firstParent = -1;
|
|
int secondParent = -1;
|
|
for (std::size_t i = 0; i < villagers.size(); ++i) {
|
|
const Villager& candidate = villagers[i];
|
|
if (!candidate.alive || candidate.food < villagerBreedingFoodCost) continue;
|
|
if (!withinVillage(candidate.cell(), center, radius)) continue;
|
|
if (firstParent < 0) firstParent = static_cast<int>(i);
|
|
else if (secondParent < 0) secondParent = static_cast<int>(i);
|
|
}
|
|
if (secondParent < 0) break;
|
|
|
|
villagers[firstParent].food -= villagerBreedingFoodCost;
|
|
villagers[secondParent].food -= villagerBreedingFoodCost;
|
|
|
|
Vec3 spawnPoint = (villagers[firstParent].body.position +
|
|
villagers[secondParent].body.position) * 0.5f;
|
|
int nextId = 0;
|
|
for (const Villager& existing : villagers)
|
|
if (existing.id >= nextId) nextId = existing.id + 1;
|
|
|
|
Villager child = makeVillager(nextId, spawnPoint, nextRandom(rng));
|
|
if (!assignVillagerBed(child, pois)) break;
|
|
villagers.push_back(child);
|
|
++born;
|
|
}
|
|
return born;
|
|
}
|
|
|
|
} // namespace vf
|