init mod
This commit is contained in:
commit
575a068bf5
13 files changed
+1082
No files matched your search
@@ -0,0 +1,2 @@
|
|||||||
|
mod_time_base_name = Time Engine
|
||||||
|
mod_time_base_desc = Engine time subsystem: the in-game clock tick controller, the clock HUD widget, the 2D lighting stack (UiLightController, LampBehaviour, LightDef) and the lighting.clear_map scenario action.
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
mod_time_base_name = Движок времени
|
||||||
|
mod_time_base_desc = Подсистема игрового времени: контроллер тиков часов, HUD-виджет часов, стек 2D-освещения (UiLightController, LampBehaviour, LightDef) и сценарное действие lighting.clear_map.
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"id": "time_base",
|
||||||
|
"name": "Time Engine",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"author": "ct",
|
||||||
|
"description": "Engine time subsystem: the in-game clock tick controller bridging SceneEventController timer pulses to RuntimeData.Time, the clock HUD widget, the 2D lighting stack (UiLightController, LampBehaviour, LightDef) installed into the kernel through LightingBridge, and the lighting.clear_map scenario action. Replaces the former Core/Settings/TimeTickController, Core/UI/Lights, Core/UI/Tool/TimeController and Core/Actions/LightingServiceImpl.",
|
||||||
|
"category": "engine, library",
|
||||||
|
"sources": [
|
||||||
|
"src/**/*.cs"
|
||||||
|
],
|
||||||
|
"depends": ["interface>=1"],
|
||||||
|
"ui": []
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
using Cthangover.Core.Actions;
|
||||||
|
|
||||||
|
namespace Cthangover.Mods.Time
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Resets the lighting depth and albedo maps to null via the lighting
|
||||||
|
/// service. This effectively disables scene-specific lighting masks,
|
||||||
|
/// reverting to default flat lighting. Used when transitioning between
|
||||||
|
/// scenes that have different lighting setups or when entering a scene
|
||||||
|
/// that doesn't use depth-based lighting.
|
||||||
|
/// </summary>
|
||||||
|
public class LightingClearMapAction : IScenarioAction
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Registered as "lighting.clear_map" — resets the depth and albedo
|
||||||
|
/// lighting maps to null via UiLightController. Both maps are
|
||||||
|
/// cleared simultaneously because the lighting shader requires
|
||||||
|
/// either both present or neither — partial clearing produces
|
||||||
|
/// visual artifacts. Use when transitioning between scenes that
|
||||||
|
/// have different lighting setups, or when entering a scene that
|
||||||
|
/// doesn't use depth-based lighting.
|
||||||
|
/// </summary>
|
||||||
|
public string Name => "lighting.clear_map";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Delegates to ctx.Lighting.ClearDepthMap which nulls out both
|
||||||
|
/// depth and albedo textures on the UiLightController singleton.
|
||||||
|
/// Safe to call when the controller hasn't been initialized
|
||||||
|
/// (null-conditional access in the implementation).
|
||||||
|
/// </summary>
|
||||||
|
public void Run(IActionContext ctx)
|
||||||
|
{
|
||||||
|
ctx.Lighting.ClearDepthMap();
|
||||||
|
ctx.Log("SHADER", "LightingClearMapAction: cleared depth and albedo maps");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
using Cthangover.Core.Actions;
|
||||||
|
|
||||||
|
namespace Cthangover.Mods.Time
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Thin wrapper around UiLightController.Instance. ClearDepthMap sets both
|
||||||
|
/// depth and albedo to null simultaneously — partial clearing would leave
|
||||||
|
/// the shader in an inconsistent state. SetUseTime delegates to the
|
||||||
|
/// controller's IsUseLight property.
|
||||||
|
/// </summary>
|
||||||
|
public class LightingServiceImpl : ILightingService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Sets both depth and albedo maps to null on UiLightController.
|
||||||
|
/// Both are cleared together because the lighting shader requires
|
||||||
|
/// either both textures present or neither — partial clearing
|
||||||
|
/// produces visual artifacts. Safe to call when the controller
|
||||||
|
/// hasn't been initialized yet (null-conditional access).
|
||||||
|
/// </summary>
|
||||||
|
public void ClearDepthMap()
|
||||||
|
{
|
||||||
|
var controller = UiLightController.Instance;
|
||||||
|
controller?.SetupDepthMap(null);
|
||||||
|
controller?.SetupAlbedoMap(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Toggles the IsUseLight property on UiLightController. When
|
||||||
|
/// true, the controller applies time-of-day lighting adjustments;
|
||||||
|
/// when false, lighting remains static. Null-safe: if the
|
||||||
|
/// controller singleton is absent (pre-initialization), the call
|
||||||
|
/// is silently skipped.
|
||||||
|
/// </summary>
|
||||||
|
public void SetUseTime(bool useTime)
|
||||||
|
{
|
||||||
|
var controller = UiLightController.Instance;
|
||||||
|
if (controller != null)
|
||||||
|
controller.IsUseLight = useTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
using Cthangover.Core.Actions;
|
||||||
|
using Cthangover.Core.Scenes;
|
||||||
|
using Godot;
|
||||||
|
|
||||||
|
namespace Cthangover.Mods.Time
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Static adapters bound into the kernel <see cref="LightingBridge"/> on
|
||||||
|
/// <c>OnModResourcesReady</c>. They reproduce the two lighting touch
|
||||||
|
/// points the kernel SceneManager used to own directly: resetting the
|
||||||
|
/// per-scene static lights and lighting maps when a scene switches, and
|
||||||
|
/// pushing background depth/albedo textures onto the scene controller.
|
||||||
|
/// Both degrade to no-ops when the lighting controller is absent, so the
|
||||||
|
/// calls stay safe in scenes without a "Lights" node.
|
||||||
|
/// </summary>
|
||||||
|
public static class LightingHooks
|
||||||
|
{
|
||||||
|
/// <summary>Clears static lights and both lighting maps on the scene's controller; no-op when the scene has none.</summary>
|
||||||
|
public static void ClearSceneLights()
|
||||||
|
{
|
||||||
|
var lightController = SceneContextNode.FindNode<UiLightController>("Lights");
|
||||||
|
if (lightController == null)
|
||||||
|
return;
|
||||||
|
lightController.ClearStaticLights();
|
||||||
|
lightController.SetupDepthMap(null);
|
||||||
|
lightController.SetupAlbedoMap(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Pushes background depth/albedo textures onto the controller singleton; skipped when it is not in the tree.</summary>
|
||||||
|
public static void SetupBackgroundMaps(Texture2D depth, Texture2D albedo)
|
||||||
|
{
|
||||||
|
var controller = UiLightController.Instance;
|
||||||
|
controller?.SetupDepthMap(depth);
|
||||||
|
controller?.SetupAlbedoMap(albedo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
using Cthangover.Core.Scenes;
|
||||||
|
using Cthangover.Core.Settings;
|
||||||
|
using Cthangover.Core.UI.Tool;
|
||||||
|
using Cthangover.Core.Utils;
|
||||||
|
using Godot;
|
||||||
|
|
||||||
|
using Cthangover.Mods.Interface.Tool;
|
||||||
|
namespace Cthangover.Mods.Time
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Draggable lamp UI element for the player's portable light source.
|
||||||
|
/// Input handling exists in both _GuiInput (bubbled) and _Input (global)
|
||||||
|
/// to catch drags that start on the lamp but continue outside its rect.
|
||||||
|
/// When the lamp is hidden, it resets position to offscreen (-1000,-1000)
|
||||||
|
/// to disable the light in the shader. GetLightParams() is a static helper
|
||||||
|
/// so the light controller can query lamp parameters without coupling to
|
||||||
|
/// the behaviour instance. Inherits from TextureRectModIconLoader for
|
||||||
|
/// mod-provided lamp icon textures.
|
||||||
|
/// </summary>
|
||||||
|
public partial class LampBehaviour : TextureRectModIconLoader
|
||||||
|
{
|
||||||
|
private UiLightController controller;
|
||||||
|
private Vector2 dragPosition;
|
||||||
|
private bool isPlaced;
|
||||||
|
|
||||||
|
private bool pressingOnLamp;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Queries lamp parameters (color, radius, influence) from
|
||||||
|
/// <see cref="GameData.Instance.Runtime.LampData"/>. Static so the
|
||||||
|
/// <see cref="UiLightController"/> can query without a direct reference.
|
||||||
|
/// </summary>
|
||||||
|
public static (Color, float, float) GetLightParams()
|
||||||
|
{
|
||||||
|
var lampData = GameData.Instance.Runtime.LampData;
|
||||||
|
var radius = lampData.Radius;
|
||||||
|
var influence = lampData.Influence;
|
||||||
|
return (Colors.Yellow, radius, influence);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Visibility toggle for the lamp. Setting to <c>false</c> resets the
|
||||||
|
/// placed state and sends the light position offscreen to disable
|
||||||
|
/// the lamp light in the shader. Uses <c>new</c> to log transitions.
|
||||||
|
/// </summary>
|
||||||
|
public new bool Visible
|
||||||
|
{
|
||||||
|
get => base.Visible;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (base.Visible != value)
|
||||||
|
GameLogger.Log("LIGHT", $"Lamp.Visible: {base.Visible} -> {value}, isPlaced={isPlaced}", LogLevel.Debug);
|
||||||
|
base.Visible = value;
|
||||||
|
if (!value)
|
||||||
|
{
|
||||||
|
pressingOnLamp = false;
|
||||||
|
if (isPlaced)
|
||||||
|
{
|
||||||
|
isPlaced = false;
|
||||||
|
controller?.UpdateLightPos(new Vector2(-1000f, -1000f));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Finds the shared <see cref="UiLightController"/> this lamp pushes positions to.</summary>
|
||||||
|
public override void _Ready()
|
||||||
|
{
|
||||||
|
base._Ready();
|
||||||
|
MouseFilter = MouseFilterEnum.Stop;
|
||||||
|
controller = SceneContextNode.FindNode<UiLightController>("Lights");
|
||||||
|
GameLogger.Log("LIGHT", $"Lamp._Ready: controller={(controller != null ? "OK" : "NULL")}", LogLevel.Debug);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Click to place the light at the cursor, drag to move it, and click a placed lamp to pick it back up.</summary>
|
||||||
|
public override void _GuiInput(InputEvent @event)
|
||||||
|
{
|
||||||
|
if (@event is InputEventMouseButton mouseButton)
|
||||||
|
{
|
||||||
|
if (mouseButton.ButtonIndex == MouseButton.Left)
|
||||||
|
{
|
||||||
|
if (mouseButton.Pressed)
|
||||||
|
{
|
||||||
|
pressingOnLamp = true;
|
||||||
|
dragPosition = GetGlobalMousePosition();
|
||||||
|
|
||||||
|
if (isPlaced)
|
||||||
|
{
|
||||||
|
dragPosition = new Vector2(-1000f, -1000f);
|
||||||
|
controller?.UpdateLightPos(dragPosition);
|
||||||
|
isPlaced = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (pressingOnLamp && !isPlaced)
|
||||||
|
{
|
||||||
|
isPlaced = true;
|
||||||
|
controller?.UpdateLightPos(dragPosition);
|
||||||
|
}
|
||||||
|
pressingOnLamp = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AcceptEvent();
|
||||||
|
}
|
||||||
|
else if (@event is InputEventMouseMotion mouseMotion)
|
||||||
|
{
|
||||||
|
if (Input.IsMouseButtonPressed(MouseButton.Left))
|
||||||
|
{
|
||||||
|
dragPosition = GetGlobalMousePosition();
|
||||||
|
controller?.UpdateLightPos(dragPosition);
|
||||||
|
isPlaced = false;
|
||||||
|
AcceptEvent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Keeps dragging the placed light while the pointer moves outside the lamp's own control rect.</summary>
|
||||||
|
public override void _Input(InputEvent @event)
|
||||||
|
{
|
||||||
|
if (!Visible)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (@event is InputEventMouseMotion mouseMotion
|
||||||
|
&& Input.IsMouseButtonPressed(MouseButton.Left)
|
||||||
|
&& pressingOnLamp)
|
||||||
|
{
|
||||||
|
dragPosition = GetGlobalMousePosition();
|
||||||
|
controller?.UpdateLightPos(dragPosition);
|
||||||
|
isPlaced = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (@event is InputEventMouseButton mouseButton
|
||||||
|
&& mouseButton.ButtonIndex == MouseButton.Left
|
||||||
|
&& !mouseButton.Pressed
|
||||||
|
&& pressingOnLamp)
|
||||||
|
{
|
||||||
|
if (!isPlaced)
|
||||||
|
{
|
||||||
|
isPlaced = true;
|
||||||
|
controller?.UpdateLightPos(dragPosition);
|
||||||
|
}
|
||||||
|
pressingOnLamp = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using Godot;
|
||||||
|
|
||||||
|
namespace Cthangover.Mods.Time
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Serializable light definition for JSON scene lighting. Coordinates are
|
||||||
|
/// viewport-relative (0.0–1.0) so scenes adapt to different resolutions.
|
||||||
|
/// ToPixelPos/FromPixelPos convert between normalized space and pixel space
|
||||||
|
/// using a reference viewport size. Color is stored as a hex string for
|
||||||
|
/// human-readable JSON editing.
|
||||||
|
/// </summary>
|
||||||
|
public class LightDef
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Normalized horizontal position (0.0–1.0) relative to viewport width.
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("x")]
|
||||||
|
public float X { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Normalized vertical position (0.0–1.0) relative to viewport height.
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("y")]
|
||||||
|
public float Y { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Light radius in pixel units. Determines the falloff distance in the
|
||||||
|
/// lighting shader.
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("radius")]
|
||||||
|
public float Radius { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Light intensity (0.0–1.0). At <c>1.0</c>, the light fully brightens
|
||||||
|
/// pixels within its radius. Lower values create subtler effects.
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("influence")]
|
||||||
|
public float Influence { get; set; } = 1f;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Light color as a hex string (e.g. "#ff8800"). Parsed by
|
||||||
|
/// <see cref="ToColor"/> for shader upload.
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("color")]
|
||||||
|
public string ColorHex { get; set; } = "#ffff00";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converts <see cref="ColorHex"/> to a Godot <c>Color</c> struct.
|
||||||
|
/// </summary>
|
||||||
|
public Color ToColor()
|
||||||
|
{
|
||||||
|
return new Color(ColorHex);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converts normalized coordinates (<see cref="X"/>, <see cref="Y"/>) to
|
||||||
|
/// pixel coordinates based on <paramref name="viewportSize"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="viewportSize">Current viewport dimensions in pixels.</param>
|
||||||
|
public Vector2 ToPixelPos(Vector2 viewportSize)
|
||||||
|
{
|
||||||
|
return new Vector2(X * viewportSize.X, Y * viewportSize.Y);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Computes normalized coordinates from pixel coordinates by dividing by
|
||||||
|
/// <paramref name="referenceSize"/>. Enables saving light positions
|
||||||
|
/// across different resolutions.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="pixelPos">Light position in pixel space.</param>
|
||||||
|
/// <param name="referenceSize">Viewport dimensions used as reference.</param>
|
||||||
|
public void FromPixelPos(Vector2 pixelPos, Vector2 referenceSize)
|
||||||
|
{
|
||||||
|
X = pixelPos.X / referenceSize.X;
|
||||||
|
Y = pixelPos.Y / referenceSize.Y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,519 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text.Json;
|
||||||
|
using Cthangover.Core.Scenes;
|
||||||
|
using Cthangover.Core.Settings;
|
||||||
|
using Cthangover.Core.UI.Event;
|
||||||
|
using Cthangover.Mods.Interface.View;
|
||||||
|
using Cthangover.Core.Utils;
|
||||||
|
using Godot;
|
||||||
|
|
||||||
|
namespace Cthangover.Mods.Time
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Singleton 2D lighting controller that drives a multi-light shader system.
|
||||||
|
/// Maintains up to 11 lights: index 0 is the player's lamp, indices 1-10 are
|
||||||
|
/// static scene lights. Pushes uniform arrays (positions, colors, radii,
|
||||||
|
/// influence) to all registered ShaderMaterials — the ViewBox background/
|
||||||
|
/// foreground plus any externally registered materials. The time-of-day
|
||||||
|
/// system blends four phases (morning/day/evening/night) via a Vector4 weights
|
||||||
|
/// uniform, with smooth transitions between phases. DarkMode overrides time
|
||||||
|
/// with full-night weights. Materials are discovered lazily via SceneContextNode
|
||||||
|
/// and cached, with a registration system for extra materials (e.g. mod-added
|
||||||
|
/// sprites that need lighting). _EnterTree manages singleton enforcement
|
||||||
|
/// with duplicate warnings, not assertions, to avoid blocking scene reloads.
|
||||||
|
/// </summary>
|
||||||
|
public partial class UiLightController : Control, IOnTimeEvent
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Singleton accessor for the active controller. Set in <see cref="_EnterTree"/>,
|
||||||
|
/// cleared in <see cref="_ExitTree"/>. Duplicate instances log an error but
|
||||||
|
/// overwrite the reference.
|
||||||
|
/// </summary>
|
||||||
|
public static UiLightController Instance { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Default depth map texture applied on startup for parallax lighting effects.
|
||||||
|
/// </summary>
|
||||||
|
[Export] public Texture2D DefaultDepthMap { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Default albedo (color) texture applied on startup for lighting blending.
|
||||||
|
/// </summary>
|
||||||
|
[Export] public Texture2D DefaultAlbedoMap { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// When <c>true</c>, forces full-night time-of-day weights regardless of
|
||||||
|
/// in-game time. Set from game settings or scenario commands.
|
||||||
|
/// </summary>
|
||||||
|
[Export] public bool DarkMode { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Master toggle for time-of-day lighting. When <c>false</c>, time blending
|
||||||
|
/// is disabled on all shader materials.
|
||||||
|
/// </summary>
|
||||||
|
[Export] public bool UseLight { get; set; } = true;
|
||||||
|
|
||||||
|
private int staticLightCount;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The currently active depth map texture, pushed to all shader materials.
|
||||||
|
/// </summary>
|
||||||
|
public Texture2D CurrentDepthMap { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The currently active albedo texture, pushed to all shader materials.
|
||||||
|
/// </summary>
|
||||||
|
public Texture2D CurrentAlbedoMap { get; private set; }
|
||||||
|
|
||||||
|
private SceneEventController eventController;
|
||||||
|
private ShaderMaterial viewBgMaterial;
|
||||||
|
private ShaderMaterial viewFgMaterial;
|
||||||
|
private readonly List<ShaderMaterial> extraMaterials = new();
|
||||||
|
|
||||||
|
private readonly Vector2[] lightPositions = new Vector2[11];
|
||||||
|
private readonly Color[] lightColors = new Color[11];
|
||||||
|
private readonly float[] lightRadii = new float[11];
|
||||||
|
private readonly float[] lightInfluence = new float[11];
|
||||||
|
|
||||||
|
private LampBehaviour lampBehaviour;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Priority <c>0</c> — the lighting controller runs before most other timer-tick
|
||||||
|
/// subscribers to ensure shader params are updated first.
|
||||||
|
/// </summary>
|
||||||
|
public int Priority => 0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Runtime dark mode toggle. Updates <see cref="DarkMode"/> and immediately
|
||||||
|
/// recalculates time-of-day weights.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsDarkMode
|
||||||
|
{
|
||||||
|
get => DarkMode;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
DarkMode = value;
|
||||||
|
SetTime(GameData.Instance?.Runtime?.Time?.Normalized ?? 0f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Runtime toggle for time-of-day blending. Pushes the new value to all
|
||||||
|
/// registered shader materials.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsUseLight
|
||||||
|
{
|
||||||
|
get => UseLight;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
UseLight = value;
|
||||||
|
ChangeUseTime(UseLight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Sets the singleton with a diagnostic log when a duplicate controller enters the tree first.</summary>
|
||||||
|
public override void _EnterTree()
|
||||||
|
{
|
||||||
|
if (Instance != null && GodotObject.IsInstanceValid(Instance))
|
||||||
|
{
|
||||||
|
var scene = GetTree()?.CurrentScene?.Name ?? "?";
|
||||||
|
var existingPath = Instance.GetPath().ToString();
|
||||||
|
var myPath = GetPath().ToString();
|
||||||
|
GameLogger.Log("DUPLICATE", $"UiLightController._EnterTree: Instance ALREADY SET by '{existingPath}', overwriting with duplicate at '{myPath}' on scene '{scene}'", LogLevel.Error);
|
||||||
|
}
|
||||||
|
Instance = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Clears the singleton and detaches from the timer ticks when the controller leaves the tree.</summary>
|
||||||
|
public override void _ExitTree()
|
||||||
|
{
|
||||||
|
if (Instance == this)
|
||||||
|
Instance = null;
|
||||||
|
eventController?.RemoveTimerTickEventListener(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Initializes shader maps, parks the lamp offscreen and subscribes to timer ticks for time-of-day blending.</summary>
|
||||||
|
public override void _Ready()
|
||||||
|
{
|
||||||
|
MouseFilter = MouseFilterEnum.Ignore;
|
||||||
|
|
||||||
|
UpdateLampLightParams();
|
||||||
|
lightPositions[0] = new Vector2(-1000f, -1000f);
|
||||||
|
|
||||||
|
SetupDepthMap(DefaultDepthMap);
|
||||||
|
SetupAlbedoMap(DefaultAlbedoMap);
|
||||||
|
|
||||||
|
eventController = SceneContextNode.FindNode<SceneEventController>("EventController");
|
||||||
|
eventController?.AddTimerTickEventListener(this);
|
||||||
|
|
||||||
|
GameLogger.Log("LIGHT", $"LightsCtrl._Ready: eventCtrl={(eventController != null ? "OK" : "NULL")}, DarkMode={DarkMode}, UseLight={UseLight}", LogLevel.Debug);
|
||||||
|
|
||||||
|
CallDeferred(nameof(SetupTime));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reads lamp parameters from <see cref="LampBehaviour.GetLightParams"/>
|
||||||
|
/// and updates the player light slot (index 0) in the uniform arrays.
|
||||||
|
/// </summary>
|
||||||
|
public void UpdateLampLightParams()
|
||||||
|
{
|
||||||
|
var lightParams = LampBehaviour.GetLightParams();
|
||||||
|
lightColors[0] = lightParams.Item1;
|
||||||
|
lightRadii[0] = lightParams.Item2;
|
||||||
|
lightInfluence[0] = lightParams.Item3;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resets all static light slots (indices 1–10) to off-screen positions
|
||||||
|
/// with zero radius and influence, then pushes to shader materials.
|
||||||
|
/// </summary>
|
||||||
|
public void ClearStaticLights()
|
||||||
|
{
|
||||||
|
for (int i = 1; i < 11; i++)
|
||||||
|
{
|
||||||
|
lightPositions[i] = new Vector2(-1000f, -1000f);
|
||||||
|
lightRadii[i] = 0f;
|
||||||
|
lightInfluence[i] = 0f;
|
||||||
|
lightColors[i] = Colors.White;
|
||||||
|
}
|
||||||
|
|
||||||
|
staticLightCount = 0;
|
||||||
|
UpdateShaders();
|
||||||
|
|
||||||
|
GameLogger.Log("LIGHT", "LightsCtrl.ClearStaticLights: all static lights cleared");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deserializes a JSON array of <see cref="LightDef"/> objects and populates
|
||||||
|
/// static light slots 1–10. Supports up to 10 static lights.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="json">JSON string containing an array of <see cref="LightDef"/> objects.</param>
|
||||||
|
public void SetStaticLights(string json)
|
||||||
|
{
|
||||||
|
ClearStaticLights();
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(json))
|
||||||
|
return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var lights = JsonSerializer.Deserialize<List<LightDef>>(json, JsonOptionsProvider.ReadOptions);
|
||||||
|
|
||||||
|
if (lights == null || lights.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
int count = Mathf.Min(lights.Count, 10);
|
||||||
|
var vpSize = GetViewport()?.GetVisibleRect().Size ?? new Vector2(1920, 1024);
|
||||||
|
for (int i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
lightPositions[i + 1] = lights[i].ToPixelPos(vpSize);
|
||||||
|
lightRadii[i + 1] = lights[i].Radius;
|
||||||
|
lightInfluence[i + 1] = lights[i].Influence;
|
||||||
|
lightColors[i + 1] = lights[i].ToColor();
|
||||||
|
}
|
||||||
|
|
||||||
|
staticLightCount = count+1;
|
||||||
|
UpdateShaders();
|
||||||
|
|
||||||
|
GameLogger.Log("LIGHT", $"LightsCtrl.SetStaticLights: parsed {lights.Count} light(s), applied {count} static lights");
|
||||||
|
}
|
||||||
|
|
||||||
|
catch (System.Exception ex)
|
||||||
|
{
|
||||||
|
GameLogger.Log("LIGHT", $"LightsCtrl.SetStaticLights: JSON parse error: {ex.Message}", LogLevel.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetupTime()
|
||||||
|
{
|
||||||
|
SetTime(GameData.Instance?.Runtime?.Time?.Normalized ?? 0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Timer-tick callback (~1/sec). Updates lamp visibility based on time of
|
||||||
|
/// day and recalculates time-of-day weights for shaders.
|
||||||
|
/// </summary>
|
||||||
|
public void OnTimerTick()
|
||||||
|
{
|
||||||
|
lampBehaviour ??= FindLampChild();
|
||||||
|
|
||||||
|
if (lampBehaviour != null)
|
||||||
|
{
|
||||||
|
var time = GameData.Instance?.Runtime?.Time;
|
||||||
|
var hours = time?.Hours ?? 0;
|
||||||
|
var newVis = DarkMode || !(hours >= 6 && hours < 22);
|
||||||
|
lampBehaviour.Visible = newVis;
|
||||||
|
}
|
||||||
|
|
||||||
|
SetTime(GameData.Instance?.Runtime?.Time?.Normalized ?? 0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Pushes the <c>use_time</c> shader parameter to all registered materials.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">The new <c>use_time</c> uniform value.</param>
|
||||||
|
public void ChangeUseTime(bool value)
|
||||||
|
{
|
||||||
|
ForEachMaterial(m => m.SetShaderParameter("use_time", value));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a <see cref="ShaderMaterial"/> to receive lighting uniform updates
|
||||||
|
/// alongside the ViewBox background/foreground. Duplicate registrations
|
||||||
|
/// are silently ignored.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="material">The material to register for lighting updates.</param>
|
||||||
|
public void RegisterMaterial(ShaderMaterial material)
|
||||||
|
{
|
||||||
|
if (material == null || extraMaterials.Contains(material))
|
||||||
|
return;
|
||||||
|
extraMaterials.Add(material);
|
||||||
|
PushAllParams(material);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes a previously registered material from the lighting update list.
|
||||||
|
/// No-op if the material is not currently registered.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="material">The material to unregister.</param>
|
||||||
|
public void UnregisterMaterial(ShaderMaterial material)
|
||||||
|
{
|
||||||
|
extraMaterials.Remove(material);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PushAllParams(ShaderMaterial material)
|
||||||
|
{
|
||||||
|
var weights = DarkMode
|
||||||
|
? new Vector4(0, 0, 0, 1)
|
||||||
|
: CalculateTimeWeights(GameData.Instance?.Runtime?.Time?.Normalized ?? 0f);
|
||||||
|
material.SetShaderParameter("time_weights", weights);
|
||||||
|
material.SetShaderParameter("use_time", UseLight);
|
||||||
|
|
||||||
|
var posArray = new Godot.Collections.Array<Vector2>();
|
||||||
|
var colArray = new Godot.Collections.Array<Color>();
|
||||||
|
var radArray = new Godot.Collections.Array<float>();
|
||||||
|
var infArray = new Godot.Collections.Array<float>();
|
||||||
|
for (int i = 0; i < 11; i++)
|
||||||
|
{
|
||||||
|
posArray.Add(lightPositions[i]);
|
||||||
|
colArray.Add(lightColors[i]);
|
||||||
|
radArray.Add(lightRadii[i]);
|
||||||
|
infArray.Add(lightInfluence[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
int count = 0;
|
||||||
|
if (lightPositions[0].X > -100)
|
||||||
|
count++;
|
||||||
|
count += staticLightCount;
|
||||||
|
|
||||||
|
material.SetShaderParameter("light_count", count);
|
||||||
|
material.SetShaderParameter("light_positions", posArray);
|
||||||
|
material.SetShaderParameter("light_colors", colArray);
|
||||||
|
material.SetShaderParameter("light_radii", radArray);
|
||||||
|
material.SetShaderParameter("light_influence", infArray);
|
||||||
|
material.SetShaderParameter("light_radius_scale", 1.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the depth map texture and pushes it to all shader materials via
|
||||||
|
/// the <c>depth_mask</c> uniform.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="depthMap">The new depth map texture.</param>
|
||||||
|
public void SetupDepthMap(Texture2D depthMap)
|
||||||
|
{
|
||||||
|
CurrentDepthMap = depthMap;
|
||||||
|
ForEachMaterial(m => m.SetShaderParameter("depth_mask", CurrentDepthMap));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the albedo map texture and pushes it to all shader materials via
|
||||||
|
/// the <c>albedo_map</c> uniform.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="albedoMap">The new albedo texture.</param>
|
||||||
|
public void SetupAlbedoMap(Texture2D albedoMap)
|
||||||
|
{
|
||||||
|
CurrentAlbedoMap = albedoMap;
|
||||||
|
ForEachMaterial(m => m.SetShaderParameter("albedo_map", CurrentAlbedoMap));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Rebuilds and pushes all lighting uniform arrays to every registered
|
||||||
|
/// shader material.
|
||||||
|
/// </summary>
|
||||||
|
public void UpdateShaders()
|
||||||
|
{
|
||||||
|
int count = 0;
|
||||||
|
if (lightPositions[0].X > -100)
|
||||||
|
count++;
|
||||||
|
count += staticLightCount;
|
||||||
|
|
||||||
|
SetShaderArrays(count);
|
||||||
|
ChangeUseTime(UseLight);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates the player lamp position (slot 0) and pushes to all shaders.
|
||||||
|
/// If the position is off-screen (X < -100), the lamp is excluded
|
||||||
|
/// from the active light count.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="position">New lamp position in screen-space pixels.</param>
|
||||||
|
public void UpdateLightPos(Vector2 position)
|
||||||
|
{
|
||||||
|
lightPositions[0] = position;
|
||||||
|
int count = 0;
|
||||||
|
if (lightPositions[0].X > -100)
|
||||||
|
count++;
|
||||||
|
count += staticLightCount;
|
||||||
|
|
||||||
|
SetShaderArrays(count);
|
||||||
|
ChangeUseTime(UseLight);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Directly sets the player light position, radius, and influence without
|
||||||
|
/// triggering a shader push. Call <see cref="UpdateShaders"/> afterward.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="position">Screen-space pixel position.</param>
|
||||||
|
/// <param name="radius">Light radius in pixels.</param>
|
||||||
|
/// <param name="influence">Light influence factor (0.0–1.0).</param>
|
||||||
|
public void SetPlayerLight(Vector2 position, float radius, float influence)
|
||||||
|
{
|
||||||
|
lightPositions[0] = position;
|
||||||
|
lightRadii[0] = radius;
|
||||||
|
lightInfluence[0] = influence;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Calculates time-of-day blend weights and pushes to all shader materials.
|
||||||
|
/// Respects <see cref="DarkMode"/> — when active, full-night weights are used
|
||||||
|
/// regardless of <paramref name="normalizedHour"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="normalizedHour">Hour in 0–24 range (may be fractional).</param>
|
||||||
|
public void SetTime(float normalizedHour)
|
||||||
|
{
|
||||||
|
if (DarkMode)
|
||||||
|
{
|
||||||
|
var night = new Vector4(0, 0, 0, 1);
|
||||||
|
ForEachMaterial(m => m.SetShaderParameter("time_weights", night));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var weights = CalculateTimeWeights(normalizedHour);
|
||||||
|
ForEachMaterial(m => m.SetShaderParameter("time_weights", weights));
|
||||||
|
}
|
||||||
|
UpdateShaders();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetShaderArrays(int count)
|
||||||
|
{
|
||||||
|
var posArray = new Godot.Collections.Array<Vector2>();
|
||||||
|
var colArray = new Godot.Collections.Array<Color>();
|
||||||
|
var radArray = new Godot.Collections.Array<float>();
|
||||||
|
var infArray = new Godot.Collections.Array<float>();
|
||||||
|
|
||||||
|
for (int i = 0; i < 11; i++)
|
||||||
|
{
|
||||||
|
posArray.Add(lightPositions[i]);
|
||||||
|
colArray.Add(lightColors[i]);
|
||||||
|
radArray.Add(lightRadii[i]);
|
||||||
|
infArray.Add(lightInfluence[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
ForEachMaterial(m =>
|
||||||
|
{
|
||||||
|
m.SetShaderParameter("light_count", count);
|
||||||
|
m.SetShaderParameter("light_positions", posArray);
|
||||||
|
m.SetShaderParameter("light_colors", colArray);
|
||||||
|
m.SetShaderParameter("light_radii", radArray);
|
||||||
|
m.SetShaderParameter("light_influence", infArray);
|
||||||
|
m.SetShaderParameter("light_radius_scale", 1.0f);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ForEachMaterial(System.Action<ShaderMaterial> action)
|
||||||
|
{
|
||||||
|
if (viewBgMaterial == null || viewFgMaterial == null)
|
||||||
|
{
|
||||||
|
var viewBox = SceneContextNode.FindNode<ViewBox>("ViewBox");
|
||||||
|
if (viewBox != null)
|
||||||
|
{
|
||||||
|
viewBgMaterial ??= viewBox.Background?.Material as ShaderMaterial;
|
||||||
|
viewFgMaterial ??= viewBox.Foreground?.Material as ShaderMaterial;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (viewBgMaterial != null) action(viewBgMaterial);
|
||||||
|
if (viewFgMaterial != null) action(viewFgMaterial);
|
||||||
|
|
||||||
|
for (int i = extraMaterials.Count - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
if (extraMaterials[i] == null)
|
||||||
|
extraMaterials.RemoveAt(i);
|
||||||
|
else
|
||||||
|
action(extraMaterials[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private LampBehaviour FindLampChild()
|
||||||
|
{
|
||||||
|
return SceneContextNode.FindNode<LampBehaviour>("Lamp");
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// 06:00-10:00 - Morning
|
||||||
|
// 10:00-18:00 - Day
|
||||||
|
// 18:00-22:00 - Evening
|
||||||
|
// 22:00-06:00 - Night
|
||||||
|
//
|
||||||
|
private static Vector4 CalculateTimeWeights(float hour)
|
||||||
|
{
|
||||||
|
float night = 0.0f;
|
||||||
|
float morning = 0.0f;
|
||||||
|
float day = 0.0f;
|
||||||
|
float evening = 0.0f;
|
||||||
|
|
||||||
|
if (hour >= 22.0f || hour < 4.0f)
|
||||||
|
{
|
||||||
|
night = 1.0f;
|
||||||
|
}
|
||||||
|
else if (hour >= 4.0f && hour < 6.0f)
|
||||||
|
{
|
||||||
|
float t = (hour - 4.0f) / 2.0f;
|
||||||
|
night = 1.0f - t;
|
||||||
|
morning = t;
|
||||||
|
}
|
||||||
|
else if (hour >= 6.0f && hour < 9.0f)
|
||||||
|
{
|
||||||
|
morning = 1.0f;
|
||||||
|
}
|
||||||
|
else if (hour >= 9.0f && hour < 11.0f)
|
||||||
|
{
|
||||||
|
float t = (hour - 9.0f) / 2.0f;
|
||||||
|
morning = 1.0f - t;
|
||||||
|
day = t;
|
||||||
|
}
|
||||||
|
else if (hour >= 11.0f && hour < 17.0f)
|
||||||
|
{
|
||||||
|
day = 1.0f;
|
||||||
|
}
|
||||||
|
else if (hour >= 17.0f && hour < 19.0f)
|
||||||
|
{
|
||||||
|
float t = (hour - 17.0f) / 2.0f;
|
||||||
|
day = 1.0f - t;
|
||||||
|
evening = t;
|
||||||
|
}
|
||||||
|
else if (hour >= 19.0f && hour < 20.0f)
|
||||||
|
{
|
||||||
|
evening = 1.0f;
|
||||||
|
}
|
||||||
|
else if (hour >= 20.0f && hour < 22.0f)
|
||||||
|
{
|
||||||
|
float t = (hour - 20.0f) / 2.0f;
|
||||||
|
evening = 1.0f - t;
|
||||||
|
night = t;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Vector4(morning, day, evening, night);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
namespace Cthangover.Mods.Time
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Marks the time engine mod for subsystem coverage discovery: the
|
||||||
|
/// coverage test scans loaded assemblies for this marker and requires a
|
||||||
|
/// declared integration test for the subsystem.
|
||||||
|
/// </summary>
|
||||||
|
public static class SubsystemRegistration
|
||||||
|
{
|
||||||
|
public static string SubsystemId => "time";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
using Cthangover.Core.Settings;
|
||||||
|
using Cthangover.Core.UI.Event;
|
||||||
|
using Godot;
|
||||||
|
|
||||||
|
namespace Cthangover.Mods.Time
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// In-game clock: subscribes to SceneEventController timer ticks and advances
|
||||||
|
/// GameData.Runtime.Time. Renders the time text on a Label and applies
|
||||||
|
/// Godot.Engine.TimeScale from the exported timeScale field for global
|
||||||
|
/// time acceleration. The #if TOOLS block updates the rendered time in the
|
||||||
|
/// editor inspector when properties change, giving designers live time preview.
|
||||||
|
/// </summary>
|
||||||
|
public partial class TimeController : Control, IOnTimeEvent
|
||||||
|
{
|
||||||
|
|
||||||
|
[Export] private float timeScale = 1f;
|
||||||
|
private Label timerField;
|
||||||
|
private SceneEventController eventController;
|
||||||
|
|
||||||
|
private PhaseType lastPhase;
|
||||||
|
|
||||||
|
/// <summary>Finds the clock label, subscribes to timer ticks and renders the initial time.</summary>
|
||||||
|
public override void _Ready()
|
||||||
|
{
|
||||||
|
timerField = GetNodeOrNull<Label>("TimeLabel");
|
||||||
|
eventController = GetTree()?.Root?.GetNodeOrNull<SceneEventController>("EventController")
|
||||||
|
?? GetNodeOrNull<SceneEventController>("/root/EventController");
|
||||||
|
eventController?.AddTimerTickEventListener(this);
|
||||||
|
OnTimerTick();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Detaches from the timer ticks when the clock leaves the scene.</summary>
|
||||||
|
public override void _ExitTree()
|
||||||
|
{
|
||||||
|
eventController?.RemoveTimerTickEventListener(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Priority in the <see cref="SceneEventController"/> listener chain. Returns 0 (default).
|
||||||
|
/// </summary>
|
||||||
|
public int Priority => 0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called by <see cref="SceneEventController"/> on each timer tick. Delegates to
|
||||||
|
/// <see cref="UpdateRenderedTime"/> to refresh the on-screen clock label.
|
||||||
|
/// </summary>
|
||||||
|
public void OnTimerTick()
|
||||||
|
{
|
||||||
|
UpdateRenderedTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Applies <c>timeScale</c> to <c>Engine.TimeScale</c> (global time speed), reads the
|
||||||
|
/// current in-game time from <see cref="GameData.Runtime.Time"/>, and writes its text
|
||||||
|
/// representation to the <c>TimeLabel</c> node.
|
||||||
|
/// </summary>
|
||||||
|
public void UpdateRenderedTime()
|
||||||
|
{
|
||||||
|
Godot.Engine.TimeScale = timeScale;
|
||||||
|
var time = GameData.Instance.Runtime.Time;
|
||||||
|
var text = time?.Text ?? string.Empty;
|
||||||
|
if (timerField != null)
|
||||||
|
timerField.Text = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
#if TOOLS
|
||||||
|
/// <summary>Refreshes the live time preview whenever an exported property changes in the editor inspector.</summary>
|
||||||
|
public override void _ValidateProperty(Godot.Collections.Dictionary property)
|
||||||
|
{
|
||||||
|
base._ValidateProperty(property);
|
||||||
|
OnTimerTick();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
using Cthangover.Core.Actions;
|
||||||
|
using Cthangover.Core.Mods;
|
||||||
|
using Cthangover.Core.Utils;
|
||||||
|
using Godot;
|
||||||
|
|
||||||
|
namespace Cthangover.Mods.Time
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Wires the time subsystem into the kernel. On
|
||||||
|
/// <c>OnModResourcesReady</c> it installs the lighting adapters into
|
||||||
|
/// <see cref="LightingBridge"/> (first registration wins) and registers
|
||||||
|
/// the <see cref="TimeTickController"/> with the scene event system, so
|
||||||
|
/// in-game time advances each timer pulse. The controller lives in a
|
||||||
|
/// static field: <c>EnsureRegistered</c> is idempotent per instance, and
|
||||||
|
/// a process-wide singleton guarantees a mod reload cannot stack a second
|
||||||
|
/// timer-tick subscription onto <c>EventController</c>.
|
||||||
|
/// </summary>
|
||||||
|
public class TimeModInitializer : IModInitializer
|
||||||
|
{
|
||||||
|
private static readonly TimeTickController TickController = new();
|
||||||
|
|
||||||
|
/// <summary>No per-mod work: all wiring happens once the whole mod set is ready.</summary>
|
||||||
|
public void OnModLoaded(string modId)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Installs the lighting bridge adapters and subscribes the clock tick.</summary>
|
||||||
|
public void OnModResourcesReady()
|
||||||
|
{
|
||||||
|
LightingBridge.Register(LightingHooks.ClearSceneLights, LightingHooks.SetupBackgroundMaps);
|
||||||
|
|
||||||
|
var root = (Godot.Engine.GetMainLoop() as SceneTree)?.Root;
|
||||||
|
if (root == null)
|
||||||
|
{
|
||||||
|
GameLogger.Log("TIME", "TimeModInitializer: no scene tree available; time tick registration skipped", LogLevel.Warning);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
TickController.EnsureRegistered(root);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
using Cthangover.Core.Bootstrap;
|
||||||
|
using Cthangover.Core.Settings;
|
||||||
|
using Cthangover.Core.Events;
|
||||||
|
using Cthangover.Core.Mods;
|
||||||
|
using Cthangover.Core.UI.Event;
|
||||||
|
using Godot;
|
||||||
|
|
||||||
|
namespace Cthangover.Mods.Time
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Bridges the scene-event timer system to the in-game clock.
|
||||||
|
/// Implements <see cref="Cthangover.Core.UI.Event.IOnTimeEvent"/>
|
||||||
|
/// with priority 0 so that the tick is processed before most
|
||||||
|
/// other listeners. The controller must be explicitly registered
|
||||||
|
/// via <see cref="EnsureRegistered"/>, which finds the
|
||||||
|
/// <see cref="Cthangover.Core.UI.Event.SceneEventController"/> in
|
||||||
|
/// the scene tree and subscribes <see cref="OnTimerTick"/> to it.
|
||||||
|
/// Registration is idempotent — subsequent calls are no-ops.
|
||||||
|
/// </summary>
|
||||||
|
public class TimeTickController : IOnTimeEvent
|
||||||
|
{
|
||||||
|
/// <summary>Executes before default-priority listeners so time
|
||||||
|
/// updates are visible to all other tick handlers.</summary>
|
||||||
|
public int Priority => 0;
|
||||||
|
|
||||||
|
private bool _registered;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Locates the <see cref="Cthangover.Core.UI.Event.SceneEventController"/>
|
||||||
|
/// in the scene tree and registers this controller as a timer-tick
|
||||||
|
/// listener. Safe to call multiple times — only the first call
|
||||||
|
/// does work.
|
||||||
|
/// </summary>
|
||||||
|
public void EnsureRegistered(Node node)
|
||||||
|
{
|
||||||
|
if (_registered)
|
||||||
|
return;
|
||||||
|
var eventController = node.GetTree()?.Root?.FindChild("EventController", true, false) as SceneEventController;
|
||||||
|
if (eventController == null)
|
||||||
|
return;
|
||||||
|
eventController.AddTimerTickEventListener(this);
|
||||||
|
_registered = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called by the event system on each timer pulse. Advances
|
||||||
|
/// the in-game clock by one minute via
|
||||||
|
/// <see cref="TimeData.AddTick"/>.
|
||||||
|
/// </summary>
|
||||||
|
public void OnTimerTick()
|
||||||
|
{
|
||||||
|
// The SceneEventController keeps ticking (1s interval, ProcessMode
|
||||||
|
// Always) while the launcher boot compiles mods in the background —
|
||||||
|
// registration in GameData._Ready succeeds because the engine puts
|
||||||
|
// every autoload into the tree before any _Ready fires. Touching
|
||||||
|
// Runtime here would create it before the starter inventory can be
|
||||||
|
// seeded through loaded mod factories: the seed then runs against an
|
||||||
|
// empty registry, logs "Initialize refused" spam and permanently
|
||||||
|
// skips the starter items. Skip boot-window ticks instead — the same
|
||||||
|
// readiness contract GameData.Runtime enforces.
|
||||||
|
if (!Boot.ModsReady)
|
||||||
|
return;
|
||||||
|
|
||||||
|
GameData.Instance.Runtime.Time.AddTick();
|
||||||
|
ModEventBus.Publish(new TimeTickEvent
|
||||||
|
{
|
||||||
|
TimeTick = GameData.Instance.Runtime.Time.Tick,
|
||||||
|
NormalizedTime = GameData.Instance.Runtime.Time.Normalized,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Reference in new issue
Block a user