From cdd944b9b5872fff30a41aacb43d84b2028d5bd9 Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Sat, 23 Aug 2025 23:38:45 +0200 Subject: [PATCH] Add EventManager and event handling system; implement event triggering and popup display --- Mods/Events/good_harvest.json | 23 ++++++++ Scenes/Main/Main.tscn | 57 +++++++++++++------- Scenes/UI/event_option_button.tscn | 8 +++ Scenes/UI/event_popup.tscn | 26 +++++++++ Scripts/Components/EventManager.cs | 74 ++++++++++++++++++++++++++ Scripts/Components/EventManager.cs.uid | 1 + Scripts/Core/EventDefinition.cs | 22 ++++++++ Scripts/Core/EventDefinition.cs.uid | 1 + Scripts/Core/EventDto.cs | 46 ++++++++++++++++ Scripts/Core/EventDto.cs.uid | 1 + Scripts/Core/EventLoader.cs | 71 ++++++++++++++++++++++++ Scripts/Core/EventLoader.cs.uid | 1 + Scripts/Core/MiracleLoader.cs | 19 +++++-- Scripts/Singletons/GameBus.cs | 14 ++++- Scripts/UI/EventPopup.cs | 40 ++++++++++++++ Scripts/UI/EventPopup.cs.uid | 1 + 16 files changed, 379 insertions(+), 26 deletions(-) create mode 100644 Mods/Events/good_harvest.json create mode 100644 Scenes/UI/event_option_button.tscn create mode 100644 Scenes/UI/event_popup.tscn create mode 100644 Scripts/Components/EventManager.cs create mode 100644 Scripts/Components/EventManager.cs.uid create mode 100644 Scripts/Core/EventDefinition.cs create mode 100644 Scripts/Core/EventDefinition.cs.uid create mode 100644 Scripts/Core/EventDto.cs create mode 100644 Scripts/Core/EventDto.cs.uid create mode 100644 Scripts/Core/EventLoader.cs create mode 100644 Scripts/Core/EventLoader.cs.uid create mode 100644 Scripts/UI/EventPopup.cs create mode 100644 Scripts/UI/EventPopup.cs.uid diff --git a/Mods/Events/good_harvest.json b/Mods/Events/good_harvest.json new file mode 100644 index 0000000..e080086 --- /dev/null +++ b/Mods/Events/good_harvest.json @@ -0,0 +1,23 @@ +{ + "id": "event_good_harvest", + "title": "Bountiful Harvest", + "description": "A miraculous confluence of weather and soil fertility has led to an unexpectedly large harvest. Our granaries are overflowing!", + "meanTimeToHappen": 120, + "trigger": { + "minFollowers": 100, + "maxCorruption": 50 + }, + "options": [ + { + "text": "A true blessing!", + "tooltip": "Gain a large amount (500) of Faith.", + "effects": [ + { + "type": "AddResource", + "targetResource": "Faith", + "value": 500 + } + ] + } + ] +} diff --git a/Scenes/Main/Main.tscn b/Scenes/Main/Main.tscn index 0b6d58f..dba3360 100644 --- a/Scenes/Main/Main.tscn +++ b/Scenes/Main/Main.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=24 format=3 uid="uid://bfil8sd154327"] +[gd_scene load_steps=26 format=3 uid="uid://bfil8sd154327"] [ext_resource type="Script" uid="uid://t71ewkpa5uqs" path="res://Scenes/Main/Main.cs" id="1_p8rbg"] [ext_resource type="Script" uid="uid://b77vh831r1e3c" path="res://Scenes/Main/MiraclePanel.cs" id="2_hcu3t"] @@ -21,6 +21,8 @@ [ext_resource type="PackedScene" uid="uid://xk2xirjd1sma" path="res://Scenes/moddable_visual.tscn" id="17_qdkat"] [ext_resource type="Script" uid="uid://furbvcmw31bx" path="res://Scripts/Components/ForestVisualizer.cs" id="18_qdkat"] [ext_resource type="Script" uid="uid://cw8gpeaq3yfjn" path="res://Scripts/Components/RoadManager.cs" id="19_qdkat"] +[ext_resource type="Script" uid="uid://2ipbgwlx1ld1" path="res://Scripts/Components/EventManager.cs" id="22_iwp64"] +[ext_resource type="PackedScene" uid="uid://gdejd44km3co" path="res://Scenes/UI/event_popup.tscn" id="23_4etfk"] [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_xggvw"] bg_color = Color(0, 0, 0, 0.733333) @@ -41,7 +43,10 @@ _productionLabel = NodePath("UiLayer/Control/Main/Top/ProductionLabel") _miraclePanel = NodePath("UiLayer/Control/Main/ScrollContainer/Bottom") _worldSprite = NodePath("World Sprite") -[node name="UiLayer" type="CanvasLayer" parent="."] +[node name="UiLayer" type="CanvasLayer" parent="." node_paths=PackedStringArray("_pauseButton", "_pauseMenu")] +script = ExtResource("11_xggvw") +_pauseButton = NodePath("MarginContainer/Button") +_pauseMenu = NodePath("PanelContainer") [node name="Control" type="Control" parent="UiLayer"] layout_mode = 3 @@ -52,6 +57,7 @@ grow_horizontal = 2 grow_vertical = 2 size_flags_horizontal = 3 size_flags_vertical = 3 +mouse_filter = 2 [node name="Main" type="MarginContainer" parent="UiLayer/Control"] layout_mode = 1 @@ -62,6 +68,7 @@ grow_horizontal = 2 grow_vertical = 2 size_flags_horizontal = 4 size_flags_vertical = 0 +mouse_filter = 2 theme_override_constants/margin_left = 8 theme_override_constants/margin_top = 8 theme_override_constants/margin_right = 8 @@ -71,6 +78,7 @@ theme_override_constants/margin_bottom = 8 layout_mode = 2 size_flags_horizontal = 4 size_flags_vertical = 0 +mouse_filter = 2 alignment = 1 [node name="FaithLabel" type="Label" parent="UiLayer/Control/Main/Top"] @@ -128,39 +136,26 @@ stream = ExtResource("6_32vk6") [node name="BuffAdded" type="AudioStreamPlayer" parent="UiLayer/Control/Main/ScrollContainer2/HBoxContainer"] stream = ExtResource("7_crdpj") -[node name="Notification Layer" type="CanvasLayer" parent="." node_paths=PackedStringArray("_sfx")] -layer = 3 -script = ExtResource("6_iwp64") -_notificationLabelScene = ExtResource("7_4etfk") -_sfx = NodePath("Advance Age") - -[node name="Advance Age" type="AudioStreamPlayer" parent="Notification Layer"] -stream = ExtResource("8_4etfk") - -[node name="PauseManager" type="CanvasLayer" parent="." node_paths=PackedStringArray("_pauseButton", "_pauseMenu")] -script = ExtResource("11_xggvw") -_pauseButton = NodePath("MarginContainer/Button") -_pauseMenu = NodePath("PanelContainer") - -[node name="MarginContainer" type="MarginContainer" parent="PauseManager"] +[node name="MarginContainer" type="MarginContainer" parent="UiLayer"] anchors_preset = 15 anchor_right = 1.0 anchor_bottom = 1.0 grow_horizontal = 2 grow_vertical = 2 +mouse_filter = 2 theme_override_constants/margin_left = 4 theme_override_constants/margin_top = 4 theme_override_constants/margin_right = 4 theme_override_constants/margin_bottom = 4 -[node name="Button" type="Button" parent="PauseManager/MarginContainer"] +[node name="Button" type="Button" parent="UiLayer/MarginContainer"] layout_mode = 2 size_flags_horizontal = 8 size_flags_vertical = 0 focus_mode = 0 text = "Pause" -[node name="PanelContainer" type="PanelContainer" parent="PauseManager"] +[node name="PanelContainer" type="PanelContainer" parent="UiLayer"] anchors_preset = 15 anchor_right = 1.0 anchor_bottom = 1.0 @@ -171,7 +166,7 @@ size_flags_vertical = 4 mouse_filter = 2 theme_override_styles/panel = SubResource("StyleBoxFlat_xggvw") -[node name="Label" type="Label" parent="PauseManager/PanelContainer"] +[node name="Label" type="Label" parent="UiLayer/PanelContainer"] layout_mode = 2 size_flags_vertical = 1 text = "Paused" @@ -179,6 +174,22 @@ horizontal_alignment = 1 vertical_alignment = 1 uppercase = true +[node name="Notification Layer" type="CanvasLayer" parent="." node_paths=PackedStringArray("_sfx")] +layer = 3 +script = ExtResource("6_iwp64") +_notificationLabelScene = ExtResource("7_4etfk") +_sfx = NodePath("Advance Age") + +[node name="Advance Age" type="AudioStreamPlayer" parent="Notification Layer"] +stream = ExtResource("8_4etfk") + +[node name="CenterContainer" type="CenterContainer" parent="Notification Layer"] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 + [node name="Camera2D" type="Camera2D" parent="."] [node name="World Sprite" type="Sprite2D" parent="."] @@ -5673,3 +5684,9 @@ _unitsPerMarker = 1 Category = 2 _moddableVisualScene = ExtResource("17_qdkat") metadata/_custom_type_script = "uid://dj2wyrq07gfp2" + +[node name="EventManager" type="Node" parent="." node_paths=PackedStringArray("_eventPopupContainer")] +script = ExtResource("22_iwp64") +_eventPopupScene = ExtResource("23_4etfk") +_eventPopupContainer = NodePath("../Notification Layer/CenterContainer") +metadata/_custom_type_script = "uid://2ipbgwlx1ld1" diff --git a/Scenes/UI/event_option_button.tscn b/Scenes/UI/event_option_button.tscn new file mode 100644 index 0000000..31124a5 --- /dev/null +++ b/Scenes/UI/event_option_button.tscn @@ -0,0 +1,8 @@ +[gd_scene format=3 uid="uid://b2o5rufqn8dpf"] + +[node name="EventOptionButton" type="Button"] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 diff --git a/Scenes/UI/event_popup.tscn b/Scenes/UI/event_popup.tscn new file mode 100644 index 0000000..591de2e --- /dev/null +++ b/Scenes/UI/event_popup.tscn @@ -0,0 +1,26 @@ +[gd_scene load_steps=3 format=3 uid="uid://gdejd44km3co"] + +[ext_resource type="Script" uid="uid://h1x5eqt0lc5m" path="res://Scripts/UI/EventPopup.cs" id="1_lkb7n"] +[ext_resource type="PackedScene" uid="uid://b2o5rufqn8dpf" path="res://Scenes/UI/event_option_button.tscn" id="2_gk0qx"] + +[node name="EventPopup" type="PanelContainer" node_paths=PackedStringArray("_titleLabel", "_descriptionLabel", "_optionsContainer")] +offset_right = 40.0 +offset_bottom = 40.0 +script = ExtResource("1_lkb7n") +_titleLabel = NodePath("VBoxContainer/Title") +_descriptionLabel = NodePath("VBoxContainer/Description") +_optionsContainer = NodePath("VBoxContainer/OptionButtons") +_optionButtonScene = ExtResource("2_gk0qx") + +[node name="VBoxContainer" type="VBoxContainer" parent="."] +layout_mode = 2 + +[node name="Title" type="Label" parent="VBoxContainer"] +layout_mode = 2 +text = "TITLE" + +[node name="Description" type="RichTextLabel" parent="VBoxContainer"] +layout_mode = 2 + +[node name="OptionButtons" type="VBoxContainer" parent="VBoxContainer"] +layout_mode = 2 diff --git a/Scripts/Components/EventManager.cs b/Scripts/Components/EventManager.cs new file mode 100644 index 0000000..4bdb7b4 --- /dev/null +++ b/Scripts/Components/EventManager.cs @@ -0,0 +1,74 @@ +using System.Collections.Generic; +using Godot; +using Limbo.Console.Sharp; +using ParasiticGod.Scripts.Core; +using ParasiticGod.Scripts.Singletons; +using ParasiticGod.Scripts.UI; + +namespace ParasiticGod.Scripts.Components; + +[GlobalClass] +public partial class EventManager : Node +{ + [Export] private double _checkInterval = 5.0; + [Export] private PackedScene _eventPopupScene; + [Export] private Container _eventPopupContainer; + + private List _allEvents; + private Timer _timer; + private RandomNumberGenerator _rng = new(); + + public override void _Ready() + { + RegisterConsoleCommands(); + _allEvents = GameBus.Instance.AllEvents; + + _timer = new Timer { WaitTime = _checkInterval, Autostart = true }; + AddChild(_timer); + _timer.Timeout += OnCheckEvents; + } + + private void OnCheckEvents() + { + if (GetTree().Paused) return; + + var state = GameBus.Instance.CurrentState; + + foreach (var ev in _allEvents) + { + if (state.Get(Stat.Followers) < ev.Trigger.MinFollowers) continue; + if (state.Get(Stat.Corruption) > ev.Trigger.MaxCorruption) continue; + + var probability = _checkInterval / ev.MeanTimeToHappen; + if (_rng.Randf() < probability) + { + FireEvent(ev); + break; + } + } + } + + private void FireEvent(EventDefinition eventDef) + { + GD.Print($"Firing event: {eventDef.Title}"); + GetTree().Paused = true; + + var popup = _eventPopupScene.Instantiate(); + _eventPopupContainer.AddChild(popup); + popup.DisplayEvent(eventDef); + } + + [ConsoleCommand("trigger_event", "Triggers an event by its ID for testing purposes.")] + private void TriggerEventCommand(string eventId) + { + var eventDef = _allEvents.Find(e => e.Id == eventId); + if (eventDef != null) + { + FireEvent(eventDef); + } + else + { + GD.PushError($"No event found with ID: {eventId}"); + } + } +} \ No newline at end of file diff --git a/Scripts/Components/EventManager.cs.uid b/Scripts/Components/EventManager.cs.uid new file mode 100644 index 0000000..8346872 --- /dev/null +++ b/Scripts/Components/EventManager.cs.uid @@ -0,0 +1 @@ +uid://2ipbgwlx1ld1 diff --git a/Scripts/Core/EventDefinition.cs b/Scripts/Core/EventDefinition.cs new file mode 100644 index 0000000..9b020c4 --- /dev/null +++ b/Scripts/Core/EventDefinition.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using Godot.Collections; +using ParasiticGod.Scripts.Core.Effects; + +namespace ParasiticGod.Scripts.Core; + +public class EventDefinition +{ + public string Id { get; set; } + public string Title { get; set; } + public string Description { get; set; } + public int MeanTimeToHappen { get; set; } + public EventTriggerDto Trigger { get; set; } + public List Options { get; set; } = []; +} + +public class EventOptionDefinition +{ + public string Text { get; set; } + public string Tooltip { get; set; } + public Array Effects { get; set; } +} \ No newline at end of file diff --git a/Scripts/Core/EventDefinition.cs.uid b/Scripts/Core/EventDefinition.cs.uid new file mode 100644 index 0000000..15eda06 --- /dev/null +++ b/Scripts/Core/EventDefinition.cs.uid @@ -0,0 +1 @@ +uid://ilyr01u70ciw diff --git a/Scripts/Core/EventDto.cs b/Scripts/Core/EventDto.cs new file mode 100644 index 0000000..c6342a1 --- /dev/null +++ b/Scripts/Core/EventDto.cs @@ -0,0 +1,46 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace ParasiticGod.Scripts.Core; + +public class EventTriggerDto +{ + [JsonProperty("minFollowers")] + public long MinFollowers { get; set; } = 0; + + [JsonProperty("maxCorruption")] + public double MaxCorruption { get; set; } = 100; +} + +public class EventOptionDto +{ + [JsonProperty("text")] + public string Text { get; set; } + + [JsonProperty("tooltip")] + public string Tooltip { get; set; } + + [JsonProperty("effects")] + public List Effects { get; set; } +} + +public class EventDto +{ + [JsonProperty("id")] + public string Id { get; set; } + + [JsonProperty("title")] + public string Title { get; set; } + + [JsonProperty("description")] + public string Description { get; set; } + + [JsonProperty("meanTimeToHappen")] + public int MeanTimeToHappen { get; set; } = 100; // in game days + + [JsonProperty("trigger")] + public EventTriggerDto Trigger { get; set; } + + [JsonProperty("options")] + public List Options { get; set; } +} \ No newline at end of file diff --git a/Scripts/Core/EventDto.cs.uid b/Scripts/Core/EventDto.cs.uid new file mode 100644 index 0000000..9d5b6a9 --- /dev/null +++ b/Scripts/Core/EventDto.cs.uid @@ -0,0 +1 @@ +uid://nwwqn028soa5 diff --git a/Scripts/Core/EventLoader.cs b/Scripts/Core/EventLoader.cs new file mode 100644 index 0000000..a248d88 --- /dev/null +++ b/Scripts/Core/EventLoader.cs @@ -0,0 +1,71 @@ +using System.Collections.Generic; +using Godot; +using Newtonsoft.Json; + +namespace ParasiticGod.Scripts.Core; + +public static class EventLoader +{ + public static List LoadAllEvents() + { + var loadedEvents = new Dictionary(); + + LoadEventsFromPath("res://Mods/Events", loadedEvents); + LoadEventsFromPath("user://Mods/Events", loadedEvents); + + GD.Print($"Finished loading. Total unique events: {loadedEvents.Count}"); + return new List(loadedEvents.Values); + } + + private static void LoadEventsFromPath(string path, Dictionary events) + { + if (!DirAccess.DirExistsAbsolute(path)) return; + + using var dir = DirAccess.Open(path); + dir.ListDirBegin(); + var fileName = dir.GetNext(); + while (!string.IsNullOrEmpty(fileName)) + { + if (!dir.CurrentIsDir() && fileName.EndsWith(".json")) + { + var filePath = path.PathJoin(fileName); + var ev = LoadEventFromFile(filePath); + if (ev != null) + { + events[ev.Id] = ev; // Add or overwrite + } + } + fileName = dir.GetNext(); + } + } + + private static EventDefinition LoadEventFromFile(string filePath) + { + var fileContent = FileAccess.GetFileAsString(filePath); + if (string.IsNullOrEmpty(fileContent)) return null; + + var dto = JsonConvert.DeserializeObject(fileContent); + if (dto == null) return null; + + var eventDef = new EventDefinition + { + Id = dto.Id, + Title = dto.Title, + Description = dto.Description, + MeanTimeToHappen = dto.MeanTimeToHappen, + Trigger = dto.Trigger + }; + + foreach (var optionDto in dto.Options) + { + var optionDef = new EventOptionDefinition + { + Text = optionDto.Text, + Tooltip = optionDto.Tooltip, + Effects = MiracleLoader.ConvertEffectDtos(optionDto.Effects) + }; + eventDef.Options.Add(optionDef); + } + return eventDef; + } +} \ No newline at end of file diff --git a/Scripts/Core/EventLoader.cs.uid b/Scripts/Core/EventLoader.cs.uid new file mode 100644 index 0000000..6c958a5 --- /dev/null +++ b/Scripts/Core/EventLoader.cs.uid @@ -0,0 +1 @@ +uid://ceimix3ejnadj diff --git a/Scripts/Core/MiracleLoader.cs b/Scripts/Core/MiracleLoader.cs index 6667e29..35295d9 100644 --- a/Scripts/Core/MiracleLoader.cs +++ b/Scripts/Core/MiracleLoader.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using Godot; using Godot.Collections; using Newtonsoft.Json; @@ -86,7 +87,17 @@ public static class MiracleLoader Effects = [] }; - foreach (var effectDto in miracleDto.Effects) + var effects = ConvertEffectDtos(miracleDto.Effects); + miracleDef.Effects = effects; + return miracleDef; + } + + public static Array ConvertEffectDtos(List dtos) + { + var effects = new Array(); + if (dtos == null) return effects; + + foreach (var effectDto in dtos) { if (EffectRegistry.TryGetValue(effectDto.Type, out var effectType)) { @@ -118,11 +129,9 @@ public static class MiracleLoader unlockMiracleEffect.MiraclesToUnlock = new Array(effectDto.MiraclesToUnlock); break; } - - miracleDef.Effects.Add(effectInstance); + effects.Add(effectInstance); } } - - return miracleDef; + return effects; } } \ No newline at end of file diff --git a/Scripts/Singletons/GameBus.cs b/Scripts/Singletons/GameBus.cs index 18474dd..826cb9c 100644 --- a/Scripts/Singletons/GameBus.cs +++ b/Scripts/Singletons/GameBus.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using Godot; +using Godot.Collections; using Limbo.Console.Sharp; using ParasiticGod.Scripts.Core; using ParasiticGod.Scripts.Core.Effects; @@ -10,10 +11,11 @@ namespace ParasiticGod.Scripts.Singletons; public partial class GameBus : Node { public static GameBus Instance { get; private set; } - public Dictionary AllMiracles { get; private set; } + public System.Collections.Generic.Dictionary AllMiracles { get; private set; } public List FollowerTiers { get; private set; } public List HutTiers { get; private set; } public List TempleTiers { get; private set; } + public List AllEvents { get; private set; } private PackedScene _gameOverScene = GD.Load("res://Scenes/game_over.tscn"); private PackedScene _winScene = GD.Load("res://Scenes/win_screen.tscn"); @@ -35,6 +37,7 @@ public partial class GameBus : Node { Instance = this; AllMiracles = MiracleLoader.LoadAllMiracles(); + AllEvents = EventLoader.LoadAllEvents(); FollowerTiers = TierLoader.LoadTiers("res://Mods/Tiers/follower_tiers.json", "user://Mods/Tiers/follower_tiers.json"); HutTiers = TierLoader.LoadTiers("res://Mods/Tiers/hut_tiers.json","user://Mods/Tiers/hut_tiers.json"); TempleTiers = TierLoader.LoadTiers("res://Mods/Tiers/temple_tiers.json","user://Mods/Tiers/temple_tiers.json"); @@ -124,6 +127,15 @@ public partial class GameBus : Node GameWon?.Invoke(); } + public void ExecuteEffects(Array effects) + { + foreach (var effect in effects) + { + effect.Execute(_gameState); + } + GetTree().Paused = false; + } + public void SubscribeToStat(Stat stat, Action listener) => _gameState.Subscribe(stat, listener); public void UnsubscribeFromStat(Stat stat, Action listener) => _gameState.Unsubscribe(stat, listener); diff --git a/Scripts/UI/EventPopup.cs b/Scripts/UI/EventPopup.cs new file mode 100644 index 0000000..418c7aa --- /dev/null +++ b/Scripts/UI/EventPopup.cs @@ -0,0 +1,40 @@ +using Godot; +using ParasiticGod.Scripts.Core; +using ParasiticGod.Scripts.Singletons; + +namespace ParasiticGod.Scripts.UI; + +[GlobalClass] +public partial class EventPopup : PanelContainer +{ + [Export] private Label _titleLabel; + [Export] private RichTextLabel _descriptionLabel; + [Export] private VBoxContainer _optionsContainer; + [Export] private PackedScene _optionButtonScene; // A scene for a single button + + public void DisplayEvent(EventDefinition eventDef) + { + _titleLabel.Text = eventDef.Title; + _descriptionLabel.Text = eventDef.Description; + + foreach (var child in _optionsContainer.GetChildren()) + { + child.QueueFree(); + } + + foreach (var option in eventDef.Options) + { + var button = _optionButtonScene.Instantiate