@@ -88,6 +88,16 @@ void recordResourceAttempt(entt::entity entity,
8888 return " Unknown" ;
8989}
9090
91+ [[nodiscard]] bool isVitalNeed (NeedType need) noexcept {
92+ switch (need) {
93+ case NeedType::Hunger:
94+ case NeedType::Thirst:
95+ return true ;
96+ default :
97+ return false ;
98+ }
99+ }
100+
91101[[nodiscard]] std::optional<std::pair<NeedType, genesis::world::ResourceType>> criticalNeedResource (
92102 const NeedComponent& component) {
93103 struct Candidate {
@@ -208,6 +218,13 @@ struct WorkshopJob {
208218 std::array<std::uint32_t , components::CarriedResources::kTypeCount > reservedConsumables{};
209219};
210220
221+ struct SocialJob {
222+ std::uint32_t partnerEntityId{0 };
223+ std::uint32_t workTotalTicks{0 };
224+ std::uint32_t workRemainingTicks{0 };
225+ float relief{0 .0f };
226+ };
227+
211228} // namespace components
212229
213230namespace {
@@ -311,6 +328,33 @@ void ActionExecutor::requestConsume(entt::entity entity,
311328 queue.tasks .push_back (consume);
312329}
313330
331+ void ActionExecutor::requestSocialize (entt::entity entity,
332+ std::uint32_t partnerEntityId,
333+ std::uint32_t workTicks,
334+ float relief,
335+ entt::registry& registry) {
336+ if (partnerEntityId == 0U ) {
337+ return ;
338+ }
339+
340+ ensureQueue (entity, registry);
341+ auto & queue = registry.get <ActionQueue>(entity);
342+
343+ if (hasPendingSocialize (queue, partnerEntityId)) {
344+ return ;
345+ }
346+
347+ ActionTask socialize{};
348+ socialize.type = ActionType::SocializeWithAgent;
349+ socialize.targetEntityId = partnerEntityId;
350+ socialize.speed = 1 .0f ;
351+ socialize.need = NeedType::Social;
352+ socialize.resource = genesis::world::ResourceType::Social;
353+ socialize.amount = std::max<std::uint32_t >(1U , workTicks);
354+ socialize.reliefPerUnit = relief;
355+ queue.tasks .push_back (socialize);
356+ }
357+
314358void ActionExecutor::update (entt::registry& registry, float deltaSeconds) {
315359 m_workshopAttempts.clear ();
316360 m_resourceAttempts.clear ();
@@ -459,31 +503,48 @@ void ActionExecutor::update(entt::registry& registry, float deltaSeconds) {
459503 if (auto * needs = registry.try_get <NeedComponent>(entity)) {
460504 if (auto critical = criticalNeedResource (*needs)) {
461505 const auto [criticalNeed, criticalResource] = *critical;
506+ if (!isVitalNeed (criticalNeed)) {
507+ // Social "critical" should not preempt work globally; it is handled by normal planning/actions.
508+ } else {
462509
463- if (auto * job = registry.try_get <components::WorkshopJob>(entity)) {
464- if (job->outputType != criticalResource) {
465- // Take a break instead of throwing away progress: eat/drink first, then resume work.
466- releaseWorkshopSlot (job->interaction , entity);
467- bool inserted = false ;
468- if (!ensureCriticalConsume (criticalNeed, criticalResource, inserted)) {
469- recordCriticalPreempt (entity, criticalNeed, registry);
470- abortWorkshopJob (*job, criticalNeed);
471- continue ;
472- }
473- if (inserted) {
474- recordCriticalPreempt (entity, criticalNeed, registry);
510+ const auto alreadyHasCriticalConsume = [&]() -> bool {
511+ for (const auto & t : queue.tasks ) {
512+ if (t.type == ActionType::ConsumeResource && t.need == criticalNeed) {
513+ return true ;
475514 }
476515 }
477- } else if (!queue.tasks .empty () && queue.tasks .front ().type == ActionType::ProduceResource) {
478- if (queue.tasks .front ().resource != criticalResource) {
479- bool inserted = false ;
480- if (!ensureCriticalConsume (criticalNeed, criticalResource, inserted)) {
481- recordCriticalPreempt (entity, criticalNeed, registry);
482- abandonAllActions ();
483- } else if (inserted) {
484- recordCriticalPreempt (entity, criticalNeed, registry);
516+ return false ;
517+ };
518+
519+ const auto preemptForCriticalNeed = [&](bool mustAbortWorkshop) {
520+ bool inserted = false ;
521+ if (!ensureCriticalConsume (criticalNeed, criticalResource, inserted)) {
522+ recordCriticalPreempt (entity, criticalNeed, registry);
523+ if (mustAbortWorkshop) {
524+ if (auto * job = registry.try_get <components::WorkshopJob>(entity)) {
525+ abortWorkshopJob (*job, criticalNeed);
526+ return ;
527+ }
485528 }
529+ abandonAllActions ();
530+ return ;
531+ }
532+ if (inserted) {
533+ recordCriticalPreempt (entity, criticalNeed, registry);
486534 }
535+ };
536+
537+ if (auto * job = registry.try_get <components::WorkshopJob>(entity)) {
538+ if (job->outputType != criticalResource && !alreadyHasCriticalConsume ()) {
539+ // Take a break instead of throwing away progress: eat/drink first, then resume work.
540+ releaseWorkshopSlot (job->interaction , entity);
541+ preemptForCriticalNeed (true );
542+ }
543+ } else if (!alreadyHasCriticalConsume ()) {
544+ // Any non-critical work (including socializing) yields to vital needs.
545+ preemptForCriticalNeed (false );
546+ }
547+
487548 }
488549 }
489550 }
@@ -509,6 +570,10 @@ void ActionExecutor::update(entt::registry& registry, float deltaSeconds) {
509570 case ActionType::ProduceResource:
510571 advanced = processProduce (entity, queue, location, registry);
511572 break ;
573+ case ActionType::SocializeWithAgent:
574+ processSocialize (entity, queue, location, registry);
575+ advanced = false ;
576+ break ;
512577 }
513578 }
514579
@@ -543,6 +608,15 @@ bool ActionExecutor::hasPendingConsume(const ActionQueue& queue,
543608 });
544609}
545610
611+ bool ActionExecutor::hasPendingSocialize (const ActionQueue& queue, std::uint32_t partnerEntityId) const {
612+ if (partnerEntityId == 0U ) {
613+ return false ;
614+ }
615+ return std::any_of (queue.tasks .begin (), queue.tasks .end (), [&](const ActionTask& task) {
616+ return task.type == ActionType::SocializeWithAgent && task.targetEntityId == partnerEntityId;
617+ });
618+ }
619+
546620void ActionExecutor::processMove (entt::entity entity,
547621 ActionQueue& queue,
548622 components::AgentLocation2D& location,
@@ -1194,6 +1268,93 @@ bool ActionExecutor::processProduce(entt::entity entity,
11941268 return active.workRemainingTicks == 0U ;
11951269}
11961270
1271+ void ActionExecutor::processSocialize (entt::entity entity,
1272+ ActionQueue& queue,
1273+ components::AgentLocation2D& location,
1274+ entt::registry& registry) {
1275+ if (queue.tasks .empty ()) {
1276+ return ;
1277+ }
1278+
1279+ auto & task = queue.tasks .front ();
1280+ const auto partner = static_cast <entt::entity>(task.targetEntityId );
1281+ if (partner == entt::null || !registry.valid (partner) || !registry.all_of <components::AgentLocation2D>(partner)) {
1282+ queue.tasks .pop_front ();
1283+ if (registry.any_of <components::SocialJob>(entity)) {
1284+ registry.remove <components::SocialJob>(entity);
1285+ }
1286+ return ;
1287+ }
1288+
1289+ const auto & partnerLoc = registry.get <components::AgentLocation2D>(partner);
1290+ if (partnerLoc.mapId != location.mapId ) {
1291+ queue.tasks .pop_front ();
1292+ if (registry.any_of <components::SocialJob>(entity)) {
1293+ registry.remove <components::SocialJob>(entity);
1294+ }
1295+ return ;
1296+ }
1297+
1298+ const float dx = partnerLoc.x - location.x ;
1299+ const float dy = partnerLoc.y - location.y ;
1300+ const float dist2 = dx * dx + dy * dy;
1301+ constexpr float kMeetEpsilon2 = 0 .05f * 0 .05f ;
1302+ if (dist2 > kMeetEpsilon2 ) {
1303+ auto & intent = registry.get_or_emplace <components::MovementIntent2D>(entity);
1304+ intent.targetMapId = location.mapId ;
1305+ intent.targetX = partnerLoc.x ;
1306+ intent.targetY = partnerLoc.y ;
1307+ intent.speed = std::max (task.speed , kMinSpeed );
1308+ return ;
1309+ }
1310+
1311+ auto & job = registry.get_or_emplace <components::SocialJob>(entity);
1312+ if (job.partnerEntityId != task.targetEntityId || job.workTotalTicks == 0U ) {
1313+ job.partnerEntityId = task.targetEntityId ;
1314+ job.workTotalTicks = std::max<std::uint32_t >(1U , task.amount );
1315+ job.workRemainingTicks = job.workTotalTicks ;
1316+ job.relief = task.reliefPerUnit ;
1317+ }
1318+
1319+ if (job.workRemainingTicks > 0U ) {
1320+ --job.workRemainingTicks ;
1321+ return ;
1322+ }
1323+
1324+ auto * needs = registry.try_get <NeedComponent>(entity);
1325+ if (needs) {
1326+ auto * state = needs->needs .state (NeedType::Social);
1327+ const auto * descriptor = needs->needs .descriptor (NeedType::Social);
1328+ if (state && descriptor) {
1329+ state->value = std::max (descriptor->minValue , state->value - std::max (0 .0f , job.relief ));
1330+ state->clamp (*descriptor);
1331+ needs->lastSamples [needIndex (NeedType::Social)] = std::nullopt ;
1332+ }
1333+ }
1334+
1335+ // Co-located social contact benefits both sides (even if only one initiated).
1336+ if (registry.valid (partner) && registry.all_of <components::AgentLocation2D>(partner)) {
1337+ const auto & pLoc = registry.get <components::AgentLocation2D>(partner);
1338+ const float pdx = pLoc.x - location.x ;
1339+ const float pdy = pLoc.y - location.y ;
1340+ const float pd2 = pdx * pdx + pdy * pdy;
1341+ if (pLoc.mapId == location.mapId && pd2 <= kMeetEpsilon2 ) {
1342+ if (auto * pNeeds = registry.try_get <NeedComponent>(partner)) {
1343+ auto * pState = pNeeds->needs .state (NeedType::Social);
1344+ const auto * pDesc = pNeeds->needs .descriptor (NeedType::Social);
1345+ if (pState && pDesc) {
1346+ pState->value = std::max (pDesc->minValue , pState->value - std::max (0 .0f , job.relief ));
1347+ pState->clamp (*pDesc);
1348+ pNeeds->lastSamples [needIndex (NeedType::Social)] = std::nullopt ;
1349+ }
1350+ }
1351+ }
1352+ }
1353+
1354+ registry.remove <components::SocialJob>(entity);
1355+ queue.tasks .pop_front ();
1356+ }
1357+
11971358bool ActionExecutor::acquireWorkshopSlot (genesis::world::InteractionId interaction, entt::entity worker, std::uint32_t slots) {
11981359 if (slots == 0U ) {
11991360 return true ;
0 commit comments