272 lines
7.0 KiB
C#
272 lines
7.0 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Text.Json;
|
|
using System.Text.Json.Serialization;
|
|
using Godot;
|
|
using Models;
|
|
|
|
/// <summary>
|
|
/// Location identifiers used by the campus behavior system.
|
|
/// These map to Node2D markers in campus.tscn so the AI can pick targets by name.
|
|
/// </summary>
|
|
public enum CampusLocationId
|
|
{
|
|
None,
|
|
Laboratory,
|
|
Library,
|
|
Canteen,
|
|
Dormitory,
|
|
ArtificialLake,
|
|
CoffeeShop,
|
|
AdministrationBuilding,
|
|
FootballField,
|
|
RandomWander
|
|
}
|
|
|
|
/// <summary>
|
|
/// Action identifiers used by the behavior planner and state machine.
|
|
/// Each action is configured via campus_behavior.json for duration and stat deltas.
|
|
/// </summary>
|
|
public enum CampusActionId
|
|
{
|
|
None,
|
|
Experimenting,
|
|
Writing,
|
|
Eating,
|
|
Sleeping,
|
|
Chilling,
|
|
Staring,
|
|
CoffeeBreak,
|
|
Administration,
|
|
Running,
|
|
Socializing,
|
|
Wandering
|
|
}
|
|
|
|
/// <summary>
|
|
/// Priority levels match the design doc ordering: lower value = higher priority.
|
|
/// </summary>
|
|
public enum CampusBehaviorPriority
|
|
{
|
|
Critical = 0,
|
|
AssignedTask = 1,
|
|
Needs = 2,
|
|
Trait = 3,
|
|
Idle = 4
|
|
}
|
|
|
|
/// <summary>
|
|
/// Minimal task types for the campus demo. These are not full gameplay tasks,
|
|
/// just drivers for the assigned-task priority in the AI.
|
|
/// </summary>
|
|
public enum CampusTaskType
|
|
{
|
|
Experiment,
|
|
Writing,
|
|
Administration,
|
|
Exercise,
|
|
Coding,
|
|
Social
|
|
}
|
|
|
|
/// <summary>
|
|
/// Action configuration loaded from JSON. Deltas are applied per second while
|
|
/// the action is running, so longer actions accumulate more effect.
|
|
/// </summary>
|
|
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; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Global behavior configuration for campus AI. This is intentionally data-driven
|
|
/// so balancing can happen in JSON without touching code.
|
|
/// </summary>
|
|
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<CampusActionConfig> ActionConfigs { get; set; } = new();
|
|
|
|
private readonly Dictionary<CampusActionId, CampusActionConfig> _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<CampusBehaviorConfig>(json, options);
|
|
return config ?? new CampusBehaviorConfig();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
GD.PushWarning($"Failed to parse campus behavior config: {ex.Message}");
|
|
return new CampusBehaviorConfig();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Simple location registry that maps logical location ids to scene positions.
|
|
/// This keeps the behavior system independent from scene tree details.
|
|
/// </summary>
|
|
public sealed class CampusLocationRegistry
|
|
{
|
|
private readonly Dictionary<CampusLocationId, Vector2> _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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tracks current occupancy per location so traits like social phobia can react
|
|
/// to crowd size without hard-coding scene knowledge.
|
|
/// </summary>
|
|
public sealed class CampusBehaviorWorld
|
|
{
|
|
private readonly Dictionary<CampusLocationId, int> _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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Lightweight task container for the campus demo; it just tracks remaining work.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Custom needs that are not yet part of the core UnitModel (hunger/social/energy).
|
|
/// Uses PropertyValue so it plugs into the existing numeric system.
|
|
/// </summary>
|
|
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);
|
|
}
|
|
}
|