Skip to content

Commit f0c4006

Browse files
committed
agents: add social interaction action
1 parent 0bdfae8 commit f0c4006

8 files changed

Lines changed: 411 additions & 30 deletions

File tree

docs/architecture/foundation/telemetry-schema.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@
6464
### 2.5 ActionSnapshot
6565

6666
- `entityId:uint32`:对应代理实体 ID。
67-
- `currentAction:string`:当前动作类型(例如 `MoveToInteraction/ConsumeResource/TakeResource/ProduceResource/Idle` 等)。
67+
- `currentAction:string`:当前动作类型(例如 `MoveToInteraction/ConsumeResource/TakeResource/ProduceResource/SocializeWithAgent/Idle` 等)。
6868
- `queueLength:uint32`:行动队列长度。
6969
- `target:InteractionId(uint32)`:当前动作目标(如移动/消耗/生产的交互点)。
7070
- `speed:float`:移动速度(仅在移动相关动作时有意义)。

include/genesis/agents/ActionSystem.hpp

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,14 @@ enum class ActionType {
2424
MoveToInteraction,
2525
ConsumeResource,
2626
TakeResource,
27-
ProduceResource
27+
ProduceResource,
28+
SocializeWithAgent
2829
};
2930

3031
struct ActionTask {
3132
ActionType type{ActionType::MoveToInteraction};
3233
genesis::world::InteractionId interaction{0};
34+
std::uint32_t targetEntityId{0};
3335
float speed{1.0f};
3436
NeedType need{NeedType::Hunger};
3537
genesis::world::ResourceType resource{genesis::world::ResourceType::Food};
@@ -56,6 +58,12 @@ class ActionExecutor {
5658
float reliefPerUnit,
5759
entt::registry& registry);
5860

61+
void requestSocialize(entt::entity entity,
62+
std::uint32_t partnerEntityId,
63+
std::uint32_t workTicks,
64+
float relief,
65+
entt::registry& registry);
66+
5967
void update(entt::registry& registry, float deltaSeconds);
6068

6169
[[nodiscard]] bool hasPendingActions(entt::entity entity, const entt::registry& registry) const;
@@ -66,10 +74,12 @@ class ActionExecutor {
6674
private:
6775
void ensureQueue(entt::entity entity, entt::registry& registry);
6876
bool hasPendingConsume(const ActionQueue& queue, genesis::world::InteractionId interaction, genesis::world::ResourceType type) const;
77+
bool hasPendingSocialize(const ActionQueue& queue, std::uint32_t partnerEntityId) const;
6978
void processMove(entt::entity entity, ActionQueue& queue, genesis::agents::components::AgentLocation2D& location, entt::registry& registry);
7079
void processConsume(entt::entity entity, ActionQueue& queue, genesis::agents::components::AgentLocation2D& location, entt::registry& registry);
7180
void processTake(entt::entity entity, ActionQueue& queue, genesis::agents::components::AgentLocation2D& location, entt::registry& registry);
7281
bool processProduce(entt::entity entity, ActionQueue& queue, genesis::agents::components::AgentLocation2D& location, entt::registry& registry);
82+
void processSocialize(entt::entity entity, ActionQueue& queue, genesis::agents::components::AgentLocation2D& location, entt::registry& registry);
7383
bool acquireWorkshopSlot(genesis::world::InteractionId interaction, entt::entity worker, std::uint32_t slots);
7484
void releaseWorkshopSlot(genesis::world::InteractionId interaction, entt::entity worker);
7585

include/genesis/agents/NeedSatisfier.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ struct NeedSatisfierConfig {
3333
std::uint32_t socialUnitsPerRequest{1};
3434
float socialReliefPerUnit{20.0f};
3535
float socialPrepareMargin{5.0f};
36+
std::uint32_t socialInteractTicksPerUnit{8};
3637
std::function<world::InteractionId(entt::entity)> socialPreferredLocator{};
3738
};
3839

src/agents/ActionSystem.cpp

Lines changed: 181 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -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

213230
namespace {
@@ -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+
314358
void 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+
546620
void 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+
11971358
bool ActionExecutor::acquireWorkshopSlot(genesis::world::InteractionId interaction, entt::entity worker, std::uint32_t slots) {
11981359
if (slots == 0U) {
11991360
return true;

0 commit comments

Comments
 (0)