1313#include < nlohmann/json.hpp>
1414
1515#include " genesis/agents/CarriedResources.hpp"
16+ #include " genesis/agents/Experience.hpp"
1617#include " genesis/agents/Needs.hpp"
1718#include " genesis/agents/ProductionPlanner.hpp"
1819#include " genesis/world/MapPathfinding.hpp"
@@ -37,13 +38,61 @@ constexpr const char* kReasonPlanningFailed = "PlanningFailed";
3738constexpr const char * kReasonUnreachable = " Unreachable" ;
3839constexpr const char * kReasonPreemptedCriticalNeed = " PreemptedCriticalNeed" ;
3940
41+ constexpr float kExperienceMinMultiplier = 1 .0f ;
42+ constexpr float kExperienceMaxMultiplier = 3 .0f ;
43+ constexpr float kExperienceBumpPreempt = 0 .18f ;
44+ constexpr float kExperienceBumpStockoutVital = 0 .06f ;
45+
4046[[nodiscard]] std::string preemptedWithNeed (const char * needName) {
4147 std::string out (kReasonPreemptedCriticalNeed );
4248 out.push_back (' :' );
4349 out.append (needName);
4450 return out;
4551}
4652
53+ [[nodiscard]] bool isVitalNeed (NeedType need) noexcept {
54+ switch (need) {
55+ case NeedType::Hunger:
56+ case NeedType::Thirst:
57+ return true ;
58+ default :
59+ return false ;
60+ }
61+ }
62+
63+ void decayExperience (entt::registry& registry, float deltaSeconds) {
64+ if (!(deltaSeconds > 0 .0f )) {
65+ return ;
66+ }
67+ auto view = registry.view <components::AgentExperience>();
68+ for (auto entity : view) {
69+ auto & exp = view.get <components::AgentExperience>(entity);
70+ const float k = std::clamp (exp.forgetPerSecond * deltaSeconds, 0 .0f , 1 .0f );
71+ if (k <= 0 .0f ) {
72+ continue ;
73+ }
74+ for (auto & m : exp.bufferMultiplier ) {
75+ const float delta = m - 1 .0f ;
76+ m = 1 .0f + delta * (1 .0f - k);
77+ m = std::clamp (m, kExperienceMinMultiplier , kExperienceMaxMultiplier );
78+ }
79+ }
80+ }
81+
82+ void bumpNeedBuffer (entt::entity entity, NeedType need, float bump, entt::registry& registry) {
83+ if (!(bump > 0 .0f )) {
84+ return ;
85+ }
86+ auto & exp = registry.get_or_emplace <components::AgentExperience>(entity);
87+ const auto idx = needIndex (need);
88+ if (idx >= exp.bufferMultiplier .size ()) {
89+ return ;
90+ }
91+ float m = std::clamp (exp.bufferMultiplier [idx], kExperienceMinMultiplier , kExperienceMaxMultiplier );
92+ m *= (1 .0f + bump);
93+ exp.bufferMultiplier [idx] = std::clamp (m, kExperienceMinMultiplier , kExperienceMaxMultiplier );
94+ }
95+
4796[[nodiscard]] const char * needTypeName (NeedType need) {
4897 switch (need) {
4998 case NeedType::Hunger:
@@ -283,16 +332,115 @@ void ActionExecutor::requestConsume(entt::entity entity,
283332 queue.tasks .push_back (consume);
284333}
285334
286- void ActionExecutor::update (entt::registry& registry, float /* deltaSeconds*/ ) {
335+ void ActionExecutor::update (entt::registry& registry, float deltaSeconds) {
287336 m_workshopAttempts.clear ();
288337 m_resourceAttempts.clear ();
289338
339+ decayExperience (registry, deltaSeconds);
340+
290341 auto view = registry.view <ActionQueue, components::AgentLocation2D>();
291342
292343 for (auto entity : view) {
293344 auto & queue = view.get <ActionQueue>(entity);
294345 auto & location = view.get <components::AgentLocation2D>(entity);
295346
347+ const auto findBestInteractionFor = [&](genesis::world::ResourceType type) -> genesis::world::InteractionId {
348+ genesis::world::InteractionId best = 0 ;
349+ float bestCost = std::numeric_limits<float >::infinity ();
350+
351+ m_resources.forEachSpawn (registry, [&](const auto & spawn, const auto & inventory) {
352+ if (spawn.type != type) {
353+ return ;
354+ }
355+
356+ const auto inter = m_db.findInteraction (spawn.interaction );
357+ if (!inter) {
358+ return ;
359+ }
360+
361+ float cost = 0 .0f ;
362+ if (inter->mapId == location.mapId ) {
363+ const auto coord = inter->worldCoord ();
364+ const float dx = static_cast <float >(coord.first ) - location.x ;
365+ const float dy = static_cast <float >(coord.second ) - location.y ;
366+ cost = dx * dx + dy * dy;
367+ } else {
368+ const auto path = genesis::world::shortestMapPath (m_db, location.mapId , inter->mapId );
369+ if (!path) {
370+ return ;
371+ }
372+ cost = 500 .0f + static_cast <float >(path->totalCost );
373+ }
374+
375+ // Prefer non-empty targets when possible, but still allow empty workshops.
376+ if (inventory.current == 0U ) {
377+ cost += 250 .0f ;
378+ }
379+
380+ if (cost < bestCost) {
381+ bestCost = cost;
382+ best = spawn.interaction ;
383+ }
384+ });
385+
386+ return best;
387+ };
388+
389+ const auto ensureCriticalConsume = [&](NeedType criticalNeed, genesis::world::ResourceType criticalResource) -> bool {
390+ for (const auto & t : queue.tasks ) {
391+ if (t.type == ActionType::ConsumeResource && t.need == criticalNeed) {
392+ return true ;
393+ }
394+ }
395+
396+ const auto target = findBestInteractionFor (criticalResource);
397+ if (target == 0 ) {
398+ return false ;
399+ }
400+
401+ float buffer = 1 .0f ;
402+ if (const auto * exp = registry.try_get <components::AgentExperience>(entity)) {
403+ const auto idx = needIndex (criticalNeed);
404+ if (idx < exp->bufferMultiplier .size ()) {
405+ buffer = std::clamp (exp->bufferMultiplier [idx], kExperienceMinMultiplier , kExperienceMaxMultiplier );
406+ }
407+ }
408+
409+ std::uint32_t baseUnits = 1U ;
410+ float reliefPerUnit = 12 .0f ;
411+ switch (criticalNeed) {
412+ case NeedType::Hunger:
413+ baseUnits = 2U ;
414+ reliefPerUnit = 12 .0f ;
415+ break ;
416+ case NeedType::Thirst:
417+ baseUnits = 2U ;
418+ reliefPerUnit = 12 .0f ;
419+ break ;
420+ case NeedType::Social:
421+ baseUnits = 1U ;
422+ reliefPerUnit = 20 .0f ;
423+ break ;
424+ default :
425+ baseUnits = 1U ;
426+ reliefPerUnit = 12 .0f ;
427+ break ;
428+ }
429+
430+ const auto scaled = static_cast <std::uint32_t >(std::lround (static_cast <double >(baseUnits) * static_cast <double >(buffer)));
431+
432+ ActionTask consume{};
433+ consume.type = ActionType::ConsumeResource;
434+ consume.interaction = target;
435+ consume.need = criticalNeed;
436+ consume.resource = criticalResource;
437+ consume.amount = std::max<std::uint32_t >(1U , scaled);
438+ consume.retries = 0 ;
439+ consume.reliefPerUnit = reliefPerUnit;
440+ queue.tasks .emplace (queue.tasks .begin (), consume);
441+ return true ;
442+ };
443+
296444 const auto abandonAllActions = [&]() {
297445 queue.tasks .clear ();
298446 if (registry.any_of <components::MovementIntent2D>(entity)) {
@@ -327,6 +475,8 @@ void ActionExecutor::update(entt::registry& registry, float /*deltaSeconds*/) {
327475 releaseWorkshopSlot (job.interaction , entity);
328476 registry.remove <components::WorkshopJob>(entity);
329477 abandonAllActions ();
478+
479+ bumpNeedBuffer (entity, criticalNeed, kExperienceBumpPreempt , registry);
330480 };
331481
332482 if (auto * needs = registry.try_get <NeedComponent>(entity)) {
@@ -335,12 +485,20 @@ void ActionExecutor::update(entt::registry& registry, float /*deltaSeconds*/) {
335485
336486 if (auto * job = registry.try_get <components::WorkshopJob>(entity)) {
337487 if (job->outputType != criticalResource) {
338- abortWorkshopJob (*job, criticalNeed);
339- continue ;
488+ // Take a break instead of throwing away progress: eat/drink first, then resume work.
489+ bumpNeedBuffer (entity, criticalNeed, kExperienceBumpPreempt , registry);
490+ releaseWorkshopSlot (job->interaction , entity);
491+ if (!ensureCriticalConsume (criticalNeed, criticalResource)) {
492+ abortWorkshopJob (*job, criticalNeed);
493+ continue ;
494+ }
340495 }
341496 } else if (!queue.tasks .empty () && queue.tasks .front ().type == ActionType::ProduceResource) {
342497 if (queue.tasks .front ().resource != criticalResource) {
343- abandonAllActions ();
498+ bumpNeedBuffer (entity, criticalNeed, kExperienceBumpPreempt , registry);
499+ if (!ensureCriticalConsume (criticalNeed, criticalResource)) {
500+ abandonAllActions ();
501+ }
344502 }
345503 }
346504 }
@@ -633,11 +791,19 @@ void ActionExecutor::processConsume(entt::entity entity,
633791 m_resourceAttempts.push_back (std::move (attempt));
634792 queue.tasks .pop_front ();
635793 queue.tasks .insert (queue.tasks .begin (), plan->begin (), plan->end ());
794+
795+ if (isVitalNeed (task.need )) {
796+ bumpNeedBuffer (entity, task.need , kExperienceBumpStockoutVital , registry);
797+ }
636798 return ;
637799 }
638800
639801 attempt.failureReason = kReasonPlanningFailed ;
640802 m_resourceAttempts.push_back (std::move (attempt));
803+
804+ if (isVitalNeed (task.need )) {
805+ bumpNeedBuffer (entity, task.need , kExperienceBumpStockoutVital * 0 .5f , registry);
806+ }
641807 queue.tasks .pop_front ();
642808 return ;
643809 }
@@ -799,6 +965,10 @@ bool ActionExecutor::processProduce(entt::entity entity,
799965 return true ;
800966 }
801967
968+ if (!acquireWorkshopSlot (job->interaction , entity, slots)) {
969+ return false ;
970+ }
971+
802972 if (job->workRemainingTicks > 0U ) {
803973 job->workRemainingTicks --;
804974 }
0 commit comments