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 float ActionDurationVariance { get; set; } = 0.25f;
public float MinPlannedActionSeconds { get; set; } = 2.0f;
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);
}
}