using System; using System.Collections.Generic; using Godot; using Models; /// /// 校园代理的运行时数据。将 Godot 节点与纯数据分离, /// 以便行为系统可以在没有场景依赖的情况下进行测试。 /// 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); } } /// /// 规划器生成的意图。它捕获了行动和目的地, /// 加上可选的轮次计划持续时间。 /// 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; } } /// /// 传递给提供者/状态的共享上下文,以便它们可以评估相同的数据 /// 而无需硬编码依赖关系。 /// public sealed class CampusBehaviorContext { public CampusBehaviorContext( CampusAgentRuntime agent, CampusBehaviorConfig config, CampusLocationRegistry locations, CampusBehaviorWorld world, Random rng, List 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 WanderPoints { get; } } /// /// 提供者表示优先级队列中的单个规则。 /// 每个提供者返回一个行为意图,或者如果它不能应用于当前上下文,则返回 null。 /// public interface ICampusBehaviorProvider { CampusBehaviorIntent TryCreateIntent(CampusBehaviorContext context); } /// /// 紧急状态提供者:处理理智崩溃、极度压力或力竭。 /// 这是决策队列中的最高优先级。 /// 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; } } /// /// 指派任务提供者:如果代理有任务,则在需求之前执行。 /// 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 }; } } /// /// 需求提供者:处理饥饿、疲劳、情绪和社交需求。 /// 它位于指派任务之下,但在特质驱动的空闲行为之上。 /// 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; } } /// /// 特质驱动提供者:当没有紧急需求时,应用长期性格或标签倾向。 /// 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; } } /// /// 闲置提供者:当没有其他适用规则时的默认后备方案。 /// public sealed class IdleBehaviorProvider : ICampusBehaviorProvider { public CampusBehaviorIntent TryCreateIntent(CampusBehaviorContext context) { return new CampusBehaviorIntent( CampusBehaviorPriority.Idle, CampusActionId.Wandering, CampusLocationId.RandomWander, "idle_wander"); } } /// /// 规划器按优先级顺序执行提供者。这允许我们添加或删除提供者 /// 而无需编辑状态机。 /// public sealed class CampusBehaviorPlanner { private readonly List _providers; public CampusBehaviorPlanner(IEnumerable providers) { _providers = new List(providers ?? Array.Empty()); } 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"); } } /// /// AI FSM 的状态接口。每个状态可以通过请求更改来转换。 /// public interface ICampusBehaviorState { void Enter(CampusBehaviorAgent agent); void Tick(CampusBehaviorAgent agent, float delta); void Exit(CampusBehaviorAgent agent); } /// /// 状态机包装器,用于强制执行进入/退出语义。 /// 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); } } /// /// 决策状态:选择一个新的意图并立即转换为移动。 /// 这使意图选择隔离且易于扩展。 /// 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) { } } /// /// 移动状态:导航到意图的目标位置。 /// 一旦代理到达,它将转换为动作状态。 /// 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) { } } /// /// 动作状态:应用每秒增量并更新任务进度。 /// 当动作持续时间结束时,转回决策状态。 /// 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); } } } /// /// 驱动一个校园角色的主要行为代理。它拥有规划器、 /// 状态机,并在每次 Tick 时应用基线属性衰减。 /// public sealed class CampusBehaviorAgent { private readonly CampusBehaviorContext _context; private readonly Action _logCallback; private readonly CampusBehaviorPlanner _planner; private readonly Queue _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 wanderPoints, Action 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()); _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 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; } } /// /// 行为系统引用的特质的集中 ID。 /// 将它们放在这里可以避免分散的魔法字符串。 /// 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"; }