supervisor-simulator/scripts/Campus/CampusBehaviorAgent.cs

974 lines
37 KiB
C#

using System;
using System.Collections.Generic;
using Godot;
using Models;
/// <summary>
/// 校园代理的运行时数据。将 Godot 节点与纯数据分离,
/// 以便行为系统可以在没有场景依赖的情况下进行测试。
/// </summary>
public sealed class CampusAgentRuntime {
public CampusAgentRuntime(StudentModel student, CampusAgentNeeds needs) {
Student = student ?? throw new ArgumentNullException(nameof(student));
Needs = needs ?? throw new ArgumentNullException(nameof(needs));
}
public StudentModel Student { get; }
public CampusAgentNeeds Needs { get; }
public CampusTask AssignedTask { get; set; }
public CampusLocationId CurrentLocationId { get; set; } = CampusLocationId.None;
public UnitModel Unit => Student.Core;
public string Name {
get => Student.Name;
set => Student.Name = value;
}
public bool HasTrait(string traitId) {
return Unit.Tags.TraitIds.Contains(traitId);
}
public bool HasRole(string roleId) {
return Unit.Tags.RoleIds.Contains(roleId);
}
public bool HasArchetype(string archetypeId) {
return Unit.Tags.ArchetypeIds.Contains(archetypeId);
}
}
/// <summary>
/// 规划器生成的意图。它捕获了行动和目的地,
/// 加上可选的轮次计划持续时间。
/// </summary>
public sealed class CampusBehaviorIntent {
public CampusBehaviorIntent(CampusBehaviorPriority priority, CampusActionId actionId, CampusLocationId locationId,
string reason, float durationSeconds = 0f) {
Priority = priority;
ActionId = actionId;
LocationId = locationId;
Reason = reason ?? string.Empty;
DurationSeconds = durationSeconds;
}
public CampusBehaviorPriority Priority { get; }
public CampusActionId ActionId { get; }
public CampusLocationId LocationId { get; }
public string Reason { get; }
public float DurationSeconds { get; }
public bool Matches(CampusBehaviorIntent other) {
if (other == null) return false;
return Priority == other.Priority && ActionId == other.ActionId && LocationId == other.LocationId;
}
}
/// <summary>
/// 传递给提供者/状态的共享上下文,以便它们可以评估相同的数据
/// 而无需硬编码依赖关系。
/// </summary>
public sealed class CampusBehaviorContext {
public CampusBehaviorContext(
CampusAgentRuntime agent,
CampusBehaviorConfig config,
CampusLocationRegistry locations,
CampusBehaviorWorld world,
Random rng,
List<Vector2> wanderPoints) {
Agent = agent;
Config = config;
Locations = locations;
World = world;
Rng = rng;
WanderPoints = wanderPoints;
}
public CampusAgentRuntime Agent { get; }
public CampusBehaviorConfig Config { get; }
public CampusLocationRegistry Locations { get; }
public CampusBehaviorWorld World { get; }
public Random Rng { get; }
public List<Vector2> WanderPoints { get; }
}
/// <summary>
/// 提供者表示优先级队列中的单个规则。
/// 每个提供者返回一个行为意图,或者如果它不能应用于当前上下文,则返回 null。
/// </summary>
public interface ICampusBehaviorProvider {
CampusBehaviorIntent TryCreateIntent(CampusBehaviorContext context);
}
/// <summary>
/// 紧急状态提供者:处理理智崩溃、极度压力或力竭。
/// 这是决策队列中的最高优先级。
/// </summary>
public sealed class CriticalBehaviorProvider : ICampusBehaviorProvider {
private readonly string _grinderArchetypeId;
public CriticalBehaviorProvider(string grinderArchetypeId) {
_grinderArchetypeId = grinderArchetypeId;
}
public CampusBehaviorIntent TryCreateIntent(CampusBehaviorContext context) {
var agent = context.Agent;
var config = context.Config;
var stamina = agent.Student.Progress.Stamina.Current.Value;
var sanity = agent.Unit.Statuses.Sanity.Current.Value;
var stress = agent.Unit.Statuses.Stress.Current.Value;
if (sanity <= config.CriticalSanityThreshold || stress >= config.CriticalStressThreshold)
return new CampusBehaviorIntent(
CampusBehaviorPriority.Critical,
CampusActionId.Staring,
CampusLocationId.ArtificialLake,
"critical_sanity_or_stress");
if (stamina <= config.CriticalStaminaThreshold) {
if (agent.HasArchetype(_grinderArchetypeId) && context.Rng.NextDouble() < 0.5)
return new CampusBehaviorIntent(
CampusBehaviorPriority.Critical,
CampusActionId.CoffeeBreak,
CampusLocationId.CoffeeShop,
"grinder_forced_coffee");
return new CampusBehaviorIntent(
CampusBehaviorPriority.Critical,
CampusActionId.Sleeping,
CampusLocationId.Dormitory,
"critical_stamina");
}
return null;
}
}
/// <summary>
/// 指派任务提供者:如果代理有任务,则在需求之前执行。
/// </summary>
public sealed class AssignedTaskBehaviorProvider : ICampusBehaviorProvider {
public CampusBehaviorIntent TryCreateIntent(CampusBehaviorContext context) {
var task = context.Agent.AssignedTask;
if (task == null || task.IsComplete) return null;
var action = MapTaskToAction(task.Type);
var location = context.Config.GetActionConfig(action)?.LocationId ?? CampusLocationId.Laboratory;
return new CampusBehaviorIntent(
CampusBehaviorPriority.AssignedTask,
action,
location,
$"assigned_task_{task.Type}");
}
public static CampusActionId MapTaskToAction(CampusTaskType taskType) {
return taskType switch {
CampusTaskType.Experiment => CampusActionId.Experimenting,
CampusTaskType.Writing => CampusActionId.Writing,
CampusTaskType.Administration => CampusActionId.Administration,
CampusTaskType.Exercise => CampusActionId.Running,
CampusTaskType.Coding => CampusActionId.Experimenting,
CampusTaskType.Social => CampusActionId.Socializing,
_ => CampusActionId.Wandering
};
}
}
/// <summary>
/// 需求提供者:处理饥饿、疲劳、情绪和社交需求。
/// 它位于指派任务之下,但在特质驱动的空闲行为之上。
/// </summary>
public sealed class NeedsBehaviorProvider : ICampusBehaviorProvider {
private readonly string _grinderArchetypeId;
private readonly string _traitCaffeineId;
private readonly string _traitNotHumanId;
public NeedsBehaviorProvider(string traitNotHumanId, string traitCaffeineId, string grinderArchetypeId) {
_traitNotHumanId = traitNotHumanId;
_traitCaffeineId = traitCaffeineId;
_grinderArchetypeId = grinderArchetypeId;
}
public CampusBehaviorIntent TryCreateIntent(CampusBehaviorContext context) {
var agent = context.Agent;
var config = context.Config;
var needs = agent.Needs;
var isNotHuman = agent.HasTrait(_traitNotHumanId);
if (!isNotHuman && needs.Hunger.Value <= config.HungerThreshold)
return new CampusBehaviorIntent(
CampusBehaviorPriority.Needs,
CampusActionId.Eating,
CampusLocationId.Canteen,
"need_hunger");
var stamina = agent.Student.Progress.Stamina.Current.Value;
if (!isNotHuman &&
(needs.Energy.Value <= config.EnergyThreshold || stamina <= config.CriticalStaminaThreshold)) {
if (agent.HasTrait(_traitCaffeineId))
return new CampusBehaviorIntent(
CampusBehaviorPriority.Needs,
CampusActionId.CoffeeBreak,
CampusLocationId.CoffeeShop,
"need_caffeine");
if (agent.HasArchetype(_grinderArchetypeId) && context.Rng.NextDouble() < 0.5)
return new CampusBehaviorIntent(
CampusBehaviorPriority.Needs,
CampusActionId.CoffeeBreak,
CampusLocationId.CoffeeShop,
"grinder_refuse_sleep");
return new CampusBehaviorIntent(
CampusBehaviorPriority.Needs,
CampusActionId.Sleeping,
CampusLocationId.Dormitory,
"need_sleep");
}
var mood = agent.Unit.Statuses.Mood.Value;
if (mood <= config.LowMoodThreshold)
return new CampusBehaviorIntent(
CampusBehaviorPriority.Needs,
CampusActionId.Chilling,
CampusLocationId.Dormitory,
"need_mood");
if (needs.Social.Value <= config.SocialThreshold)
return new CampusBehaviorIntent(
CampusBehaviorPriority.Needs,
CampusActionId.Socializing,
CampusLocationId.CoffeeShop,
"need_social");
return null;
}
}
/// <summary>
/// 特质驱动提供者:当没有紧急需求时,应用长期性格或标签倾向。
/// </summary>
public sealed class TraitBehaviorProvider : ICampusBehaviorProvider {
private readonly string _archetypeGrinderId;
private readonly string _archetypeSlackerId;
private readonly string _roleAlchemistId;
private readonly string _roleLabRatId;
private readonly string _roleScribeId;
private readonly string _roleWriterId;
private readonly string _traitSocialButterflyId;
private readonly string _traitSocialPhobiaId;
public TraitBehaviorProvider(
string archetypeGrinderId,
string archetypeSlackerId,
string traitSocialPhobiaId,
string traitSocialButterflyId,
string roleLabRatId,
string roleAlchemistId,
string roleWriterId,
string roleScribeId) {
_archetypeGrinderId = archetypeGrinderId;
_archetypeSlackerId = archetypeSlackerId;
_traitSocialPhobiaId = traitSocialPhobiaId;
_traitSocialButterflyId = traitSocialButterflyId;
_roleLabRatId = roleLabRatId;
_roleAlchemistId = roleAlchemistId;
_roleWriterId = roleWriterId;
_roleScribeId = roleScribeId;
}
public CampusBehaviorIntent TryCreateIntent(CampusBehaviorContext context) {
var agent = context.Agent;
if (agent.HasTrait(_traitSocialPhobiaId)) {
var quietSpot = PickLeastCrowded(context, CampusLocationId.ArtificialLake, CampusLocationId.Dormitory,
CampusLocationId.Library);
return new CampusBehaviorIntent(
CampusBehaviorPriority.Trait,
CampusActionId.Staring,
quietSpot,
"trait_social_phobia");
}
if (agent.HasTrait(_traitSocialButterflyId)) {
var socialSpot = PickMostCrowded(context, CampusLocationId.Canteen, CampusLocationId.CoffeeShop);
return new CampusBehaviorIntent(
CampusBehaviorPriority.Trait,
CampusActionId.Socializing,
socialSpot,
"trait_social_butterfly");
}
if (agent.HasArchetype(_archetypeGrinderId) || agent.HasRole(_roleLabRatId) || agent.HasRole(_roleAlchemistId))
return new CampusBehaviorIntent(
CampusBehaviorPriority.Trait,
CampusActionId.Experimenting,
CampusLocationId.Laboratory,
"trait_prefers_lab");
if (agent.HasRole(_roleWriterId) || agent.HasRole(_roleScribeId))
return new CampusBehaviorIntent(
CampusBehaviorPriority.Trait,
CampusActionId.Writing,
CampusLocationId.Library,
"trait_prefers_library");
if (agent.HasArchetype(_archetypeSlackerId)) {
var action = context.Rng.NextDouble() < 0.3
? CampusActionId.Eating
: CampusActionId.Chilling;
var location = action == CampusActionId.Eating ? CampusLocationId.Canteen : CampusLocationId.Dormitory;
return new CampusBehaviorIntent(
CampusBehaviorPriority.Trait,
action,
location,
"trait_slacker");
}
return null;
}
private static CampusLocationId PickLeastCrowded(CampusBehaviorContext context,
params CampusLocationId[] candidates) {
if (candidates == null || candidates.Length == 0) return CampusLocationId.ArtificialLake;
var best = candidates[0];
var bestCount = context.World.GetOccupancy(best);
for (var i = 1; i < candidates.Length; i++) {
var count = context.World.GetOccupancy(candidates[i]);
if (count < bestCount) {
best = candidates[i];
bestCount = count;
}
}
return best;
}
private static CampusLocationId
PickMostCrowded(CampusBehaviorContext context, params CampusLocationId[] candidates) {
if (candidates == null || candidates.Length == 0) return CampusLocationId.Canteen;
var best = candidates[0];
var bestCount = context.World.GetOccupancy(best);
for (var i = 1; i < candidates.Length; i++) {
var count = context.World.GetOccupancy(candidates[i]);
if (count > bestCount) {
best = candidates[i];
bestCount = count;
}
}
return best;
}
}
/// <summary>
/// 闲置提供者:当没有其他适用规则时的默认后备方案。
/// </summary>
public sealed class IdleBehaviorProvider : ICampusBehaviorProvider {
public CampusBehaviorIntent TryCreateIntent(CampusBehaviorContext context) {
return new CampusBehaviorIntent(
CampusBehaviorPriority.Idle,
CampusActionId.Wandering,
CampusLocationId.RandomWander,
"idle_wander");
}
}
/// <summary>
/// 规划器按优先级顺序执行提供者。这允许我们添加或删除提供者
/// 而无需编辑状态机。
/// </summary>
public sealed class CampusBehaviorPlanner {
private readonly List<ICampusBehaviorProvider> _providers;
public CampusBehaviorPlanner(IEnumerable<ICampusBehaviorProvider> providers) {
_providers = new List<ICampusBehaviorProvider>(providers ?? Array.Empty<ICampusBehaviorProvider>());
}
public CampusBehaviorIntent PickIntent(CampusBehaviorContext context) {
foreach (var provider in _providers) {
var intent = provider.TryCreateIntent(context);
if (intent != null) return intent;
}
return new CampusBehaviorIntent(CampusBehaviorPriority.Idle, CampusActionId.Wandering,
CampusLocationId.RandomWander, "fallback_idle");
}
}
/// <summary>
/// AI FSM 的状态接口。每个状态可以通过请求更改来转换。
/// </summary>
public interface ICampusBehaviorState {
void Enter(CampusBehaviorAgent agent);
void Tick(CampusBehaviorAgent agent, float delta);
void Exit(CampusBehaviorAgent agent);
}
/// <summary>
/// 状态机包装器,用于强制执行进入/退出语义。
/// </summary>
public sealed class CampusBehaviorStateMachine {
private ICampusBehaviorState _current;
public void ChangeState(CampusBehaviorAgent agent, ICampusBehaviorState next) {
_current?.Exit(agent);
_current = next;
_current?.Enter(agent);
}
public void Tick(CampusBehaviorAgent agent, float delta) {
_current?.Tick(agent, delta);
}
}
/// <summary>
/// 决策状态:选择一个新的意图并立即转换为移动。
/// 这使意图选择隔离且易于扩展。
/// </summary>
public sealed class CampusDecisionState : ICampusBehaviorState {
public void Enter(CampusBehaviorAgent agent) {
var intent = agent.PlanNextIntent();
agent.StartIntent(intent);
}
public void Tick(CampusBehaviorAgent agent, float delta) { }
public void Exit(CampusBehaviorAgent agent) { }
}
/// <summary>
/// 移动状态:导航到意图的目标位置。
/// 一旦代理到达,它将转换为动作状态。
/// </summary>
public sealed class CampusMoveState : ICampusBehaviorState {
private readonly CampusBehaviorIntent _intent;
public CampusMoveState(CampusBehaviorIntent intent) {
_intent = intent;
}
public void Enter(CampusBehaviorAgent agent) {
var target = agent.ResolveTargetPosition(_intent);
agent.Student.SetBehaviorTarget(target);
agent.Log($"moving_to {target} action={_intent.ActionId} reason={_intent.Reason}");
}
public void Tick(CampusBehaviorAgent agent, float delta) {
if (agent.Student.HasReachedBehaviorTarget()) {
agent.Student.ClearBehaviorTarget();
agent.StateMachine.ChangeState(agent, new CampusActionState(_intent));
}
}
public void Exit(CampusBehaviorAgent agent) { }
}
/// <summary>
/// 动作状态:应用每秒增量并更新任务进度。
/// 当动作持续时间结束时,转回决策状态。
/// </summary>
public sealed class CampusActionState : ICampusBehaviorState {
private readonly CampusBehaviorIntent _intent;
private CampusActionConfig _actionConfig;
private float _logTimer;
private float _progressAccumulator;
private float _remaining;
public CampusActionState(CampusBehaviorIntent intent) {
_intent = intent;
}
public void Enter(CampusBehaviorAgent agent) {
_actionConfig = agent.Config.GetActionConfig(_intent.ActionId);
var plannedDuration = _intent.DurationSeconds;
_remaining = plannedDuration > 0.0f
? plannedDuration
: _actionConfig?.DurationSeconds ?? 4.0f;
_logTimer = 0.0f;
_progressAccumulator = 0.0f;
agent.Runtime.CurrentLocationId = _intent.LocationId;
agent.Student.EnterBuilding(_intent.LocationId);
agent.Log($"action_start {_intent.ActionId} duration={_remaining:0.0}s reason={_intent.Reason}");
agent.HandlePhoneIdle(_intent.ActionId);
agent.LogEvent(agent.BuildActionStartMessage(_intent));
}
public void Tick(CampusBehaviorAgent agent, float delta) {
if (_remaining <= 0.0f) {
if (agent.IsRoundLocked) {
var nextIntent = agent.PeekNextRoundIntent();
agent.PrepareBuildingExit(_intent, nextIntent);
if (agent.TryStartNextRoundIntent()) return;
}
var plannedIntent = agent.PlanNextIntent();
agent.PrepareBuildingExit(_intent, plannedIntent);
agent.StartIntent(plannedIntent);
return;
}
if (_actionConfig != null) CampusBehaviorAgent.ApplyActionDelta(agent.Runtime, _actionConfig, delta);
AdvanceTask(agent, delta);
UpdateProgressLogs(agent, delta);
_remaining -= delta;
_logTimer += delta;
if (_logTimer >= 1.5f) {
_logTimer = 0.0f;
agent.Log($"action_tick {_intent.ActionId} remaining={Mathf.Max(0.0f, _remaining):0.0}s");
}
}
public void Exit(CampusBehaviorAgent agent) {
if (!agent.ConsumeSuppressBuildingExit()) agent.Student.ExitBuilding(_intent.LocationId);
agent.Runtime.CurrentLocationId = CampusLocationId.None;
agent.Log($"action_end {_intent.ActionId}");
if (_intent.ActionId == CampusActionId.Wandering)
agent.Student.StopPhoneIdle(true);
else
agent.Student.StopPhoneIdle();
}
private void AdvanceTask(CampusBehaviorAgent agent, float delta) {
var task = agent.Runtime.AssignedTask;
if (task == null) return;
var actionForTask = AssignedTaskBehaviorProvider.MapTaskToAction(task.Type);
if (actionForTask != _intent.ActionId) return;
task.Advance(delta);
if (task.IsComplete) {
agent.Log($"task_complete {task.Type}");
agent.Runtime.AssignedTask = null;
agent.LogEvent($"{agent.Runtime.Name}完成了{task.Type}任务");
}
}
private void UpdateProgressLogs(CampusBehaviorAgent agent, float delta) {
var message = agent.BuildProgressMessage(_intent);
if (string.IsNullOrWhiteSpace(message)) return;
_progressAccumulator += delta;
while (_progressAccumulator >= 1.0f) {
_progressAccumulator -= 1.0f;
agent.LogEvent(message);
}
}
}
/// <summary>
/// 驱动一个校园角色的主要行为代理。它拥有规划器、
/// 状态机,并在每次 Tick 时应用基线属性衰减。
/// </summary>
public sealed class CampusBehaviorAgent {
private readonly CampusBehaviorContext _context;
private readonly Action<string> _logCallback;
private readonly CampusBehaviorPlanner _planner;
private readonly Queue<CampusBehaviorIntent> _roundPlan = new();
private CampusBehaviorIntent _currentIntent;
private float _decisionTimer;
private int _roundPlanIndex;
private int _roundPlanTotal;
private float _roundRemaining;
private bool _suppressBuildingExit;
public CampusBehaviorAgent(
CampusStudent student,
CampusAgentRuntime runtime,
CampusBehaviorConfig config,
CampusLocationRegistry locations,
CampusBehaviorWorld world,
Random rng,
List<Vector2> wanderPoints,
Action<string> logCallback) {
Student = student ?? throw new ArgumentNullException(nameof(student));
Runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
Config = config ?? new CampusBehaviorConfig();
_context = new CampusBehaviorContext(runtime, Config, locations, world, rng ?? new Random(),
wanderPoints ?? new List<Vector2>());
_planner = new CampusBehaviorPlanner(BuildProviders());
_logCallback = logCallback;
Student.EnableBehaviorControl();
StateMachine.ChangeState(this, new CampusDecisionState());
}
public CampusStudent Student { get; }
public CampusAgentRuntime Runtime { get; }
public CampusBehaviorConfig Config { get; }
public CampusBehaviorStateMachine StateMachine { get; } = new();
public bool IsRoundLocked { get; private set; }
public void Tick(float delta) {
ApplyBaselineDecay(delta);
if (IsRoundLocked) {
_roundRemaining = Mathf.Max(0.0f, _roundRemaining - delta);
if (_roundRemaining <= 0.0f) {
IsRoundLocked = false;
ClearRoundPlan();
StateMachine.ChangeState(this, new CampusDecisionState());
}
}
TryInterrupt(delta);
StateMachine.Tick(this, delta);
}
public CampusBehaviorIntent PlanNextIntent() {
var intent = _planner.PickIntent(_context);
Log($"intent {intent.Priority} {intent.ActionId} -> {intent.LocationId} reason={intent.Reason}");
return intent;
}
public void StartIntent(CampusBehaviorIntent intent) {
_currentIntent = intent;
StateMachine.ChangeState(this, new CampusMoveState(intent));
}
public void StartRound(float durationSeconds) {
IsRoundLocked = true;
_roundRemaining = durationSeconds;
_decisionTimer = 0.0f;
BuildRoundPlan(durationSeconds);
if (!TryStartNextRoundIntent()) {
IsRoundLocked = false;
StateMachine.ChangeState(this, new CampusDecisionState());
}
}
public void EndRound() {
IsRoundLocked = false;
_roundRemaining = 0.0f;
ClearRoundPlan();
Student.StopPhoneIdle(true);
StateMachine.ChangeState(this, new CampusDecisionState());
}
public CampusBehaviorIntent PeekNextRoundIntent() {
return _roundPlan.Count > 0 ? _roundPlan.Peek() : null;
}
public void PrepareBuildingExit(CampusBehaviorIntent current, CampusBehaviorIntent next) {
_suppressBuildingExit = ShouldStayInside(current, next);
}
public bool ConsumeSuppressBuildingExit() {
var suppress = _suppressBuildingExit;
_suppressBuildingExit = false;
return suppress;
}
public bool TryStartNextRoundIntent() {
if (!IsRoundLocked || _roundPlan.Count == 0) return false;
var intent = _roundPlan.Dequeue();
_currentIntent = intent;
_roundPlanIndex += 1;
Log(
$"round_step {_roundPlanIndex}/{_roundPlanTotal} {intent.ActionId} duration={intent.DurationSeconds:0.0}s reason={intent.Reason}");
StateMachine.ChangeState(this, new CampusMoveState(intent));
return true;
}
private void BuildRoundPlan(float durationSeconds) {
ClearRoundPlan();
if (durationSeconds <= 0.0f) return;
// 先在模拟状态中计算整轮计划,避免运行时频繁重新评估。
var snapshot = CaptureRoundSnapshot();
var remaining = durationSeconds;
var minDuration = Mathf.Max(0.5f, Config.MinPlannedActionSeconds);
var maxSteps = Math.Max(1, (int)Math.Ceiling(durationSeconds / minDuration) + 1);
for (var i = 0; i < maxSteps && remaining > 0.05f; i++) {
var intent = _planner.PickIntent(_context);
var duration = ResolvePlannedDuration(intent, remaining);
_roundPlan.Enqueue(new CampusBehaviorIntent(intent.Priority, intent.ActionId, intent.LocationId,
intent.Reason, duration));
SimulateRoundStep(intent.ActionId, duration);
remaining -= duration;
}
_roundPlanTotal = _roundPlan.Count;
RestoreRoundSnapshot(snapshot);
if (_roundPlanTotal == 0) {
_roundPlan.Enqueue(new CampusBehaviorIntent(
CampusBehaviorPriority.Idle,
CampusActionId.Wandering,
CampusLocationId.RandomWander,
"round_fallback",
durationSeconds));
_roundPlanTotal = 1;
}
}
private void ClearRoundPlan() {
_roundPlan.Clear();
_roundPlanIndex = 0;
_roundPlanTotal = 0;
}
private RoundPlanSnapshot CaptureRoundSnapshot() {
var needs = Runtime.Needs;
var task = Runtime.AssignedTask;
return new RoundPlanSnapshot {
Hunger = needs.Hunger.Value,
Energy = needs.Energy.Value,
Social = needs.Social.Value,
Health = needs.Health.Value,
Stamina = Runtime.Student.Progress.Stamina.Current.Value,
Stress = Runtime.Unit.Statuses.Stress.Current.Value,
Sanity = Runtime.Unit.Statuses.Sanity.Current.Value,
Mood = Runtime.Unit.Statuses.Mood.Value,
HasTask = task != null,
TaskType = task?.Type ?? default,
TaskRemaining = task?.RemainingSeconds ?? 0f
};
}
private void RestoreRoundSnapshot(RoundPlanSnapshot snapshot) {
if (snapshot == null) return;
var needs = Runtime.Needs;
needs.Hunger.Value = snapshot.Hunger;
needs.Energy.Value = snapshot.Energy;
needs.Social.Value = snapshot.Social;
needs.Health.Value = snapshot.Health;
Runtime.Student.Progress.Stamina.Current.Value = snapshot.Stamina;
Runtime.Unit.Statuses.Stress.Current.Value = snapshot.Stress;
Runtime.Unit.Statuses.Sanity.Current.Value = snapshot.Sanity;
Runtime.Unit.Statuses.Mood.Value = snapshot.Mood;
Runtime.AssignedTask = snapshot.HasTask
? new CampusTask(snapshot.TaskType, snapshot.TaskRemaining)
: null;
}
private float ResolvePlannedDuration(CampusBehaviorIntent intent, float remainingSeconds) {
var actionConfig = Config.GetActionConfig(intent.ActionId);
var baseDuration = actionConfig?.DurationSeconds ?? 4.0f;
var variance = Mathf.Max(0.0f, Config.ActionDurationVariance);
var jitter = 1.0f;
if (variance > 0.01f) {
var roll = (float)_context.Rng.NextDouble();
jitter = 1.0f + (roll * 2.0f - 1.0f) * variance;
}
var duration = baseDuration * jitter;
var minDuration = Mathf.Max(0.5f, Config.MinPlannedActionSeconds);
if (remainingSeconds <= minDuration) return remainingSeconds;
return Mathf.Clamp(duration, minDuration, remainingSeconds);
}
private void SimulateRoundStep(CampusActionId actionId, float duration) {
ApplyBaselineDecay(duration);
var actionConfig = Config.GetActionConfig(actionId);
if (actionConfig != null) ApplyActionDelta(Runtime, actionConfig, duration);
var task = Runtime.AssignedTask;
if (task == null) return;
var taskAction = AssignedTaskBehaviorProvider.MapTaskToAction(task.Type);
if (taskAction != actionId) return;
task.Advance(duration);
if (task.IsComplete) Runtime.AssignedTask = null;
}
public Vector2 ResolveTargetPosition(CampusBehaviorIntent intent) {
if (intent.LocationId == CampusLocationId.RandomWander) return PickRandomWanderPoint();
if (_context.Locations != null && _context.Locations.TryGetPosition(intent.LocationId, out var position))
return position;
return PickRandomWanderPoint();
}
public void Log(string message) {
GD.Print($"[CampusAI:{Runtime.Name}] {message}");
}
public void LogEvent(string message) {
if (string.IsNullOrWhiteSpace(message)) return;
_logCallback?.Invoke(message);
GD.Print($"[CampusEvent] {message}");
}
public void HandlePhoneIdle(CampusActionId actionId) {
if (actionId == CampusActionId.Wandering)
Student.StartPhoneIdle();
else
Student.StopPhoneIdle();
}
public string BuildActionStartMessage(CampusBehaviorIntent intent) {
var locationName = GetLocationName(intent.LocationId);
return intent.ActionId switch {
CampusActionId.Experimenting => $"{Runtime.Name}去了{locationName},开始做实验",
CampusActionId.Writing => $"{Runtime.Name}去了{locationName},开始写作",
CampusActionId.Eating => $"{Runtime.Name}去了{locationName},开始吃饭",
CampusActionId.Sleeping => $"{Runtime.Name}去了{locationName},准备休息",
CampusActionId.Chilling => $"{Runtime.Name}去了{locationName},放松心情",
CampusActionId.Staring => $"{Runtime.Name}去了{locationName},开始发呆",
CampusActionId.CoffeeBreak => $"{Runtime.Name}去了{locationName},补充咖啡",
CampusActionId.Administration => $"{Runtime.Name}去了{locationName},开始处理事务",
CampusActionId.Running => $"{Runtime.Name}去了{locationName},开始锻炼",
CampusActionId.Socializing => $"{Runtime.Name}去了{locationName},开始社交",
CampusActionId.Wandering => $"{Runtime.Name}在校园里闲逛",
_ => $"{Runtime.Name}开始行动"
};
}
public string BuildProgressMessage(CampusBehaviorIntent intent) {
var locationName = GetLocationName(intent.LocationId);
return intent.ActionId switch {
CampusActionId.Experimenting => $"{Runtime.Name}在{locationName},科研进度+1",
CampusActionId.Writing => $"{Runtime.Name}在{locationName},科研进度+1",
CampusActionId.Administration => $"{Runtime.Name}在{locationName},行政进度+1",
CampusActionId.Running => $"{Runtime.Name}在{locationName},健康+1",
CampusActionId.Eating => $"{Runtime.Name}在{locationName},体力恢复+1",
CampusActionId.Sleeping => $"{Runtime.Name}在{locationName},体力恢复+1",
CampusActionId.Chilling => $"{Runtime.Name}在{locationName},心情恢复+1",
CampusActionId.CoffeeBreak => $"{Runtime.Name}在{locationName},精力恢复+1",
CampusActionId.Socializing => $"{Runtime.Name}在{locationName},心情+1",
_ => null
};
}
internal static void ApplyActionDelta(CampusAgentRuntime runtime, CampusActionConfig action, float delta) {
if (runtime == null || action == null) return;
var needs = runtime.Needs;
needs.Hunger.Add(action.HungerDelta * delta);
needs.Energy.Add(action.EnergyDelta * delta);
needs.Social.Add(action.SocialDelta * delta);
needs.Health.Add(action.HealthDelta * delta);
runtime.Student.Progress.Stamina.Add(action.StaminaDelta * delta);
runtime.Unit.Statuses.Stress.Add(action.StressDelta * delta);
runtime.Unit.Statuses.Sanity.Add(action.SanityDelta * delta);
runtime.Unit.Statuses.Mood.Add(action.MoodDelta * delta);
}
private void ApplyBaselineDecay(float delta) {
var needs = Runtime.Needs;
var isNotHuman = Runtime.HasTrait(CampusTraitIds.NotHuman);
var hungerDecay = Config.HungerDecayPerSecond;
if (Runtime.HasTrait(CampusTraitIds.BigEater)) hungerDecay *= 1.5f;
if (!isNotHuman) {
needs.Hunger.Add(-hungerDecay * delta);
needs.Energy.Add(-Config.EnergyDecayPerSecond * delta);
Runtime.Student.Progress.Stamina.Add(-Config.StaminaDecayPerSecond * delta);
}
needs.Social.Add(-Config.SocialDecayPerSecond * delta);
Runtime.Unit.Statuses.Stress.Add(Config.StressGrowthPerSecond * delta);
}
private void TryInterrupt(float delta) {
if (IsRoundLocked) return;
_decisionTimer += delta;
if (_decisionTimer < Config.DecisionIntervalSeconds) return;
_decisionTimer = 0.0f;
var candidate = _planner.PickIntent(_context);
if (candidate == null) return;
if (_currentIntent == null || candidate.Priority < _currentIntent.Priority) {
Log($"interrupt {candidate.Priority} {candidate.ActionId} reason={candidate.Reason}");
StartIntent(candidate);
}
}
private Vector2 PickRandomWanderPoint() {
if (_context.WanderPoints == null || _context.WanderPoints.Count == 0) return Student.GlobalPosition;
var index = _context.Rng.Next(0, _context.WanderPoints.Count);
return _context.WanderPoints[index];
}
private IEnumerable<ICampusBehaviorProvider> BuildProviders() {
yield return new CriticalBehaviorProvider(CoreIds.ArchetypeGrinder);
yield return new AssignedTaskBehaviorProvider();
yield return new NeedsBehaviorProvider(CampusTraitIds.NotHuman, CampusTraitIds.CaffeineDependence,
CoreIds.ArchetypeGrinder);
yield return new TraitBehaviorProvider(
CoreIds.ArchetypeGrinder,
CoreIds.ArchetypeSlacker,
CampusTraitIds.SocialPhobia,
CampusTraitIds.SocialButterfly,
CoreIds.RoleLabRat,
CoreIds.RoleAlchemist,
CoreIds.RoleWriter,
CoreIds.RoleScribe);
yield return new IdleBehaviorProvider();
}
private static bool ShouldStayInside(CampusBehaviorIntent current, CampusBehaviorIntent next) {
if (current == null || next == null) return false;
if (current.ActionId != next.ActionId) return false;
if (current.LocationId != next.LocationId) return false;
return current.LocationId != CampusLocationId.None && current.LocationId != CampusLocationId.RandomWander;
}
private static string GetLocationName(CampusLocationId locationId) {
return locationId switch {
CampusLocationId.Laboratory => "实验室",
CampusLocationId.Library => "图书馆",
CampusLocationId.Canteen => "食堂",
CampusLocationId.Dormitory => "宿舍",
CampusLocationId.ArtificialLake => "人工湖",
CampusLocationId.CoffeeShop => "咖啡店",
CampusLocationId.AdminBuilding => "行政楼",
CampusLocationId.FootballField => "足球场",
CampusLocationId.RandomWander => "校园",
_ => "校园"
};
}
private sealed class RoundPlanSnapshot {
public float Energy;
public bool HasTask;
public float Health;
public float Hunger;
public float Mood;
public float Sanity;
public float Social;
public float Stamina;
public float Stress;
public float TaskRemaining;
public CampusTaskType TaskType;
}
}
/// <summary>
/// 行为系统引用的特质的集中 ID。
/// 将它们放在这里可以避免分散的魔法字符串。
/// </summary>
public static class CampusTraitIds {
public const string CaffeineDependence = "core:trait_caffeine_dependence";
public const string SocialPhobia = "core:trait_social_phobia";
public const string SocialButterfly = "core:trait_social_butterfly";
public const string NotHuman = "core:trait_not_human";
public const string BigEater = "core:trait_big_eater";
}