using System; using System.Collections.Generic; using System.IO; using System.Text.Json; using System.Text.Json.Serialization; using Godot; using Models; /// /// Location identifiers used by the campus behavior system. /// These map to Node2D markers in campus.tscn so the AI can pick targets by name. /// public enum CampusLocationId { None, Laboratory, Library, Canteen, Dormitory, ArtificialLake, CoffeeShop, AdministrationBuilding, FootballField, RandomWander } /// /// Action identifiers used by the behavior planner and state machine. /// Each action is configured via campus_behavior.json for duration and stat deltas. /// public enum CampusActionId { None, Experimenting, Writing, Eating, Sleeping, Chilling, Staring, CoffeeBreak, Administration, Running, Socializing, Wandering } /// /// Priority levels match the design doc ordering: lower value = higher priority. /// public enum CampusBehaviorPriority { Critical = 0, AssignedTask = 1, Needs = 2, Trait = 3, Idle = 4 } /// /// Minimal task types for the campus demo. These are not full gameplay tasks, /// just drivers for the assigned-task priority in the AI. /// public enum CampusTaskType { Experiment, Writing, Administration, Exercise, Coding, Social } /// /// Action configuration loaded from JSON. Deltas are applied per second while /// the action is running, so longer actions accumulate more effect. /// public sealed class CampusActionConfig { public CampusActionId ActionId { get; set; } public CampusLocationId LocationId { get; set; } public float DurationSeconds { get; set; } public float HungerDelta { get; set; } public float EnergyDelta { get; set; } public float StaminaDelta { get; set; } public float StressDelta { get; set; } public float MoodDelta { get; set; } public float SocialDelta { get; set; } public float SanityDelta { get; set; } public float HealthDelta { get; set; } } /// /// Global behavior configuration for campus AI. This is intentionally data-driven /// so balancing can happen in JSON without touching code. /// public sealed class CampusBehaviorConfig { public float CriticalSanityThreshold { get; set; } = 15f; public float CriticalStaminaThreshold { get; set; } = 12f; public float CriticalStressThreshold { get; set; } = 90f; public float HungerThreshold { get; set; } = 30f; public float EnergyThreshold { get; set; } = 25f; public float SocialThreshold { get; set; } = 35f; public float LowMoodThreshold { get; set; } = 25f; public float HungerDecayPerSecond { get; set; } = 0.6f; public float EnergyDecayPerSecond { get; set; } = 0.5f; public float StaminaDecayPerSecond { get; set; } = 0.4f; public float StressGrowthPerSecond { get; set; } = 0.45f; public float SocialDecayPerSecond { get; set; } = 0.35f; public float DecisionIntervalSeconds { get; set; } = 0.5f; public List ActionConfigs { get; set; } = new(); private readonly Dictionary _actionLookup = new(); public CampusActionConfig GetActionConfig(CampusActionId id) { if (_actionLookup.Count == 0) { BuildLookup(); } return _actionLookup.TryGetValue(id, out var config) ? config : null; } private void BuildLookup() { _actionLookup.Clear(); if (ActionConfigs == null) return; foreach (var config in ActionConfigs) { _actionLookup[config.ActionId] = config; } } public static CampusBehaviorConfig Load(string path) { if (string.IsNullOrWhiteSpace(path)) { GD.PushWarning("Campus behavior config path is empty; using defaults."); return new CampusBehaviorConfig(); } var resolvedPath = path.StartsWith("res://") || path.StartsWith("user://") ? ProjectSettings.GlobalizePath(path) : path; if (!File.Exists(resolvedPath)) { GD.PushWarning($"Campus behavior config not found at {resolvedPath}; using defaults."); return new CampusBehaviorConfig(); } var json = File.ReadAllText(resolvedPath); if (string.IsNullOrWhiteSpace(json)) { GD.PushWarning($"Campus behavior config is empty at {resolvedPath}; using defaults."); return new CampusBehaviorConfig(); } var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; options.Converters.Add(new JsonStringEnumConverter()); try { var config = JsonSerializer.Deserialize(json, options); return config ?? new CampusBehaviorConfig(); } catch (Exception ex) { GD.PushWarning($"Failed to parse campus behavior config: {ex.Message}"); return new CampusBehaviorConfig(); } } } /// /// Simple location registry that maps logical location ids to scene positions. /// This keeps the behavior system independent from scene tree details. /// public sealed class CampusLocationRegistry { private readonly Dictionary _locations = new(); public void Register(CampusLocationId id, Vector2 position) { if (id == CampusLocationId.None) return; _locations[id] = position; } public bool TryGetPosition(CampusLocationId id, out Vector2 position) { return _locations.TryGetValue(id, out position); } } /// /// Tracks current occupancy per location so traits like social phobia can react /// to crowd size without hard-coding scene knowledge. /// public sealed class CampusBehaviorWorld { private readonly Dictionary _occupancy = new(); public void Clear() { _occupancy.Clear(); } public void AddOccupant(CampusLocationId id) { if (id == CampusLocationId.None || id == CampusLocationId.RandomWander) return; if (!_occupancy.ContainsKey(id)) { _occupancy[id] = 0; } _occupancy[id] += 1; } public int GetOccupancy(CampusLocationId id) { return _occupancy.TryGetValue(id, out var count) ? count : 0; } } /// /// Lightweight task container for the campus demo; it just tracks remaining work. /// public sealed class CampusTask { public CampusTaskType Type { get; } public float RemainingSeconds { get; private set; } public CampusTask(CampusTaskType type, float remainingSeconds) { Type = type; RemainingSeconds = Mathf.Max(0f, remainingSeconds); } public void Advance(float delta) { RemainingSeconds = Mathf.Max(0f, RemainingSeconds - delta); } public bool IsComplete => RemainingSeconds <= 0f; } /// /// Custom needs that are not yet part of the core UnitModel (hunger/social/energy). /// Uses PropertyValue so it plugs into the existing numeric system. /// public sealed class CampusAgentNeeds { public PropertyValue Hunger { get; } public PropertyValue Energy { get; } public PropertyValue Social { get; } public PropertyValue Health { get; } public CampusAgentNeeds(float hunger, float energy, float social, float health) { Hunger = new PropertyValue(hunger); Energy = new PropertyValue(energy); Social = new PropertyValue(social); Health = new PropertyValue(health); } }