Compare commits
32 Commits
bf11d6a9cb
...
master
Author | SHA1 | Date | |
---|---|---|---|
bc3108ea37 | |||
7257242fce | |||
03abf91f59 | |||
e6f8989d16 | |||
db2a090acc | |||
dfc9201f62 | |||
46553a351a | |||
a8ff492aed | |||
aa73e54b3e | |||
98b3202361 | |||
6e2bdcdf95 | |||
f229ff5b7d | |||
f9cb59d182 | |||
ead52f6d51 | |||
bd40c797d4 | |||
2d520a708f | |||
257a492f72 | |||
fd10e566b3 | |||
021e984877 | |||
4b7c38397c | |||
6d0e337bd6 | |||
51aecf7da5 | |||
604520cad5 | |||
36d1cac284 | |||
53b5f968fd | |||
88c7a0a055 | |||
d786ef4c22 | |||
d84f7d1740 | |||
2ad0fe26d2 | |||
c4f7be1b10 | |||
b957b56567 | |||
cce93286be |
2
.gitattributes
vendored
2
.gitattributes
vendored
@@ -1,2 +1,4 @@
|
|||||||
# Normalize EOL for all files that Git considers text files.
|
# Normalize EOL for all files that Git considers text files.
|
||||||
* text=auto eol=lf
|
* text=auto eol=lf
|
||||||
|
addons/* linguist-vendored
|
||||||
|
*.gd linguist-vendored
|
||||||
|
105
Autoloads/AchievementManager.cs
Normal file
105
Autoloads/AchievementManager.cs
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
using Godot;
|
||||||
|
using Godot.Collections;
|
||||||
|
using Mr.BrickAdventures.scripts.Resources;
|
||||||
|
|
||||||
|
namespace Mr.BrickAdventures.Autoloads;
|
||||||
|
|
||||||
|
public partial class AchievementManager : Node
|
||||||
|
{
|
||||||
|
[Export] private string AchievementsFolderPath = "res://achievements/";
|
||||||
|
[Export] private PackedScene AchievementPopupScene { get; set; }
|
||||||
|
|
||||||
|
private System.Collections.Generic.Dictionary<string, AchievementResource> _achievements = new();
|
||||||
|
private Array<string> _unlockedAchievementIds = [];
|
||||||
|
private GameManager _gameManager;
|
||||||
|
|
||||||
|
public override void _Ready()
|
||||||
|
{
|
||||||
|
_gameManager = GetNode<GameManager>("/root/GameManager");
|
||||||
|
LoadAchievementsFromFolder();
|
||||||
|
LoadUnlockedAchievements();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LoadAchievementsFromFolder()
|
||||||
|
{
|
||||||
|
using var dir = DirAccess.Open(AchievementsFolderPath);
|
||||||
|
if (dir == null)
|
||||||
|
{
|
||||||
|
GD.PrintErr($"AchievementManager: Could not open achievements folder at '{AchievementsFolderPath}'");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dir.ListDirBegin();
|
||||||
|
var fileName = dir.GetNext();
|
||||||
|
while (fileName != "")
|
||||||
|
{
|
||||||
|
if (!dir.CurrentIsDir() && fileName.EndsWith(".tres"))
|
||||||
|
{
|
||||||
|
var achievement = GD.Load<AchievementResource>(AchievementsFolderPath + fileName);
|
||||||
|
if (achievement != null)
|
||||||
|
{
|
||||||
|
_achievements.TryAdd(achievement.Id, achievement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fileName = dir.GetNext();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UnlockAchievement(string achievementId)
|
||||||
|
{
|
||||||
|
if (!_achievements.TryGetValue(achievementId, out var achievement))
|
||||||
|
{
|
||||||
|
GD.PrintErr($"Attempted to unlock non-existent achievement: '{achievementId}'");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_unlockedAchievementIds.Contains(achievementId))
|
||||||
|
{
|
||||||
|
return; // Already unlocked
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Mark as unlocked internally
|
||||||
|
_unlockedAchievementIds.Add(achievementId);
|
||||||
|
GD.Print($"Achievement Unlocked: {achievement.DisplayName}");
|
||||||
|
|
||||||
|
// 2. Show the UI popup
|
||||||
|
if (AchievementPopupScene != null)
|
||||||
|
{
|
||||||
|
var popup = AchievementPopupScene.Instantiate<scripts.UI.AchievementPopup>();
|
||||||
|
GetTree().Root.AddChild(popup);
|
||||||
|
_ = popup.ShowAchievement(achievement);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Call SteamManager if it's available
|
||||||
|
if (SteamManager.IsSteamInitialized)
|
||||||
|
{
|
||||||
|
SteamManager.UnlockAchievement(achievement.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Save progress
|
||||||
|
SaveUnlockedAchievements();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void LockAchievement(string achievementId)
|
||||||
|
{
|
||||||
|
if (_unlockedAchievementIds.Contains(achievementId))
|
||||||
|
{
|
||||||
|
_unlockedAchievementIds.Remove(achievementId);
|
||||||
|
SaveUnlockedAchievements();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SaveUnlockedAchievements()
|
||||||
|
{
|
||||||
|
_gameManager.PlayerState["unlocked_achievements"] = _unlockedAchievementIds;
|
||||||
|
// You might want to trigger a save game here, depending on your SaveSystem
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LoadUnlockedAchievements()
|
||||||
|
{
|
||||||
|
if (_gameManager.PlayerState.TryGetValue("unlocked_achievements", out var unlocked))
|
||||||
|
{
|
||||||
|
_unlockedAchievementIds = (Array<string>)unlocked;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
1
Autoloads/AchievementManager.cs.uid
Normal file
1
Autoloads/AchievementManager.cs.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://c4vvuqnx5y33u
|
27
Autoloads/ConfigFileHandler.cs
Normal file
27
Autoloads/ConfigFileHandler.cs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
using Godot;
|
||||||
|
|
||||||
|
namespace Mr.BrickAdventures.Autoloads;
|
||||||
|
|
||||||
|
public partial class ConfigFileHandler : Node
|
||||||
|
{
|
||||||
|
private ConfigFile _settingsConfig = new();
|
||||||
|
public const string SettingsPath = "user://settings.ini";
|
||||||
|
|
||||||
|
public ConfigFile SettingsConfig => _settingsConfig;
|
||||||
|
|
||||||
|
public override void _Ready()
|
||||||
|
{
|
||||||
|
if (!FileAccess.FileExists(SettingsPath))
|
||||||
|
{
|
||||||
|
var err = _settingsConfig.Save(SettingsPath);
|
||||||
|
if (err != Error.Ok)
|
||||||
|
GD.PushError($"Failed to create settings file at {SettingsPath}: {err}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var err = _settingsConfig.Load(SettingsPath);
|
||||||
|
if (err != Error.Ok)
|
||||||
|
GD.PushError($"Failed to load settings file at {SettingsPath}: {err}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
1
Autoloads/ConfigFileHandler.cs.uid
Normal file
1
Autoloads/ConfigFileHandler.cs.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://8cyvbeyd13cj
|
173
Autoloads/ConsoleManager.cs
Normal file
173
Autoloads/ConsoleManager.cs
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
using Godot;
|
||||||
|
using Limbo.Console.Sharp;
|
||||||
|
using Mr.BrickAdventures.scripts;
|
||||||
|
using Mr.BrickAdventures.scripts.components;
|
||||||
|
|
||||||
|
namespace Mr.BrickAdventures.Autoloads;
|
||||||
|
|
||||||
|
public partial class ConsoleManager : Node
|
||||||
|
{
|
||||||
|
private GameManager _gameManager;
|
||||||
|
private SkillManager _skillManager;
|
||||||
|
private SkillUnlockerComponent _skillUnlockerComponent;
|
||||||
|
private AchievementManager _achievementManager;
|
||||||
|
|
||||||
|
public override void _Ready()
|
||||||
|
{
|
||||||
|
_gameManager = GetNode<GameManager>("/root/GameManager");
|
||||||
|
_achievementManager = GetNode<AchievementManager>("/root/AchievementManager");
|
||||||
|
_skillManager = GetNode<SkillManager>("/root/SkillManager");
|
||||||
|
|
||||||
|
RegisterConsoleCommands();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void _ExitTree()
|
||||||
|
{
|
||||||
|
UnregisterConsoleCommands();
|
||||||
|
}
|
||||||
|
|
||||||
|
[ConsoleCommand("add_coins", "Adds a specified amount of coins to the player's total.")]
|
||||||
|
private void AddCoinsCommand(int amount)
|
||||||
|
{
|
||||||
|
_gameManager.AddCoins(amount);
|
||||||
|
LimboConsole.Info($"Increased coins by {amount}. Total coins: {_gameManager.GetCoins()}");
|
||||||
|
}
|
||||||
|
|
||||||
|
[ConsoleCommand("set_coins", "Sets the player's total coins to a specified amount.")]
|
||||||
|
private void SetCoinsCommand(int amount)
|
||||||
|
{
|
||||||
|
_gameManager.SetCoins(amount);
|
||||||
|
LimboConsole.Info($"Set coins to {amount}. Total coins: {_gameManager.GetCoins()}");
|
||||||
|
}
|
||||||
|
|
||||||
|
[ConsoleCommand("set_lives", "Sets the player's total lives to a specified amount.")]
|
||||||
|
private void SetLivesCommand(int amount)
|
||||||
|
{
|
||||||
|
_gameManager.SetLives(amount);
|
||||||
|
LimboConsole.Info($"Set lives to {amount}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
[ConsoleCommand("add_lives", "Adds a specified amount of lives to the player's total.")]
|
||||||
|
private void AddLivesCommand(int amount)
|
||||||
|
{
|
||||||
|
_gameManager.AddLives(amount);
|
||||||
|
LimboConsole.Info($"Increased lives by {amount}. Total lives: {_gameManager.GetLives()}");
|
||||||
|
}
|
||||||
|
|
||||||
|
[ConsoleCommand("set_health", "Sets the player's health to a specified amount.")]
|
||||||
|
private void SetHealthCommand(float amount)
|
||||||
|
{
|
||||||
|
var playerHealthComponent = _gameManager.Player.GetNode<HealthComponent>("HealthComponent");
|
||||||
|
if (playerHealthComponent != null)
|
||||||
|
{
|
||||||
|
playerHealthComponent.Health = amount;
|
||||||
|
LimboConsole.Info($"Set player health to {amount}.");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LimboConsole.Warn("Player HealthComponent not found.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[ConsoleCommand("reset_session", "Resets the current session state.")]
|
||||||
|
private void ResetSessionCommand()
|
||||||
|
{
|
||||||
|
_gameManager.ResetCurrentSessionState();
|
||||||
|
LimboConsole.Info("Current session state has been reset.");
|
||||||
|
}
|
||||||
|
|
||||||
|
[ConsoleCommand("unlock_skill", "Unlocks and activates a skill by its name.")]
|
||||||
|
private void UnlockSkillCommand(string skillName)
|
||||||
|
{
|
||||||
|
if (!GetSkillManagement()) return;
|
||||||
|
|
||||||
|
var skill = _skillManager.GetSkillByName(skillName);
|
||||||
|
if (skill == null)
|
||||||
|
{
|
||||||
|
LimboConsole.Warn($"Skill '{skillName}' not found.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_gameManager.UnlockSkill(skill);
|
||||||
|
_skillManager.ActivateSkill(skill);
|
||||||
|
_skillUnlockerComponent.EmitSignal(SkillUnlockerComponent.SignalName.SkillUnlocked, skill);
|
||||||
|
LimboConsole.Info($"Skill '{skillName}' has been unlocked and activated.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool GetSkillManagement()
|
||||||
|
{
|
||||||
|
var player = _gameManager.Player;
|
||||||
|
if (player == null || !IsInstanceValid(player))
|
||||||
|
{
|
||||||
|
LimboConsole.Warn("Player node not found or is invalid.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_skillUnlockerComponent ??= player.GetNode<SkillUnlockerComponent>("SkillUnlockerComponent");
|
||||||
|
|
||||||
|
if (_skillManager != null && _skillUnlockerComponent != null) return true;
|
||||||
|
|
||||||
|
LimboConsole.Warn("SkillManager or SkillUnlockerComponent not found on the player.");
|
||||||
|
return false;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
[ConsoleCommand("unlock_all_skills", "Unlocks and activates all available skills.")]
|
||||||
|
private void UnlockAllSkillsCommand()
|
||||||
|
{
|
||||||
|
if (!GetSkillManagement()) return;
|
||||||
|
|
||||||
|
_skillUnlockerComponent.UnlockAllSkills();
|
||||||
|
LimboConsole.Info("All skills have been unlocked and activated.");
|
||||||
|
}
|
||||||
|
|
||||||
|
[ConsoleCommand("remove_skill", "Deactivates and removes a skill by its name.")]
|
||||||
|
private void RemoveSkillCommand(string skillName)
|
||||||
|
{
|
||||||
|
if (!GetSkillManagement()) return;
|
||||||
|
|
||||||
|
var skill = _skillManager.GetSkillByName(skillName);
|
||||||
|
if (skill == null)
|
||||||
|
{
|
||||||
|
LimboConsole.Warn($"Skill '{skillName}' not found.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_gameManager.RemoveSkill(skill.Name);
|
||||||
|
_skillManager.DeactivateSkill(skill);
|
||||||
|
LimboConsole.Info($"Skill '{skillName}' has been deactivated.");
|
||||||
|
}
|
||||||
|
|
||||||
|
[ConsoleCommand("remove_all_skills", "Deactivates and removes all skills.")]
|
||||||
|
private void RemoveAllSkillsCommand()
|
||||||
|
{
|
||||||
|
if (!GetSkillManagement()) return;
|
||||||
|
|
||||||
|
foreach (var skill in _skillManager.AvailableSkills)
|
||||||
|
{
|
||||||
|
_gameManager.RemoveSkill(skill.Name);
|
||||||
|
_skillManager.DeactivateSkill(skill);
|
||||||
|
}
|
||||||
|
LimboConsole.Info("All skills have been deactivated.");
|
||||||
|
}
|
||||||
|
|
||||||
|
[ConsoleCommand("next_level", "Advances the game to the next level.")]
|
||||||
|
private void GoToNextLevelCommand()
|
||||||
|
{
|
||||||
|
_gameManager.OnLevelComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
[ConsoleCommand("unlock_achievement", "Unlocks an achievement by its ID.")]
|
||||||
|
private void UnlockAchievementCommand(string achievementId)
|
||||||
|
{
|
||||||
|
_achievementManager.UnlockAchievement(achievementId);
|
||||||
|
LimboConsole.Info($"Attempted to unlock achievement '{achievementId}'.");
|
||||||
|
}
|
||||||
|
|
||||||
|
[ConsoleCommand("reset_achievement", "Resets (locks) an achievement by its ID.")]
|
||||||
|
private void ResetAchievementCommand(string achievementId)
|
||||||
|
{
|
||||||
|
_achievementManager.LockAchievement(achievementId);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
1
Autoloads/ConsoleManager.cs.uid
Normal file
1
Autoloads/ConsoleManager.cs.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://bv2teruv4s6vg
|
9
Autoloads/EventBus.cs
Normal file
9
Autoloads/EventBus.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
using Godot;
|
||||||
|
|
||||||
|
namespace Mr.BrickAdventures.Autoloads;
|
||||||
|
|
||||||
|
public partial class EventBus : Node
|
||||||
|
{
|
||||||
|
[Signal] public delegate void LevelStartedEventHandler(int levelIndex, Node currentScene);
|
||||||
|
[Signal] public delegate void LevelCompletedEventHandler(int levelIndex, Node currentScene, double completionTime);
|
||||||
|
}
|
1
Autoloads/EventBus.cs.uid
Normal file
1
Autoloads/EventBus.cs.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://bb2yq5wggdw3w
|
52
Autoloads/FloatingTextManager.cs
Normal file
52
Autoloads/FloatingTextManager.cs
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using Godot;
|
||||||
|
using Mr.BrickAdventures.scripts.UI;
|
||||||
|
|
||||||
|
namespace Mr.BrickAdventures.Autoloads;
|
||||||
|
|
||||||
|
public partial class FloatingTextManager : Node
|
||||||
|
{
|
||||||
|
[Export] public PackedScene FloatingTextScene { get; set; }
|
||||||
|
|
||||||
|
[ExportGroup("Colors")]
|
||||||
|
[Export] public Color DamageColor { get; set; } = new Color("#b21030"); // Red
|
||||||
|
[Export] public Color HealColor { get; set; } = new Color("#71f341"); // Green
|
||||||
|
[Export] public Color CoinColor { get; set; } = new Color("#ebd320"); // Gold
|
||||||
|
[Export] public Color MessageColor { get; set; } = new Color("#ffffff"); // White
|
||||||
|
|
||||||
|
public void ShowDamage(float amount, Vector2 position)
|
||||||
|
{
|
||||||
|
var text = Mathf.Round(amount * 100f).ToString(CultureInfo.InvariantCulture);
|
||||||
|
CreateFloatingText(text, position, DamageColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ShowHeal(float amount, Vector2 position)
|
||||||
|
{
|
||||||
|
var text = $"+{Mathf.Round(amount)}";
|
||||||
|
CreateFloatingText(text, position, HealColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ShowCoin(int amount, Vector2 position)
|
||||||
|
{
|
||||||
|
var text = $"+{amount}";
|
||||||
|
CreateFloatingText(text, position, CoinColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ShowMessage(string message, Vector2 position)
|
||||||
|
{
|
||||||
|
CreateFloatingText(message, position, MessageColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CreateFloatingText(string text, Vector2 position, Color color)
|
||||||
|
{
|
||||||
|
if (FloatingTextScene == null)
|
||||||
|
{
|
||||||
|
GD.PushError("FloatingTextManager: FloatingTextScene is not set!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var popup = FloatingTextScene.Instantiate<FloatingText>();
|
||||||
|
GetTree().CurrentScene.AddChild(popup);
|
||||||
|
popup.Show(text, position, color);
|
||||||
|
}
|
||||||
|
}
|
1
Autoloads/FloatingTextManager.cs.uid
Normal file
1
Autoloads/FloatingTextManager.cs.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://cobgfsr3gw7cn
|
270
Autoloads/GameManager.cs
Normal file
270
Autoloads/GameManager.cs
Normal file
@@ -0,0 +1,270 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using Godot;
|
||||||
|
using Godot.Collections;
|
||||||
|
using Mr.BrickAdventures.scripts.components;
|
||||||
|
using Mr.BrickAdventures.scripts.Resources;
|
||||||
|
using Double = System.Double;
|
||||||
|
|
||||||
|
namespace Mr.BrickAdventures.Autoloads;
|
||||||
|
|
||||||
|
public partial class GameManager : Node
|
||||||
|
{
|
||||||
|
[Export] public Array<PackedScene> LevelScenes { get; set; } = [];
|
||||||
|
|
||||||
|
public PlayerController Player {
|
||||||
|
get => GetPlayer();
|
||||||
|
private set => _player = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Node> _sceneNodes = [];
|
||||||
|
private PlayerController _player;
|
||||||
|
private SpeedRunManager _speedRunManager;
|
||||||
|
private EventBus _eventBus;
|
||||||
|
|
||||||
|
[Export]
|
||||||
|
public Dictionary PlayerState { get; set; } = new()
|
||||||
|
{
|
||||||
|
{ "coins", 0 },
|
||||||
|
{ "lives", 3 },
|
||||||
|
{ "current_level", 0 },
|
||||||
|
{ "completed_levels", new Array<int>() },
|
||||||
|
{ "unlocked_levels", new Array<int>() {0}},
|
||||||
|
{ "unlocked_skills", new Array<SkillData>() }
|
||||||
|
};
|
||||||
|
|
||||||
|
[Export]
|
||||||
|
public Dictionary CurrentSessionState { get; private set; } = new()
|
||||||
|
{
|
||||||
|
{ "coins_collected", 0 },
|
||||||
|
{ "skills_unlocked", new Array<SkillData>() }
|
||||||
|
};
|
||||||
|
|
||||||
|
public override void _EnterTree()
|
||||||
|
{
|
||||||
|
GetTree().NodeAdded += OnNodeAdded;
|
||||||
|
GetTree().NodeRemoved += OnNodeRemoved;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void _ExitTree()
|
||||||
|
{
|
||||||
|
GetTree().NodeAdded -= OnNodeAdded;
|
||||||
|
GetTree().NodeRemoved -= OnNodeRemoved;
|
||||||
|
_sceneNodes.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void _Ready()
|
||||||
|
{
|
||||||
|
_speedRunManager = GetNode<SpeedRunManager>("/root/SpeedRunManager");
|
||||||
|
_eventBus = GetNode<EventBus>("/root/EventBus");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnNodeAdded(Node node)
|
||||||
|
{
|
||||||
|
_sceneNodes.Add(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnNodeRemoved(Node node)
|
||||||
|
{
|
||||||
|
_sceneNodes.Remove(node);
|
||||||
|
if (node == _player)
|
||||||
|
{
|
||||||
|
_player = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddCoins(int amount)
|
||||||
|
{
|
||||||
|
PlayerState["coins"] = Mathf.Max(0, (int)PlayerState["coins"] + amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetCoins(int amount) => PlayerState["coins"] = Mathf.Max(0, amount);
|
||||||
|
|
||||||
|
public int GetCoins() => (int)PlayerState["coins"] + (int)CurrentSessionState["coins_collected"];
|
||||||
|
|
||||||
|
public void RemoveCoins(int amount)
|
||||||
|
{
|
||||||
|
var sessionCoins = (int)CurrentSessionState["coins_collected"];
|
||||||
|
if (amount <= sessionCoins)
|
||||||
|
{
|
||||||
|
CurrentSessionState["coins_collected"] = sessionCoins - amount;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var remaining = amount - sessionCoins;
|
||||||
|
CurrentSessionState["coins_collected"] = 0;
|
||||||
|
PlayerState["coins"] = Mathf.Max(0, (int)PlayerState["coins"] - remaining);
|
||||||
|
}
|
||||||
|
PlayerState["coins"] = Mathf.Max(0, (int)PlayerState["coins"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddLives(int amount) => PlayerState["lives"] = (int)PlayerState["lives"] + amount;
|
||||||
|
public void RemoveLives(int amount) => PlayerState["lives"] = (int)PlayerState["lives"] - amount;
|
||||||
|
public void SetLives(int amount) => PlayerState["lives"] = amount;
|
||||||
|
public int GetLives() => (int)PlayerState["lives"];
|
||||||
|
|
||||||
|
public bool IsSkillUnlocked(SkillData skill)
|
||||||
|
{
|
||||||
|
return ((Array)PlayerState["unlocked_skills"]).Contains(skill)
|
||||||
|
|| ((Array)CurrentSessionState["skills_unlocked"]).Contains(skill);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UnlockSkill(SkillData skill)
|
||||||
|
{
|
||||||
|
if (!IsSkillUnlocked(skill))
|
||||||
|
((Array)PlayerState["unlocked_skills"]).Add(skill);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveSkill(string skillName)
|
||||||
|
{
|
||||||
|
var arr = (Array)PlayerState["unlocked_skills"];
|
||||||
|
foreach (SkillData s in arr)
|
||||||
|
{
|
||||||
|
if (s.Name != skillName) continue;
|
||||||
|
|
||||||
|
arr.Remove(s);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UnlockSkills(Array<SkillData> skills)
|
||||||
|
{
|
||||||
|
foreach (var s in skills)
|
||||||
|
UnlockSkill(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ResetPlayerState()
|
||||||
|
{
|
||||||
|
PlayerState = new Dictionary
|
||||||
|
{
|
||||||
|
{ "coins", 0 },
|
||||||
|
{ "lives", 3 },
|
||||||
|
{ "current_level", 0 },
|
||||||
|
{ "completed_levels", new Array<int>() },
|
||||||
|
{ "unlocked_levels", new Array<int>() {0}},
|
||||||
|
{ "unlocked_skills", new Array<SkillData>() },
|
||||||
|
{ "statistics", new Godot.Collections.Dictionary<string, Variant>()}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UnlockLevel(int levelIndex)
|
||||||
|
{
|
||||||
|
var unlocked = (Array)PlayerState["unlocked_levels"];
|
||||||
|
if (!unlocked.Contains(levelIndex)) unlocked.Add(levelIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void TryToGoToNextLevel()
|
||||||
|
{
|
||||||
|
var next = (int)PlayerState["current_level"] + 1;
|
||||||
|
var unlocked = (Array)PlayerState["unlocked_levels"];
|
||||||
|
if (next < LevelScenes.Count && unlocked.Contains(next))
|
||||||
|
{
|
||||||
|
PlayerState["current_level"] = next;
|
||||||
|
GetTree().ChangeSceneToPacked(LevelScenes[next]);
|
||||||
|
_eventBus.EmitSignal(EventBus.SignalName.LevelStarted, next, GetTree().CurrentScene);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void MarkLevelComplete(int levelIndex)
|
||||||
|
{
|
||||||
|
UnlockLevel(levelIndex + 1);
|
||||||
|
var completed = (Array)PlayerState["completed_levels"];
|
||||||
|
if (!completed.Contains(levelIndex)) completed.Add(levelIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ResetCurrentSessionState()
|
||||||
|
{
|
||||||
|
CurrentSessionState = new Dictionary
|
||||||
|
{
|
||||||
|
{ "coins_collected", 0 },
|
||||||
|
{ "skills_unlocked", new Array<SkillData>() }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RestartGame()
|
||||||
|
{
|
||||||
|
ResetPlayerState();
|
||||||
|
ResetCurrentSessionState();
|
||||||
|
GetTree().ChangeSceneToPacked(LevelScenes[0]);
|
||||||
|
GetNode<SaveSystem>("/root/SaveSystem").SaveGame();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void QuitGame() => GetTree().Quit();
|
||||||
|
|
||||||
|
public void PauseGame() => Engine.TimeScale = 0;
|
||||||
|
public void ResumeGame() => Engine.TimeScale = 1;
|
||||||
|
|
||||||
|
public void StartNewGame()
|
||||||
|
{
|
||||||
|
ResetPlayerState();
|
||||||
|
ResetCurrentSessionState();
|
||||||
|
|
||||||
|
_speedRunManager?.StartTimer();
|
||||||
|
|
||||||
|
GetTree().ChangeSceneToPacked(LevelScenes[0]);
|
||||||
|
GetNode<SaveSystem>("/root/SaveSystem").SaveGame();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ContinueGame()
|
||||||
|
{
|
||||||
|
var save = GetNode<SaveSystem>("/root/SaveSystem");
|
||||||
|
if (!save.LoadGame())
|
||||||
|
{
|
||||||
|
GD.PrintErr("Failed to load game. Starting a new game instead.");
|
||||||
|
StartNewGame();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var idx = (int)PlayerState["current_level"];
|
||||||
|
if (idx < LevelScenes.Count)
|
||||||
|
GetTree().ChangeSceneToPacked(LevelScenes[idx]);
|
||||||
|
else
|
||||||
|
GD.PrintErr("No levels unlocked to continue.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnLevelComplete()
|
||||||
|
{
|
||||||
|
var levelIndex = (int)PlayerState["current_level"];
|
||||||
|
MarkLevelComplete(levelIndex);
|
||||||
|
|
||||||
|
AddCoins((int)CurrentSessionState["coins_collected"]);
|
||||||
|
foreach (var s in (Array)CurrentSessionState["skills_unlocked"])
|
||||||
|
UnlockSkill((SkillData)s);
|
||||||
|
|
||||||
|
var completionTime = _speedRunManager?.GetCurrentLevelTime() ?? 0.0;
|
||||||
|
_eventBus.EmitSignal(EventBus.SignalName.LevelCompleted, levelIndex, GetTree().CurrentScene, completionTime);
|
||||||
|
|
||||||
|
ResetCurrentSessionState();
|
||||||
|
TryToGoToNextLevel();
|
||||||
|
GetNode<SaveSystem>("/root/SaveSystem").SaveGame();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Array<SkillData> GetUnlockedSkills()
|
||||||
|
{
|
||||||
|
var unlocked = (Array<SkillData>)PlayerState["unlocked_skills"];
|
||||||
|
var session = (Array<SkillData>)CurrentSessionState["skills_unlocked"];
|
||||||
|
if (session!.Count == 0) return unlocked;
|
||||||
|
if (unlocked!.Count == 0) return session;
|
||||||
|
var joined = new Array<SkillData>();
|
||||||
|
joined.AddRange(unlocked);
|
||||||
|
joined.AddRange(session);
|
||||||
|
return joined;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PlayerController GetPlayer()
|
||||||
|
{
|
||||||
|
if (_player != null && IsInstanceValid(_player)) return _player;
|
||||||
|
|
||||||
|
_player = null;
|
||||||
|
|
||||||
|
foreach (var node in _sceneNodes)
|
||||||
|
{
|
||||||
|
if (node is not PlayerController player) continue;
|
||||||
|
|
||||||
|
_player = player;
|
||||||
|
return _player;
|
||||||
|
}
|
||||||
|
|
||||||
|
GD.PrintErr("PlayerController not found in the scene tree.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
1
Autoloads/GameManager.cs.uid
Normal file
1
Autoloads/GameManager.cs.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://c6eoi3ymefc0x
|
112
Autoloads/GhostManager.cs
Normal file
112
Autoloads/GhostManager.cs
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using Godot;
|
||||||
|
using Godot.Collections;
|
||||||
|
using Mr.BrickAdventures.scripts;
|
||||||
|
|
||||||
|
namespace Mr.BrickAdventures.Autoloads;
|
||||||
|
|
||||||
|
public partial class GhostManager : Node
|
||||||
|
{
|
||||||
|
[Export] private PackedScene GhostPlayerScene { get; set; }
|
||||||
|
|
||||||
|
public bool IsRecording { get; private set; } = false;
|
||||||
|
public bool IsPlaybackEnabled { get; private set; } = true;
|
||||||
|
|
||||||
|
private List<GhostFrame> _currentRecording = [];
|
||||||
|
private double _startTime = 0.0;
|
||||||
|
private int _currentLevelIndex = -1;
|
||||||
|
|
||||||
|
public void StartRecording(int levelIndex)
|
||||||
|
{
|
||||||
|
if (!IsPlaybackEnabled) return;
|
||||||
|
|
||||||
|
_currentLevelIndex = levelIndex;
|
||||||
|
_currentRecording.Clear();
|
||||||
|
_startTime = Time.GetTicksMsec() / 1000.0;
|
||||||
|
IsRecording = true;
|
||||||
|
GD.Print("Ghost recording started.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void StopRecording(bool levelCompleted, double finalTime)
|
||||||
|
{
|
||||||
|
if (!IsRecording) return;
|
||||||
|
IsRecording = false;
|
||||||
|
|
||||||
|
if (levelCompleted)
|
||||||
|
{
|
||||||
|
var bestTime = LoadBestTime(_currentLevelIndex);
|
||||||
|
if (finalTime < bestTime)
|
||||||
|
{
|
||||||
|
SaveGhostData(_currentLevelIndex, finalTime);
|
||||||
|
GD.Print($"New best ghost saved for level {_currentLevelIndex}. Time: {finalTime}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_currentRecording.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RecordFrame(Vector2 position)
|
||||||
|
{
|
||||||
|
if (!IsRecording) return;
|
||||||
|
|
||||||
|
var frame = new GhostFrame
|
||||||
|
{
|
||||||
|
Timestamp = (Time.GetTicksMsec() / 1000.0) - _startTime,
|
||||||
|
Position = position
|
||||||
|
};
|
||||||
|
_currentRecording.Add(frame);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SpawnGhostPlayer(int levelIndex, Node parent)
|
||||||
|
{
|
||||||
|
if (!IsPlaybackEnabled || GhostPlayerScene == null) return;
|
||||||
|
|
||||||
|
var ghostData = LoadGhostData(levelIndex);
|
||||||
|
if (ghostData.Count > 0)
|
||||||
|
{
|
||||||
|
var ghostPlayer = GhostPlayerScene.Instantiate<GhostPlayer>();
|
||||||
|
parent.AddChild(ghostPlayer);
|
||||||
|
ghostPlayer.StartPlayback(ghostData);
|
||||||
|
GD.Print($"Ghost player spawned for level {levelIndex}.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SaveGhostData(int levelIndex, double time)
|
||||||
|
{
|
||||||
|
var path = $"user://ghost_level_{levelIndex}.dat";
|
||||||
|
using var file = FileAccess.Open(path, FileAccess.ModeFlags.Write);
|
||||||
|
|
||||||
|
var dataToSave = new Godot.Collections.Dictionary
|
||||||
|
{
|
||||||
|
{ "time", time },
|
||||||
|
{ "frames", _currentRecording.ToArray() }
|
||||||
|
};
|
||||||
|
file.StoreVar(dataToSave);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<GhostFrame> LoadGhostData(int levelIndex)
|
||||||
|
{
|
||||||
|
var path = $"user://ghost_level_{levelIndex}.dat";
|
||||||
|
if (!FileAccess.FileExists(path)) return [];
|
||||||
|
|
||||||
|
using var file = FileAccess.Open(path, FileAccess.ModeFlags.Read);
|
||||||
|
var savedData = (Dictionary)file.GetVar();
|
||||||
|
var framesArray = (Array)savedData["frames"];
|
||||||
|
|
||||||
|
var frames = new List<GhostFrame>();
|
||||||
|
foreach (var obj in framesArray)
|
||||||
|
{
|
||||||
|
frames.Add((GhostFrame)obj);
|
||||||
|
}
|
||||||
|
return frames;
|
||||||
|
}
|
||||||
|
|
||||||
|
private double LoadBestTime(int levelIndex)
|
||||||
|
{
|
||||||
|
var path = $"user://ghost_level_{levelIndex}.dat";
|
||||||
|
if (!FileAccess.FileExists(path)) return double.MaxValue;
|
||||||
|
|
||||||
|
using var file = FileAccess.Open(path, FileAccess.ModeFlags.Read);
|
||||||
|
var data = (Dictionary)file.GetVar();
|
||||||
|
return (double)data["time"];
|
||||||
|
}
|
||||||
|
}
|
1
Autoloads/GhostManager.cs.uid
Normal file
1
Autoloads/GhostManager.cs.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://cgmuod4p2hg5h
|
61
Autoloads/SaveSystem.cs
Normal file
61
Autoloads/SaveSystem.cs
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
using Godot;
|
||||||
|
using Godot.Collections;
|
||||||
|
using Mr.BrickAdventures.scripts.Resources;
|
||||||
|
|
||||||
|
namespace Mr.BrickAdventures.Autoloads;
|
||||||
|
|
||||||
|
public partial class SaveSystem : Node
|
||||||
|
{
|
||||||
|
[Export] public string SavePath { get; set; } = "user://savegame.save";
|
||||||
|
[Export] public int Version { get; set; } = 1;
|
||||||
|
|
||||||
|
private GameManager _gameManager;
|
||||||
|
|
||||||
|
public override void _Ready()
|
||||||
|
{
|
||||||
|
_gameManager = GetNode<GameManager>("/root/GameManager");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SaveGame()
|
||||||
|
{
|
||||||
|
var saveData = new Dictionary
|
||||||
|
{
|
||||||
|
{ "player_state", _gameManager.PlayerState},
|
||||||
|
{ "version", Version}
|
||||||
|
};
|
||||||
|
|
||||||
|
using var file = FileAccess.Open(SavePath, FileAccess.ModeFlags.Write);
|
||||||
|
file.StoreVar(saveData);
|
||||||
|
GD.Print("Game state saved to: ", SavePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool LoadGame()
|
||||||
|
{
|
||||||
|
if (!FileAccess.FileExists(SavePath))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
using var file = FileAccess.Open(SavePath, FileAccess.ModeFlags.Read);
|
||||||
|
var saveDataObj = (Dictionary)file.GetVar();
|
||||||
|
|
||||||
|
if (saveDataObj.ContainsKey("version") && (int)saveDataObj["version"] != Version)
|
||||||
|
{
|
||||||
|
GD.Print($"Save file version mismatch. Expected: {Version}, Found: {saveDataObj["version"]}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
GD.Print("Game state loaded from: ", SavePath);
|
||||||
|
GD.Print("Player state: ", saveDataObj["player_state"]);
|
||||||
|
_gameManager.PlayerState = (Dictionary)saveDataObj["player_state"];
|
||||||
|
|
||||||
|
var skills = new Array<SkillData>();
|
||||||
|
foreach (var skill in (Array<SkillData>)_gameManager.PlayerState["unlocked_skills"])
|
||||||
|
{
|
||||||
|
skills.Add(skill);
|
||||||
|
}
|
||||||
|
|
||||||
|
_gameManager.UnlockSkills(skills);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool CheckSaveExists() => FileAccess.FileExists(SavePath);
|
||||||
|
}
|
1
Autoloads/SaveSystem.cs.uid
Normal file
1
Autoloads/SaveSystem.cs.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://bh20fqbyifidc
|
217
Autoloads/SkillManager.cs
Normal file
217
Autoloads/SkillManager.cs
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Godot;
|
||||||
|
using Godot.Collections;
|
||||||
|
using Mr.BrickAdventures.scripts.components;
|
||||||
|
using Mr.BrickAdventures.scripts.interfaces;
|
||||||
|
using Mr.BrickAdventures.scripts.Resources;
|
||||||
|
|
||||||
|
namespace Mr.BrickAdventures.Autoloads;
|
||||||
|
|
||||||
|
public partial class SkillManager : Node
|
||||||
|
{
|
||||||
|
private GameManager _gameManager;
|
||||||
|
private PlayerController _player;
|
||||||
|
|
||||||
|
[Export] public Array<SkillData> AvailableSkills { get; set; } = [];
|
||||||
|
|
||||||
|
public Dictionary ActiveComponents { get; private set; } = new();
|
||||||
|
|
||||||
|
[Signal]
|
||||||
|
public delegate void ActiveThrowSkillChangedEventHandler(BrickThrowComponent throwComponent);
|
||||||
|
[Signal]
|
||||||
|
public delegate void SkillRemovedEventHandler(SkillData skillData);
|
||||||
|
|
||||||
|
public override void _Ready()
|
||||||
|
{
|
||||||
|
_gameManager = GetNode<GameManager>("/root/GameManager");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called by the PlayerController from its _Ready method to register itself with the manager.
|
||||||
|
/// </summary>
|
||||||
|
public void RegisterPlayer(PlayerController player)
|
||||||
|
{
|
||||||
|
if (_player == player) return;
|
||||||
|
|
||||||
|
// If a player is already registered (e.g., from a previous scene), unregister it first.
|
||||||
|
if (_player != null && IsInstanceValid(_player))
|
||||||
|
{
|
||||||
|
UnregisterPlayer();
|
||||||
|
}
|
||||||
|
|
||||||
|
_player = player;
|
||||||
|
if (_player != null)
|
||||||
|
{
|
||||||
|
// Automatically unregister when the player node is removed from the scene.
|
||||||
|
_player.TreeExiting += UnregisterPlayer;
|
||||||
|
ApplyUnlockedSkills();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Cleans up skills and references related to the current player.
|
||||||
|
/// </summary>
|
||||||
|
private void UnregisterPlayer()
|
||||||
|
{
|
||||||
|
if (_player != null && IsInstanceValid(_player))
|
||||||
|
{
|
||||||
|
_player.TreeExiting -= UnregisterPlayer;
|
||||||
|
RemoveAllActiveSkills();
|
||||||
|
}
|
||||||
|
_player = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddSkill(SkillData skillData)
|
||||||
|
{
|
||||||
|
// Ensure a valid player is registered before adding a skill.
|
||||||
|
if (_player == null || !IsInstanceValid(_player))
|
||||||
|
{
|
||||||
|
GD.Print("SkillManager: Player not available to add skill.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ActiveComponents.ContainsKey(skillData.Name))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (skillData.Type == SkillType.Throw)
|
||||||
|
{
|
||||||
|
var unlocked = _gameManager.GetUnlockedSkills();
|
||||||
|
foreach (var sd in unlocked)
|
||||||
|
{
|
||||||
|
SkillData data = null;
|
||||||
|
foreach (var s in AvailableSkills)
|
||||||
|
{
|
||||||
|
if (s == sd)
|
||||||
|
{
|
||||||
|
data = s;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Remove other throw skills if a new one is added
|
||||||
|
if (data is { Type: SkillType.Throw })
|
||||||
|
RemoveSkill(data.Name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var instance = skillData.Node.Instantiate();
|
||||||
|
if (instance is ISkill skill)
|
||||||
|
{
|
||||||
|
// Initialize the skill with the registered player instance.
|
||||||
|
skill.Initialize(_player, skillData);
|
||||||
|
skill.Activate();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
GD.PrintErr($"Skill scene for '{skillData.Name}' does not implement ISkill!");
|
||||||
|
instance.QueueFree();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the skill node as a child of the player.
|
||||||
|
_player.AddChild(instance);
|
||||||
|
ActiveComponents[skillData.Name] = instance;
|
||||||
|
|
||||||
|
if (instance is BrickThrowComponent btc)
|
||||||
|
{
|
||||||
|
EmitSignalActiveThrowSkillChanged(btc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveSkill(string skillName)
|
||||||
|
{
|
||||||
|
if (!ActiveComponents.TryGetValue(skillName, out var component))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (component.AsGodotObject() is BrickThrowComponent)
|
||||||
|
{
|
||||||
|
EmitSignalActiveThrowSkillChanged(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
var inst = (Node)component;
|
||||||
|
if (inst is ISkill skill)
|
||||||
|
{
|
||||||
|
skill.Deactivate();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsInstanceValid(inst))
|
||||||
|
inst.QueueFree();
|
||||||
|
|
||||||
|
var skills = _gameManager.GetUnlockedSkills();
|
||||||
|
foreach (var s in skills)
|
||||||
|
{
|
||||||
|
if (s.Name == skillName)
|
||||||
|
{
|
||||||
|
s.IsActive = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ActiveComponents.Remove(skillName);
|
||||||
|
var sd = GetSkillByName(skillName);
|
||||||
|
if (sd != null) EmitSignalSkillRemoved(sd);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RemoveAllActiveSkills()
|
||||||
|
{
|
||||||
|
// Create a copy of keys to avoid modification during iteration
|
||||||
|
var keys = ActiveComponents.Keys.ToArray();
|
||||||
|
var skillNames = keys.Select(key => key.ToString()).ToList();
|
||||||
|
|
||||||
|
foreach (var skillName in skillNames)
|
||||||
|
{
|
||||||
|
RemoveSkill(skillName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ApplyUnlockedSkills()
|
||||||
|
{
|
||||||
|
if (_player == null || !IsInstanceValid(_player)) return;
|
||||||
|
|
||||||
|
foreach (var sd in AvailableSkills)
|
||||||
|
{
|
||||||
|
if (_gameManager.IsSkillUnlocked(sd))
|
||||||
|
{
|
||||||
|
CallDeferred(MethodName.AddSkill, sd);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
RemoveSkill(sd.Name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public SkillData GetSkillByName(string skillName)
|
||||||
|
{
|
||||||
|
foreach (var sd in AvailableSkills)
|
||||||
|
if (sd.Name == skillName) return sd;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ActivateSkill(SkillData skill)
|
||||||
|
{
|
||||||
|
if (!ActiveComponents.ContainsKey(skill.Name))
|
||||||
|
{
|
||||||
|
AddSkill(skill);
|
||||||
|
skill.IsActive = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DeactivateSkill(SkillData skill)
|
||||||
|
{
|
||||||
|
if (ActiveComponents.ContainsKey(skill.Name))
|
||||||
|
{
|
||||||
|
RemoveSkill(skill.Name);
|
||||||
|
skill.IsActive = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ToggleSkillActivation(SkillData skill)
|
||||||
|
{
|
||||||
|
if (skill == null) return;
|
||||||
|
|
||||||
|
if (ActiveComponents.ContainsKey(skill.Name))
|
||||||
|
DeactivateSkill(skill);
|
||||||
|
else
|
||||||
|
ActivateSkill(skill);
|
||||||
|
}
|
||||||
|
}
|
1
Autoloads/SkillManager.cs.uid
Normal file
1
Autoloads/SkillManager.cs.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://dru77vj07e18s
|
62
Autoloads/SpeedRunManager.cs
Normal file
62
Autoloads/SpeedRunManager.cs
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using Godot;
|
||||||
|
|
||||||
|
namespace Mr.BrickAdventures.Autoloads;
|
||||||
|
|
||||||
|
public partial class SpeedRunManager : Node
|
||||||
|
{
|
||||||
|
public bool IsRunning { get; private set; } = false;
|
||||||
|
public bool IsVisible { get; private set; } = false;
|
||||||
|
|
||||||
|
private double _startTime;
|
||||||
|
private double _levelStartTime;
|
||||||
|
private List<double> _splits = [];
|
||||||
|
|
||||||
|
[Signal] public delegate void TimeUpdatedEventHandler(double totalTime, double levelTime);
|
||||||
|
|
||||||
|
public override void _Process(double delta)
|
||||||
|
{
|
||||||
|
if (!IsRunning || !IsVisible) return;
|
||||||
|
|
||||||
|
EmitSignalTimeUpdated(GetCurrentTotalTime(), GetCurrentLevelTime());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void StartTimer()
|
||||||
|
{
|
||||||
|
_startTime = Time.GetTicksMsec() / 1000.0;
|
||||||
|
_levelStartTime = _startTime;
|
||||||
|
_splits.Clear();
|
||||||
|
IsRunning = true;
|
||||||
|
GD.Print("Speedrun timer started.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void StopTimer()
|
||||||
|
{
|
||||||
|
if (!IsRunning) return;
|
||||||
|
IsRunning = false;
|
||||||
|
var finalTime = GetCurrentTotalTime();
|
||||||
|
GD.Print($"Speedrun finished. Final time: {FormatTime(finalTime)}");
|
||||||
|
|
||||||
|
// Save personal best if applicable
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Split()
|
||||||
|
{
|
||||||
|
if (!IsRunning) return;
|
||||||
|
|
||||||
|
var now = Time.GetTicksMsec() / 1000.0;
|
||||||
|
var splitTime = now - _levelStartTime;
|
||||||
|
_splits.Add(splitTime);
|
||||||
|
_levelStartTime = now;
|
||||||
|
GD.Print($"Split recorded: {FormatTime(splitTime)}");
|
||||||
|
}
|
||||||
|
|
||||||
|
public double GetCurrentTotalTime() => IsRunning ? (Time.GetTicksMsec() / 1000.0) - _startTime : 0;
|
||||||
|
public double GetCurrentLevelTime() => IsRunning ? (Time.GetTicksMsec() / 1000.0) - _levelStartTime : 0;
|
||||||
|
|
||||||
|
public static string FormatTime(double time)
|
||||||
|
{
|
||||||
|
var span = System.TimeSpan.FromSeconds(time);
|
||||||
|
return $"{(int)span.TotalMinutes:00}:{span.Seconds:00}.{span.Milliseconds:000}";
|
||||||
|
}
|
||||||
|
}
|
1
Autoloads/SpeedRunManager.cs.uid
Normal file
1
Autoloads/SpeedRunManager.cs.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://c6ohe36xw1h21
|
78
Autoloads/StatisticsManager.cs
Normal file
78
Autoloads/StatisticsManager.cs
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
using Godot;
|
||||||
|
using Godot.Collections;
|
||||||
|
|
||||||
|
namespace Mr.BrickAdventures.Autoloads;
|
||||||
|
|
||||||
|
public partial class StatisticsManager : Node
|
||||||
|
{
|
||||||
|
private GameManager _gameManager;
|
||||||
|
private AchievementManager _achievementManager;
|
||||||
|
private Dictionary<string, Variant> _stats = new();
|
||||||
|
|
||||||
|
public override void _Ready()
|
||||||
|
{
|
||||||
|
_gameManager = GetNode<GameManager>("/root/GameManager");
|
||||||
|
_achievementManager = GetNode<AchievementManager>("/root/AchievementManager");
|
||||||
|
LoadStatistics();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LoadStatistics()
|
||||||
|
{
|
||||||
|
if (_gameManager.PlayerState.TryGetValue("statistics", out var statsObj))
|
||||||
|
{
|
||||||
|
_stats = (Dictionary<string, Variant>)statsObj;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_stats = new Dictionary<string, Variant>();
|
||||||
|
_gameManager.PlayerState["statistics"] = _stats;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Increases a numerical statistic by a given amount.
|
||||||
|
/// </summary>
|
||||||
|
public void IncrementStat(string statName, int amount = 1)
|
||||||
|
{
|
||||||
|
if (_stats.TryGetValue(statName, out var currentValue))
|
||||||
|
{
|
||||||
|
_stats[statName] = (int)currentValue + amount;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_stats[statName] = amount;
|
||||||
|
}
|
||||||
|
GD.Print($"Stat '{statName}' updated to: {_stats[statName]}");
|
||||||
|
CheckAchievementsForStat(statName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the value of a statistic.
|
||||||
|
/// </summary>
|
||||||
|
public Variant GetStat(string statName, Variant defaultValue = default)
|
||||||
|
{
|
||||||
|
return _stats.TryGetValue(statName, out var value) ? value : defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if the updated stat meets the criteria for any achievements.
|
||||||
|
/// </summary>
|
||||||
|
private void CheckAchievementsForStat(string statName)
|
||||||
|
{
|
||||||
|
switch (statName)
|
||||||
|
{
|
||||||
|
case "enemies_defeated":
|
||||||
|
if ((int)GetStat(statName, 0) >= 100)
|
||||||
|
{
|
||||||
|
_achievementManager.UnlockAchievement("slayer_100_enemies");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "jumps_made":
|
||||||
|
if ((int)GetStat(statName, 0) >= 1000)
|
||||||
|
{
|
||||||
|
_achievementManager.UnlockAchievement("super_jumper");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
1
Autoloads/StatisticsManager.cs.uid
Normal file
1
Autoloads/StatisticsManager.cs.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://c5p3l2mhkw0p4
|
77
Autoloads/SteamManager.cs
Normal file
77
Autoloads/SteamManager.cs
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
using System;
|
||||||
|
using Godot;
|
||||||
|
using Steamworks;
|
||||||
|
using Steamworks.Data;
|
||||||
|
|
||||||
|
namespace Mr.BrickAdventures.Autoloads;
|
||||||
|
|
||||||
|
public partial class SteamManager : Node
|
||||||
|
{
|
||||||
|
private const uint AppId = 3575090;
|
||||||
|
|
||||||
|
public static string PlayerName { get; private set; } = "Player";
|
||||||
|
public static bool IsSteamInitialized { get; private set; } = false;
|
||||||
|
|
||||||
|
public override void _Ready()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
SteamClient.Init(AppId);
|
||||||
|
IsSteamInitialized = true;
|
||||||
|
|
||||||
|
PlayerName = SteamClient.Name;
|
||||||
|
|
||||||
|
GD.Print($"Steam initialized successfully for user: {PlayerName}");
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
GD.PushError("Failed to initialize Steamworks. Is Steam running?");
|
||||||
|
GD.PushError(e.Message);
|
||||||
|
IsSteamInitialized = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void _Process(double delta)
|
||||||
|
{
|
||||||
|
if (IsSteamInitialized) SteamClient.RunCallbacks();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void _Notification(int what)
|
||||||
|
{
|
||||||
|
if (what == NotificationWMCloseRequest)
|
||||||
|
{
|
||||||
|
if (IsSteamInitialized)
|
||||||
|
{
|
||||||
|
SteamClient.Shutdown();
|
||||||
|
}
|
||||||
|
GetTree().Quit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void UnlockAchievement(string achievementId)
|
||||||
|
{
|
||||||
|
if (!IsSteamInitialized)
|
||||||
|
{
|
||||||
|
GD.Print($"Steam not initialized. Cannot unlock achievement '{achievementId}'.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var ach = new Achievement(achievementId);
|
||||||
|
|
||||||
|
if (ach.State)
|
||||||
|
{
|
||||||
|
GD.Print($"Achievement '{achievementId}' is already unlocked.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ach.Trigger())
|
||||||
|
{
|
||||||
|
SteamUserStats.StoreStats();
|
||||||
|
GD.Print($"Successfully triggered achievement: {ach.Name} ({ach.Identifier})");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
GD.PrintErr($"Failed to trigger achievement: {achievementId}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
1
Autoloads/SteamManager.cs.uid
Normal file
1
Autoloads/SteamManager.cs.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://b5abjlnbia63q
|
65
Autoloads/UIManager.cs
Normal file
65
Autoloads/UIManager.cs
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
using Godot;
|
||||||
|
using Godot.Collections;
|
||||||
|
|
||||||
|
namespace Mr.BrickAdventures.Autoloads;
|
||||||
|
|
||||||
|
public partial class UIManager : Node
|
||||||
|
{
|
||||||
|
[Export] public Array<Control> UiStack { get; set; } = new();
|
||||||
|
|
||||||
|
[Signal] public delegate void ScreenPushedEventHandler(Control screen);
|
||||||
|
[Signal] public delegate void ScreenPoppedEventHandler(Control screen);
|
||||||
|
|
||||||
|
public void PushScreen(Control screen)
|
||||||
|
{
|
||||||
|
if (screen == null)
|
||||||
|
{
|
||||||
|
GD.PushError($"Cannot push a null screen.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
UiStack.Add(screen);
|
||||||
|
screen.Show();
|
||||||
|
screen.SetProcessInput(true);
|
||||||
|
screen.SetFocusMode(Control.FocusModeEnum.All);
|
||||||
|
screen.GrabFocus();
|
||||||
|
EmitSignalScreenPushed(screen);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void PopScreen()
|
||||||
|
{
|
||||||
|
if (UiStack.Count == 0)
|
||||||
|
{
|
||||||
|
GD.PushError($"Cannot pop screen from an empty stack.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var top = (Control)UiStack[^1];
|
||||||
|
UiStack.RemoveAt(UiStack.Count - 1);
|
||||||
|
top.Hide();
|
||||||
|
top.SetProcessInput(false);
|
||||||
|
EmitSignalScreenPopped(top);
|
||||||
|
top.AcceptEvent();
|
||||||
|
|
||||||
|
if (UiStack.Count > 0) ((Control)UiStack[^1]).GrabFocus();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Control TopScreen() => UiStack.Count > 0 ? (Control)UiStack[^1] : null;
|
||||||
|
|
||||||
|
public bool IsScreenOnTop(Control screen) => UiStack.Count > 0 && (Control)UiStack[^1] == screen;
|
||||||
|
|
||||||
|
public bool IsVisibleOnStack(Control screen) => UiStack.Contains(screen) && screen.Visible;
|
||||||
|
|
||||||
|
public void CloseAll()
|
||||||
|
{
|
||||||
|
while (UiStack.Count > 0)
|
||||||
|
PopScreen();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void HideAndDisable(Control screen)
|
||||||
|
{
|
||||||
|
screen.Hide();
|
||||||
|
screen.SetProcessInput(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
1
Autoloads/UIManager.cs.uid
Normal file
1
Autoloads/UIManager.cs.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://c3ldmnrwperr4
|
13
Mr. Brick Adventures.csproj
Normal file
13
Mr. Brick Adventures.csproj
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<Project Sdk="Godot.NET.Sdk/4.4.1">
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<EnableDynamicLoading>true</EnableDynamicLoading>
|
||||||
|
<RootNamespace>Mr.BrickAdventures</RootNamespace>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Facepunch.Steamworks" Version="2.3.3" />
|
||||||
|
<PackageReference Include="Facepunch.Steamworks.Dlls" Version="2.3.2" />
|
||||||
|
<PackageReference Include="Facepunch.Steamworks.Library" Version="2.3.3" />
|
||||||
|
<PackageReference Include="LimboConsole.Sharp" Version="0.0.1-beta-008" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
19
Mr. Brick Adventures.sln
Normal file
19
Mr. Brick Adventures.sln
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
|
# Visual Studio 2012
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mr. Brick Adventures", "Mr. Brick Adventures.csproj", "{A1D482B9-207B-4D6C-A0A0-D9E6D1AE2356}"
|
||||||
|
EndProject
|
||||||
|
Global
|
||||||
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
ExportDebug|Any CPU = ExportDebug|Any CPU
|
||||||
|
ExportRelease|Any CPU = ExportRelease|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
|
{A1D482B9-207B-4D6C-A0A0-D9E6D1AE2356}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{A1D482B9-207B-4D6C-A0A0-D9E6D1AE2356}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{A1D482B9-207B-4D6C-A0A0-D9E6D1AE2356}.ExportDebug|Any CPU.ActiveCfg = ExportDebug|Any CPU
|
||||||
|
{A1D482B9-207B-4D6C-A0A0-D9E6D1AE2356}.ExportDebug|Any CPU.Build.0 = ExportDebug|Any CPU
|
||||||
|
{A1D482B9-207B-4D6C-A0A0-D9E6D1AE2356}.ExportRelease|Any CPU.ActiveCfg = ExportRelease|Any CPU
|
||||||
|
{A1D482B9-207B-4D6C-A0A0-D9E6D1AE2356}.ExportRelease|Any CPU.Build.0 = ExportRelease|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
14
Mr. Brick Adventures.sln.DotSettings.user
Normal file
14
Mr. Brick Adventures.sln.DotSettings.user
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||||
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ACamera2D_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FSourcesCache_003Fa2e12a1a67ad701a97608de6be85250e3e353951ecf8058a02c703490c753_003FCamera2D_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ACanvasItem_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FSourcesCache_003Fef7b819b226fab796d1dfe66d415dd7510bcac87675020ddb8f03a828e763_003FCanvasItem_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ACecovym_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002E_002E_003Ftmp_003FJetBrainsPerUserTemp_002D1000_002D1_003FSandboxFiles_003FSadijuw_003FCecovym_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ACharacterBody2D_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FSourcesCache_003Fbba0bbd7a98ee58286e9484fbe86e01afff6232283f6efd3556eb7116453_003FCharacterBody2D_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ACollisionShape2D_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FSourcesCache_003F2ca9b7334678f5c97c7c2a9fbe4837be71cae11b6a30408dd4791b18f997e4a_003FCollisionShape2D_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ACSharpInstanceBridge_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F94701b444afa4c3d9a7a53ebcaa35fd1583c00_003F14_003F4b4ade3f_003FCSharpInstanceBridge_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ADelegateUtils_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F94701b444afa4c3d9a7a53ebcaa35fd1583c00_003F08_003F45f75e10_003FDelegateUtils_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ADictionary_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F94701b444afa4c3d9a7a53ebcaa35fd1583c00_003F47_003Fda1d8d31_003FDictionary_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AList_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FSourcesCache_003Fe747192abb38e2df82cbdb37e721567726f559914a7b81f8b26ba537de632f4_003FList_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AMr_002EBrickAdventures_002Escripts_002Ecomponents_002ECollectableComponent_005FScriptSignals_002Egenerated_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FSourcesCache_003F80d9408eb7280c15eb4a12b61cdf8f7f1b0c5a2_003FMr_002EBrickAdventures_002Escripts_002Ecomponents_002ECollectableComponent_005FScriptSignals_002Egenerated_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ANode2D_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FSourcesCache_003F86db9cd834346aad02d74c1b66dd9c64d6ef3147435dd9c9c9477b48f7_003FNode2D_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ARectangleShape2D_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FSourcesCache_003Fa1cc98873548652da0c14ecefa4737431426fcbb24a7f0641e3d9c266c3_003FRectangleShape2D_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AShape2D_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FSourcesCache_003F3671dbbd9b17cdf2bf9075b468b6bd7e3ab13fc3be7a116484085d3b6cc9fe_003FShape2D_002Ecs/@EntryIndexedValue">ForceIncluded</s:String></wpf:ResourceDictionary>
|
72
README.md
Normal file
72
README.md
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
# Przygody Pana Cegły
|
||||||
|
|
||||||
|
[](https://store.steampowered.com/app/3575090/Mr_Brick_Adventures/)
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
*"The world’s only brick-throwing dad simulator (probably)."*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The Story
|
||||||
|
|
||||||
|
Disaster has struck!
|
||||||
|
Mr. Brick’s kids have gone missing, scattered across mysterious lands filled with treacherous traps, cranky critters, and more collapsing bridges than an OSHA nightmare.
|
||||||
|
|
||||||
|
Armed with nothing but his legs, his wits, and an infinite supply of throwable bricks (don’t ask where he keeps them), Mr. Brick will stop at nothing to bring his children home.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The Mission
|
||||||
|
|
||||||
|
Run, jump, and hurl bricks with pinpoint accuracy as you navigate dangerous worlds. Smash enemies, trigger ancient mechanisms, and uncover hidden treasures, all while dodging hazards that seem designed specifically to ruin your day.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Features That’ll Knock Your Bricks Off
|
||||||
|
|
||||||
|
* **Tight, Classic Platforming**
|
||||||
|
Leap over pits, time your jumps, and try not to land in something unpleasant.
|
||||||
|
|
||||||
|
* **Brick-Fu Combat**
|
||||||
|
Toss bricks to flatten foes, flip switches, and solve puzzles in the most dad-like way possible.
|
||||||
|
|
||||||
|
* **Secrets & Collectibles**
|
||||||
|
Hunt for coins, discover hidden rooms, and find upgrades to make you just a *little* less fragile.
|
||||||
|
|
||||||
|
* **NES-Inspired Pixel Art**
|
||||||
|
All the charm of an 8-bit classic, no cartridge blowing required.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Play It Here
|
||||||
|
|
||||||
|
[**Wishlist on Steam**](https://store.steampowered.com/app/3575090/Mr_Brick_Adventures/) so you’re ready for launch day.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Project Stats
|
||||||
|
📦 **Lines of Code:**
|
||||||
|

|
||||||
|
|
||||||
|
📈 **Repo Activity:**
|
||||||
|

|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Check the [LICENSE](./LICENSE) for the legal stuff.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Not open to code contributions right now but you *can* help by:
|
||||||
|
|
||||||
|
* Reporting bugs
|
||||||
|
* Suggesting evil new trap ideas
|
||||||
|
* Spreading the word so Mr. Brick can find his kids faster
|
13
achievements/level_complete_1.tres
Normal file
13
achievements/level_complete_1.tres
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
[gd_resource type="Resource" script_class="AchievementResource" load_steps=3 format=3 uid="uid://3odfkm1ig5"]
|
||||||
|
|
||||||
|
[ext_resource type="Texture2D" uid="uid://cebeyr4wnibvk" path="res://sprites/achievement.png" id="1_usw25"]
|
||||||
|
[ext_resource type="Script" uid="uid://duib5phrmpro5" path="res://scripts/Resources/AchievementResource.cs" id="2_n7ktn"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("2_n7ktn")
|
||||||
|
Id = "level_complete_1"
|
||||||
|
DisplayName = "Complete level 1"
|
||||||
|
Description = ""
|
||||||
|
Icon = ExtResource("1_usw25")
|
||||||
|
IsSecret = false
|
||||||
|
metadata/_custom_type_script = "uid://duib5phrmpro5"
|
528
addons/console/console.gd
vendored
528
addons/console/console.gd
vendored
@@ -1,528 +0,0 @@
|
|||||||
extends Node
|
|
||||||
|
|
||||||
var enabled := true
|
|
||||||
var enable_on_release_build := false: set = set_enable_on_release_build
|
|
||||||
var pause_enabled := false
|
|
||||||
signal console_opened
|
|
||||||
signal console_closed
|
|
||||||
signal console_unknown_command
|
|
||||||
|
|
||||||
|
|
||||||
class ConsoleCommand:
|
|
||||||
var function: Callable
|
|
||||||
var arguments: PackedStringArray
|
|
||||||
var required: int
|
|
||||||
var description: String
|
|
||||||
var hidden: bool
|
|
||||||
|
|
||||||
|
|
||||||
func _init(in_function: Callable, in_arguments: PackedStringArray, in_required: int = 0, in_description: String = ""):
|
|
||||||
function = in_function
|
|
||||||
arguments = in_arguments
|
|
||||||
required = in_required
|
|
||||||
description = in_description
|
|
||||||
|
|
||||||
|
|
||||||
var control := Control.new()
|
|
||||||
# If you want to customize the way the console looks, you can direcly modify
|
|
||||||
# the properties of the rich text and line edit here:
|
|
||||||
var rich_label := RichTextLabel.new()
|
|
||||||
var line_edit := LineEdit.new()
|
|
||||||
var console_commands := {}
|
|
||||||
var command_parameters := {}
|
|
||||||
var console_history := []
|
|
||||||
var console_history_index := 0
|
|
||||||
var was_paused_already := false
|
|
||||||
|
|
||||||
|
|
||||||
## Usage: Console.add_command("command_name", <function to call>, <number of arguments or array of argument names>, <required number of arguments>, "Help description")
|
|
||||||
func add_command(command_name: String, function: Callable, arguments = [], required: int = 0, description: String = "") -> void:
|
|
||||||
if (arguments is int):
|
|
||||||
# Legacy call using an argument number
|
|
||||||
var param_array: PackedStringArray
|
|
||||||
for i in range(arguments):
|
|
||||||
param_array.append("arg_" + str(i + 1))
|
|
||||||
console_commands[command_name] = ConsoleCommand.new(function, param_array, required, description)
|
|
||||||
elif (arguments is Array):
|
|
||||||
# New array argument system
|
|
||||||
var str_args: PackedStringArray
|
|
||||||
for argument in arguments:
|
|
||||||
str_args.append(str(argument))
|
|
||||||
console_commands[command_name] = ConsoleCommand.new(function, str_args, required, description)
|
|
||||||
|
|
||||||
|
|
||||||
## Adds a secret command that will not show up in the help or auto-complete.
|
|
||||||
func add_hidden_command(command_name: String, function: Callable, arguments = [], required: int = 0) -> void:
|
|
||||||
add_command(command_name, function, arguments, required)
|
|
||||||
console_commands[command_name].hidden = true
|
|
||||||
|
|
||||||
|
|
||||||
## Removes a command from the console. This should be called on a script's _exit_tree()
|
|
||||||
## if you have console commands for things that are unloaded before the project closes.
|
|
||||||
func remove_command(command_name: String) -> void:
|
|
||||||
console_commands.erase(command_name)
|
|
||||||
command_parameters.erase(command_name)
|
|
||||||
|
|
||||||
|
|
||||||
## Useful if you have a list of possible parameters (ex: level names).
|
|
||||||
func add_command_autocomplete_list(command_name: String, param_list: PackedStringArray):
|
|
||||||
command_parameters[command_name] = param_list
|
|
||||||
|
|
||||||
|
|
||||||
func _enter_tree() -> void:
|
|
||||||
var console_history_file := FileAccess.open("user://console_history.txt", FileAccess.READ)
|
|
||||||
if (console_history_file):
|
|
||||||
while (!console_history_file.eof_reached()):
|
|
||||||
var line := console_history_file.get_line()
|
|
||||||
if (line.length()):
|
|
||||||
add_input_history(line)
|
|
||||||
|
|
||||||
var canvas_layer := CanvasLayer.new()
|
|
||||||
canvas_layer.layer = 3
|
|
||||||
add_child(canvas_layer)
|
|
||||||
control.anchor_bottom = 1.0
|
|
||||||
control.anchor_right = 1.0
|
|
||||||
canvas_layer.add_child(control)
|
|
||||||
var style := StyleBoxFlat.new()
|
|
||||||
style.bg_color = Color("000000d7")
|
|
||||||
rich_label.selection_enabled = true
|
|
||||||
rich_label.context_menu_enabled = true
|
|
||||||
rich_label.bbcode_enabled = false
|
|
||||||
rich_label.scroll_following = true
|
|
||||||
rich_label.anchor_right = 1.0
|
|
||||||
rich_label.anchor_bottom = 0.5
|
|
||||||
rich_label.add_theme_stylebox_override("normal", style)
|
|
||||||
control.add_child(rich_label)
|
|
||||||
rich_label.append_text("Development console.\n")
|
|
||||||
line_edit.anchor_top = 0.5
|
|
||||||
line_edit.anchor_right = 1.0
|
|
||||||
line_edit.anchor_bottom = 0.5
|
|
||||||
line_edit.placeholder_text = "Enter \"help\" for instructions"
|
|
||||||
control.add_child(line_edit)
|
|
||||||
line_edit.text_submitted.connect(on_text_entered)
|
|
||||||
line_edit.text_changed.connect(on_line_edit_text_changed)
|
|
||||||
control.visible = false
|
|
||||||
process_mode = PROCESS_MODE_ALWAYS
|
|
||||||
|
|
||||||
|
|
||||||
func _exit_tree() -> void:
|
|
||||||
var console_history_file := FileAccess.open("user://console_history.txt", FileAccess.WRITE)
|
|
||||||
if (console_history_file):
|
|
||||||
var write_index := 0
|
|
||||||
var start_write_index := console_history.size() - 100 # Max lines to write
|
|
||||||
for line in console_history:
|
|
||||||
if (write_index >= start_write_index):
|
|
||||||
console_history_file.store_line(line)
|
|
||||||
write_index += 1
|
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
|
||||||
add_command("quit", quit, 0, 0, "Quits the game.")
|
|
||||||
add_command("exit", quit, 0, 0, "Quits the game.")
|
|
||||||
add_command("clear", clear, 0, 0, "Clears the text on the console.")
|
|
||||||
add_command("delete_history", delete_history, 0, 0, "Deletes the history of previously entered commands.")
|
|
||||||
add_command("help", help, 0, 0, "Displays instructions on how to use the console.")
|
|
||||||
add_command("commands_list", commands_list, 0, 0, "Lists all commands and their descriptions.")
|
|
||||||
add_command("commands", commands, 0, 0, "Lists commands with no descriptions.")
|
|
||||||
add_command("calc", calculate, ["mathematical expression to evaluate"], 0, "Evaluates the math passed in for quick arithmetic.")
|
|
||||||
add_command("echo", print_line, ["string"], 1, "Prints given string to the console.")
|
|
||||||
add_command("echo_warning", print_warning, ["string"], 1, "Prints given string as warning to the console.")
|
|
||||||
add_command("echo_info", print_info, ["string"], 1, "Prints given string as info to the console.")
|
|
||||||
add_command("echo_error", print_error, ["string"], 1, "Prints given string as an error to the console.")
|
|
||||||
add_command("pause", pause, 0, 0, "Pauses node processing.")
|
|
||||||
add_command("unpause", unpause, 0, 0, "Unpauses node processing.")
|
|
||||||
add_command("exec", exec, 1, 1, "Execute a script.")
|
|
||||||
|
|
||||||
|
|
||||||
func _input(event: InputEvent) -> void:
|
|
||||||
if (event is InputEventKey):
|
|
||||||
if (event.get_physical_keycode_with_modifiers() == KEY_QUOTELEFT): # ~ key.
|
|
||||||
if (event.pressed):
|
|
||||||
toggle_console()
|
|
||||||
get_tree().get_root().set_input_as_handled()
|
|
||||||
elif (event.physical_keycode == KEY_QUOTELEFT and event.is_command_or_control_pressed()): # Toggles console size or opens big console.
|
|
||||||
if (event.pressed):
|
|
||||||
if (control.visible):
|
|
||||||
toggle_size()
|
|
||||||
else:
|
|
||||||
toggle_console()
|
|
||||||
toggle_size()
|
|
||||||
get_tree().get_root().set_input_as_handled()
|
|
||||||
elif (event.get_physical_keycode_with_modifiers() == KEY_ESCAPE && control.visible): # Disable console on ESC
|
|
||||||
if (event.pressed):
|
|
||||||
toggle_console()
|
|
||||||
get_tree().get_root().set_input_as_handled()
|
|
||||||
if (control.visible and event.pressed):
|
|
||||||
if (event.get_physical_keycode_with_modifiers() == KEY_UP):
|
|
||||||
get_tree().get_root().set_input_as_handled()
|
|
||||||
if (console_history_index > 0):
|
|
||||||
console_history_index -= 1
|
|
||||||
if (console_history_index >= 0):
|
|
||||||
line_edit.text = console_history[console_history_index]
|
|
||||||
line_edit.caret_column = line_edit.text.length()
|
|
||||||
reset_autocomplete()
|
|
||||||
if (event.get_physical_keycode_with_modifiers() == KEY_DOWN):
|
|
||||||
get_tree().get_root().set_input_as_handled()
|
|
||||||
if (console_history_index < console_history.size()):
|
|
||||||
console_history_index += 1
|
|
||||||
if (console_history_index < console_history.size()):
|
|
||||||
line_edit.text = console_history[console_history_index]
|
|
||||||
line_edit.caret_column = line_edit.text.length()
|
|
||||||
reset_autocomplete()
|
|
||||||
else:
|
|
||||||
line_edit.text = ""
|
|
||||||
reset_autocomplete()
|
|
||||||
if (event.get_physical_keycode_with_modifiers() == KEY_PAGEUP):
|
|
||||||
var scroll := rich_label.get_v_scroll_bar()
|
|
||||||
var tween := create_tween()
|
|
||||||
tween.tween_property(scroll, "value", scroll.value - (scroll.page - scroll.page * 0.1), 0.1)
|
|
||||||
get_tree().get_root().set_input_as_handled()
|
|
||||||
if (event.get_physical_keycode_with_modifiers() == KEY_PAGEDOWN):
|
|
||||||
var scroll := rich_label.get_v_scroll_bar()
|
|
||||||
var tween := create_tween()
|
|
||||||
tween.tween_property(scroll, "value", scroll.value + (scroll.page - scroll.page * 0.1), 0.1)
|
|
||||||
get_tree().get_root().set_input_as_handled()
|
|
||||||
if (event.get_physical_keycode_with_modifiers() == KEY_TAB):
|
|
||||||
autocomplete()
|
|
||||||
get_tree().get_root().set_input_as_handled()
|
|
||||||
|
|
||||||
|
|
||||||
var suggestions := []
|
|
||||||
var current_suggest := 0
|
|
||||||
var suggesting := false
|
|
||||||
|
|
||||||
|
|
||||||
func autocomplete() -> void:
|
|
||||||
if (suggesting):
|
|
||||||
for i in range(suggestions.size()):
|
|
||||||
if (current_suggest == i):
|
|
||||||
line_edit.text = str(suggestions[i])
|
|
||||||
line_edit.caret_column = line_edit.text.length()
|
|
||||||
if (current_suggest == suggestions.size() - 1):
|
|
||||||
current_suggest = 0
|
|
||||||
else:
|
|
||||||
current_suggest += 1
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
suggesting = true
|
|
||||||
|
|
||||||
if (" " in line_edit.text): # We're searching for a parameter to autocomplete
|
|
||||||
var split_text := parse_line_input(line_edit.text)
|
|
||||||
if (split_text.size() > 1):
|
|
||||||
var command := split_text[0]
|
|
||||||
var param_input := split_text[1]
|
|
||||||
if (command_parameters.has(command)):
|
|
||||||
for param in command_parameters[command]:
|
|
||||||
if (param_input in param):
|
|
||||||
suggestions.append(str(command, " ", param))
|
|
||||||
else:
|
|
||||||
var sorted_commands := []
|
|
||||||
for command in console_commands:
|
|
||||||
if (!console_commands[command].hidden):
|
|
||||||
sorted_commands.append(str(command))
|
|
||||||
sorted_commands.sort()
|
|
||||||
sorted_commands.reverse()
|
|
||||||
|
|
||||||
var prev_index := 0
|
|
||||||
for command in sorted_commands:
|
|
||||||
if (!line_edit.text || command.contains(line_edit.text)):
|
|
||||||
var index: int = command.find(line_edit.text)
|
|
||||||
if (index <= prev_index):
|
|
||||||
suggestions.push_front(command)
|
|
||||||
else:
|
|
||||||
suggestions.push_back(command)
|
|
||||||
prev_index = index
|
|
||||||
autocomplete()
|
|
||||||
|
|
||||||
|
|
||||||
func reset_autocomplete() -> void:
|
|
||||||
suggestions.clear()
|
|
||||||
current_suggest = 0
|
|
||||||
suggesting = false
|
|
||||||
|
|
||||||
|
|
||||||
func toggle_size() -> void:
|
|
||||||
if (control.anchor_bottom == 1.0):
|
|
||||||
control.anchor_bottom = 1.9
|
|
||||||
else:
|
|
||||||
control.anchor_bottom = 1.0
|
|
||||||
|
|
||||||
|
|
||||||
func disable():
|
|
||||||
enabled = false
|
|
||||||
toggle_console() # Ensure hidden if opened
|
|
||||||
|
|
||||||
|
|
||||||
func enable():
|
|
||||||
enabled = true
|
|
||||||
|
|
||||||
|
|
||||||
func toggle_console() -> void:
|
|
||||||
if (enabled):
|
|
||||||
control.visible = !control.visible
|
|
||||||
else:
|
|
||||||
control.visible = false
|
|
||||||
|
|
||||||
if (control.visible):
|
|
||||||
was_paused_already = get_tree().paused
|
|
||||||
get_tree().paused = was_paused_already || pause_enabled
|
|
||||||
line_edit.grab_focus()
|
|
||||||
console_opened.emit()
|
|
||||||
else:
|
|
||||||
control.anchor_bottom = 1.0
|
|
||||||
scroll_to_bottom()
|
|
||||||
reset_autocomplete()
|
|
||||||
if (pause_enabled && !was_paused_already):
|
|
||||||
get_tree().paused = false
|
|
||||||
console_closed.emit()
|
|
||||||
|
|
||||||
|
|
||||||
func is_visible():
|
|
||||||
return control.visible
|
|
||||||
|
|
||||||
|
|
||||||
func scroll_to_bottom() -> void:
|
|
||||||
var scroll: ScrollBar = rich_label.get_v_scroll_bar()
|
|
||||||
scroll.value = scroll.max_value - scroll.page
|
|
||||||
|
|
||||||
|
|
||||||
func print_error(text: Variant, print_godot := false) -> void:
|
|
||||||
if not text is String:
|
|
||||||
text = str(text)
|
|
||||||
print_line("[color=light_coral] ERROR:[/color] %s" % text, print_godot)
|
|
||||||
|
|
||||||
|
|
||||||
func print_info(text: Variant, print_godot := false) -> void:
|
|
||||||
if not text is String:
|
|
||||||
text = str(text)
|
|
||||||
print_line("[color=light_blue] INFO:[/color] %s" % text, print_godot)
|
|
||||||
|
|
||||||
|
|
||||||
func print_warning(text: Variant, print_godot := false) -> void:
|
|
||||||
if not text is String:
|
|
||||||
text = str(text)
|
|
||||||
print_line("[color=gold] WARNING:[/color] %s" % text, print_godot)
|
|
||||||
|
|
||||||
|
|
||||||
func print_line(text: Variant, print_godot := false) -> void:
|
|
||||||
if not text is String:
|
|
||||||
text = str(text)
|
|
||||||
if (!rich_label): # Tried to print something before the console was loaded.
|
|
||||||
call_deferred("print_line", text)
|
|
||||||
else:
|
|
||||||
rich_label.append_text(text)
|
|
||||||
rich_label.append_text("\n")
|
|
||||||
if (print_godot):
|
|
||||||
print(text)
|
|
||||||
|
|
||||||
|
|
||||||
func parse_line_input(text: String) -> PackedStringArray:
|
|
||||||
var out_array: PackedStringArray
|
|
||||||
var first_char := true
|
|
||||||
var in_quotes := false
|
|
||||||
var escaped := false
|
|
||||||
var token: String
|
|
||||||
for c in text:
|
|
||||||
if (c == '\\'):
|
|
||||||
escaped = true
|
|
||||||
continue
|
|
||||||
elif (escaped):
|
|
||||||
if (c == 'n'):
|
|
||||||
c = '\n'
|
|
||||||
elif (c == 't'):
|
|
||||||
c = '\t'
|
|
||||||
elif (c == 'r'):
|
|
||||||
c = '\r'
|
|
||||||
elif (c == 'a'):
|
|
||||||
c = '\a'
|
|
||||||
elif (c == 'b'):
|
|
||||||
c = '\b'
|
|
||||||
elif (c == 'f'):
|
|
||||||
c = '\f'
|
|
||||||
escaped = false
|
|
||||||
elif (c == '\"'):
|
|
||||||
in_quotes = !in_quotes
|
|
||||||
continue
|
|
||||||
elif (c == ' ' || c == '\t'):
|
|
||||||
if (!in_quotes):
|
|
||||||
out_array.push_back(token)
|
|
||||||
token = ""
|
|
||||||
continue
|
|
||||||
token += c
|
|
||||||
out_array.push_back(token)
|
|
||||||
return out_array
|
|
||||||
|
|
||||||
|
|
||||||
func on_text_entered(new_text: String) -> void:
|
|
||||||
scroll_to_bottom()
|
|
||||||
reset_autocomplete()
|
|
||||||
line_edit.clear()
|
|
||||||
if (line_edit.has_method(&"edit")):
|
|
||||||
line_edit.call_deferred(&"edit")
|
|
||||||
|
|
||||||
if not new_text.strip_edges().is_empty():
|
|
||||||
add_input_history(new_text)
|
|
||||||
print_line("[i]> " + new_text + "[/i]")
|
|
||||||
var text_split := parse_line_input(new_text)
|
|
||||||
var text_command := text_split[0]
|
|
||||||
|
|
||||||
if console_commands.has(text_command):
|
|
||||||
var arguments := text_split.slice(1)
|
|
||||||
var console_command: ConsoleCommand = console_commands[text_command]
|
|
||||||
|
|
||||||
# calc is a especial command that needs special treatment
|
|
||||||
if (text_command.match("calc")):
|
|
||||||
var expression := ""
|
|
||||||
for word in arguments:
|
|
||||||
expression += word
|
|
||||||
console_command.function.callv([expression])
|
|
||||||
return
|
|
||||||
|
|
||||||
if (arguments.size() < console_command.required):
|
|
||||||
print_error("Too few arguments! Required < %d >" % console_command.required)
|
|
||||||
return
|
|
||||||
elif (arguments.size() > console_command.arguments.size()):
|
|
||||||
arguments.resize(console_command.arguments.size())
|
|
||||||
|
|
||||||
# Functions fail to call if passed the incorrect number of arguments, so fill out with blank strings.
|
|
||||||
while (arguments.size() < console_command.arguments.size()):
|
|
||||||
arguments.append("")
|
|
||||||
|
|
||||||
console_command.function.callv(arguments)
|
|
||||||
else:
|
|
||||||
console_unknown_command.emit(text_command)
|
|
||||||
print_error("Command not found.")
|
|
||||||
|
|
||||||
|
|
||||||
func on_line_edit_text_changed(new_text: String) -> void:
|
|
||||||
reset_autocomplete()
|
|
||||||
|
|
||||||
|
|
||||||
func quit() -> void:
|
|
||||||
get_tree().quit()
|
|
||||||
|
|
||||||
|
|
||||||
func clear() -> void:
|
|
||||||
rich_label.clear()
|
|
||||||
|
|
||||||
|
|
||||||
func delete_history() -> void:
|
|
||||||
console_history.clear()
|
|
||||||
console_history_index = 0
|
|
||||||
DirAccess.remove_absolute("user://console_history.txt")
|
|
||||||
|
|
||||||
|
|
||||||
func help() -> void:
|
|
||||||
rich_label.append_text(" Built in commands:
|
|
||||||
[color=light_green]calc[/color]: Calculates a given expresion
|
|
||||||
[color=light_green]clear[/color]: Clears the registry view
|
|
||||||
[color=light_green]commands[/color]: Shows a reduced list of all the currently registered commands
|
|
||||||
[color=light_green]commands_list[/color]: Shows a detailed list of all the currently registered commands
|
|
||||||
[color=light_green]delete_history[/color]: Deletes the commands history
|
|
||||||
[color=light_green]echo[/color]: Prints a given string to the console
|
|
||||||
[color=light_green]echo_error[/color]: Prints a given string as an error to the console
|
|
||||||
[color=light_green]echo_info[/color]: Prints a given string as info to the console
|
|
||||||
[color=light_green]echo_warning[/color]: Prints a given string as warning to the console
|
|
||||||
[color=light_green]pause[/color]: Pauses node processing
|
|
||||||
[color=light_green]unpause[/color]: Unpauses node processing
|
|
||||||
[color=light_green]quit[/color]: Quits the game
|
|
||||||
Controls:
|
|
||||||
[color=light_blue]Up[/color] and [color=light_blue]Down[/color] arrow keys to navigate commands history
|
|
||||||
[color=light_blue]PageUp[/color] and [color=light_blue]PageDown[/color] to scroll registry
|
|
||||||
[[color=light_blue]Ctrl[/color] + [color=light_blue]~[/color]] to change console size between half screen and full screen
|
|
||||||
[color=light_blue]~[/color] or [color=light_blue]Esc[/color] key to close the console
|
|
||||||
[color=light_blue]Tab[/color] key to autocomplete, [color=light_blue]Tab[/color] again to cycle between matching suggestions\n\n")
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
func calculate(command: String) -> void:
|
|
||||||
var expression := Expression.new()
|
|
||||||
var error = expression.parse(command)
|
|
||||||
if error:
|
|
||||||
print_error("%s" % expression.get_error_text())
|
|
||||||
return
|
|
||||||
var result = expression.execute()
|
|
||||||
if not expression.has_execute_failed():
|
|
||||||
print_line(str(result))
|
|
||||||
else:
|
|
||||||
print_error("%s" % expression.get_error_text())
|
|
||||||
|
|
||||||
|
|
||||||
func commands() -> void:
|
|
||||||
var commands := []
|
|
||||||
for command in console_commands:
|
|
||||||
if (!console_commands[command].hidden):
|
|
||||||
commands.append(str(command))
|
|
||||||
commands.sort()
|
|
||||||
rich_label.append_text(" ")
|
|
||||||
rich_label.append_text(str(commands) + "\n\n")
|
|
||||||
|
|
||||||
|
|
||||||
func commands_list() -> void:
|
|
||||||
var commands := []
|
|
||||||
for command in console_commands:
|
|
||||||
if (!console_commands[command].hidden):
|
|
||||||
commands.append(str(command))
|
|
||||||
commands.sort()
|
|
||||||
|
|
||||||
for command in commands:
|
|
||||||
var arguments_string := ""
|
|
||||||
var description: String = console_commands[command].description
|
|
||||||
for i in range(console_commands[command].arguments.size()):
|
|
||||||
if i < console_commands[command].required:
|
|
||||||
arguments_string += " [color=cornflower_blue]<" + console_commands[command].arguments[i] + ">[/color]"
|
|
||||||
else:
|
|
||||||
arguments_string += " <" + console_commands[command].arguments[i] + ">"
|
|
||||||
rich_label.append_text(" [color=light_green]%s[/color][color=gray]%s[/color]: %s\n" % [command, arguments_string, description])
|
|
||||||
rich_label.append_text("\n")
|
|
||||||
|
|
||||||
|
|
||||||
func add_input_history(text: String) -> void:
|
|
||||||
if (!console_history.size() || text != console_history.back()): # Don't add consecutive duplicates
|
|
||||||
console_history.append(text)
|
|
||||||
console_history_index = console_history.size()
|
|
||||||
|
|
||||||
|
|
||||||
func set_enable_on_release_build(enable: bool):
|
|
||||||
enable_on_release_build = enable
|
|
||||||
if (!enable_on_release_build):
|
|
||||||
if (!OS.is_debug_build()):
|
|
||||||
disable()
|
|
||||||
|
|
||||||
|
|
||||||
func pause() -> void:
|
|
||||||
get_tree().paused = true
|
|
||||||
|
|
||||||
|
|
||||||
func unpause() -> void:
|
|
||||||
get_tree().paused = false
|
|
||||||
|
|
||||||
|
|
||||||
func exec(filename: String) -> void:
|
|
||||||
var path := "user://%s.txt" % [filename]
|
|
||||||
var script := FileAccess.open(path, FileAccess.READ)
|
|
||||||
if (script):
|
|
||||||
while (!script.eof_reached()):
|
|
||||||
on_text_entered(script.get_line())
|
|
||||||
else:
|
|
||||||
print_error("File %s not found." % [path])
|
|
@@ -1 +0,0 @@
|
|||||||
uid://ouiu5xh1cs8n
|
|
11
addons/console/console_plugin.gd
vendored
11
addons/console/console_plugin.gd
vendored
@@ -1,11 +0,0 @@
|
|||||||
@tool
|
|
||||||
extends EditorPlugin
|
|
||||||
|
|
||||||
|
|
||||||
func _enter_tree():
|
|
||||||
print("Console plugin activated.")
|
|
||||||
add_autoload_singleton("Console", "res://addons/console/console.gd")
|
|
||||||
|
|
||||||
|
|
||||||
func _exit_tree():
|
|
||||||
remove_autoload_singleton("Console")
|
|
@@ -1 +0,0 @@
|
|||||||
uid://cv2joe2dgkub1
|
|
@@ -1,7 +0,0 @@
|
|||||||
[plugin]
|
|
||||||
|
|
||||||
name="Developer Console"
|
|
||||||
description="Developer console. Press ~ to activate it in game and execute commands."
|
|
||||||
author="jitspoe"
|
|
||||||
version="1.3.1"
|
|
||||||
script="console_plugin.gd"
|
|
576
addons/dialogue_manager/DialogueManager.cs
Normal file
576
addons/dialogue_manager/DialogueManager.cs
Normal file
@@ -0,0 +1,576 @@
|
|||||||
|
using Godot;
|
||||||
|
using Godot.Collections;
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
namespace DialogueManagerRuntime
|
||||||
|
{
|
||||||
|
public enum TranslationSource
|
||||||
|
{
|
||||||
|
None,
|
||||||
|
Guess,
|
||||||
|
CSV,
|
||||||
|
PO
|
||||||
|
}
|
||||||
|
|
||||||
|
public partial class DialogueManager : RefCounted
|
||||||
|
{
|
||||||
|
public delegate void DialogueStartedEventHandler(Resource dialogueResource);
|
||||||
|
public delegate void PassedTitleEventHandler(string title);
|
||||||
|
public delegate void GotDialogueEventHandler(DialogueLine dialogueLine);
|
||||||
|
public delegate void MutatedEventHandler(Dictionary mutation);
|
||||||
|
public delegate void DialogueEndedEventHandler(Resource dialogueResource);
|
||||||
|
|
||||||
|
public static DialogueStartedEventHandler? DialogueStarted;
|
||||||
|
public static PassedTitleEventHandler? PassedTitle;
|
||||||
|
public static GotDialogueEventHandler? GotDialogue;
|
||||||
|
public static MutatedEventHandler? Mutated;
|
||||||
|
public static DialogueEndedEventHandler? DialogueEnded;
|
||||||
|
|
||||||
|
[Signal] public delegate void ResolvedEventHandler(Variant value);
|
||||||
|
|
||||||
|
private static GodotObject? instance;
|
||||||
|
public static GodotObject Instance
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (instance == null)
|
||||||
|
{
|
||||||
|
instance = Engine.GetSingleton("DialogueManager");
|
||||||
|
instance.Connect("bridge_dialogue_started", Callable.From((Resource dialogueResource) => DialogueStarted?.Invoke(dialogueResource)));
|
||||||
|
}
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static Godot.Collections.Array GameStates
|
||||||
|
{
|
||||||
|
get => (Godot.Collections.Array)Instance.Get("game_states");
|
||||||
|
set => Instance.Set("game_states", value);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static bool IncludeSingletons
|
||||||
|
{
|
||||||
|
get => (bool)Instance.Get("include_singletons");
|
||||||
|
set => Instance.Set("include_singletons", value);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static bool IncludeClasses
|
||||||
|
{
|
||||||
|
get => (bool)Instance.Get("include_classes");
|
||||||
|
set => Instance.Set("include_classes", value);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static TranslationSource TranslationSource
|
||||||
|
{
|
||||||
|
get => (TranslationSource)(int)Instance.Get("translation_source");
|
||||||
|
set => Instance.Set("translation_source", (int)value);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static Func<Node> GetCurrentScene
|
||||||
|
{
|
||||||
|
set => Instance.Set("get_current_scene", Callable.From(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static void Prepare(GodotObject instance)
|
||||||
|
{
|
||||||
|
instance.Connect("passed_title", Callable.From((string title) => PassedTitle?.Invoke(title)));
|
||||||
|
instance.Connect("got_dialogue", Callable.From((RefCounted line) => GotDialogue?.Invoke(new DialogueLine(line))));
|
||||||
|
instance.Connect("mutated", Callable.From((Dictionary mutation) => Mutated?.Invoke(mutation)));
|
||||||
|
instance.Connect("dialogue_ended", Callable.From((Resource dialogueResource) => DialogueEnded?.Invoke(dialogueResource)));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static async Task<GodotObject> GetSingleton()
|
||||||
|
{
|
||||||
|
if (instance != null) return instance;
|
||||||
|
|
||||||
|
var tree = Engine.GetMainLoop();
|
||||||
|
int x = 0;
|
||||||
|
|
||||||
|
// Try and find the singleton for a few seconds
|
||||||
|
while (!Engine.HasSingleton("DialogueManager") && x < 300)
|
||||||
|
{
|
||||||
|
await tree.ToSignal(tree, SceneTree.SignalName.ProcessFrame);
|
||||||
|
x++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it times out something is wrong
|
||||||
|
if (x >= 300)
|
||||||
|
{
|
||||||
|
throw new Exception("The DialogueManager singleton is missing.");
|
||||||
|
}
|
||||||
|
|
||||||
|
instance = Engine.GetSingleton("DialogueManager");
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Resource CreateResourceFromText(string text)
|
||||||
|
{
|
||||||
|
return (Resource)Instance.Call("create_resource_from_text", text);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<DialogueLine?> GetNextDialogueLine(Resource dialogueResource, string key = "", Array<Variant>? extraGameStates = null)
|
||||||
|
{
|
||||||
|
var instance = (Node)Instance.Call("_bridge_get_new_instance");
|
||||||
|
Prepare(instance);
|
||||||
|
instance.Call("_bridge_get_next_dialogue_line", dialogueResource, key, extraGameStates ?? new Array<Variant>());
|
||||||
|
var result = await instance.ToSignal(instance, "bridge_get_next_dialogue_line_completed");
|
||||||
|
instance.QueueFree();
|
||||||
|
|
||||||
|
if ((RefCounted)result[0] == null) return null;
|
||||||
|
|
||||||
|
return new DialogueLine((RefCounted)result[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static CanvasLayer ShowExampleDialogueBalloon(Resource dialogueResource, string key = "", Array<Variant>? extraGameStates = null)
|
||||||
|
{
|
||||||
|
return (CanvasLayer)Instance.Call("show_example_dialogue_balloon", dialogueResource, key, extraGameStates ?? new Array<Variant>());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static Node ShowDialogueBalloonScene(string balloonScene, Resource dialogueResource, string key = "", Array<Variant>? extraGameStates = null)
|
||||||
|
{
|
||||||
|
return (Node)Instance.Call("show_dialogue_balloon_scene", balloonScene, dialogueResource, key, extraGameStates ?? new Array<Variant>());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Node ShowDialogueBalloonScene(PackedScene balloonScene, Resource dialogueResource, string key = "", Array<Variant>? extraGameStates = null)
|
||||||
|
{
|
||||||
|
return (Node)Instance.Call("show_dialogue_balloon_scene", balloonScene, dialogueResource, key, extraGameStates ?? new Array<Variant>());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Node ShowDialogueBalloonScene(Node balloonScene, Resource dialogueResource, string key = "", Array<Variant>? extraGameStates = null)
|
||||||
|
{
|
||||||
|
return (Node)Instance.Call("show_dialogue_balloon_scene", balloonScene, dialogueResource, key, extraGameStates ?? new Array<Variant>());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static Node ShowDialogueBalloon(Resource dialogueResource, string key = "", Array<Variant>? extraGameStates = null)
|
||||||
|
{
|
||||||
|
return (Node)Instance.Call("show_dialogue_balloon", dialogueResource, key, extraGameStates ?? new Array<Variant>());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static Array<string> StaticIdToLineIds(Resource dialogueResource, string staticId)
|
||||||
|
{
|
||||||
|
return (Array<string>)Instance.Call("static_id_to_line_ids", dialogueResource, staticId);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static string StaticIdToLineId(Resource dialogueResource, string staticId)
|
||||||
|
{
|
||||||
|
return (string)Instance.Call("static_id_to_line_id", dialogueResource, staticId);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static async void Mutate(Dictionary mutation, Array<Variant>? extraGameStates = null, bool isInlineMutation = false)
|
||||||
|
{
|
||||||
|
Instance.Call("_bridge_mutate", mutation, extraGameStates ?? new Array<Variant>(), isInlineMutation);
|
||||||
|
await Instance.ToSignal(Instance, "bridge_mutated");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static Array<Dictionary> GetMembersForAutoload(Script script)
|
||||||
|
{
|
||||||
|
Array<Dictionary> members = new Array<Dictionary>();
|
||||||
|
|
||||||
|
string typeName = script.ResourcePath.GetFile().GetBaseName();
|
||||||
|
var matchingTypes = Assembly.GetExecutingAssembly().GetTypes().Where(t => t.Name == typeName);
|
||||||
|
foreach (var matchingType in matchingTypes)
|
||||||
|
{
|
||||||
|
var memberInfos = matchingType.GetMembers(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.DeclaredOnly);
|
||||||
|
foreach (var memberInfo in memberInfos)
|
||||||
|
{
|
||||||
|
string type;
|
||||||
|
switch (memberInfo.MemberType)
|
||||||
|
{
|
||||||
|
case MemberTypes.Field:
|
||||||
|
FieldInfo fieldInfo = memberInfo as FieldInfo;
|
||||||
|
|
||||||
|
if (fieldInfo.FieldType.ToString().Contains("EventHandler"))
|
||||||
|
{
|
||||||
|
type = "signal";
|
||||||
|
}
|
||||||
|
else if (fieldInfo.IsLiteral)
|
||||||
|
{
|
||||||
|
type = "constant";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
type = "property";
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case MemberTypes.Method:
|
||||||
|
type = "method";
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
members.Add(new Dictionary() {
|
||||||
|
{ "name", memberInfo.Name },
|
||||||
|
{ "type", type }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return members;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public bool ThingHasConstant(GodotObject thing, string property)
|
||||||
|
{
|
||||||
|
var fieldInfos = thing.GetType().GetFields(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.DeclaredOnly);
|
||||||
|
foreach (var fieldInfo in fieldInfos)
|
||||||
|
{
|
||||||
|
if (fieldInfo.Name == property && fieldInfo.IsLiteral)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public Variant ResolveThingConstant(GodotObject thing, string property)
|
||||||
|
{
|
||||||
|
var fieldInfos = thing.GetType().GetFields(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.DeclaredOnly);
|
||||||
|
foreach (var fieldInfo in fieldInfos)
|
||||||
|
{
|
||||||
|
if (fieldInfo.Name == property && fieldInfo.IsLiteral)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Variant value = fieldInfo.GetValue(thing) switch
|
||||||
|
{
|
||||||
|
int v => Variant.From((long)v),
|
||||||
|
float v => Variant.From((double)v),
|
||||||
|
System.String v => Variant.From((string)v),
|
||||||
|
_ => Variant.From(fieldInfo.GetValue(thing))
|
||||||
|
};
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
throw new Exception($"Constant {property} of type ${fieldInfo.GetValue(thing).GetType()} is not supported by Variant.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Exception($"{property} is not a public constant on {thing}");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public bool ThingHasMethod(GodotObject thing, string method, Array<Variant> args)
|
||||||
|
{
|
||||||
|
var methodInfos = thing.GetType().GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.DeclaredOnly);
|
||||||
|
foreach (var methodInfo in methodInfos)
|
||||||
|
{
|
||||||
|
if (methodInfo.Name == method && args.Count >= methodInfo.GetParameters().Where(p => !p.HasDefaultValue).Count())
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public async void ResolveThingMethod(GodotObject thing, string method, Array<Variant> args)
|
||||||
|
{
|
||||||
|
MethodInfo? info = null;
|
||||||
|
var methodInfos = thing.GetType().GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.DeclaredOnly);
|
||||||
|
foreach (var methodInfo in methodInfos)
|
||||||
|
{
|
||||||
|
if (methodInfo.Name == method && args.Count >= methodInfo.GetParameters().Where(p => !p.HasDefaultValue).Count())
|
||||||
|
{
|
||||||
|
info = methodInfo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (info == null) return;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
// Convert the method args to something reflection can handle
|
||||||
|
ParameterInfo[] argTypes = info.GetParameters();
|
||||||
|
object[] _args = new object[argTypes.Length];
|
||||||
|
for (int i = 0; i < argTypes.Length; i++)
|
||||||
|
{
|
||||||
|
// check if args is assignable from derived type
|
||||||
|
if (i < args.Count && args[i].Obj != null)
|
||||||
|
{
|
||||||
|
if (argTypes[i].ParameterType.IsAssignableFrom(args[i].Obj.GetType()))
|
||||||
|
{
|
||||||
|
_args[i] = args[i].Obj;
|
||||||
|
}
|
||||||
|
// fallback to assigning primitive types
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_args[i] = Convert.ChangeType(args[i].Obj, argTypes[i].ParameterType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (argTypes[i].DefaultValue != null)
|
||||||
|
{
|
||||||
|
_args[i] = argTypes[i].DefaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a single frame wait in case the method returns before signals can listen
|
||||||
|
await ToSignal(Engine.GetMainLoop(), SceneTree.SignalName.ProcessFrame);
|
||||||
|
|
||||||
|
// invoke method and handle the result based on return type
|
||||||
|
object result = info.Invoke(thing, _args);
|
||||||
|
|
||||||
|
if (result is Task taskResult)
|
||||||
|
{
|
||||||
|
await taskResult;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Variant value = (Variant)taskResult.GetType().GetProperty("Result").GetValue(taskResult);
|
||||||
|
EmitSignal(SignalName.Resolved, value);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
EmitSignal(SignalName.Resolved);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
EmitSignal(SignalName.Resolved, (Variant)result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#nullable enable
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public partial class DialogueLine : RefCounted
|
||||||
|
{
|
||||||
|
private string id = "";
|
||||||
|
public string Id
|
||||||
|
{
|
||||||
|
get => id;
|
||||||
|
set => id = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string type = "dialogue";
|
||||||
|
public string Type
|
||||||
|
{
|
||||||
|
get => type;
|
||||||
|
set => type = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string next_id = "";
|
||||||
|
public string NextId
|
||||||
|
{
|
||||||
|
get => next_id;
|
||||||
|
set => next_id = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string character = "";
|
||||||
|
public string Character
|
||||||
|
{
|
||||||
|
get => character;
|
||||||
|
set => character = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string text = "";
|
||||||
|
public string Text
|
||||||
|
{
|
||||||
|
get => text;
|
||||||
|
set => text = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string translation_key = "";
|
||||||
|
public string TranslationKey
|
||||||
|
{
|
||||||
|
get => translation_key;
|
||||||
|
set => translation_key = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Array<DialogueResponse> responses = new Array<DialogueResponse>();
|
||||||
|
public Array<DialogueResponse> Responses
|
||||||
|
{
|
||||||
|
get => responses;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string? time = null;
|
||||||
|
public string? Time
|
||||||
|
{
|
||||||
|
get => time;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Dictionary pauses = new Dictionary();
|
||||||
|
public Dictionary Pauses
|
||||||
|
{
|
||||||
|
get => pauses;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Dictionary speeds = new Dictionary();
|
||||||
|
public Dictionary Speeds
|
||||||
|
{
|
||||||
|
get => speeds;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Array<Godot.Collections.Array> inline_mutations = new Array<Godot.Collections.Array>();
|
||||||
|
public Array<Godot.Collections.Array> InlineMutations
|
||||||
|
{
|
||||||
|
get => inline_mutations;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Array<DialogueLine> concurrent_lines = new Array<DialogueLine>();
|
||||||
|
public Array<DialogueLine> ConcurrentLines
|
||||||
|
{
|
||||||
|
get => concurrent_lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Array<Variant> extra_game_states = new Array<Variant>();
|
||||||
|
public Array<Variant> ExtraGameStates
|
||||||
|
{
|
||||||
|
get => extra_game_states;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Array<string> tags = new Array<string>();
|
||||||
|
public Array<string> Tags
|
||||||
|
{
|
||||||
|
get => tags;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DialogueLine(RefCounted data)
|
||||||
|
{
|
||||||
|
id = (string)data.Get("id");
|
||||||
|
type = (string)data.Get("type");
|
||||||
|
next_id = (string)data.Get("next_id");
|
||||||
|
character = (string)data.Get("character");
|
||||||
|
text = (string)data.Get("text");
|
||||||
|
translation_key = (string)data.Get("translation_key");
|
||||||
|
pauses = (Dictionary)data.Get("pauses");
|
||||||
|
speeds = (Dictionary)data.Get("speeds");
|
||||||
|
inline_mutations = (Array<Godot.Collections.Array>)data.Get("inline_mutations");
|
||||||
|
time = (string)data.Get("time");
|
||||||
|
tags = (Array<string>)data.Get("tags");
|
||||||
|
|
||||||
|
foreach (var concurrent_line_data in (Array<RefCounted>)data.Get("concurrent_lines"))
|
||||||
|
{
|
||||||
|
concurrent_lines.Add(new DialogueLine(concurrent_line_data));
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var response in (Array<RefCounted>)data.Get("responses"))
|
||||||
|
{
|
||||||
|
responses.Add(new DialogueResponse(response));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public string GetTagValue(string tagName)
|
||||||
|
{
|
||||||
|
string wrapped = $"{tagName}=";
|
||||||
|
foreach (var tag in tags)
|
||||||
|
{
|
||||||
|
if (tag.StartsWith(wrapped))
|
||||||
|
{
|
||||||
|
return tag.Substring(wrapped.Length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
switch (type)
|
||||||
|
{
|
||||||
|
case "dialogue":
|
||||||
|
return $"<DialogueLine character=\"{character}\" text=\"{text}\">";
|
||||||
|
case "mutation":
|
||||||
|
return "<DialogueLine mutation>";
|
||||||
|
default:
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public partial class DialogueResponse : RefCounted
|
||||||
|
{
|
||||||
|
private string next_id = "";
|
||||||
|
public string NextId
|
||||||
|
{
|
||||||
|
get => next_id;
|
||||||
|
set => next_id = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool is_allowed = true;
|
||||||
|
public bool IsAllowed
|
||||||
|
{
|
||||||
|
get => is_allowed;
|
||||||
|
set => is_allowed = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string condition_as_text = "";
|
||||||
|
public string ConditionAsText
|
||||||
|
{
|
||||||
|
get => condition_as_text;
|
||||||
|
set => condition_as_text = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string text = "";
|
||||||
|
public string Text
|
||||||
|
{
|
||||||
|
get => text;
|
||||||
|
set => text = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string translation_key = "";
|
||||||
|
public string TranslationKey
|
||||||
|
{
|
||||||
|
get => translation_key;
|
||||||
|
set => translation_key = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Array<string> tags = new Array<string>();
|
||||||
|
public Array<string> Tags
|
||||||
|
{
|
||||||
|
get => tags;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DialogueResponse(RefCounted data)
|
||||||
|
{
|
||||||
|
next_id = (string)data.Get("next_id");
|
||||||
|
is_allowed = (bool)data.Get("is_allowed");
|
||||||
|
text = (string)data.Get("text");
|
||||||
|
translation_key = (string)data.Get("translation_key");
|
||||||
|
tags = (Array<string>)data.Get("tags");
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetTagValue(string tagName)
|
||||||
|
{
|
||||||
|
string wrapped = $"{tagName}=";
|
||||||
|
foreach (var tag in tags)
|
||||||
|
{
|
||||||
|
if (tag.StartsWith(wrapped))
|
||||||
|
{
|
||||||
|
return tag.Substring(wrapped.Length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return $"<DialogueResponse text=\"{text}\"";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
1
addons/dialogue_manager/DialogueManager.cs.uid
Normal file
1
addons/dialogue_manager/DialogueManager.cs.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://c4c5lsrwy3opj
|
21
addons/dialogue_manager/LICENSE
Normal file
21
addons/dialogue_manager/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2022-present Nathan Hoad and Dialogue Manager contributors.
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
BIN
addons/dialogue_manager/assets/banner.png
Normal file
BIN
addons/dialogue_manager/assets/banner.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 38 KiB |
34
addons/dialogue_manager/assets/banner.png.import
Normal file
34
addons/dialogue_manager/assets/banner.png.import
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://cnm67htuohhlo"
|
||||||
|
path="res://.godot/imported/banner.png-7e9e6a304eef850602c8d5afb80df9c3.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://addons/dialogue_manager/assets/banner.png"
|
||||||
|
dest_files=["res://.godot/imported/banner.png-7e9e6a304eef850602c8d5afb80df9c3.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
52
addons/dialogue_manager/assets/icon.svg
Normal file
52
addons/dialogue_manager/assets/icon.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 7.8 KiB |
38
addons/dialogue_manager/assets/icon.svg.import
Normal file
38
addons/dialogue_manager/assets/icon.svg.import
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://d3lr2uas6ax8v"
|
||||||
|
path="res://.godot/imported/icon.svg-17eb5d3e2a3cfbe59852220758c5b7bd.ctex"
|
||||||
|
metadata={
|
||||||
|
"has_editor_variant": true,
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://addons/dialogue_manager/assets/icon.svg"
|
||||||
|
dest_files=["res://.godot/imported/icon.svg-17eb5d3e2a3cfbe59852220758c5b7bd.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
|
svg/scale=1.0
|
||||||
|
editor/scale_with_editor_scale=true
|
||||||
|
editor/convert_colors_with_editor_theme=true
|
52
addons/dialogue_manager/assets/responses_menu.svg
Normal file
52
addons/dialogue_manager/assets/responses_menu.svg
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 4.2333333 4.2333335"
|
||||||
|
version="1.1"
|
||||||
|
id="svg291"
|
||||||
|
inkscape:version="1.3 (0e150ed6c4, 2023-07-21)"
|
||||||
|
sodipodi:docname="responses_menu.svg"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview293"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:document-units="px"
|
||||||
|
showgrid="false"
|
||||||
|
width="1920px"
|
||||||
|
units="px"
|
||||||
|
borderlayer="true"
|
||||||
|
inkscape:showpageshadow="false"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:zoom="45.254834"
|
||||||
|
inkscape:cx="7.8334173"
|
||||||
|
inkscape:cy="6.5959804"
|
||||||
|
inkscape:window-width="2560"
|
||||||
|
inkscape:window-height="1377"
|
||||||
|
inkscape:window-x="-8"
|
||||||
|
inkscape:window-y="-8"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="layer1" />
|
||||||
|
<defs
|
||||||
|
id="defs288" />
|
||||||
|
<g
|
||||||
|
inkscape:label="Layer 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1">
|
||||||
|
<path
|
||||||
|
id="rect181"
|
||||||
|
style="fill:#e0e0e0;fill-opacity:1;stroke:none;stroke-width:1.77487;stroke-linecap:round;stroke-linejoin:round;paint-order:stroke markers fill"
|
||||||
|
d="M 1.5875 0.26458334 L 1.5875 0.79375001 L 4.2333334 0.79375001 L 4.2333334 0.26458334 L 1.5875 0.26458334 z M 0 0.83147381 L 0 2.4189738 L 1.3229167 1.6252238 L 0 0.83147381 z M 1.5875 1.3229167 L 1.5875 1.8520834 L 4.2333334 1.8520834 L 4.2333334 1.3229167 L 1.5875 1.3229167 z M 1.5875 2.38125 L 1.5875 2.9104167 L 4.2333334 2.9104167 L 4.2333334 2.38125 L 1.5875 2.38125 z M 1.5875 3.4395834 L 1.5875 3.9687501 L 4.2333334 3.9687501 L 4.2333334 3.4395834 L 1.5875 3.4395834 z "
|
||||||
|
fill="#E0E0E0" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.0 KiB |
38
addons/dialogue_manager/assets/responses_menu.svg.import
Normal file
38
addons/dialogue_manager/assets/responses_menu.svg.import
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://drjfciwitjm83"
|
||||||
|
path="res://.godot/imported/responses_menu.svg-87cf63ca685d53616205049572f4eb8f.ctex"
|
||||||
|
metadata={
|
||||||
|
"has_editor_variant": true,
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://addons/dialogue_manager/assets/responses_menu.svg"
|
||||||
|
dest_files=["res://.godot/imported/responses_menu.svg-87cf63ca685d53616205049572f4eb8f.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
|
svg/scale=1.0
|
||||||
|
editor/scale_with_editor_scale=true
|
||||||
|
editor/convert_colors_with_editor_theme=true
|
71
addons/dialogue_manager/assets/update.svg
Normal file
71
addons/dialogue_manager/assets/update.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 11 KiB |
37
addons/dialogue_manager/assets/update.svg.import
Normal file
37
addons/dialogue_manager/assets/update.svg.import
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://d3baj6rygkb3f"
|
||||||
|
path="res://.godot/imported/update.svg-f1628866ed4eb2e13e3b81f75443687e.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://addons/dialogue_manager/assets/update.svg"
|
||||||
|
dest_files=["res://.godot/imported/update.svg-f1628866ed4eb2e13e3b81f75443687e.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
|
svg/scale=1.0
|
||||||
|
editor/scale_with_editor_scale=false
|
||||||
|
editor/convert_colors_with_editor_theme=false
|
1111
addons/dialogue_manager/compiler/compilation.gd
vendored
Normal file
1111
addons/dialogue_manager/compiler/compilation.gd
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
addons/dialogue_manager/compiler/compilation.gd.uid
Normal file
1
addons/dialogue_manager/compiler/compilation.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://dsgpnyqg6cprg
|
161
addons/dialogue_manager/compiler/compiled_line.gd
vendored
Normal file
161
addons/dialogue_manager/compiler/compiled_line.gd
vendored
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
## A compiled line of dialogue.
|
||||||
|
class_name DMCompiledLine extends RefCounted
|
||||||
|
|
||||||
|
|
||||||
|
## The ID of the line
|
||||||
|
var id: String
|
||||||
|
## The translation key (or static line ID).
|
||||||
|
var translation_key: String = ""
|
||||||
|
## The type of line.
|
||||||
|
var type: String = ""
|
||||||
|
## The character name.
|
||||||
|
var character: String = ""
|
||||||
|
## Any interpolation expressions for the character name.
|
||||||
|
var character_replacements: Array[Dictionary] = []
|
||||||
|
## The text of the line.
|
||||||
|
var text: String = ""
|
||||||
|
## Any interpolation expressions for the text.
|
||||||
|
var text_replacements: Array[Dictionary] = []
|
||||||
|
## Any response siblings associated with this line.
|
||||||
|
var responses: PackedStringArray = []
|
||||||
|
## Any randomise or case siblings for this line.
|
||||||
|
var siblings: Array[Dictionary] = []
|
||||||
|
## Any lines said simultaneously.
|
||||||
|
var concurrent_lines: PackedStringArray = []
|
||||||
|
## Any tags on this line.
|
||||||
|
var tags: PackedStringArray = []
|
||||||
|
## The condition or mutation expression for this line.
|
||||||
|
var expression: Dictionary = {}
|
||||||
|
## The express as the raw text that was given.
|
||||||
|
var expression_text: String = ""
|
||||||
|
## The next sequential line to go to after this line.
|
||||||
|
var next_id: String = ""
|
||||||
|
## The next line to go to after this line if it is unknown and compile time.
|
||||||
|
var next_id_expression: Array[Dictionary] = []
|
||||||
|
## Whether this jump line should return after the jump target sequence has ended.
|
||||||
|
var is_snippet: bool = false
|
||||||
|
## The ID of the next sibling line.
|
||||||
|
var next_sibling_id: String = ""
|
||||||
|
## The ID after this line if it belongs to a block (eg. conditions).
|
||||||
|
var next_id_after: String = ""
|
||||||
|
## Any doc comments attached to this line.
|
||||||
|
var notes: String = ""
|
||||||
|
|
||||||
|
|
||||||
|
#region Hooks
|
||||||
|
|
||||||
|
|
||||||
|
func _init(initial_id: String, initial_type: String) -> void:
|
||||||
|
id = initial_id
|
||||||
|
type = initial_type
|
||||||
|
|
||||||
|
|
||||||
|
func _to_string() -> String:
|
||||||
|
var s: Array = [
|
||||||
|
"[%s]" % [type],
|
||||||
|
"%s:" % [character] if character != "" else null,
|
||||||
|
text if text != "" else null,
|
||||||
|
expression if expression.size() > 0 else null,
|
||||||
|
"[%s]" % [",".join(tags)] if tags.size() > 0 else null,
|
||||||
|
str(siblings) if siblings.size() > 0 else null,
|
||||||
|
str(responses) if responses.size() > 0 else null,
|
||||||
|
"=> END" if "end" in next_id else "=> %s" % [next_id],
|
||||||
|
"(~> %s)" % [next_sibling_id] if next_sibling_id != "" else null,
|
||||||
|
"(==> %s)" % [next_id_after] if next_id_after != "" else null,
|
||||||
|
].filter(func(item): return item != null)
|
||||||
|
|
||||||
|
return " ".join(s)
|
||||||
|
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Helpers
|
||||||
|
|
||||||
|
|
||||||
|
## Express this line as a [Dictionary] that can be stored in a resource.
|
||||||
|
func to_data() -> Dictionary:
|
||||||
|
var d: Dictionary = {
|
||||||
|
id = id,
|
||||||
|
type = type,
|
||||||
|
next_id = next_id
|
||||||
|
}
|
||||||
|
|
||||||
|
if next_id_expression.size() > 0:
|
||||||
|
d.next_id_expression = next_id_expression
|
||||||
|
|
||||||
|
match type:
|
||||||
|
DMConstants.TYPE_CONDITION:
|
||||||
|
d.condition = expression
|
||||||
|
if not next_sibling_id.is_empty():
|
||||||
|
d.next_sibling_id = next_sibling_id
|
||||||
|
d.next_id_after = next_id_after
|
||||||
|
|
||||||
|
DMConstants.TYPE_WHILE:
|
||||||
|
d.condition = expression
|
||||||
|
d.next_id_after = next_id_after
|
||||||
|
|
||||||
|
DMConstants.TYPE_MATCH:
|
||||||
|
d.condition = expression
|
||||||
|
d.next_id_after = next_id_after
|
||||||
|
d.cases = siblings
|
||||||
|
|
||||||
|
DMConstants.TYPE_MUTATION:
|
||||||
|
d.mutation = expression
|
||||||
|
|
||||||
|
DMConstants.TYPE_GOTO:
|
||||||
|
d.is_snippet = is_snippet
|
||||||
|
d.next_id_after = next_id_after
|
||||||
|
if not siblings.is_empty():
|
||||||
|
d.siblings = siblings
|
||||||
|
|
||||||
|
DMConstants.TYPE_RANDOM:
|
||||||
|
d.siblings = siblings
|
||||||
|
|
||||||
|
DMConstants.TYPE_RESPONSE:
|
||||||
|
d.text = text
|
||||||
|
|
||||||
|
if not responses.is_empty():
|
||||||
|
d.responses = responses
|
||||||
|
|
||||||
|
if translation_key != text:
|
||||||
|
d.translation_key = translation_key
|
||||||
|
if not expression.is_empty():
|
||||||
|
d.condition = expression
|
||||||
|
if not character.is_empty():
|
||||||
|
d.character = character
|
||||||
|
if not character_replacements.is_empty():
|
||||||
|
d.character_replacements = character_replacements
|
||||||
|
if not text_replacements.is_empty():
|
||||||
|
d.text_replacements = text_replacements
|
||||||
|
if not tags.is_empty():
|
||||||
|
d.tags = tags
|
||||||
|
if not notes.is_empty():
|
||||||
|
d.notes = notes
|
||||||
|
if not expression_text.is_empty():
|
||||||
|
d.condition_as_text = expression_text
|
||||||
|
|
||||||
|
DMConstants.TYPE_DIALOGUE:
|
||||||
|
d.text = text
|
||||||
|
|
||||||
|
if translation_key != text:
|
||||||
|
d.translation_key = translation_key
|
||||||
|
|
||||||
|
if not character.is_empty():
|
||||||
|
d.character = character
|
||||||
|
if not character_replacements.is_empty():
|
||||||
|
d.character_replacements = character_replacements
|
||||||
|
if not text_replacements.is_empty():
|
||||||
|
d.text_replacements = text_replacements
|
||||||
|
if not tags.is_empty():
|
||||||
|
d.tags = tags
|
||||||
|
if not notes.is_empty():
|
||||||
|
d.notes = notes
|
||||||
|
if not siblings.is_empty():
|
||||||
|
d.siblings = siblings
|
||||||
|
if not concurrent_lines.is_empty():
|
||||||
|
d.concurrent_lines = concurrent_lines
|
||||||
|
|
||||||
|
return d
|
||||||
|
|
||||||
|
|
||||||
|
#endregion
|
1
addons/dialogue_manager/compiler/compiled_line.gd.uid
Normal file
1
addons/dialogue_manager/compiler/compiled_line.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://dg8j5hudp4210
|
51
addons/dialogue_manager/compiler/compiler.gd
vendored
Normal file
51
addons/dialogue_manager/compiler/compiler.gd
vendored
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
## A compiler of Dialogue Manager dialogue.
|
||||||
|
class_name DMCompiler extends RefCounted
|
||||||
|
|
||||||
|
|
||||||
|
## Compile a dialogue script.
|
||||||
|
static func compile_string(text: String, path: String) -> DMCompilerResult:
|
||||||
|
var compilation: DMCompilation = DMCompilation.new()
|
||||||
|
compilation.compile(text, path)
|
||||||
|
|
||||||
|
var result: DMCompilerResult = DMCompilerResult.new()
|
||||||
|
result.imported_paths = compilation.imported_paths
|
||||||
|
result.using_states = compilation.using_states
|
||||||
|
result.character_names = compilation.character_names
|
||||||
|
result.titles = compilation.titles
|
||||||
|
result.first_title = compilation.first_title
|
||||||
|
result.errors = compilation.errors
|
||||||
|
result.lines = compilation.data
|
||||||
|
result.raw_text = text
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
## Get the line type of a string. The returned string will match one of the [code]TYPE_[/code] constants of [DMConstants].
|
||||||
|
static func get_line_type(text: String) -> String:
|
||||||
|
var compilation: DMCompilation = DMCompilation.new()
|
||||||
|
return compilation.get_line_type(text)
|
||||||
|
|
||||||
|
|
||||||
|
## Get the static line ID (eg. [code][ID:SOMETHING][/code]) of some text.
|
||||||
|
static func get_static_line_id(text: String) -> String:
|
||||||
|
var compilation: DMCompilation = DMCompilation.new()
|
||||||
|
return compilation.extract_static_line_id(text)
|
||||||
|
|
||||||
|
|
||||||
|
## Get the translatable part of a line.
|
||||||
|
static func extract_translatable_string(text: String) -> String:
|
||||||
|
var compilation: DMCompilation = DMCompilation.new()
|
||||||
|
|
||||||
|
var tree_line = DMTreeLine.new("")
|
||||||
|
tree_line.text = text
|
||||||
|
var line: DMCompiledLine = DMCompiledLine.new("", compilation.get_line_type(text))
|
||||||
|
compilation.parse_character_and_dialogue(tree_line, line, [tree_line], 0, null)
|
||||||
|
|
||||||
|
return line.text
|
||||||
|
|
||||||
|
|
||||||
|
## Get the known titles in a dialogue script.
|
||||||
|
static func get_titles_in_text(text: String, path: String) -> Dictionary:
|
||||||
|
var compilation: DMCompilation = DMCompilation.new()
|
||||||
|
compilation.build_line_tree(compilation.inject_imported_files(text, path))
|
||||||
|
return compilation.titles
|
1
addons/dialogue_manager/compiler/compiler.gd.uid
Normal file
1
addons/dialogue_manager/compiler/compiler.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://chtfdmr0cqtp4
|
50
addons/dialogue_manager/compiler/compiler_regex.gd
vendored
Normal file
50
addons/dialogue_manager/compiler/compiler_regex.gd
vendored
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
## A collection of [RegEx] for use by the [DMCompiler].
|
||||||
|
class_name DMCompilerRegEx extends RefCounted
|
||||||
|
|
||||||
|
|
||||||
|
var IMPORT_REGEX: RegEx = RegEx.create_from_string("import \"(?<path>[^\"]+)\" as (?<prefix>[a-zA-Z_\\p{Emoji_Presentation}\\p{Han}\\p{Katakana}\\p{Hiragana}\\p{Cyrillic}][a-zA-Z_0-9\\p{Emoji_Presentation}\\p{Han}\\p{Katakana}\\p{Hiragana}\\p{Cyrillic}]+)")
|
||||||
|
var USING_REGEX: RegEx = RegEx.create_from_string("^using (?<state>.*)$")
|
||||||
|
var INDENT_REGEX: RegEx = RegEx.create_from_string("^\\t+")
|
||||||
|
var VALID_TITLE_REGEX: RegEx = RegEx.create_from_string("^[a-zA-Z_0-9\\p{Emoji_Presentation}\\p{Han}\\p{Katakana}\\p{Hiragana}\\p{Cyrillic}][a-zA-Z_0-9\\p{Emoji_Presentation}\\p{Han}\\p{Katakana}\\p{Hiragana}\\p{Cyrillic}]*$")
|
||||||
|
var BEGINS_WITH_NUMBER_REGEX: RegEx = RegEx.create_from_string("^\\d")
|
||||||
|
var CONDITION_REGEX: RegEx = RegEx.create_from_string("(if|elif|while|else if|match|when) (?<expression>.*)\\:?")
|
||||||
|
var WRAPPED_CONDITION_REGEX: RegEx = RegEx.create_from_string("\\[if (?<expression>.*)\\]")
|
||||||
|
var MUTATION_REGEX: RegEx = RegEx.create_from_string("(?<keyword>do|do!|set) (?<expression>.*)")
|
||||||
|
var STATIC_LINE_ID_REGEX: RegEx = RegEx.create_from_string("\\[ID:(?<id>.*?)\\]")
|
||||||
|
var WEIGHTED_RANDOM_SIBLINGS_REGEX: RegEx = RegEx.create_from_string("^\\%(?<weight>[\\d.]+)?( \\[if (?<condition>.+?)\\])? ")
|
||||||
|
var GOTO_REGEX: RegEx = RegEx.create_from_string("=><? (?<goto>.*)")
|
||||||
|
|
||||||
|
var INLINE_RANDOM_REGEX: RegEx = RegEx.create_from_string("\\[\\[(?<options>.*?)\\]\\]")
|
||||||
|
var INLINE_CONDITIONALS_REGEX: RegEx = RegEx.create_from_string("\\[if (?<condition>.+?)\\](?<body>.*?)\\[\\/if\\]")
|
||||||
|
|
||||||
|
var TAGS_REGEX: RegEx = RegEx.create_from_string("\\[#(?<tags>.*?)\\]")
|
||||||
|
|
||||||
|
var REPLACEMENTS_REGEX: RegEx = RegEx.create_from_string("{{(.*?)}}")
|
||||||
|
|
||||||
|
var ALPHA_NUMERIC: RegEx = RegEx.create_from_string("[^a-zA-Z0-9\\p{Han}\\p{Katakana}\\p{Hiragana}\\p{Cyrillic}]+")
|
||||||
|
|
||||||
|
var TOKEN_DEFINITIONS: Dictionary = {
|
||||||
|
DMConstants.TOKEN_FUNCTION: RegEx.create_from_string("^[a-zA-Z_\\p{Emoji_Presentation}\\p{Han}\\p{Katakana}\\p{Hiragana}\\p{Cyrillic}][a-zA-Z_0-9\\p{Emoji_Presentation}\\p{Han}\\p{Katakana}\\p{Hiragana}\\p{Cyrillic}]*\\("),
|
||||||
|
DMConstants.TOKEN_DICTIONARY_REFERENCE: RegEx.create_from_string("^[a-zA-Z_\\p{Emoji_Presentation}\\p{Han}\\p{Katakana}\\p{Hiragana}\\p{Cyrillic}][a-zA-Z_0-9\\p{Emoji_Presentation}\\p{Han}\\p{Katakana}\\p{Hiragana}\\p{Cyrillic}]*\\["),
|
||||||
|
DMConstants.TOKEN_PARENS_OPEN: RegEx.create_from_string("^\\("),
|
||||||
|
DMConstants.TOKEN_PARENS_CLOSE: RegEx.create_from_string("^\\)"),
|
||||||
|
DMConstants.TOKEN_BRACKET_OPEN: RegEx.create_from_string("^\\["),
|
||||||
|
DMConstants.TOKEN_BRACKET_CLOSE: RegEx.create_from_string("^\\]"),
|
||||||
|
DMConstants.TOKEN_BRACE_OPEN: RegEx.create_from_string("^\\{"),
|
||||||
|
DMConstants.TOKEN_BRACE_CLOSE: RegEx.create_from_string("^\\}"),
|
||||||
|
DMConstants.TOKEN_COLON: RegEx.create_from_string("^:"),
|
||||||
|
DMConstants.TOKEN_COMPARISON: RegEx.create_from_string("^(==|<=|>=|<|>|!=|in )"),
|
||||||
|
DMConstants.TOKEN_ASSIGNMENT: RegEx.create_from_string("^(\\+=|\\-=|\\*=|/=|=)"),
|
||||||
|
DMConstants.TOKEN_NUMBER: RegEx.create_from_string("^\\-?\\d+(\\.\\d+)?"),
|
||||||
|
DMConstants.TOKEN_OPERATOR: RegEx.create_from_string("^(\\+|\\-|\\*|/|%)"),
|
||||||
|
DMConstants.TOKEN_COMMA: RegEx.create_from_string("^,"),
|
||||||
|
DMConstants.TOKEN_NULL_COALESCE: RegEx.create_from_string("^\\?\\."),
|
||||||
|
DMConstants.TOKEN_DOT: RegEx.create_from_string("^\\."),
|
||||||
|
DMConstants.TOKEN_STRING: RegEx.create_from_string("^&?(\".*?\"|\'.*?\')"),
|
||||||
|
DMConstants.TOKEN_NOT: RegEx.create_from_string("^(not( |$)|!)"),
|
||||||
|
DMConstants.TOKEN_AND_OR: RegEx.create_from_string("^(and|or|&&|\\|\\|)( |$)"),
|
||||||
|
DMConstants.TOKEN_VARIABLE: RegEx.create_from_string("^[a-zA-Z_\\p{Emoji_Presentation}\\p{Han}\\p{Katakana}\\p{Hiragana}\\p{Cyrillic}][a-zA-Z_0-9\\p{Emoji_Presentation}\\p{Han}\\p{Katakana}\\p{Hiragana}\\p{Cyrillic}]*"),
|
||||||
|
DMConstants.TOKEN_COMMENT: RegEx.create_from_string("^#.*"),
|
||||||
|
DMConstants.TOKEN_CONDITION: RegEx.create_from_string("^(if|elif|else)"),
|
||||||
|
DMConstants.TOKEN_BOOL: RegEx.create_from_string("^(true|false)")
|
||||||
|
}
|
1
addons/dialogue_manager/compiler/compiler_regex.gd.uid
Normal file
1
addons/dialogue_manager/compiler/compiler_regex.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://d3tvcrnicjibp
|
27
addons/dialogue_manager/compiler/compiler_result.gd
vendored
Normal file
27
addons/dialogue_manager/compiler/compiler_result.gd
vendored
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
## The result of using the [DMCompiler] to compile some dialogue.
|
||||||
|
class_name DMCompilerResult extends RefCounted
|
||||||
|
|
||||||
|
|
||||||
|
## Any paths that were imported into the compiled dialogue file.
|
||||||
|
var imported_paths: PackedStringArray = []
|
||||||
|
|
||||||
|
## Any "using" directives.
|
||||||
|
var using_states: PackedStringArray = []
|
||||||
|
|
||||||
|
## All titles in the file and the line they point to.
|
||||||
|
var titles: Dictionary = {}
|
||||||
|
|
||||||
|
## The first title in the file.
|
||||||
|
var first_title: String = ""
|
||||||
|
|
||||||
|
## All character names.
|
||||||
|
var character_names: PackedStringArray = []
|
||||||
|
|
||||||
|
## Any compilation errors.
|
||||||
|
var errors: Array[Dictionary] = []
|
||||||
|
|
||||||
|
## A map of all compiled lines.
|
||||||
|
var lines: Dictionary = {}
|
||||||
|
|
||||||
|
## The raw dialogue text.
|
||||||
|
var raw_text: String = ""
|
1
addons/dialogue_manager/compiler/compiler_result.gd.uid
Normal file
1
addons/dialogue_manager/compiler/compiler_result.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://dmk74tknimqvg
|
529
addons/dialogue_manager/compiler/expression_parser.gd
vendored
Normal file
529
addons/dialogue_manager/compiler/expression_parser.gd
vendored
Normal file
@@ -0,0 +1,529 @@
|
|||||||
|
## A class for parsing a condition/mutation expression for use with the [DMCompiler].
|
||||||
|
class_name DMExpressionParser extends RefCounted
|
||||||
|
|
||||||
|
|
||||||
|
var include_comments: bool = false
|
||||||
|
|
||||||
|
|
||||||
|
# Reference to the common [RegEx] that the parser needs.
|
||||||
|
var regex: DMCompilerRegEx = DMCompilerRegEx.new()
|
||||||
|
|
||||||
|
|
||||||
|
## Break a string down into an expression.
|
||||||
|
func tokenise(text: String, line_type: String, index: int) -> Array:
|
||||||
|
var tokens: Array[Dictionary] = []
|
||||||
|
var limit: int = 0
|
||||||
|
while text.strip_edges() != "" and limit < 1000:
|
||||||
|
limit += 1
|
||||||
|
var found = _find_match(text)
|
||||||
|
if found.size() > 0:
|
||||||
|
tokens.append({
|
||||||
|
index = index,
|
||||||
|
type = found.type,
|
||||||
|
value = found.value
|
||||||
|
})
|
||||||
|
index += found.value.length()
|
||||||
|
text = found.remaining_text
|
||||||
|
elif text.begins_with(" "):
|
||||||
|
index += 1
|
||||||
|
text = text.substr(1)
|
||||||
|
else:
|
||||||
|
return _build_token_tree_error([], DMConstants.ERR_INVALID_EXPRESSION, index)
|
||||||
|
|
||||||
|
return _build_token_tree(tokens, line_type, "")[0]
|
||||||
|
|
||||||
|
|
||||||
|
## Extract any expressions from some text
|
||||||
|
func extract_replacements(text: String, index: int) -> Array[Dictionary]:
|
||||||
|
var founds: Array[RegExMatch] = regex.REPLACEMENTS_REGEX.search_all(text)
|
||||||
|
|
||||||
|
if founds == null or founds.size() == 0:
|
||||||
|
return []
|
||||||
|
|
||||||
|
var replacements: Array[Dictionary] = []
|
||||||
|
for found in founds:
|
||||||
|
var replacement: Dictionary = {}
|
||||||
|
var value_in_text: String = found.strings[0].substr(0, found.strings[0].length() - 2).substr(2)
|
||||||
|
|
||||||
|
# If there are closing curlie hard-up against the end of a {{...}} block then check for further
|
||||||
|
# curlies just outside of the block.
|
||||||
|
var text_suffix: String = text.substr(found.get_end(0))
|
||||||
|
var expression_suffix: String = ""
|
||||||
|
while text_suffix.begins_with("}"):
|
||||||
|
expression_suffix += "}"
|
||||||
|
text_suffix = text_suffix.substr(1)
|
||||||
|
value_in_text += expression_suffix
|
||||||
|
|
||||||
|
var expression: Array = tokenise(value_in_text, DMConstants.TYPE_DIALOGUE, index + found.get_start(1))
|
||||||
|
if expression.size() == 0:
|
||||||
|
replacement = {
|
||||||
|
index = index + found.get_start(1),
|
||||||
|
error = DMConstants.ERR_INCOMPLETE_EXPRESSION
|
||||||
|
}
|
||||||
|
elif expression[0].type == DMConstants.TYPE_ERROR:
|
||||||
|
replacement = {
|
||||||
|
index = expression[0].i,
|
||||||
|
error = expression[0].value
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
replacement = {
|
||||||
|
value_in_text = "{{%s}}" % value_in_text,
|
||||||
|
expression = expression
|
||||||
|
}
|
||||||
|
replacements.append(replacement)
|
||||||
|
|
||||||
|
return replacements
|
||||||
|
|
||||||
|
|
||||||
|
#region Helpers
|
||||||
|
|
||||||
|
|
||||||
|
# Create a token that represents an error.
|
||||||
|
func _build_token_tree_error(tree: Array, error: int, index: int) -> Array:
|
||||||
|
tree.insert(0, {
|
||||||
|
type = DMConstants.TOKEN_ERROR,
|
||||||
|
value = error,
|
||||||
|
i = index
|
||||||
|
})
|
||||||
|
return tree
|
||||||
|
|
||||||
|
|
||||||
|
# Convert a list of tokens into an abstract syntax tree.
|
||||||
|
func _build_token_tree(tokens: Array[Dictionary], line_type: String, expected_close_token: String) -> Array:
|
||||||
|
var tree: Array[Dictionary] = []
|
||||||
|
var limit = 0
|
||||||
|
while tokens.size() > 0 and limit < 1000:
|
||||||
|
limit += 1
|
||||||
|
var token = tokens.pop_front()
|
||||||
|
|
||||||
|
var error = _check_next_token(token, tokens, line_type, expected_close_token)
|
||||||
|
if error != OK:
|
||||||
|
var error_token: Dictionary = tokens[1] if tokens.size() > 1 else token
|
||||||
|
return [_build_token_tree_error(tree, error, error_token.index), tokens]
|
||||||
|
|
||||||
|
match token.type:
|
||||||
|
DMConstants.TOKEN_COMMENT:
|
||||||
|
if include_comments:
|
||||||
|
tree.append({
|
||||||
|
type = DMConstants.TOKEN_COMMENT,
|
||||||
|
value = token.value,
|
||||||
|
i = token.index
|
||||||
|
})
|
||||||
|
|
||||||
|
DMConstants.TOKEN_FUNCTION:
|
||||||
|
var sub_tree = _build_token_tree(tokens, line_type, DMConstants.TOKEN_PARENS_CLOSE)
|
||||||
|
|
||||||
|
if sub_tree[0].size() > 0 and sub_tree[0][0].type == DMConstants.TOKEN_ERROR:
|
||||||
|
return [_build_token_tree_error(tree, sub_tree[0][0].value, sub_tree[0][0].i), tokens]
|
||||||
|
|
||||||
|
tree.append({
|
||||||
|
type = DMConstants.TOKEN_FUNCTION,
|
||||||
|
# Consume the trailing "("
|
||||||
|
function = token.value.substr(0, token.value.length() - 1),
|
||||||
|
value = _tokens_to_list(sub_tree[0]),
|
||||||
|
i = token.index
|
||||||
|
})
|
||||||
|
tokens = sub_tree[1]
|
||||||
|
|
||||||
|
DMConstants.TOKEN_DICTIONARY_REFERENCE:
|
||||||
|
var sub_tree = _build_token_tree(tokens, line_type, DMConstants.TOKEN_BRACKET_CLOSE)
|
||||||
|
|
||||||
|
if sub_tree[0].size() > 0 and sub_tree[0][0].type == DMConstants.TOKEN_ERROR:
|
||||||
|
return [_build_token_tree_error(tree, sub_tree[0][0].value, sub_tree[0][0].i), tokens]
|
||||||
|
|
||||||
|
var args = _tokens_to_list(sub_tree[0])
|
||||||
|
if args.size() != 1:
|
||||||
|
return [_build_token_tree_error(tree, DMConstants.ERR_INVALID_INDEX, token.index), tokens]
|
||||||
|
|
||||||
|
tree.append({
|
||||||
|
type = DMConstants.TOKEN_DICTIONARY_REFERENCE,
|
||||||
|
# Consume the trailing "["
|
||||||
|
variable = token.value.substr(0, token.value.length() - 1),
|
||||||
|
value = args[0],
|
||||||
|
i = token.index
|
||||||
|
})
|
||||||
|
tokens = sub_tree[1]
|
||||||
|
|
||||||
|
DMConstants.TOKEN_BRACE_OPEN:
|
||||||
|
var sub_tree = _build_token_tree(tokens, line_type, DMConstants.TOKEN_BRACE_CLOSE)
|
||||||
|
|
||||||
|
if sub_tree[0].size() > 0 and sub_tree[0][0].type == DMConstants.TOKEN_ERROR:
|
||||||
|
return [_build_token_tree_error(tree, sub_tree[0][0].value, sub_tree[0][0].i), tokens]
|
||||||
|
|
||||||
|
var t = sub_tree[0]
|
||||||
|
for i in range(0, t.size() - 2):
|
||||||
|
# Convert Lua style dictionaries to string keys
|
||||||
|
if t[i].type == DMConstants.TOKEN_VARIABLE and t[i+1].type == DMConstants.TOKEN_ASSIGNMENT:
|
||||||
|
t[i].type = DMConstants.TOKEN_STRING
|
||||||
|
t[i+1].type = DMConstants.TOKEN_COLON
|
||||||
|
t[i+1].erase("value")
|
||||||
|
|
||||||
|
tree.append({
|
||||||
|
type = DMConstants.TOKEN_DICTIONARY,
|
||||||
|
value = _tokens_to_dictionary(sub_tree[0]),
|
||||||
|
i = token.index
|
||||||
|
})
|
||||||
|
|
||||||
|
tokens = sub_tree[1]
|
||||||
|
|
||||||
|
DMConstants.TOKEN_BRACKET_OPEN:
|
||||||
|
var sub_tree = _build_token_tree(tokens, line_type, DMConstants.TOKEN_BRACKET_CLOSE)
|
||||||
|
|
||||||
|
if sub_tree[0].size() > 0 and sub_tree[0][0].type == DMConstants.TOKEN_ERROR:
|
||||||
|
return [_build_token_tree_error(tree, sub_tree[0][0].value, sub_tree[0][0].i), tokens]
|
||||||
|
|
||||||
|
var type = DMConstants.TOKEN_ARRAY
|
||||||
|
var value = _tokens_to_list(sub_tree[0])
|
||||||
|
|
||||||
|
# See if this is referencing a nested dictionary value
|
||||||
|
if tree.size() > 0:
|
||||||
|
var previous_token = tree[tree.size() - 1]
|
||||||
|
if previous_token.type in [DMConstants.TOKEN_DICTIONARY_REFERENCE, DMConstants.TOKEN_DICTIONARY_NESTED_REFERENCE]:
|
||||||
|
type = DMConstants.TOKEN_DICTIONARY_NESTED_REFERENCE
|
||||||
|
value = value[0]
|
||||||
|
|
||||||
|
tree.append({
|
||||||
|
type = type,
|
||||||
|
value = value,
|
||||||
|
i = token.index
|
||||||
|
})
|
||||||
|
tokens = sub_tree[1]
|
||||||
|
|
||||||
|
DMConstants.TOKEN_PARENS_OPEN:
|
||||||
|
var sub_tree = _build_token_tree(tokens, line_type, DMConstants.TOKEN_PARENS_CLOSE)
|
||||||
|
|
||||||
|
if sub_tree[0].size() > 0 and sub_tree[0][0].type == DMConstants.TOKEN_ERROR:
|
||||||
|
return [_build_token_tree_error(tree, sub_tree[0][0].value, sub_tree[0][0].i), tokens]
|
||||||
|
|
||||||
|
tree.append({
|
||||||
|
type = DMConstants.TOKEN_GROUP,
|
||||||
|
value = sub_tree[0],
|
||||||
|
i = token.index
|
||||||
|
})
|
||||||
|
tokens = sub_tree[1]
|
||||||
|
|
||||||
|
DMConstants.TOKEN_PARENS_CLOSE, \
|
||||||
|
DMConstants.TOKEN_BRACE_CLOSE, \
|
||||||
|
DMConstants.TOKEN_BRACKET_CLOSE:
|
||||||
|
if token.type != expected_close_token:
|
||||||
|
return [_build_token_tree_error(tree, DMConstants.ERR_UNEXPECTED_CLOSING_BRACKET, token.index), tokens]
|
||||||
|
|
||||||
|
tree.append({
|
||||||
|
type = token.type,
|
||||||
|
i = token.index
|
||||||
|
})
|
||||||
|
|
||||||
|
return [tree, tokens]
|
||||||
|
|
||||||
|
DMConstants.TOKEN_NOT:
|
||||||
|
# Double nots negate each other
|
||||||
|
if tokens.size() > 0 and tokens.front().type == DMConstants.TOKEN_NOT:
|
||||||
|
tokens.pop_front()
|
||||||
|
else:
|
||||||
|
tree.append({
|
||||||
|
type = token.type,
|
||||||
|
i = token.index
|
||||||
|
})
|
||||||
|
|
||||||
|
DMConstants.TOKEN_COMMA, \
|
||||||
|
DMConstants.TOKEN_COLON, \
|
||||||
|
DMConstants.TOKEN_DOT, \
|
||||||
|
DMConstants.TOKEN_NULL_COALESCE:
|
||||||
|
tree.append({
|
||||||
|
type = token.type,
|
||||||
|
i = token.index
|
||||||
|
})
|
||||||
|
|
||||||
|
DMConstants.TOKEN_COMPARISON, \
|
||||||
|
DMConstants.TOKEN_ASSIGNMENT, \
|
||||||
|
DMConstants.TOKEN_OPERATOR, \
|
||||||
|
DMConstants.TOKEN_AND_OR, \
|
||||||
|
DMConstants.TOKEN_VARIABLE:
|
||||||
|
var value = token.value.strip_edges()
|
||||||
|
if value == "&&":
|
||||||
|
value = "and"
|
||||||
|
elif value == "||":
|
||||||
|
value = "or"
|
||||||
|
tree.append({
|
||||||
|
type = token.type,
|
||||||
|
value = value,
|
||||||
|
i = token.index
|
||||||
|
})
|
||||||
|
|
||||||
|
DMConstants.TOKEN_STRING:
|
||||||
|
if token.value.begins_with("&"):
|
||||||
|
tree.append({
|
||||||
|
type = token.type,
|
||||||
|
value = StringName(token.value.substr(2, token.value.length() - 3)),
|
||||||
|
i = token.index
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
tree.append({
|
||||||
|
type = token.type,
|
||||||
|
value = token.value.substr(1, token.value.length() - 2),
|
||||||
|
i = token.index
|
||||||
|
})
|
||||||
|
|
||||||
|
DMConstants.TOKEN_CONDITION:
|
||||||
|
return [_build_token_tree_error(tree, DMConstants.ERR_UNEXPECTED_CONDITION, token.index), token]
|
||||||
|
|
||||||
|
DMConstants.TOKEN_BOOL:
|
||||||
|
tree.append({
|
||||||
|
type = token.type,
|
||||||
|
value = token.value.to_lower() == "true",
|
||||||
|
i = token.index
|
||||||
|
})
|
||||||
|
|
||||||
|
DMConstants.TOKEN_NUMBER:
|
||||||
|
var value = token.value.to_float() if "." in token.value else token.value.to_int()
|
||||||
|
# If previous token is a number and this one is a negative number then
|
||||||
|
# inject a minus operator token in between them.
|
||||||
|
if tree.size() > 0 and token.value.begins_with("-") and tree[tree.size() - 1].type == DMConstants.TOKEN_NUMBER:
|
||||||
|
tree.append(({
|
||||||
|
type = DMConstants.TOKEN_OPERATOR,
|
||||||
|
value = "-",
|
||||||
|
i = token.index
|
||||||
|
}))
|
||||||
|
tree.append({
|
||||||
|
type = token.type,
|
||||||
|
value = -1 * value,
|
||||||
|
i = token.index
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
tree.append({
|
||||||
|
type = token.type,
|
||||||
|
value = value,
|
||||||
|
i = token.index
|
||||||
|
})
|
||||||
|
|
||||||
|
if expected_close_token != "":
|
||||||
|
var index: int = tokens[0].i if tokens.size() > 0 else 0
|
||||||
|
return [_build_token_tree_error(tree, DMConstants.ERR_MISSING_CLOSING_BRACKET, index), tokens]
|
||||||
|
|
||||||
|
return [tree, tokens]
|
||||||
|
|
||||||
|
|
||||||
|
# Check the next token to see if it is valid to follow this one.
|
||||||
|
func _check_next_token(token: Dictionary, next_tokens: Array[Dictionary], line_type: String, expected_close_token: String) -> Error:
|
||||||
|
var next_token: Dictionary = { type = null }
|
||||||
|
if next_tokens.size() > 0:
|
||||||
|
next_token = next_tokens.front()
|
||||||
|
|
||||||
|
# Guard for assigning in a condition. If the assignment token isn't inside a Lua dictionary
|
||||||
|
# then it's an unexpected assignment in a condition line.
|
||||||
|
if token.type == DMConstants.TOKEN_ASSIGNMENT and line_type == DMConstants.TYPE_CONDITION and not next_tokens.any(func(t): return t.type == expected_close_token):
|
||||||
|
return DMConstants.ERR_UNEXPECTED_ASSIGNMENT
|
||||||
|
|
||||||
|
# Special case for a negative number after this one
|
||||||
|
if token.type == DMConstants.TOKEN_NUMBER and next_token.type == DMConstants.TOKEN_NUMBER and next_token.value.begins_with("-"):
|
||||||
|
return OK
|
||||||
|
|
||||||
|
var expected_token_types = []
|
||||||
|
var unexpected_token_types = []
|
||||||
|
match token.type:
|
||||||
|
DMConstants.TOKEN_FUNCTION, \
|
||||||
|
DMConstants.TOKEN_PARENS_OPEN:
|
||||||
|
unexpected_token_types = [
|
||||||
|
null,
|
||||||
|
DMConstants.TOKEN_COMMA,
|
||||||
|
DMConstants.TOKEN_COLON,
|
||||||
|
DMConstants.TOKEN_COMPARISON,
|
||||||
|
DMConstants.TOKEN_ASSIGNMENT,
|
||||||
|
DMConstants.TOKEN_OPERATOR,
|
||||||
|
DMConstants.TOKEN_AND_OR,
|
||||||
|
DMConstants.TOKEN_DOT
|
||||||
|
]
|
||||||
|
|
||||||
|
DMConstants.TOKEN_BRACKET_CLOSE:
|
||||||
|
unexpected_token_types = [
|
||||||
|
DMConstants.TOKEN_NOT,
|
||||||
|
DMConstants.TOKEN_BOOL,
|
||||||
|
DMConstants.TOKEN_STRING,
|
||||||
|
DMConstants.TOKEN_NUMBER,
|
||||||
|
DMConstants.TOKEN_VARIABLE
|
||||||
|
]
|
||||||
|
|
||||||
|
DMConstants.TOKEN_BRACE_OPEN:
|
||||||
|
expected_token_types = [
|
||||||
|
DMConstants.TOKEN_STRING,
|
||||||
|
DMConstants.TOKEN_VARIABLE,
|
||||||
|
DMConstants.TOKEN_NUMBER,
|
||||||
|
DMConstants.TOKEN_BRACE_CLOSE
|
||||||
|
]
|
||||||
|
|
||||||
|
DMConstants.TOKEN_PARENS_CLOSE, \
|
||||||
|
DMConstants.TOKEN_BRACE_CLOSE:
|
||||||
|
unexpected_token_types = [
|
||||||
|
DMConstants.TOKEN_NOT,
|
||||||
|
DMConstants.TOKEN_ASSIGNMENT,
|
||||||
|
DMConstants.TOKEN_BOOL,
|
||||||
|
DMConstants.TOKEN_STRING,
|
||||||
|
DMConstants.TOKEN_NUMBER,
|
||||||
|
DMConstants.TOKEN_VARIABLE
|
||||||
|
]
|
||||||
|
|
||||||
|
DMConstants.TOKEN_COMPARISON, \
|
||||||
|
DMConstants.TOKEN_OPERATOR, \
|
||||||
|
DMConstants.TOKEN_DOT, \
|
||||||
|
DMConstants.TOKEN_NULL_COALESCE, \
|
||||||
|
DMConstants.TOKEN_NOT, \
|
||||||
|
DMConstants.TOKEN_AND_OR, \
|
||||||
|
DMConstants.TOKEN_DICTIONARY_REFERENCE:
|
||||||
|
unexpected_token_types = [
|
||||||
|
null,
|
||||||
|
DMConstants.TOKEN_COMMA,
|
||||||
|
DMConstants.TOKEN_COLON,
|
||||||
|
DMConstants.TOKEN_COMPARISON,
|
||||||
|
DMConstants.TOKEN_ASSIGNMENT,
|
||||||
|
DMConstants.TOKEN_OPERATOR,
|
||||||
|
DMConstants.TOKEN_AND_OR,
|
||||||
|
DMConstants.TOKEN_PARENS_CLOSE,
|
||||||
|
DMConstants.TOKEN_BRACE_CLOSE,
|
||||||
|
DMConstants.TOKEN_BRACKET_CLOSE,
|
||||||
|
DMConstants.TOKEN_DOT
|
||||||
|
]
|
||||||
|
|
||||||
|
DMConstants.TOKEN_COMMA:
|
||||||
|
unexpected_token_types = [
|
||||||
|
null,
|
||||||
|
DMConstants.TOKEN_COMMA,
|
||||||
|
DMConstants.TOKEN_COLON,
|
||||||
|
DMConstants.TOKEN_ASSIGNMENT,
|
||||||
|
DMConstants.TOKEN_OPERATOR,
|
||||||
|
DMConstants.TOKEN_AND_OR,
|
||||||
|
DMConstants.TOKEN_PARENS_CLOSE,
|
||||||
|
DMConstants.TOKEN_BRACE_CLOSE,
|
||||||
|
DMConstants.TOKEN_BRACKET_CLOSE,
|
||||||
|
DMConstants.TOKEN_DOT
|
||||||
|
]
|
||||||
|
|
||||||
|
DMConstants.TOKEN_COLON:
|
||||||
|
unexpected_token_types = [
|
||||||
|
DMConstants.TOKEN_COMMA,
|
||||||
|
DMConstants.TOKEN_COLON,
|
||||||
|
DMConstants.TOKEN_COMPARISON,
|
||||||
|
DMConstants.TOKEN_ASSIGNMENT,
|
||||||
|
DMConstants.TOKEN_OPERATOR,
|
||||||
|
DMConstants.TOKEN_AND_OR,
|
||||||
|
DMConstants.TOKEN_PARENS_CLOSE,
|
||||||
|
DMConstants.TOKEN_BRACE_CLOSE,
|
||||||
|
DMConstants.TOKEN_BRACKET_CLOSE,
|
||||||
|
DMConstants.TOKEN_DOT
|
||||||
|
]
|
||||||
|
|
||||||
|
DMConstants.TOKEN_BOOL, \
|
||||||
|
DMConstants.TOKEN_STRING, \
|
||||||
|
DMConstants.TOKEN_NUMBER:
|
||||||
|
unexpected_token_types = [
|
||||||
|
DMConstants.TOKEN_NOT,
|
||||||
|
DMConstants.TOKEN_ASSIGNMENT,
|
||||||
|
DMConstants.TOKEN_BOOL,
|
||||||
|
DMConstants.TOKEN_STRING,
|
||||||
|
DMConstants.TOKEN_NUMBER,
|
||||||
|
DMConstants.TOKEN_VARIABLE,
|
||||||
|
DMConstants.TOKEN_FUNCTION,
|
||||||
|
DMConstants.TOKEN_PARENS_OPEN,
|
||||||
|
DMConstants.TOKEN_BRACE_OPEN,
|
||||||
|
DMConstants.TOKEN_BRACKET_OPEN
|
||||||
|
]
|
||||||
|
|
||||||
|
DMConstants.TOKEN_VARIABLE:
|
||||||
|
unexpected_token_types = [
|
||||||
|
DMConstants.TOKEN_NOT,
|
||||||
|
DMConstants.TOKEN_BOOL,
|
||||||
|
DMConstants.TOKEN_STRING,
|
||||||
|
DMConstants.TOKEN_NUMBER,
|
||||||
|
DMConstants.TOKEN_VARIABLE,
|
||||||
|
DMConstants.TOKEN_FUNCTION,
|
||||||
|
DMConstants.TOKEN_PARENS_OPEN,
|
||||||
|
DMConstants.TOKEN_BRACE_OPEN,
|
||||||
|
DMConstants.TOKEN_BRACKET_OPEN
|
||||||
|
]
|
||||||
|
|
||||||
|
if (expected_token_types.size() > 0 and not next_token.type in expected_token_types) \
|
||||||
|
or (unexpected_token_types.size() > 0 and next_token.type in unexpected_token_types):
|
||||||
|
match next_token.type:
|
||||||
|
null:
|
||||||
|
return DMConstants.ERR_UNEXPECTED_END_OF_EXPRESSION
|
||||||
|
|
||||||
|
DMConstants.TOKEN_FUNCTION:
|
||||||
|
return DMConstants.ERR_UNEXPECTED_FUNCTION
|
||||||
|
|
||||||
|
DMConstants.TOKEN_PARENS_OPEN, \
|
||||||
|
DMConstants.TOKEN_PARENS_CLOSE:
|
||||||
|
return DMConstants.ERR_UNEXPECTED_BRACKET
|
||||||
|
|
||||||
|
DMConstants.TOKEN_COMPARISON, \
|
||||||
|
DMConstants.TOKEN_ASSIGNMENT, \
|
||||||
|
DMConstants.TOKEN_OPERATOR, \
|
||||||
|
DMConstants.TOKEN_NOT, \
|
||||||
|
DMConstants.TOKEN_AND_OR:
|
||||||
|
return DMConstants.ERR_UNEXPECTED_OPERATOR
|
||||||
|
|
||||||
|
DMConstants.TOKEN_COMMA:
|
||||||
|
return DMConstants.ERR_UNEXPECTED_COMMA
|
||||||
|
DMConstants.TOKEN_COLON:
|
||||||
|
return DMConstants.ERR_UNEXPECTED_COLON
|
||||||
|
DMConstants.TOKEN_DOT:
|
||||||
|
return DMConstants.ERR_UNEXPECTED_DOT
|
||||||
|
|
||||||
|
DMConstants.TOKEN_BOOL:
|
||||||
|
return DMConstants.ERR_UNEXPECTED_BOOLEAN
|
||||||
|
DMConstants.TOKEN_STRING:
|
||||||
|
return DMConstants.ERR_UNEXPECTED_STRING
|
||||||
|
DMConstants.TOKEN_NUMBER:
|
||||||
|
return DMConstants.ERR_UNEXPECTED_NUMBER
|
||||||
|
DMConstants.TOKEN_VARIABLE:
|
||||||
|
return DMConstants.ERR_UNEXPECTED_VARIABLE
|
||||||
|
|
||||||
|
return DMConstants.ERR_INVALID_EXPRESSION
|
||||||
|
|
||||||
|
return OK
|
||||||
|
|
||||||
|
|
||||||
|
# Convert a series of comma separated tokens to an [Array].
|
||||||
|
func _tokens_to_list(tokens: Array[Dictionary]) -> Array[Array]:
|
||||||
|
var list: Array[Array] = []
|
||||||
|
var current_item: Array[Dictionary] = []
|
||||||
|
for token in tokens:
|
||||||
|
if token.type == DMConstants.TOKEN_COMMA:
|
||||||
|
list.append(current_item)
|
||||||
|
current_item = []
|
||||||
|
else:
|
||||||
|
current_item.append(token)
|
||||||
|
|
||||||
|
if current_item.size() > 0:
|
||||||
|
list.append(current_item)
|
||||||
|
|
||||||
|
return list
|
||||||
|
|
||||||
|
|
||||||
|
# Convert a series of key/value tokens into a [Dictionary]
|
||||||
|
func _tokens_to_dictionary(tokens: Array[Dictionary]) -> Dictionary:
|
||||||
|
var dictionary = {}
|
||||||
|
for i in range(0, tokens.size()):
|
||||||
|
if tokens[i].type == DMConstants.TOKEN_COLON:
|
||||||
|
if tokens.size() == i + 2:
|
||||||
|
dictionary[tokens[i - 1]] = tokens[i + 1]
|
||||||
|
else:
|
||||||
|
dictionary[tokens[i - 1]] = { type = DMConstants.TOKEN_GROUP, value = tokens.slice(i + 1), i = tokens[0].i }
|
||||||
|
|
||||||
|
return dictionary
|
||||||
|
|
||||||
|
|
||||||
|
# Work out what the next token is from a string.
|
||||||
|
func _find_match(input: String) -> Dictionary:
|
||||||
|
for key in regex.TOKEN_DEFINITIONS.keys():
|
||||||
|
var regex = regex.TOKEN_DEFINITIONS.get(key)
|
||||||
|
var found = regex.search(input)
|
||||||
|
if found:
|
||||||
|
return {
|
||||||
|
type = key,
|
||||||
|
remaining_text = input.substr(found.strings[0].length()),
|
||||||
|
value = found.strings[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
#endregion
|
@@ -0,0 +1 @@
|
|||||||
|
uid://dbi4hbar8ubwu
|
68
addons/dialogue_manager/compiler/resolved_goto_data.gd
vendored
Normal file
68
addons/dialogue_manager/compiler/resolved_goto_data.gd
vendored
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
## Data associated with a dialogue jump/goto line.
|
||||||
|
class_name DMResolvedGotoData extends RefCounted
|
||||||
|
|
||||||
|
|
||||||
|
## The title that was specified
|
||||||
|
var title: String = ""
|
||||||
|
## The target line's ID
|
||||||
|
var next_id: String = ""
|
||||||
|
## An expression to determine the target line at runtime.
|
||||||
|
var expression: Array[Dictionary] = []
|
||||||
|
## The given line text with the jump syntax removed.
|
||||||
|
var text_without_goto: String = ""
|
||||||
|
## Whether this is a jump-and-return style jump.
|
||||||
|
var is_snippet: bool = false
|
||||||
|
## A parse error if there was one.
|
||||||
|
var error: int
|
||||||
|
## The index in the string where
|
||||||
|
var index: int = 0
|
||||||
|
|
||||||
|
# An instance of the compiler [RegEx] list.
|
||||||
|
var regex: DMCompilerRegEx = DMCompilerRegEx.new()
|
||||||
|
|
||||||
|
|
||||||
|
func _init(text: String, titles: Dictionary) -> void:
|
||||||
|
if not "=> " in text and not "=>< " in text: return
|
||||||
|
|
||||||
|
if "=> " in text:
|
||||||
|
text_without_goto = text.substr(0, text.find("=> ")).strip_edges()
|
||||||
|
elif "=>< " in text:
|
||||||
|
is_snippet = true
|
||||||
|
text_without_goto = text.substr(0, text.find("=>< ")).strip_edges()
|
||||||
|
|
||||||
|
var found: RegExMatch = regex.GOTO_REGEX.search(text)
|
||||||
|
if found == null:
|
||||||
|
return
|
||||||
|
|
||||||
|
title = found.strings[found.names.goto].strip_edges()
|
||||||
|
index = found.get_start(0)
|
||||||
|
|
||||||
|
if title == "":
|
||||||
|
error = DMConstants.ERR_UNKNOWN_TITLE
|
||||||
|
return
|
||||||
|
|
||||||
|
# "=> END!" means end the conversation, ignoring any "=><" chains.
|
||||||
|
if title == "END!":
|
||||||
|
next_id = DMConstants.ID_END_CONVERSATION
|
||||||
|
|
||||||
|
# "=> END" means end the current title (and go back to the previous one if there is one
|
||||||
|
# in the stack)
|
||||||
|
elif title == "END":
|
||||||
|
next_id = DMConstants.ID_END
|
||||||
|
|
||||||
|
elif titles.has(title):
|
||||||
|
next_id = titles.get(title)
|
||||||
|
elif title.begins_with("{{"):
|
||||||
|
var expression_parser: DMExpressionParser = DMExpressionParser.new()
|
||||||
|
var title_expression: Array[Dictionary] = expression_parser.extract_replacements(title, 0)
|
||||||
|
if title_expression[0].has("error"):
|
||||||
|
error = title_expression[0].error
|
||||||
|
else:
|
||||||
|
expression = title_expression[0].expression
|
||||||
|
else:
|
||||||
|
next_id = title
|
||||||
|
error = DMConstants.ERR_UNKNOWN_TITLE
|
||||||
|
|
||||||
|
|
||||||
|
func _to_string() -> String:
|
||||||
|
return "%s =>%s %s (%s)" % [text_without_goto, "<" if is_snippet else "", title, next_id]
|
@@ -0,0 +1 @@
|
|||||||
|
uid://llhl5pt47eoq
|
167
addons/dialogue_manager/compiler/resolved_line_data.gd
vendored
Normal file
167
addons/dialogue_manager/compiler/resolved_line_data.gd
vendored
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
## Any data associated with inline dialogue BBCodes.
|
||||||
|
class_name DMResolvedLineData extends RefCounted
|
||||||
|
|
||||||
|
## The line's text
|
||||||
|
var text: String = ""
|
||||||
|
## A map of pauses against where they are found in the text.
|
||||||
|
var pauses: Dictionary = {}
|
||||||
|
## A map of speed changes against where they are found in the text.
|
||||||
|
var speeds: Dictionary = {}
|
||||||
|
## A list of any mutations to run and where they are found in the text.
|
||||||
|
var mutations: Array[Array] = []
|
||||||
|
## A duration reference for the line. Represented as "auto" or a stringified number.
|
||||||
|
var time: String = ""
|
||||||
|
|
||||||
|
|
||||||
|
func _init(line: String) -> void:
|
||||||
|
text = line
|
||||||
|
pauses = {}
|
||||||
|
speeds = {}
|
||||||
|
mutations = []
|
||||||
|
time = ""
|
||||||
|
|
||||||
|
var bbcodes: Array = []
|
||||||
|
|
||||||
|
# Remove any escaped brackets (ie. "\[")
|
||||||
|
var escaped_open_brackets: PackedInt32Array = []
|
||||||
|
var escaped_close_brackets: PackedInt32Array = []
|
||||||
|
for i in range(0, text.length() - 1):
|
||||||
|
if text.substr(i, 2) == "\\[":
|
||||||
|
text = text.substr(0, i) + "!" + text.substr(i + 2)
|
||||||
|
escaped_open_brackets.append(i)
|
||||||
|
elif text.substr(i, 2) == "\\]":
|
||||||
|
text = text.substr(0, i) + "!" + text.substr(i + 2)
|
||||||
|
escaped_close_brackets.append(i)
|
||||||
|
|
||||||
|
# Extract all of the BB codes so that we know the actual text (we could do this easier with
|
||||||
|
# a RichTextLabel but then we'd need to await idle_frame which is annoying)
|
||||||
|
var bbcode_positions = find_bbcode_positions_in_string(text)
|
||||||
|
var accumulaive_length_offset = 0
|
||||||
|
for position in bbcode_positions:
|
||||||
|
# Ignore our own markers
|
||||||
|
if position.code in ["wait", "speed", "/speed", "do", "do!", "set", "next", "if", "else", "/if"]:
|
||||||
|
continue
|
||||||
|
|
||||||
|
bbcodes.append({
|
||||||
|
bbcode = position.bbcode,
|
||||||
|
start = position.start,
|
||||||
|
offset_start = position.start - accumulaive_length_offset
|
||||||
|
})
|
||||||
|
accumulaive_length_offset += position.bbcode.length()
|
||||||
|
|
||||||
|
for bb in bbcodes:
|
||||||
|
text = text.substr(0, bb.offset_start) + text.substr(bb.offset_start + bb.bbcode.length())
|
||||||
|
|
||||||
|
# Now find any dialogue markers
|
||||||
|
var next_bbcode_position = find_bbcode_positions_in_string(text, false)
|
||||||
|
var limit = 0
|
||||||
|
while next_bbcode_position.size() > 0 and limit < 1000:
|
||||||
|
limit += 1
|
||||||
|
|
||||||
|
var bbcode = next_bbcode_position[0]
|
||||||
|
|
||||||
|
var index = bbcode.start
|
||||||
|
var code = bbcode.code
|
||||||
|
var raw_args = bbcode.raw_args
|
||||||
|
var args = {}
|
||||||
|
if code in ["do", "do!", "set"]:
|
||||||
|
var compilation: DMCompilation = DMCompilation.new()
|
||||||
|
args["value"] = compilation.extract_mutation("%s %s" % [code, raw_args])
|
||||||
|
else:
|
||||||
|
# Could be something like:
|
||||||
|
# "=1.0"
|
||||||
|
# " rate=20 level=10"
|
||||||
|
if raw_args and raw_args[0] == "=":
|
||||||
|
raw_args = "value" + raw_args
|
||||||
|
for pair in raw_args.strip_edges().split(" "):
|
||||||
|
if "=" in pair:
|
||||||
|
var bits = pair.split("=")
|
||||||
|
args[bits[0]] = bits[1]
|
||||||
|
|
||||||
|
match code:
|
||||||
|
"wait":
|
||||||
|
if pauses.has(index):
|
||||||
|
pauses[index] += args.get("value").to_float()
|
||||||
|
else:
|
||||||
|
pauses[index] = args.get("value").to_float()
|
||||||
|
"speed":
|
||||||
|
speeds[index] = args.get("value").to_float()
|
||||||
|
"/speed":
|
||||||
|
speeds[index] = 1.0
|
||||||
|
"do", "do!", "set":
|
||||||
|
mutations.append([index, args.get("value")])
|
||||||
|
"next":
|
||||||
|
time = args.get("value") if args.has("value") else "0"
|
||||||
|
|
||||||
|
# Find any BB codes that are after this index and remove the length from their start
|
||||||
|
var length = bbcode.bbcode.length()
|
||||||
|
for bb in bbcodes:
|
||||||
|
if bb.offset_start > bbcode.start:
|
||||||
|
bb.offset_start -= length
|
||||||
|
bb.start -= length
|
||||||
|
|
||||||
|
# Find any escaped brackets after this that need moving
|
||||||
|
for i in range(0, escaped_open_brackets.size()):
|
||||||
|
if escaped_open_brackets[i] > bbcode.start:
|
||||||
|
escaped_open_brackets[i] -= length
|
||||||
|
for i in range(0, escaped_close_brackets.size()):
|
||||||
|
if escaped_close_brackets[i] > bbcode.start:
|
||||||
|
escaped_close_brackets[i] -= length
|
||||||
|
|
||||||
|
text = text.substr(0, index) + text.substr(index + length)
|
||||||
|
next_bbcode_position = find_bbcode_positions_in_string(text, false)
|
||||||
|
|
||||||
|
# Put the BB Codes back in
|
||||||
|
for bb in bbcodes:
|
||||||
|
text = text.insert(bb.start, bb.bbcode)
|
||||||
|
|
||||||
|
# Put the escaped brackets back in
|
||||||
|
for index in escaped_open_brackets:
|
||||||
|
text = text.left(index) + "[" + text.right(text.length() - index - 1)
|
||||||
|
for index in escaped_close_brackets:
|
||||||
|
text = text.left(index) + "]" + text.right(text.length() - index - 1)
|
||||||
|
|
||||||
|
|
||||||
|
func find_bbcode_positions_in_string(string: String, find_all: bool = true, include_conditions: bool = false) -> Array[Dictionary]:
|
||||||
|
if not "[" in string: return []
|
||||||
|
|
||||||
|
var positions: Array[Dictionary] = []
|
||||||
|
|
||||||
|
var open_brace_count: int = 0
|
||||||
|
var start: int = 0
|
||||||
|
var bbcode: String = ""
|
||||||
|
var code: String = ""
|
||||||
|
var is_finished_code: bool = false
|
||||||
|
for i in range(0, string.length()):
|
||||||
|
if string[i] == "[":
|
||||||
|
if open_brace_count == 0:
|
||||||
|
start = i
|
||||||
|
bbcode = ""
|
||||||
|
code = ""
|
||||||
|
is_finished_code = false
|
||||||
|
open_brace_count += 1
|
||||||
|
|
||||||
|
else:
|
||||||
|
if not is_finished_code and (string[i].to_upper() != string[i] or string[i] == "/" or string[i] == "!"):
|
||||||
|
code += string[i]
|
||||||
|
else:
|
||||||
|
is_finished_code = true
|
||||||
|
|
||||||
|
if open_brace_count > 0:
|
||||||
|
bbcode += string[i]
|
||||||
|
|
||||||
|
if string[i] == "]":
|
||||||
|
open_brace_count -= 1
|
||||||
|
if open_brace_count == 0 and (include_conditions or not code in ["if", "else", "/if"]):
|
||||||
|
positions.append({
|
||||||
|
bbcode = bbcode,
|
||||||
|
code = code,
|
||||||
|
start = start,
|
||||||
|
end = i,
|
||||||
|
raw_args = bbcode.substr(code.length() + 1, bbcode.length() - code.length() - 2).strip_edges()
|
||||||
|
})
|
||||||
|
|
||||||
|
if not find_all:
|
||||||
|
return positions
|
||||||
|
|
||||||
|
return positions
|
@@ -0,0 +1 @@
|
|||||||
|
uid://0k6q8kukq0qa
|
26
addons/dialogue_manager/compiler/resolved_tag_data.gd
vendored
Normal file
26
addons/dialogue_manager/compiler/resolved_tag_data.gd
vendored
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
## Tag data associated with a line of dialogue.
|
||||||
|
class_name DMResolvedTagData extends RefCounted
|
||||||
|
|
||||||
|
|
||||||
|
## The list of tags.
|
||||||
|
var tags: PackedStringArray = []
|
||||||
|
## The line with any tag syntax removed.
|
||||||
|
var text_without_tags: String = ""
|
||||||
|
|
||||||
|
# An instance of the compiler [RegEx].
|
||||||
|
var regex: DMCompilerRegEx = DMCompilerRegEx.new()
|
||||||
|
|
||||||
|
|
||||||
|
func _init(text: String) -> void:
|
||||||
|
var resolved_tags: PackedStringArray = []
|
||||||
|
var tag_matches: Array[RegExMatch] = regex.TAGS_REGEX.search_all(text)
|
||||||
|
for tag_match in tag_matches:
|
||||||
|
text = text.replace(tag_match.get_string(), "")
|
||||||
|
var tags = tag_match.get_string().replace("[#", "").replace("]", "").replace(", ", ",").split(",")
|
||||||
|
for tag in tags:
|
||||||
|
tag = tag.replace("#", "")
|
||||||
|
if not tag in resolved_tags:
|
||||||
|
resolved_tags.append(tag)
|
||||||
|
|
||||||
|
tags = resolved_tags
|
||||||
|
text_without_tags = text
|
@@ -0,0 +1 @@
|
|||||||
|
uid://cqai3ikuilqfq
|
46
addons/dialogue_manager/compiler/tree_line.gd
vendored
Normal file
46
addons/dialogue_manager/compiler/tree_line.gd
vendored
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
## An intermediate representation of a dialogue line before it gets compiled.
|
||||||
|
class_name DMTreeLine extends RefCounted
|
||||||
|
|
||||||
|
|
||||||
|
## The line number where this dialogue was found (after imported files have had their content imported).
|
||||||
|
var line_number: int = 0
|
||||||
|
## The parent [DMTreeLine] of this line.
|
||||||
|
## This is stored as a Weak Reference so that this RefCounted can elegantly free itself.
|
||||||
|
## Without it being a Weak Reference, this can easily cause a cyclical reference that keeps this resource alive.
|
||||||
|
var parent: WeakRef
|
||||||
|
## The ID of this line.
|
||||||
|
var id: String
|
||||||
|
## The type of this line (as a [String] defined in [DMConstants].
|
||||||
|
var type: String = ""
|
||||||
|
## Is this line part of a randomised group?
|
||||||
|
var is_random: bool = false
|
||||||
|
## The indent count for this line.
|
||||||
|
var indent: int = 0
|
||||||
|
## The text of this line.
|
||||||
|
var text: String = ""
|
||||||
|
## The child [DMTreeLine]s of this line.
|
||||||
|
var children: Array[DMTreeLine] = []
|
||||||
|
## Any doc comments attached to this line.
|
||||||
|
var notes: String = ""
|
||||||
|
## Is this a dialogue line that is the child of another dialogue line?
|
||||||
|
var is_nested_dialogue: bool = false
|
||||||
|
|
||||||
|
|
||||||
|
func _init(initial_id: String) -> void:
|
||||||
|
id = initial_id
|
||||||
|
|
||||||
|
|
||||||
|
func _to_string() -> String:
|
||||||
|
var tabs = []
|
||||||
|
tabs.resize(indent)
|
||||||
|
tabs.fill("\t")
|
||||||
|
tabs = "".join(tabs)
|
||||||
|
|
||||||
|
return tabs.join([tabs + "{\n",
|
||||||
|
"\tid: %s\n" % [id],
|
||||||
|
"\ttype: %s\n" % [type],
|
||||||
|
"\tis_random: %s\n" % ["true" if is_random else "false"],
|
||||||
|
"\ttext: %s\n" % [text],
|
||||||
|
"\tnotes: %s\n" % [notes],
|
||||||
|
"\tchildren: []\n" if children.size() == 0 else "\tchildren: [\n" + ",\n".join(children.map(func(child): return str(child))) + "]\n",
|
||||||
|
"}"])
|
1
addons/dialogue_manager/compiler/tree_line.gd.uid
Normal file
1
addons/dialogue_manager/compiler/tree_line.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://dsu4i84dpif14
|
610
addons/dialogue_manager/components/code_edit.gd
vendored
Normal file
610
addons/dialogue_manager/components/code_edit.gd
vendored
Normal file
@@ -0,0 +1,610 @@
|
|||||||
|
@tool
|
||||||
|
class_name DMCodeEdit extends CodeEdit
|
||||||
|
|
||||||
|
|
||||||
|
signal active_title_change(title: String)
|
||||||
|
signal error_clicked(line_number: int)
|
||||||
|
signal external_file_requested(path: String, title: String)
|
||||||
|
|
||||||
|
|
||||||
|
# A link back to the owner `MainView`
|
||||||
|
var main_view
|
||||||
|
|
||||||
|
# Theme overrides for syntax highlighting, etc
|
||||||
|
var theme_overrides: Dictionary:
|
||||||
|
set(value):
|
||||||
|
theme_overrides = value
|
||||||
|
|
||||||
|
syntax_highlighter = DMSyntaxHighlighter.new()
|
||||||
|
|
||||||
|
# General UI
|
||||||
|
add_theme_color_override("font_color", theme_overrides.text_color)
|
||||||
|
add_theme_color_override("background_color", theme_overrides.background_color)
|
||||||
|
add_theme_color_override("current_line_color", theme_overrides.current_line_color)
|
||||||
|
add_theme_font_override("font", get_theme_font("source", "EditorFonts"))
|
||||||
|
add_theme_font_size_override("font_size", theme_overrides.font_size * theme_overrides.scale)
|
||||||
|
font_size = round(theme_overrides.font_size)
|
||||||
|
get:
|
||||||
|
return theme_overrides
|
||||||
|
|
||||||
|
# Any parse errors
|
||||||
|
var errors: Array:
|
||||||
|
set(next_errors):
|
||||||
|
errors = next_errors
|
||||||
|
for i in range(0, get_line_count()):
|
||||||
|
var is_error: bool = false
|
||||||
|
for error in errors:
|
||||||
|
if error.line_number == i:
|
||||||
|
is_error = true
|
||||||
|
mark_line_as_error(i, is_error)
|
||||||
|
_on_code_edit_caret_changed()
|
||||||
|
get:
|
||||||
|
return errors
|
||||||
|
|
||||||
|
# The last selection (if there was one) so we can remember it for refocusing
|
||||||
|
var last_selected_text: String
|
||||||
|
|
||||||
|
var font_size: int:
|
||||||
|
set(value):
|
||||||
|
font_size = value
|
||||||
|
add_theme_font_size_override("font_size", font_size * theme_overrides.scale)
|
||||||
|
get:
|
||||||
|
return font_size
|
||||||
|
|
||||||
|
var WEIGHTED_RANDOM_PREFIX: RegEx = RegEx.create_from_string("^\\%[\\d.]+\\s")
|
||||||
|
|
||||||
|
var compiler_regex: DMCompilerRegEx = DMCompilerRegEx.new()
|
||||||
|
var _autoloads: Dictionary[String, String] = {}
|
||||||
|
var _autoload_member_cache: Dictionary[String, Dictionary] = {}
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
# Add error gutter
|
||||||
|
add_gutter(0)
|
||||||
|
set_gutter_type(0, TextEdit.GUTTER_TYPE_ICON)
|
||||||
|
|
||||||
|
# Add comment delimiter
|
||||||
|
if not has_comment_delimiter("#"):
|
||||||
|
add_comment_delimiter("#", "", true)
|
||||||
|
|
||||||
|
syntax_highlighter = DMSyntaxHighlighter.new()
|
||||||
|
|
||||||
|
# Keep track of any autoloads
|
||||||
|
ProjectSettings.settings_changed.connect(_on_project_settings_changed)
|
||||||
|
_on_project_settings_changed()
|
||||||
|
|
||||||
|
|
||||||
|
func _gui_input(event: InputEvent) -> void:
|
||||||
|
# Handle shortcuts that come from the editor
|
||||||
|
if event is InputEventKey and event.is_pressed():
|
||||||
|
var shortcut: String = Engine.get_meta("DialogueManagerPlugin").get_editor_shortcut(event)
|
||||||
|
match shortcut:
|
||||||
|
"toggle_comment":
|
||||||
|
toggle_comment()
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
"delete_line":
|
||||||
|
delete_current_line()
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
"move_up":
|
||||||
|
move_line(-1)
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
"move_down":
|
||||||
|
move_line(1)
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
"text_size_increase":
|
||||||
|
self.font_size += 1
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
"text_size_decrease":
|
||||||
|
self.font_size -= 1
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
"text_size_reset":
|
||||||
|
self.font_size = theme_overrides.font_size
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
|
||||||
|
elif event is InputEventMouse:
|
||||||
|
match event.as_text():
|
||||||
|
"Ctrl+Mouse Wheel Up", "Command+Mouse Wheel Up":
|
||||||
|
self.font_size += 1
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
"Ctrl+Mouse Wheel Down", "Command+Mouse Wheel Down":
|
||||||
|
self.font_size -= 1
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
|
||||||
|
|
||||||
|
func _can_drop_data(at_position: Vector2, data) -> bool:
|
||||||
|
if typeof(data) != TYPE_DICTIONARY: return false
|
||||||
|
if data.type != "files": return false
|
||||||
|
|
||||||
|
var files: PackedStringArray = Array(data.files)
|
||||||
|
return files.size() > 0
|
||||||
|
|
||||||
|
|
||||||
|
func _drop_data(at_position: Vector2, data) -> void:
|
||||||
|
var replace_regex: RegEx = RegEx.create_from_string("[^a-zA-Z_0-9]+")
|
||||||
|
|
||||||
|
var files: PackedStringArray = Array(data.files)
|
||||||
|
for file in files:
|
||||||
|
# Don't import the file into itself
|
||||||
|
if file == main_view.current_file_path: continue
|
||||||
|
|
||||||
|
if file.get_extension() == "dialogue":
|
||||||
|
var path = file.replace("res://", "").replace(".dialogue", "")
|
||||||
|
# Find the first non-import line in the file to add our import
|
||||||
|
var lines = text.split("\n")
|
||||||
|
for i in range(0, lines.size()):
|
||||||
|
if not lines[i].begins_with("import "):
|
||||||
|
insert_line_at(i, "import \"%s\" as %s\n" % [file, replace_regex.sub(path, "_", true)])
|
||||||
|
set_caret_line(i)
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
var cursor: Vector2 = get_line_column_at_pos(at_position)
|
||||||
|
if cursor.x > -1 and cursor.y > -1:
|
||||||
|
set_cursor(cursor)
|
||||||
|
remove_secondary_carets()
|
||||||
|
insert_text("\"%s\"" % file, cursor.y, cursor.x)
|
||||||
|
grab_focus()
|
||||||
|
|
||||||
|
|
||||||
|
func _request_code_completion(force: bool) -> void:
|
||||||
|
var cursor: Vector2 = get_cursor()
|
||||||
|
var current_line: String = get_line(cursor.y)
|
||||||
|
|
||||||
|
# Match jumps
|
||||||
|
if ("=> " in current_line or "=>< " in current_line) and (cursor.x > current_line.find("=>")):
|
||||||
|
var prompt: String = current_line.split("=>")[1]
|
||||||
|
if prompt.begins_with("< "):
|
||||||
|
prompt = prompt.substr(2)
|
||||||
|
else:
|
||||||
|
prompt = prompt.substr(1)
|
||||||
|
|
||||||
|
if "=> " in current_line:
|
||||||
|
if matches_prompt(prompt, "end"):
|
||||||
|
add_code_completion_option(CodeEdit.KIND_CLASS, "END", "END".substr(prompt.length()), theme_overrides.text_color, get_theme_icon("Stop", "EditorIcons"))
|
||||||
|
if matches_prompt(prompt, "end!"):
|
||||||
|
add_code_completion_option(CodeEdit.KIND_CLASS, "END!", "END!".substr(prompt.length()), theme_overrides.text_color, get_theme_icon("Stop", "EditorIcons"))
|
||||||
|
|
||||||
|
# Get all titles, including those in imports
|
||||||
|
for title: String in DMCompiler.get_titles_in_text(text, main_view.current_file_path):
|
||||||
|
# Ignore any imported titles that aren't resolved to human readable.
|
||||||
|
if title.to_int() > 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
elif "/" in title:
|
||||||
|
var bits = title.split("/")
|
||||||
|
if matches_prompt(prompt, bits[0]) or matches_prompt(prompt, bits[1]):
|
||||||
|
add_code_completion_option(CodeEdit.KIND_CLASS, title, title.substr(prompt.length()), theme_overrides.text_color, get_theme_icon("CombineLines", "EditorIcons"))
|
||||||
|
elif matches_prompt(prompt, title):
|
||||||
|
add_code_completion_option(CodeEdit.KIND_CLASS, title, title.substr(prompt.length()), theme_overrides.text_color, get_theme_icon("ArrowRight", "EditorIcons"))
|
||||||
|
|
||||||
|
# Match character names
|
||||||
|
var name_so_far: String = WEIGHTED_RANDOM_PREFIX.sub(current_line.strip_edges(), "")
|
||||||
|
if name_so_far != "" and name_so_far[0].to_upper() == name_so_far[0]:
|
||||||
|
# Only show names starting with that character
|
||||||
|
var names: PackedStringArray = get_character_names(name_so_far)
|
||||||
|
if names.size() > 0:
|
||||||
|
for name in names:
|
||||||
|
add_code_completion_option(CodeEdit.KIND_CLASS, name + ": ", name.substr(name_so_far.length()) + ": ", theme_overrides.text_color, get_theme_icon("Sprite2D", "EditorIcons"))
|
||||||
|
|
||||||
|
# Match autoloads on mutation lines
|
||||||
|
for prefix in ["do ", "do! ", "set ", "if ", "elif ", "else if ", "match ", "when ", "using "]:
|
||||||
|
if (current_line.strip_edges().begins_with(prefix) and (cursor.x > current_line.find(prefix))):
|
||||||
|
var expression: String = current_line.substr(0, cursor.x).strip_edges().substr(3)
|
||||||
|
# Find the last couple of tokens
|
||||||
|
var possible_prompt: String = expression.reverse()
|
||||||
|
possible_prompt = possible_prompt.substr(0, possible_prompt.find(" "))
|
||||||
|
possible_prompt = possible_prompt.substr(0, possible_prompt.find("("))
|
||||||
|
possible_prompt = possible_prompt.reverse()
|
||||||
|
var segments: PackedStringArray = possible_prompt.split(".").slice(-2)
|
||||||
|
var auto_completes: Array[Dictionary] = []
|
||||||
|
|
||||||
|
# Autoloads and state shortcuts
|
||||||
|
if segments.size() == 1:
|
||||||
|
var prompt: String = segments[0]
|
||||||
|
for autoload in _autoloads.keys():
|
||||||
|
if matches_prompt(prompt, autoload):
|
||||||
|
auto_completes.append({
|
||||||
|
prompt = prompt,
|
||||||
|
text = autoload,
|
||||||
|
type = "script"
|
||||||
|
})
|
||||||
|
for autoload in get_state_shortcuts():
|
||||||
|
for member: Dictionary in get_members_for_autoload(autoload):
|
||||||
|
if matches_prompt(prompt, member.name):
|
||||||
|
auto_completes.append({
|
||||||
|
prompt = prompt,
|
||||||
|
text = member.name,
|
||||||
|
type = member.type
|
||||||
|
})
|
||||||
|
|
||||||
|
# Members of an autoload
|
||||||
|
elif segments[0] in _autoloads.keys() and not current_line.strip_edges().begins_with("using "):
|
||||||
|
var prompt: String = segments[1]
|
||||||
|
for member: Dictionary in get_members_for_autoload(segments[0]):
|
||||||
|
if matches_prompt(prompt, member.name):
|
||||||
|
auto_completes.append({
|
||||||
|
prompt = prompt,
|
||||||
|
text = member.name,
|
||||||
|
type = member.type
|
||||||
|
})
|
||||||
|
|
||||||
|
auto_completes.sort_custom(func(a, b): return a.text < b.text)
|
||||||
|
|
||||||
|
for auto_complete in auto_completes:
|
||||||
|
var icon: Texture2D
|
||||||
|
var text: String = auto_complete.text
|
||||||
|
match auto_complete.type:
|
||||||
|
"script":
|
||||||
|
icon = get_theme_icon("Script", "EditorIcons")
|
||||||
|
"property":
|
||||||
|
icon = get_theme_icon("MemberProperty", "EditorIcons")
|
||||||
|
"method":
|
||||||
|
icon = get_theme_icon("MemberMethod", "EditorIcons")
|
||||||
|
text += "()"
|
||||||
|
"signal":
|
||||||
|
icon = get_theme_icon("MemberSignal", "EditorIcons")
|
||||||
|
"constant":
|
||||||
|
icon = get_theme_icon("MemberConstant", "EditorIcons")
|
||||||
|
var insert: String = text.substr(auto_complete.prompt.length())
|
||||||
|
add_code_completion_option(CodeEdit.KIND_CLASS, text, insert, theme_overrides.text_color, icon)
|
||||||
|
|
||||||
|
update_code_completion_options(true)
|
||||||
|
if get_code_completion_options().size() == 0:
|
||||||
|
cancel_code_completion()
|
||||||
|
|
||||||
|
|
||||||
|
func _filter_code_completion_candidates(candidates: Array) -> Array:
|
||||||
|
# Not sure why but if this method isn't overridden then all completions are wrapped in quotes.
|
||||||
|
return candidates
|
||||||
|
|
||||||
|
|
||||||
|
func _confirm_code_completion(replace: bool) -> void:
|
||||||
|
var completion = get_code_completion_option(get_code_completion_selected_index())
|
||||||
|
begin_complex_operation()
|
||||||
|
# Delete any part of the text that we've already typed
|
||||||
|
if completion.insert_text.length() > 0:
|
||||||
|
for i in range(0, completion.display_text.length() - completion.insert_text.length()):
|
||||||
|
backspace()
|
||||||
|
# Insert the whole match
|
||||||
|
insert_text_at_caret(completion.display_text)
|
||||||
|
end_complex_operation()
|
||||||
|
|
||||||
|
if completion.display_text.ends_with("()"):
|
||||||
|
set_cursor(get_cursor() - Vector2.RIGHT)
|
||||||
|
|
||||||
|
# Close the autocomplete menu on the next tick
|
||||||
|
call_deferred("cancel_code_completion")
|
||||||
|
|
||||||
|
|
||||||
|
#region Helpers
|
||||||
|
|
||||||
|
|
||||||
|
# Get the current caret as a Vector2
|
||||||
|
func get_cursor() -> Vector2:
|
||||||
|
return Vector2(get_caret_column(), get_caret_line())
|
||||||
|
|
||||||
|
|
||||||
|
# Set the caret from a Vector2
|
||||||
|
func set_cursor(from_cursor: Vector2) -> void:
|
||||||
|
set_caret_line(from_cursor.y, false)
|
||||||
|
set_caret_column(from_cursor.x, false)
|
||||||
|
|
||||||
|
|
||||||
|
# Check if a prompt is the start of a string without actually being that string
|
||||||
|
func matches_prompt(prompt: String, matcher: String) -> bool:
|
||||||
|
return prompt.length() < matcher.length() and matcher.to_lower().begins_with(prompt.to_lower())
|
||||||
|
|
||||||
|
|
||||||
|
func get_state_shortcuts() -> PackedStringArray:
|
||||||
|
# Get any shortcuts defined in settings
|
||||||
|
var shortcuts: PackedStringArray = DMSettings.get_setting(DMSettings.STATE_AUTOLOAD_SHORTCUTS, [])
|
||||||
|
# Check for "using" clauses
|
||||||
|
for line: String in text.split("\n"):
|
||||||
|
var found: RegExMatch = compiler_regex.USING_REGEX.search(line)
|
||||||
|
if found:
|
||||||
|
shortcuts.append(found.strings[found.names.state])
|
||||||
|
# Check for any other script sources
|
||||||
|
for extra_script_source in DMSettings.get_setting(DMSettings.EXTRA_AUTO_COMPLETE_SCRIPT_SOURCES, []):
|
||||||
|
shortcuts.append(extra_script_source)
|
||||||
|
|
||||||
|
return shortcuts
|
||||||
|
|
||||||
|
|
||||||
|
func get_members_for_autoload(autoload_name: String) -> Array[Dictionary]:
|
||||||
|
# Debounce method list lookups
|
||||||
|
if _autoload_member_cache.has(autoload_name) and _autoload_member_cache.get(autoload_name).get("at") > Time.get_ticks_msec() - 5000:
|
||||||
|
return _autoload_member_cache.get(autoload_name).get("members")
|
||||||
|
|
||||||
|
if not _autoloads.has(autoload_name) and not autoload_name.begins_with("res://") and not autoload_name.begins_with("uid://"): return []
|
||||||
|
|
||||||
|
var autoload = load(_autoloads.get(autoload_name, autoload_name))
|
||||||
|
var script: Script = autoload if autoload is Script else autoload.get_script()
|
||||||
|
|
||||||
|
if not is_instance_valid(script): return []
|
||||||
|
|
||||||
|
var members: Array[Dictionary] = []
|
||||||
|
if script.resource_path.ends_with(".gd"):
|
||||||
|
for m: Dictionary in script.get_script_method_list():
|
||||||
|
if not m.name.begins_with("@"):
|
||||||
|
members.append({
|
||||||
|
name = m.name,
|
||||||
|
type = "method"
|
||||||
|
})
|
||||||
|
for m: Dictionary in script.get_script_property_list():
|
||||||
|
members.append({
|
||||||
|
name = m.name,
|
||||||
|
type = "property"
|
||||||
|
})
|
||||||
|
for m: Dictionary in script.get_script_signal_list():
|
||||||
|
members.append({
|
||||||
|
name = m.name,
|
||||||
|
type = "signal"
|
||||||
|
})
|
||||||
|
for c: String in script.get_script_constant_map():
|
||||||
|
members.append({
|
||||||
|
name = c,
|
||||||
|
type = "constant"
|
||||||
|
})
|
||||||
|
elif script.resource_path.ends_with(".cs"):
|
||||||
|
var dotnet = load(Engine.get_meta("DialogueManagerPlugin").get_plugin_path() + "/DialogueManager.cs").new()
|
||||||
|
for m: Dictionary in dotnet.GetMembersForAutoload(script):
|
||||||
|
members.append(m)
|
||||||
|
|
||||||
|
_autoload_member_cache[autoload_name] = {
|
||||||
|
at = Time.get_ticks_msec(),
|
||||||
|
members = members
|
||||||
|
}
|
||||||
|
|
||||||
|
return members
|
||||||
|
|
||||||
|
|
||||||
|
## Get a list of titles from the current text
|
||||||
|
func get_titles() -> PackedStringArray:
|
||||||
|
var titles = PackedStringArray([])
|
||||||
|
var lines = text.split("\n")
|
||||||
|
for line in lines:
|
||||||
|
if line.strip_edges().begins_with("~ "):
|
||||||
|
titles.append(line.strip_edges().substr(2))
|
||||||
|
|
||||||
|
return titles
|
||||||
|
|
||||||
|
|
||||||
|
## Work out what the next title above the current line is
|
||||||
|
func check_active_title() -> void:
|
||||||
|
var line_number = get_caret_line()
|
||||||
|
var lines = text.split("\n")
|
||||||
|
# Look at each line above this one to find the next title line
|
||||||
|
for i in range(line_number, -1, -1):
|
||||||
|
if lines[i].begins_with("~ "):
|
||||||
|
active_title_change.emit(lines[i].replace("~ ", ""))
|
||||||
|
return
|
||||||
|
|
||||||
|
active_title_change.emit("")
|
||||||
|
|
||||||
|
|
||||||
|
# Move the caret line to match a given title
|
||||||
|
func go_to_title(title: String) -> void:
|
||||||
|
var lines = text.split("\n")
|
||||||
|
for i in range(0, lines.size()):
|
||||||
|
if lines[i].strip_edges() == "~ " + title:
|
||||||
|
set_caret_line(i)
|
||||||
|
center_viewport_to_caret()
|
||||||
|
|
||||||
|
|
||||||
|
func get_character_names(beginning_with: String) -> PackedStringArray:
|
||||||
|
var names: PackedStringArray = []
|
||||||
|
var lines = text.split("\n")
|
||||||
|
for line in lines:
|
||||||
|
if ": " in line:
|
||||||
|
var name: String = WEIGHTED_RANDOM_PREFIX.sub(line.split(": ")[0].strip_edges(), "")
|
||||||
|
if not name in names and matches_prompt(beginning_with, name):
|
||||||
|
names.append(name)
|
||||||
|
return names
|
||||||
|
|
||||||
|
|
||||||
|
# Mark a line as an error or not
|
||||||
|
func mark_line_as_error(line_number: int, is_error: bool) -> void:
|
||||||
|
# Lines display counting from 1 but are actually indexed from 0
|
||||||
|
line_number -= 1
|
||||||
|
|
||||||
|
if line_number < 0: return
|
||||||
|
|
||||||
|
if is_error:
|
||||||
|
set_line_background_color(line_number, theme_overrides.error_line_color)
|
||||||
|
set_line_gutter_icon(line_number, 0, get_theme_icon("StatusError", "EditorIcons"))
|
||||||
|
else:
|
||||||
|
set_line_background_color(line_number, theme_overrides.background_color)
|
||||||
|
set_line_gutter_icon(line_number, 0, null)
|
||||||
|
|
||||||
|
|
||||||
|
# Insert or wrap some bbcode at the caret/selection
|
||||||
|
func insert_bbcode(open_tag: String, close_tag: String = "") -> void:
|
||||||
|
if close_tag == "":
|
||||||
|
insert_text_at_caret(open_tag)
|
||||||
|
grab_focus()
|
||||||
|
else:
|
||||||
|
var selected_text = get_selected_text()
|
||||||
|
insert_text_at_caret("%s%s%s" % [open_tag, selected_text, close_tag])
|
||||||
|
grab_focus()
|
||||||
|
set_caret_column(get_caret_column() - close_tag.length())
|
||||||
|
|
||||||
|
# Insert text at current caret position
|
||||||
|
# Move Caret down 1 line if not => END
|
||||||
|
func insert_text_at_cursor(text: String) -> void:
|
||||||
|
if text != "=> END":
|
||||||
|
insert_text_at_caret(text+"\n")
|
||||||
|
set_caret_line(get_caret_line()+1)
|
||||||
|
else:
|
||||||
|
insert_text_at_caret(text)
|
||||||
|
grab_focus()
|
||||||
|
|
||||||
|
|
||||||
|
# Toggle the selected lines as comments
|
||||||
|
func toggle_comment() -> void:
|
||||||
|
begin_complex_operation()
|
||||||
|
|
||||||
|
var comment_delimiter: String = delimiter_comments[0]
|
||||||
|
var is_first_line: bool = true
|
||||||
|
var will_comment: bool = true
|
||||||
|
var selections: Array = []
|
||||||
|
var line_offsets: Dictionary = {}
|
||||||
|
|
||||||
|
for caret_index in range(0, get_caret_count()):
|
||||||
|
var from_line: int = get_caret_line(caret_index)
|
||||||
|
var from_column: int = get_caret_column(caret_index)
|
||||||
|
var to_line: int = get_caret_line(caret_index)
|
||||||
|
var to_column: int = get_caret_column(caret_index)
|
||||||
|
|
||||||
|
if has_selection(caret_index):
|
||||||
|
from_line = get_selection_from_line(caret_index)
|
||||||
|
to_line = get_selection_to_line(caret_index)
|
||||||
|
from_column = get_selection_from_column(caret_index)
|
||||||
|
to_column = get_selection_to_column(caret_index)
|
||||||
|
|
||||||
|
selections.append({
|
||||||
|
from_line = from_line,
|
||||||
|
from_column = from_column,
|
||||||
|
to_line = to_line,
|
||||||
|
to_column = to_column
|
||||||
|
})
|
||||||
|
|
||||||
|
for line_number in range(from_line, to_line + 1):
|
||||||
|
if line_offsets.has(line_number): continue
|
||||||
|
|
||||||
|
var line_text: String = get_line(line_number)
|
||||||
|
|
||||||
|
# The first line determines if we are commenting or uncommentingg
|
||||||
|
if is_first_line:
|
||||||
|
is_first_line = false
|
||||||
|
will_comment = not line_text.strip_edges().begins_with(comment_delimiter)
|
||||||
|
|
||||||
|
# Only comment/uncomment if the current line needs to
|
||||||
|
if will_comment:
|
||||||
|
set_line(line_number, comment_delimiter + line_text)
|
||||||
|
line_offsets[line_number] = 1
|
||||||
|
elif line_text.begins_with(comment_delimiter):
|
||||||
|
set_line(line_number, line_text.substr(comment_delimiter.length()))
|
||||||
|
line_offsets[line_number] = -1
|
||||||
|
else:
|
||||||
|
line_offsets[line_number] = 0
|
||||||
|
|
||||||
|
for caret_index in range(0, get_caret_count()):
|
||||||
|
var selection: Dictionary = selections[caret_index]
|
||||||
|
select(
|
||||||
|
selection.from_line,
|
||||||
|
selection.from_column + line_offsets[selection.from_line],
|
||||||
|
selection.to_line,
|
||||||
|
selection.to_column + line_offsets[selection.to_line],
|
||||||
|
caret_index
|
||||||
|
)
|
||||||
|
set_caret_column(selection.from_column + line_offsets[selection.from_line], false, caret_index)
|
||||||
|
|
||||||
|
end_complex_operation()
|
||||||
|
|
||||||
|
text_set.emit()
|
||||||
|
text_changed.emit()
|
||||||
|
|
||||||
|
|
||||||
|
# Remove the current line
|
||||||
|
func delete_current_line() -> void:
|
||||||
|
var cursor = get_cursor()
|
||||||
|
if get_line_count() == 1:
|
||||||
|
select_all()
|
||||||
|
elif cursor.y == 0:
|
||||||
|
select(0, 0, 1, 0)
|
||||||
|
else:
|
||||||
|
select(cursor.y - 1, get_line_width(cursor.y - 1), cursor.y, get_line_width(cursor.y))
|
||||||
|
delete_selection()
|
||||||
|
text_changed.emit()
|
||||||
|
|
||||||
|
|
||||||
|
# Move the selected lines up or down
|
||||||
|
func move_line(offset: int) -> void:
|
||||||
|
offset = clamp(offset, -1, 1)
|
||||||
|
|
||||||
|
var starting_scroll := scroll_vertical
|
||||||
|
var cursor = get_cursor()
|
||||||
|
var reselect: bool = false
|
||||||
|
var from: int = cursor.y
|
||||||
|
var to: int = cursor.y
|
||||||
|
if has_selection():
|
||||||
|
reselect = true
|
||||||
|
from = get_selection_from_line()
|
||||||
|
to = get_selection_to_line()
|
||||||
|
|
||||||
|
var lines := text.split("\n")
|
||||||
|
|
||||||
|
# Prevent the lines from being out of bounds
|
||||||
|
if from + offset < 0 or to + offset >= lines.size(): return
|
||||||
|
|
||||||
|
var target_from_index = from - 1 if offset == -1 else to + 1
|
||||||
|
var target_to_index = to if offset == -1 else from
|
||||||
|
var line_to_move = lines[target_from_index]
|
||||||
|
lines.remove_at(target_from_index)
|
||||||
|
lines.insert(target_to_index, line_to_move)
|
||||||
|
|
||||||
|
text = "\n".join(lines)
|
||||||
|
|
||||||
|
cursor.y += offset
|
||||||
|
set_cursor(cursor)
|
||||||
|
from += offset
|
||||||
|
to += offset
|
||||||
|
if reselect:
|
||||||
|
select(from, 0, to, get_line_width(to))
|
||||||
|
|
||||||
|
text_changed.emit()
|
||||||
|
scroll_vertical = starting_scroll + offset
|
||||||
|
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Signals
|
||||||
|
|
||||||
|
|
||||||
|
func _on_project_settings_changed() -> void:
|
||||||
|
_autoloads = {}
|
||||||
|
var project = ConfigFile.new()
|
||||||
|
project.load("res://project.godot")
|
||||||
|
for autoload in project.get_section_keys("autoload"):
|
||||||
|
if autoload != "DialogueManager":
|
||||||
|
_autoloads[autoload] = project.get_value("autoload", autoload).substr(1)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_code_edit_symbol_validate(symbol: String) -> void:
|
||||||
|
if symbol.begins_with("res://") and symbol.ends_with(".dialogue"):
|
||||||
|
set_symbol_lookup_word_as_valid(true)
|
||||||
|
return
|
||||||
|
|
||||||
|
for title in get_titles():
|
||||||
|
if symbol == title:
|
||||||
|
set_symbol_lookup_word_as_valid(true)
|
||||||
|
return
|
||||||
|
set_symbol_lookup_word_as_valid(false)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_code_edit_symbol_lookup(symbol: String, line: int, column: int) -> void:
|
||||||
|
if symbol.begins_with("res://") and symbol.ends_with(".dialogue"):
|
||||||
|
external_file_requested.emit(symbol, "")
|
||||||
|
else:
|
||||||
|
go_to_title(symbol)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_code_edit_text_changed() -> void:
|
||||||
|
request_code_completion(true)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_code_edit_text_set() -> void:
|
||||||
|
queue_redraw()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_code_edit_caret_changed() -> void:
|
||||||
|
check_active_title()
|
||||||
|
last_selected_text = get_selected_text()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_code_edit_gutter_clicked(line: int, gutter: int) -> void:
|
||||||
|
var line_errors = errors.filter(func(error): return error.line_number == line)
|
||||||
|
if line_errors.size() > 0:
|
||||||
|
error_clicked.emit(line)
|
||||||
|
|
||||||
|
|
||||||
|
#endregion
|
1
addons/dialogue_manager/components/code_edit.gd.uid
Normal file
1
addons/dialogue_manager/components/code_edit.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://djeybvlb332mp
|
56
addons/dialogue_manager/components/code_edit.tscn
Normal file
56
addons/dialogue_manager/components/code_edit.tscn
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
[gd_scene load_steps=4 format=3 uid="uid://civ6shmka5e8u"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://klpiq4tk3t7a" path="res://addons/dialogue_manager/components/code_edit_syntax_highlighter.gd" id="1_58cfo"]
|
||||||
|
[ext_resource type="Script" uid="uid://djeybvlb332mp" path="res://addons/dialogue_manager/components/code_edit.gd" id="1_g324i"]
|
||||||
|
|
||||||
|
[sub_resource type="SyntaxHighlighter" id="SyntaxHighlighter_cobxx"]
|
||||||
|
script = ExtResource("1_58cfo")
|
||||||
|
|
||||||
|
[node name="CodeEdit" type="CodeEdit"]
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
text = "~ title_thing
|
||||||
|
|
||||||
|
if this = \"that\" or 'this'
|
||||||
|
Nathan: Something
|
||||||
|
- Then [if test.thing() == 2.0] => somewhere
|
||||||
|
- Other => END!
|
||||||
|
|
||||||
|
~ somewhere
|
||||||
|
|
||||||
|
set has_something = true
|
||||||
|
=> END"
|
||||||
|
highlight_all_occurrences = true
|
||||||
|
highlight_current_line = true
|
||||||
|
draw_tabs = true
|
||||||
|
syntax_highlighter = SubResource("SyntaxHighlighter_cobxx")
|
||||||
|
scroll_past_end_of_file = true
|
||||||
|
minimap_draw = true
|
||||||
|
symbol_lookup_on_click = true
|
||||||
|
line_folding = true
|
||||||
|
gutters_draw_line_numbers = true
|
||||||
|
gutters_draw_fold_gutter = true
|
||||||
|
delimiter_strings = Array[String](["\" \""])
|
||||||
|
delimiter_comments = Array[String](["#"])
|
||||||
|
code_completion_enabled = true
|
||||||
|
code_completion_prefixes = Array[String]([">", "<"])
|
||||||
|
indent_automatic = true
|
||||||
|
auto_brace_completion_enabled = true
|
||||||
|
auto_brace_completion_highlight_matching = true
|
||||||
|
auto_brace_completion_pairs = {
|
||||||
|
"\"": "\"",
|
||||||
|
"(": ")",
|
||||||
|
"[": "]",
|
||||||
|
"{": "}"
|
||||||
|
}
|
||||||
|
script = ExtResource("1_g324i")
|
||||||
|
|
||||||
|
[connection signal="caret_changed" from="." to="." method="_on_code_edit_caret_changed"]
|
||||||
|
[connection signal="gutter_clicked" from="." to="." method="_on_code_edit_gutter_clicked"]
|
||||||
|
[connection signal="symbol_lookup" from="." to="." method="_on_code_edit_symbol_lookup"]
|
||||||
|
[connection signal="symbol_validate" from="." to="." method="_on_code_edit_symbol_validate"]
|
||||||
|
[connection signal="text_changed" from="." to="." method="_on_code_edit_text_changed"]
|
||||||
|
[connection signal="text_set" from="." to="." method="_on_code_edit_text_set"]
|
231
addons/dialogue_manager/components/code_edit_syntax_highlighter.gd
vendored
Normal file
231
addons/dialogue_manager/components/code_edit_syntax_highlighter.gd
vendored
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
@tool
|
||||||
|
class_name DMSyntaxHighlighter extends SyntaxHighlighter
|
||||||
|
|
||||||
|
|
||||||
|
var regex: DMCompilerRegEx = DMCompilerRegEx.new()
|
||||||
|
var compilation: DMCompilation = DMCompilation.new()
|
||||||
|
var expression_parser = DMExpressionParser.new()
|
||||||
|
|
||||||
|
var cache: Dictionary = {}
|
||||||
|
|
||||||
|
|
||||||
|
func _clear_highlighting_cache() -> void:
|
||||||
|
cache.clear()
|
||||||
|
|
||||||
|
|
||||||
|
func _get_line_syntax_highlighting(line: int) -> Dictionary:
|
||||||
|
expression_parser.include_comments = true
|
||||||
|
|
||||||
|
var colors: Dictionary = {}
|
||||||
|
var text_edit: TextEdit = get_text_edit()
|
||||||
|
var text: String = text_edit.get_line(line)
|
||||||
|
|
||||||
|
# Prevent an error from popping up while developing
|
||||||
|
if not is_instance_valid(text_edit) or text_edit.theme_overrides.is_empty():
|
||||||
|
return colors
|
||||||
|
|
||||||
|
# Disable this, as well as the line at the bottom of this function to remove the cache.
|
||||||
|
if text in cache:
|
||||||
|
return cache[text]
|
||||||
|
|
||||||
|
var theme: Dictionary = text_edit.theme_overrides
|
||||||
|
|
||||||
|
var index: int = 0
|
||||||
|
|
||||||
|
match DMCompiler.get_line_type(text):
|
||||||
|
DMConstants.TYPE_USING:
|
||||||
|
colors[index] = { color = theme.conditions_color }
|
||||||
|
colors[index + "using ".length()] = { color = theme.text_color }
|
||||||
|
|
||||||
|
DMConstants.TYPE_IMPORT:
|
||||||
|
colors[index] = { color = theme.conditions_color }
|
||||||
|
var import: RegExMatch = regex.IMPORT_REGEX.search(text)
|
||||||
|
if import:
|
||||||
|
colors[index + import.get_start("path") - 1] = { color = theme.strings_color }
|
||||||
|
colors[index + import.get_end("path") + 1] = { color = theme.conditions_color }
|
||||||
|
colors[index + import.get_start("prefix")] = { color = theme.text_color }
|
||||||
|
|
||||||
|
DMConstants.TYPE_COMMENT:
|
||||||
|
colors[index] = { color = theme.comments_color }
|
||||||
|
|
||||||
|
DMConstants.TYPE_TITLE:
|
||||||
|
colors[index] = { color = theme.titles_color }
|
||||||
|
|
||||||
|
DMConstants.TYPE_CONDITION, DMConstants.TYPE_WHILE, DMConstants.TYPE_MATCH, DMConstants.TYPE_WHEN:
|
||||||
|
colors[0] = { color = theme.conditions_color }
|
||||||
|
index = text.find(" ")
|
||||||
|
if index > -1:
|
||||||
|
var expression: Array = expression_parser.tokenise(text.substr(index), DMConstants.TYPE_CONDITION, 0)
|
||||||
|
if expression.size() == 0:
|
||||||
|
colors[index] = { color = theme.critical_color }
|
||||||
|
else:
|
||||||
|
_highlight_expression(expression, colors, index)
|
||||||
|
|
||||||
|
DMConstants.TYPE_MUTATION:
|
||||||
|
colors[0] = { color = theme.mutations_color }
|
||||||
|
index = text.find(" ")
|
||||||
|
var expression: Array = expression_parser.tokenise(text.substr(index), DMConstants.TYPE_MUTATION, 0)
|
||||||
|
if expression.size() == 0:
|
||||||
|
colors[index] = { color = theme.critical_color }
|
||||||
|
else:
|
||||||
|
_highlight_expression(expression, colors, index)
|
||||||
|
|
||||||
|
DMConstants.TYPE_GOTO:
|
||||||
|
if text.strip_edges().begins_with("%"):
|
||||||
|
colors[index] = { color = theme.symbols_color }
|
||||||
|
index = text.find(" ")
|
||||||
|
_highlight_goto(text, colors, index)
|
||||||
|
|
||||||
|
DMConstants.TYPE_RANDOM:
|
||||||
|
colors[index] = { color = theme.symbols_color }
|
||||||
|
|
||||||
|
DMConstants.TYPE_DIALOGUE, DMConstants.TYPE_RESPONSE:
|
||||||
|
if text.strip_edges().begins_with("%"):
|
||||||
|
colors[index] = { color = theme.symbols_color }
|
||||||
|
index = text.find(" ", text.find("%"))
|
||||||
|
colors[index] = { color = theme.text_color.lerp(theme.symbols_color, 0.5) }
|
||||||
|
|
||||||
|
var dialogue_text: String = text.substr(index, text.find("=>"))
|
||||||
|
|
||||||
|
# Highlight character name (but ignore ":" within line ID reference)
|
||||||
|
var split_index: int = dialogue_text.replace("\\:", "??").find(":")
|
||||||
|
if text.substr(split_index - 3, 3) != "[ID":
|
||||||
|
colors[index + split_index + 1] = { color = theme.text_color }
|
||||||
|
else:
|
||||||
|
# If there's no character name then just highlight the text as dialogue.
|
||||||
|
colors[index] = { color = theme.text_color }
|
||||||
|
|
||||||
|
# Interpolation
|
||||||
|
var replacements: Array[RegExMatch] = regex.REPLACEMENTS_REGEX.search_all(dialogue_text)
|
||||||
|
for replacement: RegExMatch in replacements:
|
||||||
|
var expression_text: String = replacement.get_string().substr(0, replacement.get_string().length() - 2).substr(2)
|
||||||
|
var expression: Array = expression_parser.tokenise(expression_text, DMConstants.TYPE_MUTATION, replacement.get_start())
|
||||||
|
var expression_index: int = index + replacement.get_start()
|
||||||
|
colors[expression_index] = { color = theme.symbols_color }
|
||||||
|
if expression.size() == 0 or expression[0].type == DMConstants.TYPE_ERROR:
|
||||||
|
colors[expression_index] = { color = theme.critical_color }
|
||||||
|
else:
|
||||||
|
_highlight_expression(expression, colors, index + 2)
|
||||||
|
colors[expression_index + expression_text.length() + 2] = { color = theme.symbols_color }
|
||||||
|
colors[expression_index + expression_text.length() + 4] = { color = theme.text_color }
|
||||||
|
# Tags (and inline mutations)
|
||||||
|
var resolved_line_data: DMResolvedLineData = DMResolvedLineData.new("")
|
||||||
|
var bbcodes: Array[Dictionary] = resolved_line_data.find_bbcode_positions_in_string(dialogue_text, true, true)
|
||||||
|
for bbcode: Dictionary in bbcodes:
|
||||||
|
var tag: String = bbcode.code
|
||||||
|
var code: String = bbcode.raw_args
|
||||||
|
if code.begins_with("["):
|
||||||
|
colors[index + bbcode.start] = { color = theme.symbols_color }
|
||||||
|
colors[index + bbcode.start + 2] = { color = theme.text_color }
|
||||||
|
var pipe_cursor: int = code.find("|")
|
||||||
|
while pipe_cursor > -1:
|
||||||
|
colors[index + bbcode.start + pipe_cursor + 1] = { color = theme.symbols_color }
|
||||||
|
colors[index + bbcode.start + pipe_cursor + 2] = { color = theme.text_color }
|
||||||
|
pipe_cursor = code.find("|", pipe_cursor + 1)
|
||||||
|
colors[index + bbcode.end - 1] = { color = theme.symbols_color }
|
||||||
|
colors[index + bbcode.end + 1] = { color = theme.text_color }
|
||||||
|
else:
|
||||||
|
colors[index + bbcode.start] = { color = theme.symbols_color }
|
||||||
|
if tag.begins_with("do") or tag.begins_with("set") or tag.begins_with("if"):
|
||||||
|
if tag.begins_with("if"):
|
||||||
|
colors[index + bbcode.start + 1] = { color = theme.conditions_color }
|
||||||
|
else:
|
||||||
|
colors[index + bbcode.start + 1] = { color = theme.mutations_color }
|
||||||
|
var expression: Array = expression_parser.tokenise(code, DMConstants.TYPE_MUTATION, bbcode.start + bbcode.code.length())
|
||||||
|
if expression.size() == 0 or expression[0].type == DMConstants.TYPE_ERROR:
|
||||||
|
colors[index + bbcode.start + tag.length() + 1] = { color = theme.critical_color }
|
||||||
|
else:
|
||||||
|
_highlight_expression(expression, colors, index + 2)
|
||||||
|
# else and closing if have no expression
|
||||||
|
elif tag.begins_with("else") or tag.begins_with("/if"):
|
||||||
|
colors[index + bbcode.start + 1] = { color = theme.conditions_color }
|
||||||
|
colors[index + bbcode.end] = { color = theme.symbols_color }
|
||||||
|
colors[index + bbcode.end + 1] = { color = theme.text_color }
|
||||||
|
# Jumps
|
||||||
|
if "=> " in text or "=>< " in text:
|
||||||
|
_highlight_goto(text, colors, index)
|
||||||
|
|
||||||
|
# Order the dictionary keys to prevent CodeEdit from having issues
|
||||||
|
var ordered_colors: Dictionary = {}
|
||||||
|
var ordered_keys: Array = colors.keys()
|
||||||
|
ordered_keys.sort()
|
||||||
|
for key_index: int in ordered_keys:
|
||||||
|
ordered_colors[key_index] = colors[key_index]
|
||||||
|
|
||||||
|
cache[text] = ordered_colors
|
||||||
|
return ordered_colors
|
||||||
|
|
||||||
|
|
||||||
|
func _highlight_expression(tokens: Array, colors: Dictionary, index: int) -> int:
|
||||||
|
var theme: Dictionary = get_text_edit().theme_overrides
|
||||||
|
var last_index: int = index
|
||||||
|
for token: Dictionary in tokens:
|
||||||
|
last_index = token.i
|
||||||
|
match token.type:
|
||||||
|
DMConstants.TOKEN_COMMENT:
|
||||||
|
colors[index + token.i] = { color = theme.comments_color }
|
||||||
|
|
||||||
|
DMConstants.TOKEN_CONDITION, DMConstants.TOKEN_AND_OR:
|
||||||
|
colors[index + token.i] = { color = theme.conditions_color }
|
||||||
|
|
||||||
|
DMConstants.TOKEN_VARIABLE:
|
||||||
|
if token.value in ["true", "false"]:
|
||||||
|
colors[index + token.i] = { color = theme.conditions_color }
|
||||||
|
else:
|
||||||
|
colors[index + token.i] = { color = theme.members_color }
|
||||||
|
|
||||||
|
DMConstants.TOKEN_OPERATOR, DMConstants.TOKEN_COLON, \
|
||||||
|
DMConstants.TOKEN_COMMA, DMConstants.TOKEN_DOT, DMConstants.TOKEN_NULL_COALESCE, \
|
||||||
|
DMConstants.TOKEN_NUMBER, DMConstants.TOKEN_ASSIGNMENT:
|
||||||
|
colors[index + token.i] = { color = theme.symbols_color }
|
||||||
|
|
||||||
|
DMConstants.TOKEN_STRING:
|
||||||
|
colors[index + token.i] = { color = theme.strings_color }
|
||||||
|
|
||||||
|
DMConstants.TOKEN_FUNCTION:
|
||||||
|
colors[index + token.i] = { color = theme.mutations_color }
|
||||||
|
colors[index + token.i + token.function.length()] = { color = theme.symbols_color }
|
||||||
|
for parameter: Array in token.value:
|
||||||
|
last_index = _highlight_expression(parameter, colors, index)
|
||||||
|
DMConstants.TOKEN_PARENS_CLOSE:
|
||||||
|
colors[index + token.i] = { color = theme.symbols_color }
|
||||||
|
|
||||||
|
DMConstants.TOKEN_DICTIONARY_REFERENCE:
|
||||||
|
colors[index + token.i] = { color = theme.members_color }
|
||||||
|
colors[index + token.i + token.variable.length()] = { color = theme.symbols_color }
|
||||||
|
last_index = _highlight_expression(token.value, colors, index)
|
||||||
|
DMConstants.TOKEN_ARRAY:
|
||||||
|
colors[index + token.i] = { color = theme.symbols_color }
|
||||||
|
for item: Array in token.value:
|
||||||
|
last_index = _highlight_expression(item, colors, index)
|
||||||
|
DMConstants.TOKEN_BRACKET_CLOSE:
|
||||||
|
colors[index + token.i] = { color = theme.symbols_color }
|
||||||
|
|
||||||
|
DMConstants.TOKEN_DICTIONARY:
|
||||||
|
colors[index + token.i] = { color = theme.symbols_color }
|
||||||
|
last_index = _highlight_expression(token.value.keys() + token.value.values(), colors, index)
|
||||||
|
DMConstants.TOKEN_BRACE_CLOSE:
|
||||||
|
colors[index + token.i] = { color = theme.symbols_color }
|
||||||
|
last_index += 1
|
||||||
|
|
||||||
|
DMConstants.TOKEN_GROUP:
|
||||||
|
last_index = _highlight_expression(token.value, colors, index)
|
||||||
|
|
||||||
|
return last_index
|
||||||
|
|
||||||
|
|
||||||
|
func _highlight_goto(text: String, colors: Dictionary, index: int) -> int:
|
||||||
|
var theme: Dictionary = get_text_edit().theme_overrides
|
||||||
|
var goto_data: DMResolvedGotoData = DMResolvedGotoData.new(text, {})
|
||||||
|
colors[goto_data.index] = { color = theme.jumps_color }
|
||||||
|
if "{{" in text:
|
||||||
|
index = text.find("{{", goto_data.index)
|
||||||
|
var last_index: int = 0
|
||||||
|
if goto_data.error:
|
||||||
|
colors[index + 2] = { color = theme.critical_color }
|
||||||
|
else:
|
||||||
|
last_index = _highlight_expression(goto_data.expression, colors, index)
|
||||||
|
index = text.find("}}", index + last_index)
|
||||||
|
colors[index] = { color = theme.jumps_color }
|
||||||
|
|
||||||
|
return index
|
@@ -0,0 +1 @@
|
|||||||
|
uid://klpiq4tk3t7a
|
84
addons/dialogue_manager/components/download_update_panel.gd
vendored
Normal file
84
addons/dialogue_manager/components/download_update_panel.gd
vendored
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
@tool
|
||||||
|
extends Control
|
||||||
|
|
||||||
|
|
||||||
|
signal failed()
|
||||||
|
signal updated(updated_to_version: String)
|
||||||
|
|
||||||
|
|
||||||
|
const DialogueConstants = preload("../constants.gd")
|
||||||
|
|
||||||
|
const TEMP_FILE_NAME = "user://temp.zip"
|
||||||
|
|
||||||
|
|
||||||
|
@onready var logo: TextureRect = %Logo
|
||||||
|
@onready var label: Label = $VBox/Label
|
||||||
|
@onready var http_request: HTTPRequest = $HTTPRequest
|
||||||
|
@onready var download_button: Button = %DownloadButton
|
||||||
|
|
||||||
|
var next_version_release: Dictionary:
|
||||||
|
set(value):
|
||||||
|
next_version_release = value
|
||||||
|
label.text = DialogueConstants.translate(&"update.is_available_for_download") % value.tag_name.substr(1)
|
||||||
|
get:
|
||||||
|
return next_version_release
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
$VBox/Center/DownloadButton.text = DialogueConstants.translate(&"update.download_update")
|
||||||
|
$VBox/Center2/NotesButton.text = DialogueConstants.translate(&"update.release_notes")
|
||||||
|
|
||||||
|
|
||||||
|
### Signals
|
||||||
|
|
||||||
|
|
||||||
|
func _on_download_button_pressed() -> void:
|
||||||
|
# Safeguard the actual dialogue manager repo from accidentally updating itself
|
||||||
|
if FileAccess.file_exists("res://tests/test_basic_dialogue.gd"):
|
||||||
|
prints("You can't update the addon from within itself.")
|
||||||
|
failed.emit()
|
||||||
|
return
|
||||||
|
|
||||||
|
http_request.request(next_version_release.zipball_url)
|
||||||
|
download_button.disabled = true
|
||||||
|
download_button.text = DialogueConstants.translate(&"update.downloading")
|
||||||
|
|
||||||
|
|
||||||
|
func _on_http_request_request_completed(result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray) -> void:
|
||||||
|
if result != HTTPRequest.RESULT_SUCCESS:
|
||||||
|
failed.emit()
|
||||||
|
return
|
||||||
|
|
||||||
|
# Save the downloaded zip
|
||||||
|
var zip_file: FileAccess = FileAccess.open(TEMP_FILE_NAME, FileAccess.WRITE)
|
||||||
|
zip_file.store_buffer(body)
|
||||||
|
zip_file.close()
|
||||||
|
|
||||||
|
OS.move_to_trash(ProjectSettings.globalize_path("res://addons/dialogue_manager"))
|
||||||
|
|
||||||
|
var zip_reader: ZIPReader = ZIPReader.new()
|
||||||
|
zip_reader.open(TEMP_FILE_NAME)
|
||||||
|
var files: PackedStringArray = zip_reader.get_files()
|
||||||
|
|
||||||
|
var base_path = files[1]
|
||||||
|
# Remove archive folder
|
||||||
|
files.remove_at(0)
|
||||||
|
# Remove assets folder
|
||||||
|
files.remove_at(0)
|
||||||
|
|
||||||
|
for path in files:
|
||||||
|
var new_file_path: String = path.replace(base_path, "")
|
||||||
|
if path.ends_with("/"):
|
||||||
|
DirAccess.make_dir_recursive_absolute("res://addons/%s" % new_file_path)
|
||||||
|
else:
|
||||||
|
var file: FileAccess = FileAccess.open("res://addons/%s" % new_file_path, FileAccess.WRITE)
|
||||||
|
file.store_buffer(zip_reader.read_file(path))
|
||||||
|
|
||||||
|
zip_reader.close()
|
||||||
|
DirAccess.remove_absolute(TEMP_FILE_NAME)
|
||||||
|
|
||||||
|
updated.emit(next_version_release.tag_name.substr(1))
|
||||||
|
|
||||||
|
|
||||||
|
func _on_notes_button_pressed() -> void:
|
||||||
|
OS.shell_open(next_version_release.html_url)
|
@@ -0,0 +1 @@
|
|||||||
|
uid://kpwo418lb2t2
|
@@ -0,0 +1,60 @@
|
|||||||
|
[gd_scene load_steps=3 format=3 uid="uid://qdxrxv3c3hxk"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://kpwo418lb2t2" path="res://addons/dialogue_manager/components/download_update_panel.gd" id="1_4tm1k"]
|
||||||
|
[ext_resource type="Texture2D" uid="uid://d3baj6rygkb3f" path="res://addons/dialogue_manager/assets/update.svg" id="2_4o2m6"]
|
||||||
|
|
||||||
|
[node name="DownloadUpdatePanel" type="Control"]
|
||||||
|
layout_mode = 3
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
script = ExtResource("1_4tm1k")
|
||||||
|
|
||||||
|
[node name="HTTPRequest" type="HTTPRequest" parent="."]
|
||||||
|
|
||||||
|
[node name="VBox" type="VBoxContainer" parent="."]
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
offset_left = -1.0
|
||||||
|
offset_top = 9.0
|
||||||
|
offset_right = -1.0
|
||||||
|
offset_bottom = 9.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
theme_override_constants/separation = 10
|
||||||
|
|
||||||
|
[node name="Logo" type="TextureRect" parent="VBox"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
clip_contents = true
|
||||||
|
custom_minimum_size = Vector2(300, 80)
|
||||||
|
layout_mode = 2
|
||||||
|
texture = ExtResource("2_4o2m6")
|
||||||
|
stretch_mode = 5
|
||||||
|
|
||||||
|
[node name="Label" type="Label" parent="VBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "v1.2.3 is available for download."
|
||||||
|
horizontal_alignment = 1
|
||||||
|
|
||||||
|
[node name="Center" type="CenterContainer" parent="VBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="DownloadButton" type="Button" parent="VBox/Center"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Download update"
|
||||||
|
|
||||||
|
[node name="Center2" type="CenterContainer" parent="VBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="NotesButton" type="LinkButton" parent="VBox/Center2"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Read release notes"
|
||||||
|
|
||||||
|
[connection signal="request_completed" from="HTTPRequest" to="." method="_on_http_request_request_completed"]
|
||||||
|
[connection signal="pressed" from="VBox/Center/DownloadButton" to="." method="_on_download_button_pressed"]
|
||||||
|
[connection signal="pressed" from="VBox/Center2/NotesButton" to="." method="_on_notes_button_pressed"]
|
48
addons/dialogue_manager/components/editor_property/editor_property.gd
vendored
Normal file
48
addons/dialogue_manager/components/editor_property/editor_property.gd
vendored
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
@tool
|
||||||
|
extends EditorProperty
|
||||||
|
|
||||||
|
|
||||||
|
const DialoguePropertyEditorControl = preload("./editor_property_control.tscn")
|
||||||
|
|
||||||
|
|
||||||
|
var editor_plugin: EditorPlugin
|
||||||
|
|
||||||
|
var control = DialoguePropertyEditorControl.instantiate()
|
||||||
|
var current_value: Resource
|
||||||
|
var is_updating: bool = false
|
||||||
|
|
||||||
|
|
||||||
|
func _init() -> void:
|
||||||
|
add_child(control)
|
||||||
|
|
||||||
|
control.resource = current_value
|
||||||
|
|
||||||
|
control.pressed.connect(_on_button_pressed)
|
||||||
|
control.resource_changed.connect(_on_resource_changed)
|
||||||
|
|
||||||
|
|
||||||
|
func _update_property() -> void:
|
||||||
|
var next_value = get_edited_object()[get_edited_property()]
|
||||||
|
|
||||||
|
# The resource might have been deleted elsewhere so check that it's not in a weird state
|
||||||
|
if is_instance_valid(next_value) and not next_value.resource_path.ends_with(".dialogue"):
|
||||||
|
emit_changed(get_edited_property(), null)
|
||||||
|
return
|
||||||
|
|
||||||
|
if next_value == current_value: return
|
||||||
|
|
||||||
|
is_updating = true
|
||||||
|
current_value = next_value
|
||||||
|
control.resource = current_value
|
||||||
|
is_updating = false
|
||||||
|
|
||||||
|
|
||||||
|
### Signals
|
||||||
|
|
||||||
|
|
||||||
|
func _on_button_pressed() -> void:
|
||||||
|
editor_plugin.edit(current_value)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_resource_changed(next_resource: Resource) -> void:
|
||||||
|
emit_changed(get_edited_property(), next_resource)
|
@@ -0,0 +1 @@
|
|||||||
|
uid://nyypeje1a036
|
147
addons/dialogue_manager/components/editor_property/editor_property_control.gd
vendored
Normal file
147
addons/dialogue_manager/components/editor_property/editor_property_control.gd
vendored
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
@tool
|
||||||
|
extends HBoxContainer
|
||||||
|
|
||||||
|
|
||||||
|
signal pressed()
|
||||||
|
signal resource_changed(next_resource: Resource)
|
||||||
|
|
||||||
|
|
||||||
|
const ITEM_NEW = 100
|
||||||
|
const ITEM_QUICK_LOAD = 200
|
||||||
|
const ITEM_LOAD = 201
|
||||||
|
const ITEM_EDIT = 300
|
||||||
|
const ITEM_CLEAR = 301
|
||||||
|
const ITEM_FILESYSTEM = 400
|
||||||
|
|
||||||
|
|
||||||
|
@onready var button: Button = $ResourceButton
|
||||||
|
@onready var menu_button: Button = $MenuButton
|
||||||
|
@onready var menu: PopupMenu = $Menu
|
||||||
|
@onready var quick_open_dialog: ConfirmationDialog = $QuickOpenDialog
|
||||||
|
@onready var files_list = $QuickOpenDialog/FilesList
|
||||||
|
@onready var new_dialog: FileDialog = $NewDialog
|
||||||
|
@onready var open_dialog: FileDialog = $OpenDialog
|
||||||
|
|
||||||
|
var editor_plugin: EditorPlugin
|
||||||
|
|
||||||
|
var resource: Resource:
|
||||||
|
set(next_resource):
|
||||||
|
resource = next_resource
|
||||||
|
if button:
|
||||||
|
button.resource = resource
|
||||||
|
get:
|
||||||
|
return resource
|
||||||
|
|
||||||
|
var is_waiting_for_file: bool = false
|
||||||
|
var quick_selected_file: String = ""
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
menu_button.icon = get_theme_icon("GuiDropdown", "EditorIcons")
|
||||||
|
editor_plugin = Engine.get_meta("DialogueManagerPlugin")
|
||||||
|
|
||||||
|
|
||||||
|
func build_menu() -> void:
|
||||||
|
menu.clear()
|
||||||
|
|
||||||
|
menu.add_icon_item(editor_plugin._get_plugin_icon(), "New Dialogue", ITEM_NEW)
|
||||||
|
menu.add_separator()
|
||||||
|
menu.add_icon_item(get_theme_icon("Load", "EditorIcons"), "Quick Load", ITEM_QUICK_LOAD)
|
||||||
|
menu.add_icon_item(get_theme_icon("Load", "EditorIcons"), "Load", ITEM_LOAD)
|
||||||
|
if resource:
|
||||||
|
menu.add_icon_item(get_theme_icon("Edit", "EditorIcons"), "Edit", ITEM_EDIT)
|
||||||
|
menu.add_icon_item(get_theme_icon("Clear", "EditorIcons"), "Clear", ITEM_CLEAR)
|
||||||
|
menu.add_separator()
|
||||||
|
menu.add_item("Show in FileSystem", ITEM_FILESYSTEM)
|
||||||
|
|
||||||
|
menu.size = Vector2.ZERO
|
||||||
|
|
||||||
|
|
||||||
|
### Signals
|
||||||
|
|
||||||
|
|
||||||
|
func _on_new_dialog_file_selected(path: String) -> void:
|
||||||
|
editor_plugin.main_view.new_file(path)
|
||||||
|
is_waiting_for_file = false
|
||||||
|
if Engine.get_meta("DMCache").has_file(path):
|
||||||
|
resource_changed.emit(load(path))
|
||||||
|
else:
|
||||||
|
var next_resource: Resource = await editor_plugin.import_plugin.compiled_resource
|
||||||
|
next_resource.resource_path = path
|
||||||
|
resource_changed.emit(next_resource)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_open_dialog_file_selected(file: String) -> void:
|
||||||
|
resource_changed.emit(load(file))
|
||||||
|
|
||||||
|
|
||||||
|
func _on_file_dialog_canceled() -> void:
|
||||||
|
is_waiting_for_file = false
|
||||||
|
|
||||||
|
|
||||||
|
func _on_resource_button_pressed() -> void:
|
||||||
|
if is_instance_valid(resource):
|
||||||
|
EditorInterface.call_deferred("edit_resource", resource)
|
||||||
|
else:
|
||||||
|
build_menu()
|
||||||
|
menu.position = get_viewport().position + Vector2i(
|
||||||
|
button.global_position.x + button.size.x - menu.size.x,
|
||||||
|
2 + menu_button.global_position.y + button.size.y
|
||||||
|
)
|
||||||
|
menu.popup()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_resource_button_resource_dropped(next_resource: Resource) -> void:
|
||||||
|
resource_changed.emit(next_resource)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_menu_button_pressed() -> void:
|
||||||
|
build_menu()
|
||||||
|
menu.position = get_viewport().position + Vector2i(
|
||||||
|
menu_button.global_position.x + menu_button.size.x - menu.size.x,
|
||||||
|
2 + menu_button.global_position.y + menu_button.size.y
|
||||||
|
)
|
||||||
|
menu.popup()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_menu_id_pressed(id: int) -> void:
|
||||||
|
match id:
|
||||||
|
ITEM_NEW:
|
||||||
|
is_waiting_for_file = true
|
||||||
|
new_dialog.popup_centered()
|
||||||
|
|
||||||
|
ITEM_QUICK_LOAD:
|
||||||
|
quick_selected_file = ""
|
||||||
|
files_list.files = Engine.get_meta("DMCache").get_files()
|
||||||
|
if resource:
|
||||||
|
files_list.select_file(resource.resource_path)
|
||||||
|
quick_open_dialog.popup_centered()
|
||||||
|
files_list.focus_filter()
|
||||||
|
|
||||||
|
ITEM_LOAD:
|
||||||
|
is_waiting_for_file = true
|
||||||
|
open_dialog.popup_centered()
|
||||||
|
|
||||||
|
ITEM_EDIT:
|
||||||
|
EditorInterface.call_deferred("edit_resource", resource)
|
||||||
|
|
||||||
|
ITEM_CLEAR:
|
||||||
|
resource_changed.emit(null)
|
||||||
|
|
||||||
|
ITEM_FILESYSTEM:
|
||||||
|
var file_system = EditorInterface.get_file_system_dock()
|
||||||
|
file_system.navigate_to_path(resource.resource_path)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_files_list_file_double_clicked(file_path: String) -> void:
|
||||||
|
resource_changed.emit(load(file_path))
|
||||||
|
quick_open_dialog.hide()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_files_list_file_selected(file_path: String) -> void:
|
||||||
|
quick_selected_file = file_path
|
||||||
|
|
||||||
|
|
||||||
|
func _on_quick_open_dialog_confirmed() -> void:
|
||||||
|
if quick_selected_file != "":
|
||||||
|
resource_changed.emit(load(quick_selected_file))
|
@@ -0,0 +1 @@
|
|||||||
|
uid://dooe2pflnqtve
|
@@ -0,0 +1,58 @@
|
|||||||
|
[gd_scene load_steps=4 format=3 uid="uid://ycn6uaj7dsrh"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://dooe2pflnqtve" path="res://addons/dialogue_manager/components/editor_property/editor_property_control.gd" id="1_het12"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://b16uuqjuof3n5" path="res://addons/dialogue_manager/components/editor_property/resource_button.tscn" id="2_hh3d4"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://dnufpcdrreva3" path="res://addons/dialogue_manager/components/files_list.tscn" id="3_l8fp6"]
|
||||||
|
|
||||||
|
[node name="PropertyEditorButton" type="HBoxContainer"]
|
||||||
|
offset_right = 40.0
|
||||||
|
offset_bottom = 40.0
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
theme_override_constants/separation = 0
|
||||||
|
script = ExtResource("1_het12")
|
||||||
|
|
||||||
|
[node name="ResourceButton" parent="." instance=ExtResource("2_hh3d4")]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "<empty>"
|
||||||
|
text_overrun_behavior = 3
|
||||||
|
clip_text = true
|
||||||
|
|
||||||
|
[node name="MenuButton" type="Button" parent="."]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="Menu" type="PopupMenu" parent="."]
|
||||||
|
|
||||||
|
[node name="QuickOpenDialog" type="ConfirmationDialog" parent="."]
|
||||||
|
title = "Find Dialogue Resource"
|
||||||
|
size = Vector2i(400, 600)
|
||||||
|
min_size = Vector2i(400, 600)
|
||||||
|
ok_button_text = "Open"
|
||||||
|
|
||||||
|
[node name="FilesList" parent="QuickOpenDialog" instance=ExtResource("3_l8fp6")]
|
||||||
|
|
||||||
|
[node name="NewDialog" type="FileDialog" parent="."]
|
||||||
|
size = Vector2i(900, 750)
|
||||||
|
min_size = Vector2i(900, 750)
|
||||||
|
dialog_hide_on_ok = true
|
||||||
|
filters = PackedStringArray("*.dialogue ; Dialogue")
|
||||||
|
|
||||||
|
[node name="OpenDialog" type="FileDialog" parent="."]
|
||||||
|
title = "Open a File"
|
||||||
|
size = Vector2i(900, 750)
|
||||||
|
min_size = Vector2i(900, 750)
|
||||||
|
ok_button_text = "Open"
|
||||||
|
dialog_hide_on_ok = true
|
||||||
|
file_mode = 0
|
||||||
|
filters = PackedStringArray("*.dialogue ; Dialogue")
|
||||||
|
|
||||||
|
[connection signal="pressed" from="ResourceButton" to="." method="_on_resource_button_pressed"]
|
||||||
|
[connection signal="resource_dropped" from="ResourceButton" to="." method="_on_resource_button_resource_dropped"]
|
||||||
|
[connection signal="pressed" from="MenuButton" to="." method="_on_menu_button_pressed"]
|
||||||
|
[connection signal="id_pressed" from="Menu" to="." method="_on_menu_id_pressed"]
|
||||||
|
[connection signal="confirmed" from="QuickOpenDialog" to="." method="_on_quick_open_dialog_confirmed"]
|
||||||
|
[connection signal="file_double_clicked" from="QuickOpenDialog/FilesList" to="." method="_on_files_list_file_double_clicked"]
|
||||||
|
[connection signal="file_selected" from="QuickOpenDialog/FilesList" to="." method="_on_files_list_file_selected"]
|
||||||
|
[connection signal="canceled" from="NewDialog" to="." method="_on_file_dialog_canceled"]
|
||||||
|
[connection signal="file_selected" from="NewDialog" to="." method="_on_new_dialog_file_selected"]
|
||||||
|
[connection signal="canceled" from="OpenDialog" to="." method="_on_file_dialog_canceled"]
|
||||||
|
[connection signal="file_selected" from="OpenDialog" to="." method="_on_open_dialog_file_selected"]
|
48
addons/dialogue_manager/components/editor_property/resource_button.gd
vendored
Normal file
48
addons/dialogue_manager/components/editor_property/resource_button.gd
vendored
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
@tool
|
||||||
|
extends Button
|
||||||
|
|
||||||
|
|
||||||
|
signal resource_dropped(next_resource: Resource)
|
||||||
|
|
||||||
|
|
||||||
|
var resource: Resource:
|
||||||
|
set(next_resource):
|
||||||
|
resource = next_resource
|
||||||
|
if resource:
|
||||||
|
icon = Engine.get_meta("DialogueManagerPlugin")._get_plugin_icon()
|
||||||
|
text = resource.resource_path.get_file().replace(".dialogue", "")
|
||||||
|
else:
|
||||||
|
icon = null
|
||||||
|
text = "<empty>"
|
||||||
|
get:
|
||||||
|
return resource
|
||||||
|
|
||||||
|
|
||||||
|
func _notification(what: int) -> void:
|
||||||
|
match what:
|
||||||
|
NOTIFICATION_DRAG_BEGIN:
|
||||||
|
var data = get_viewport().gui_get_drag_data()
|
||||||
|
if typeof(data) == TYPE_DICTIONARY and data.type == "files" and data.files.size() > 0 and data.files[0].ends_with(".dialogue"):
|
||||||
|
add_theme_stylebox_override("normal", get_theme_stylebox("focus", "LineEdit"))
|
||||||
|
add_theme_stylebox_override("hover", get_theme_stylebox("focus", "LineEdit"))
|
||||||
|
|
||||||
|
NOTIFICATION_DRAG_END:
|
||||||
|
self.resource = resource
|
||||||
|
remove_theme_stylebox_override("normal")
|
||||||
|
remove_theme_stylebox_override("hover")
|
||||||
|
|
||||||
|
|
||||||
|
func _can_drop_data(at_position: Vector2, data) -> bool:
|
||||||
|
if typeof(data) != TYPE_DICTIONARY: return false
|
||||||
|
if data.type != "files": return false
|
||||||
|
|
||||||
|
var files: PackedStringArray = Array(data.files).filter(func(f): return f.get_extension() == "dialogue")
|
||||||
|
return files.size() > 0
|
||||||
|
|
||||||
|
|
||||||
|
func _drop_data(at_position: Vector2, data) -> void:
|
||||||
|
var files: PackedStringArray = Array(data.files).filter(func(f): return f.get_extension() == "dialogue")
|
||||||
|
|
||||||
|
if files.size() == 0: return
|
||||||
|
|
||||||
|
resource_dropped.emit(load(files[0]))
|
@@ -0,0 +1 @@
|
|||||||
|
uid://damhqta55t67c
|
@@ -0,0 +1,9 @@
|
|||||||
|
[gd_scene load_steps=2 format=3 uid="uid://b16uuqjuof3n5"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://damhqta55t67c" path="res://addons/dialogue_manager/components/editor_property/resource_button.gd" id="1_7u2i7"]
|
||||||
|
|
||||||
|
[node name="ResourceButton" type="Button"]
|
||||||
|
offset_right = 8.0
|
||||||
|
offset_bottom = 8.0
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
script = ExtResource("1_7u2i7")
|
85
addons/dialogue_manager/components/errors_panel.gd
vendored
Normal file
85
addons/dialogue_manager/components/errors_panel.gd
vendored
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
@tool
|
||||||
|
extends HBoxContainer
|
||||||
|
|
||||||
|
|
||||||
|
signal error_pressed(line_number)
|
||||||
|
|
||||||
|
|
||||||
|
const DialogueConstants = preload("../constants.gd")
|
||||||
|
|
||||||
|
|
||||||
|
@onready var error_button: Button = $ErrorButton
|
||||||
|
@onready var next_button: Button = $NextButton
|
||||||
|
@onready var count_label: Label = $CountLabel
|
||||||
|
@onready var previous_button: Button = $PreviousButton
|
||||||
|
|
||||||
|
## The index of the current error being shown
|
||||||
|
var error_index: int = 0:
|
||||||
|
set(next_error_index):
|
||||||
|
error_index = wrap(next_error_index, 0, errors.size())
|
||||||
|
show_error()
|
||||||
|
get:
|
||||||
|
return error_index
|
||||||
|
|
||||||
|
## The list of all errors
|
||||||
|
var errors: Array = []:
|
||||||
|
set(next_errors):
|
||||||
|
errors = next_errors
|
||||||
|
self.error_index = 0
|
||||||
|
get:
|
||||||
|
return errors
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
apply_theme()
|
||||||
|
hide()
|
||||||
|
|
||||||
|
|
||||||
|
## Set up colors and icons
|
||||||
|
func apply_theme() -> void:
|
||||||
|
error_button.add_theme_color_override("font_color", get_theme_color("error_color", "Editor"))
|
||||||
|
error_button.add_theme_color_override("font_hover_color", get_theme_color("error_color", "Editor"))
|
||||||
|
error_button.icon = get_theme_icon("StatusError", "EditorIcons")
|
||||||
|
previous_button.icon = get_theme_icon("ArrowLeft", "EditorIcons")
|
||||||
|
next_button.icon = get_theme_icon("ArrowRight", "EditorIcons")
|
||||||
|
|
||||||
|
|
||||||
|
## Move the error index to match a given line
|
||||||
|
func show_error_for_line_number(line_number: int) -> void:
|
||||||
|
for i in range(0, errors.size()):
|
||||||
|
if errors[i].line_number == line_number:
|
||||||
|
self.error_index = i
|
||||||
|
|
||||||
|
|
||||||
|
## Show the current error
|
||||||
|
func show_error() -> void:
|
||||||
|
if errors.size() == 0:
|
||||||
|
hide()
|
||||||
|
else:
|
||||||
|
show()
|
||||||
|
count_label.text = DialogueConstants.translate(&"n_of_n").format({ index = error_index + 1, total = errors.size() })
|
||||||
|
var error = errors[error_index]
|
||||||
|
error_button.text = DialogueConstants.translate(&"errors.line_and_message").format({ line = error.line_number, column = error.column_number, message = DialogueConstants.get_error_message(error.error) })
|
||||||
|
if error.has("external_error"):
|
||||||
|
error_button.text += " " + DialogueConstants.get_error_message(error.external_error)
|
||||||
|
|
||||||
|
|
||||||
|
### Signals
|
||||||
|
|
||||||
|
|
||||||
|
func _on_errors_panel_theme_changed() -> void:
|
||||||
|
apply_theme()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_error_button_pressed() -> void:
|
||||||
|
error_pressed.emit(errors[error_index].line_number, errors[error_index].column_number)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_previous_button_pressed() -> void:
|
||||||
|
self.error_index -= 1
|
||||||
|
_on_error_button_pressed()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_next_button_pressed() -> void:
|
||||||
|
self.error_index += 1
|
||||||
|
_on_error_button_pressed()
|
1
addons/dialogue_manager/components/errors_panel.gd.uid
Normal file
1
addons/dialogue_manager/components/errors_panel.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://d2l8nlb6hhrfp
|
56
addons/dialogue_manager/components/errors_panel.tscn
Normal file
56
addons/dialogue_manager/components/errors_panel.tscn
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
[gd_scene load_steps=4 format=3 uid="uid://cs8pwrxr5vxix"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://d2l8nlb6hhrfp" path="res://addons/dialogue_manager/components/errors_panel.gd" id="1_nfm3c"]
|
||||||
|
|
||||||
|
[sub_resource type="Image" id="Image_w0gko"]
|
||||||
|
data = {
|
||||||
|
"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 231, 255, 93, 93, 55, 255, 97, 97, 58, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 93, 93, 41, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 97, 97, 42, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 98, 98, 47, 255, 97, 97, 42, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 93, 93, 233, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 94, 94, 46, 255, 93, 93, 236, 255, 93, 93, 233, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
|
||||||
|
"format": "RGBA8",
|
||||||
|
"height": 16,
|
||||||
|
"mipmaps": false,
|
||||||
|
"width": 16
|
||||||
|
}
|
||||||
|
|
||||||
|
[sub_resource type="ImageTexture" id="ImageTexture_s6fxl"]
|
||||||
|
image = SubResource("Image_w0gko")
|
||||||
|
|
||||||
|
[node name="ErrorsPanel" type="HBoxContainer"]
|
||||||
|
visible = false
|
||||||
|
offset_right = 1024.0
|
||||||
|
offset_bottom = 600.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
script = ExtResource("1_nfm3c")
|
||||||
|
metadata/_edit_layout_mode = 1
|
||||||
|
|
||||||
|
[node name="ErrorButton" type="Button" parent="."]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
theme_override_colors/font_color = Color(0, 0, 0, 1)
|
||||||
|
theme_override_colors/font_hover_color = Color(0, 0, 0, 1)
|
||||||
|
theme_override_constants/h_separation = 3
|
||||||
|
icon = SubResource("ImageTexture_s6fxl")
|
||||||
|
flat = true
|
||||||
|
alignment = 0
|
||||||
|
text_overrun_behavior = 4
|
||||||
|
|
||||||
|
[node name="Spacer" type="Control" parent="."]
|
||||||
|
custom_minimum_size = Vector2(40, 0)
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="PreviousButton" type="Button" parent="."]
|
||||||
|
layout_mode = 2
|
||||||
|
icon = SubResource("ImageTexture_s6fxl")
|
||||||
|
flat = true
|
||||||
|
|
||||||
|
[node name="CountLabel" type="Label" parent="."]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="NextButton" type="Button" parent="."]
|
||||||
|
layout_mode = 2
|
||||||
|
icon = SubResource("ImageTexture_s6fxl")
|
||||||
|
flat = true
|
||||||
|
|
||||||
|
[connection signal="pressed" from="ErrorButton" to="." method="_on_error_button_pressed"]
|
||||||
|
[connection signal="pressed" from="PreviousButton" to="." method="_on_previous_button_pressed"]
|
||||||
|
[connection signal="pressed" from="NextButton" to="." method="_on_next_button_pressed"]
|
150
addons/dialogue_manager/components/files_list.gd
vendored
Normal file
150
addons/dialogue_manager/components/files_list.gd
vendored
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
@tool
|
||||||
|
extends VBoxContainer
|
||||||
|
|
||||||
|
|
||||||
|
signal file_selected(file_path: String)
|
||||||
|
signal file_popup_menu_requested(at_position: Vector2)
|
||||||
|
signal file_double_clicked(file_path: String)
|
||||||
|
signal file_middle_clicked(file_path: String)
|
||||||
|
|
||||||
|
|
||||||
|
const DialogueConstants = preload("../constants.gd")
|
||||||
|
|
||||||
|
const MODIFIED_SUFFIX = "(*)"
|
||||||
|
|
||||||
|
|
||||||
|
@export var icon: Texture2D
|
||||||
|
|
||||||
|
@onready var filter_edit: LineEdit = $FilterEdit
|
||||||
|
@onready var list: ItemList = $List
|
||||||
|
|
||||||
|
var file_map: Dictionary = {}
|
||||||
|
|
||||||
|
var current_file_path: String = ""
|
||||||
|
var last_selected_file_path: String = ""
|
||||||
|
|
||||||
|
var files: PackedStringArray = []:
|
||||||
|
set(next_files):
|
||||||
|
files = next_files
|
||||||
|
files.sort()
|
||||||
|
update_file_map()
|
||||||
|
apply_filter()
|
||||||
|
get:
|
||||||
|
return files
|
||||||
|
|
||||||
|
var unsaved_files: Array[String] = []
|
||||||
|
|
||||||
|
var filter: String = "":
|
||||||
|
set(next_filter):
|
||||||
|
filter = next_filter
|
||||||
|
apply_filter()
|
||||||
|
get:
|
||||||
|
return filter
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
apply_theme()
|
||||||
|
|
||||||
|
filter_edit.placeholder_text = DialogueConstants.translate(&"files_list.filter")
|
||||||
|
|
||||||
|
|
||||||
|
func focus_filter() -> void:
|
||||||
|
filter_edit.grab_focus()
|
||||||
|
|
||||||
|
|
||||||
|
func select_file(file: String) -> void:
|
||||||
|
list.deselect_all()
|
||||||
|
for i in range(0, list.get_item_count()):
|
||||||
|
var item_text = list.get_item_text(i).replace(MODIFIED_SUFFIX, "")
|
||||||
|
if item_text == get_nice_file(file, item_text.count("/") + 1):
|
||||||
|
list.select(i)
|
||||||
|
last_selected_file_path = file
|
||||||
|
|
||||||
|
|
||||||
|
func mark_file_as_unsaved(file: String, is_unsaved: bool) -> void:
|
||||||
|
if not file in unsaved_files and is_unsaved:
|
||||||
|
unsaved_files.append(file)
|
||||||
|
elif file in unsaved_files and not is_unsaved:
|
||||||
|
unsaved_files.erase(file)
|
||||||
|
apply_filter()
|
||||||
|
|
||||||
|
|
||||||
|
func update_file_map() -> void:
|
||||||
|
file_map = {}
|
||||||
|
for file in files:
|
||||||
|
var nice_file: String = get_nice_file(file)
|
||||||
|
|
||||||
|
# See if a value with just the file name is already in the map
|
||||||
|
for key in file_map.keys():
|
||||||
|
if file_map[key] == nice_file:
|
||||||
|
var bit_count = nice_file.count("/") + 2
|
||||||
|
|
||||||
|
var existing_nice_file = get_nice_file(key, bit_count)
|
||||||
|
nice_file = get_nice_file(file, bit_count)
|
||||||
|
|
||||||
|
while nice_file == existing_nice_file:
|
||||||
|
bit_count += 1
|
||||||
|
existing_nice_file = get_nice_file(key, bit_count)
|
||||||
|
nice_file = get_nice_file(file, bit_count)
|
||||||
|
|
||||||
|
file_map[key] = existing_nice_file
|
||||||
|
|
||||||
|
file_map[file] = nice_file
|
||||||
|
|
||||||
|
|
||||||
|
func get_nice_file(file_path: String, path_bit_count: int = 1) -> String:
|
||||||
|
var bits = file_path.replace("res://", "").replace(".dialogue", "").split("/")
|
||||||
|
bits = bits.slice(-path_bit_count)
|
||||||
|
return "/".join(bits)
|
||||||
|
|
||||||
|
|
||||||
|
func apply_filter() -> void:
|
||||||
|
list.clear()
|
||||||
|
for file in file_map.keys():
|
||||||
|
if filter == "" or filter.to_lower() in file.to_lower():
|
||||||
|
var nice_file = file_map[file]
|
||||||
|
if file in unsaved_files:
|
||||||
|
nice_file += MODIFIED_SUFFIX
|
||||||
|
var new_id := list.add_item(nice_file)
|
||||||
|
list.set_item_icon(new_id, icon)
|
||||||
|
|
||||||
|
select_file(current_file_path)
|
||||||
|
|
||||||
|
|
||||||
|
func apply_theme() -> void:
|
||||||
|
if is_instance_valid(filter_edit):
|
||||||
|
filter_edit.right_icon = get_theme_icon("Search", "EditorIcons")
|
||||||
|
if is_instance_valid(list):
|
||||||
|
list.add_theme_stylebox_override("panel", get_theme_stylebox("panel", "Panel"))
|
||||||
|
|
||||||
|
|
||||||
|
### Signals
|
||||||
|
|
||||||
|
|
||||||
|
func _on_theme_changed() -> void:
|
||||||
|
apply_theme()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_filter_edit_text_changed(new_text: String) -> void:
|
||||||
|
self.filter = new_text
|
||||||
|
|
||||||
|
|
||||||
|
func _on_list_item_clicked(index: int, at_position: Vector2, mouse_button_index: int) -> void:
|
||||||
|
var item_text = list.get_item_text(index).replace(MODIFIED_SUFFIX, "")
|
||||||
|
var file = file_map.find_key(item_text)
|
||||||
|
|
||||||
|
if mouse_button_index == MOUSE_BUTTON_LEFT or mouse_button_index == MOUSE_BUTTON_RIGHT:
|
||||||
|
select_file(file)
|
||||||
|
file_selected.emit(file)
|
||||||
|
if mouse_button_index == MOUSE_BUTTON_RIGHT:
|
||||||
|
file_popup_menu_requested.emit(at_position)
|
||||||
|
|
||||||
|
if mouse_button_index == MOUSE_BUTTON_MIDDLE:
|
||||||
|
file_middle_clicked.emit(file)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_list_item_activated(index: int) -> void:
|
||||||
|
var item_text = list.get_item_text(index).replace(MODIFIED_SUFFIX, "")
|
||||||
|
var file = file_map.find_key(item_text)
|
||||||
|
select_file(file)
|
||||||
|
file_double_clicked.emit(file)
|
1
addons/dialogue_manager/components/files_list.gd.uid
Normal file
1
addons/dialogue_manager/components/files_list.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://dqa4a4wwoo0aa
|
29
addons/dialogue_manager/components/files_list.tscn
Normal file
29
addons/dialogue_manager/components/files_list.tscn
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
[gd_scene load_steps=3 format=3 uid="uid://dnufpcdrreva3"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://dqa4a4wwoo0aa" path="res://addons/dialogue_manager/components/files_list.gd" id="1_cytii"]
|
||||||
|
[ext_resource type="Texture2D" uid="uid://d3lr2uas6ax8v" path="res://addons/dialogue_manager/assets/icon.svg" id="2_3ijx1"]
|
||||||
|
|
||||||
|
[node name="FilesList" type="VBoxContainer"]
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
size_flags_vertical = 3
|
||||||
|
script = ExtResource("1_cytii")
|
||||||
|
icon = ExtResource("2_3ijx1")
|
||||||
|
|
||||||
|
[node name="FilterEdit" type="LineEdit" parent="."]
|
||||||
|
layout_mode = 2
|
||||||
|
placeholder_text = "Filter files"
|
||||||
|
clear_button_enabled = true
|
||||||
|
|
||||||
|
[node name="List" type="ItemList" parent="."]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_vertical = 3
|
||||||
|
allow_rmb_select = true
|
||||||
|
|
||||||
|
[connection signal="theme_changed" from="." to="." method="_on_theme_changed"]
|
||||||
|
[connection signal="text_changed" from="FilterEdit" to="." method="_on_filter_edit_text_changed"]
|
||||||
|
[connection signal="item_activated" from="List" to="." method="_on_list_item_activated"]
|
||||||
|
[connection signal="item_clicked" from="List" to="." method="_on_list_item_clicked"]
|
229
addons/dialogue_manager/components/find_in_files.gd
vendored
Normal file
229
addons/dialogue_manager/components/find_in_files.gd
vendored
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
@tool
|
||||||
|
extends Control
|
||||||
|
|
||||||
|
signal result_selected(path: String, cursor: Vector2, length: int)
|
||||||
|
|
||||||
|
|
||||||
|
const DialogueConstants = preload("../constants.gd")
|
||||||
|
|
||||||
|
|
||||||
|
@export var main_view: Control
|
||||||
|
@export var code_edit: CodeEdit
|
||||||
|
|
||||||
|
@onready var input: LineEdit = %Input
|
||||||
|
@onready var search_button: Button = %SearchButton
|
||||||
|
@onready var match_case_button: CheckBox = %MatchCaseButton
|
||||||
|
@onready var replace_toggle: CheckButton = %ReplaceToggle
|
||||||
|
@onready var replace_container: VBoxContainer = %ReplaceContainer
|
||||||
|
@onready var replace_input: LineEdit = %ReplaceInput
|
||||||
|
@onready var replace_selected_button: Button = %ReplaceSelectedButton
|
||||||
|
@onready var replace_all_button: Button = %ReplaceAllButton
|
||||||
|
@onready var results_container: VBoxContainer = %ResultsContainer
|
||||||
|
@onready var result_template: HBoxContainer = %ResultTemplate
|
||||||
|
|
||||||
|
var current_results: Dictionary = {}:
|
||||||
|
set(value):
|
||||||
|
current_results = value
|
||||||
|
update_results_view()
|
||||||
|
if current_results.size() == 0:
|
||||||
|
replace_selected_button.disabled = true
|
||||||
|
replace_all_button.disabled = true
|
||||||
|
else:
|
||||||
|
replace_selected_button.disabled = false
|
||||||
|
replace_all_button.disabled = false
|
||||||
|
get:
|
||||||
|
return current_results
|
||||||
|
|
||||||
|
var selections: PackedStringArray = []
|
||||||
|
|
||||||
|
|
||||||
|
func prepare() -> void:
|
||||||
|
input.grab_focus()
|
||||||
|
|
||||||
|
var template_label = result_template.get_node("Label")
|
||||||
|
template_label.get_theme_stylebox(&"focus").bg_color = code_edit.theme_overrides.current_line_color
|
||||||
|
template_label.add_theme_font_override(&"normal_font", code_edit.get_theme_font(&"font"))
|
||||||
|
|
||||||
|
replace_toggle.set_pressed_no_signal(false)
|
||||||
|
replace_container.hide()
|
||||||
|
|
||||||
|
$VBoxContainer/HBoxContainer/FindContainer/Label.text = DialogueConstants.translate(&"search.find")
|
||||||
|
input.placeholder_text = DialogueConstants.translate(&"search.placeholder")
|
||||||
|
input.text = ""
|
||||||
|
search_button.text = DialogueConstants.translate(&"search.find_all")
|
||||||
|
match_case_button.text = DialogueConstants.translate(&"search.match_case")
|
||||||
|
replace_toggle.text = DialogueConstants.translate(&"search.toggle_replace")
|
||||||
|
$VBoxContainer/HBoxContainer/ReplaceContainer/ReplaceLabel.text = DialogueConstants.translate(&"search.replace_with")
|
||||||
|
replace_input.placeholder_text = DialogueConstants.translate(&"search.replace_placeholder")
|
||||||
|
replace_input.text = ""
|
||||||
|
replace_all_button.text = DialogueConstants.translate(&"search.replace_all")
|
||||||
|
replace_selected_button.text = DialogueConstants.translate(&"search.replace_selected")
|
||||||
|
|
||||||
|
selections.clear()
|
||||||
|
self.current_results = {}
|
||||||
|
|
||||||
|
#region helpers
|
||||||
|
|
||||||
|
|
||||||
|
func update_results_view() -> void:
|
||||||
|
for child in results_container.get_children():
|
||||||
|
child.queue_free()
|
||||||
|
|
||||||
|
for path in current_results.keys():
|
||||||
|
var path_label: Label = Label.new()
|
||||||
|
path_label.text = path
|
||||||
|
# Show open files
|
||||||
|
if main_view.open_buffers.has(path):
|
||||||
|
path_label.text += "(*)"
|
||||||
|
results_container.add_child(path_label)
|
||||||
|
for path_result in current_results.get(path):
|
||||||
|
var result_item: HBoxContainer = result_template.duplicate()
|
||||||
|
|
||||||
|
var checkbox: CheckBox = result_item.get_node("CheckBox") as CheckBox
|
||||||
|
var key: String = get_selection_key(path, path_result)
|
||||||
|
checkbox.toggled.connect(func(is_pressed):
|
||||||
|
if is_pressed:
|
||||||
|
if not selections.has(key):
|
||||||
|
selections.append(key)
|
||||||
|
else:
|
||||||
|
if selections.has(key):
|
||||||
|
selections.remove_at(selections.find(key))
|
||||||
|
)
|
||||||
|
checkbox.set_pressed_no_signal(selections.has(key))
|
||||||
|
checkbox.visible = replace_toggle.button_pressed
|
||||||
|
|
||||||
|
var result_label: RichTextLabel = result_item.get_node("Label") as RichTextLabel
|
||||||
|
var colors: Dictionary = code_edit.theme_overrides
|
||||||
|
var highlight: String = ""
|
||||||
|
if replace_toggle.button_pressed:
|
||||||
|
var matched_word: String = "[bgcolor=" + colors.critical_color.to_html() + "][color=" + colors.text_color.to_html() + "]" + path_result.matched_text + "[/color][/bgcolor]"
|
||||||
|
highlight = "[s]" + matched_word + "[/s][bgcolor=" + colors.notice_color.to_html() + "][color=" + colors.text_color.to_html() + "]" + replace_input.text + "[/color][/bgcolor]"
|
||||||
|
else:
|
||||||
|
highlight = "[bgcolor=" + colors.notice_color.to_html() + "][color=" + colors.text_color.to_html() + "]" + path_result.matched_text + "[/color][/bgcolor]"
|
||||||
|
var text: String = path_result.text.substr(0, path_result.index) + highlight + path_result.text.substr(path_result.index + path_result.query.length())
|
||||||
|
result_label.text = "%s: %s" % [str(path_result.line).lpad(4), text]
|
||||||
|
result_label.gui_input.connect(func(event):
|
||||||
|
if event is InputEventMouseButton and (event as InputEventMouseButton).button_index == MOUSE_BUTTON_LEFT and (event as InputEventMouseButton).double_click:
|
||||||
|
result_selected.emit(path, Vector2(path_result.index, path_result.line), path_result.query.length())
|
||||||
|
)
|
||||||
|
|
||||||
|
results_container.add_child(result_item)
|
||||||
|
|
||||||
|
|
||||||
|
func find_in_files() -> Dictionary:
|
||||||
|
var results: Dictionary = {}
|
||||||
|
|
||||||
|
var q: String = input.text
|
||||||
|
var cache = Engine.get_meta("DMCache")
|
||||||
|
var file: FileAccess
|
||||||
|
for path in cache.get_files():
|
||||||
|
var path_results: Array = []
|
||||||
|
var lines: PackedStringArray = []
|
||||||
|
|
||||||
|
if main_view.open_buffers.has(path):
|
||||||
|
lines = main_view.open_buffers.get(path).text.split("\n")
|
||||||
|
else:
|
||||||
|
file = FileAccess.open(path, FileAccess.READ)
|
||||||
|
lines = file.get_as_text().split("\n")
|
||||||
|
|
||||||
|
for i in range(0, lines.size()):
|
||||||
|
var index: int = find_in_line(lines[i], q)
|
||||||
|
while index > -1:
|
||||||
|
path_results.append({
|
||||||
|
line = i,
|
||||||
|
index = index,
|
||||||
|
text = lines[i],
|
||||||
|
matched_text = lines[i].substr(index, q.length()),
|
||||||
|
query = q
|
||||||
|
})
|
||||||
|
index = find_in_line(lines[i], q, index + q.length())
|
||||||
|
|
||||||
|
if file != null and file.is_open():
|
||||||
|
file.close()
|
||||||
|
|
||||||
|
if path_results.size() > 0:
|
||||||
|
results[path] = path_results
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
func get_selection_key(path: String, path_result: Dictionary) -> String:
|
||||||
|
return "%s-%d-%d" % [path, path_result.line, path_result.index]
|
||||||
|
|
||||||
|
|
||||||
|
func find_in_line(line: String, query: String, from_index: int = 0) -> int:
|
||||||
|
if match_case_button.button_pressed:
|
||||||
|
return line.find(query, from_index)
|
||||||
|
else:
|
||||||
|
return line.findn(query, from_index)
|
||||||
|
|
||||||
|
|
||||||
|
func replace_results(only_selected: bool) -> void:
|
||||||
|
var file: FileAccess
|
||||||
|
var lines: PackedStringArray = []
|
||||||
|
for path in current_results:
|
||||||
|
if main_view.open_buffers.has(path):
|
||||||
|
lines = main_view.open_buffers.get(path).text.split("\n")
|
||||||
|
else:
|
||||||
|
file = FileAccess.open(path, FileAccess.READ_WRITE)
|
||||||
|
lines = file.get_as_text().split("\n")
|
||||||
|
|
||||||
|
# Read the results in reverse because we're going to be modifying them as we go
|
||||||
|
var path_results: Array = current_results.get(path).duplicate()
|
||||||
|
path_results.reverse()
|
||||||
|
for path_result in path_results:
|
||||||
|
var key: String = get_selection_key(path, path_result)
|
||||||
|
if not only_selected or (only_selected and selections.has(key)):
|
||||||
|
lines[path_result.line] = lines[path_result.line].substr(0, path_result.index) + replace_input.text + lines[path_result.line].substr(path_result.index + path_result.matched_text.length())
|
||||||
|
|
||||||
|
var replaced_text: String = "\n".join(lines)
|
||||||
|
if file != null and file.is_open():
|
||||||
|
file.seek(0)
|
||||||
|
file.store_string(replaced_text)
|
||||||
|
file.close()
|
||||||
|
else:
|
||||||
|
main_view.open_buffers.get(path).text = replaced_text
|
||||||
|
if main_view.current_file_path == path:
|
||||||
|
code_edit.text = replaced_text
|
||||||
|
|
||||||
|
current_results = find_in_files()
|
||||||
|
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region signals
|
||||||
|
|
||||||
|
|
||||||
|
func _on_search_button_pressed() -> void:
|
||||||
|
selections.clear()
|
||||||
|
self.current_results = find_in_files()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_input_text_submitted(new_text: String) -> void:
|
||||||
|
_on_search_button_pressed()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_replace_toggle_toggled(toggled_on: bool) -> void:
|
||||||
|
replace_container.visible = toggled_on
|
||||||
|
if toggled_on:
|
||||||
|
replace_input.grab_focus()
|
||||||
|
update_results_view()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_replace_input_text_changed(new_text: String) -> void:
|
||||||
|
update_results_view()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_replace_selected_button_pressed() -> void:
|
||||||
|
replace_results(true)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_replace_all_button_pressed() -> void:
|
||||||
|
replace_results(false)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_match_case_button_toggled(toggled_on: bool) -> void:
|
||||||
|
_on_search_button_pressed()
|
||||||
|
|
||||||
|
|
||||||
|
#endregion
|
1
addons/dialogue_manager/components/find_in_files.gd.uid
Normal file
1
addons/dialogue_manager/components/find_in_files.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://q368fmxxa8sd
|
139
addons/dialogue_manager/components/find_in_files.tscn
Normal file
139
addons/dialogue_manager/components/find_in_files.tscn
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
[gd_scene load_steps=3 format=3 uid="uid://0n7hwviyyly4"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://q368fmxxa8sd" path="res://addons/dialogue_manager/components/find_in_files.gd" id="1_3xicy"]
|
||||||
|
|
||||||
|
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_owohg"]
|
||||||
|
bg_color = Color(0.266667, 0.278431, 0.352941, 0.243137)
|
||||||
|
corner_detail = 1
|
||||||
|
|
||||||
|
[node name="FindInFiles" type="Control"]
|
||||||
|
layout_mode = 3
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
size_flags_vertical = 3
|
||||||
|
script = ExtResource("1_3xicy")
|
||||||
|
|
||||||
|
[node name="VBoxContainer" type="VBoxContainer" parent="."]
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
|
||||||
|
[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="FindContainer" type="VBoxContainer" parent="VBoxContainer/HBoxContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
|
||||||
|
[node name="Label" type="Label" parent="VBoxContainer/HBoxContainer/FindContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Find:"
|
||||||
|
|
||||||
|
[node name="Input" type="LineEdit" parent="VBoxContainer/HBoxContainer/FindContainer"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
clear_button_enabled = true
|
||||||
|
|
||||||
|
[node name="FindToolbar" type="HBoxContainer" parent="VBoxContainer/HBoxContainer/FindContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="SearchButton" type="Button" parent="VBoxContainer/HBoxContainer/FindContainer/FindToolbar"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Find all..."
|
||||||
|
|
||||||
|
[node name="MatchCaseButton" type="CheckBox" parent="VBoxContainer/HBoxContainer/FindContainer/FindToolbar"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Match case"
|
||||||
|
|
||||||
|
[node name="Control" type="Control" parent="VBoxContainer/HBoxContainer/FindContainer/FindToolbar"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
|
||||||
|
[node name="ReplaceToggle" type="CheckButton" parent="VBoxContainer/HBoxContainer/FindContainer/FindToolbar"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Replace"
|
||||||
|
|
||||||
|
[node name="ReplaceContainer" type="VBoxContainer" parent="VBoxContainer/HBoxContainer"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
|
||||||
|
[node name="ReplaceLabel" type="Label" parent="VBoxContainer/HBoxContainer/ReplaceContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Replace with:"
|
||||||
|
|
||||||
|
[node name="ReplaceInput" type="LineEdit" parent="VBoxContainer/HBoxContainer/ReplaceContainer"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
clear_button_enabled = true
|
||||||
|
|
||||||
|
[node name="ReplaceToolbar" type="HBoxContainer" parent="VBoxContainer/HBoxContainer/ReplaceContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="ReplaceSelectedButton" type="Button" parent="VBoxContainer/HBoxContainer/ReplaceContainer/ReplaceToolbar"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Replace selected"
|
||||||
|
|
||||||
|
[node name="ReplaceAllButton" type="Button" parent="VBoxContainer/HBoxContainer/ReplaceContainer/ReplaceToolbar"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Replace all"
|
||||||
|
|
||||||
|
[node name="VBoxContainer" type="VBoxContainer" parent="VBoxContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="ReplaceToolbar" type="HBoxContainer" parent="VBoxContainer/VBoxContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="ScrollContainer" type="ScrollContainer" parent="VBoxContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_vertical = 3
|
||||||
|
follow_focus = true
|
||||||
|
|
||||||
|
[node name="ResultsContainer" type="VBoxContainer" parent="VBoxContainer/ScrollContainer"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
size_flags_vertical = 3
|
||||||
|
theme_override_constants/separation = 0
|
||||||
|
|
||||||
|
[node name="ResultTemplate" type="HBoxContainer" parent="."]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 0
|
||||||
|
offset_left = 155.0
|
||||||
|
offset_top = -74.0
|
||||||
|
offset_right = 838.0
|
||||||
|
offset_bottom = -51.0
|
||||||
|
|
||||||
|
[node name="CheckBox" type="CheckBox" parent="ResultTemplate"]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="Label" type="RichTextLabel" parent="ResultTemplate"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
focus_mode = 2
|
||||||
|
theme_override_styles/focus = SubResource("StyleBoxFlat_owohg")
|
||||||
|
bbcode_enabled = true
|
||||||
|
text = "Result"
|
||||||
|
fit_content = true
|
||||||
|
scroll_active = false
|
||||||
|
|
||||||
|
[connection signal="text_submitted" from="VBoxContainer/HBoxContainer/FindContainer/Input" to="." method="_on_input_text_submitted"]
|
||||||
|
[connection signal="pressed" from="VBoxContainer/HBoxContainer/FindContainer/FindToolbar/SearchButton" to="." method="_on_search_button_pressed"]
|
||||||
|
[connection signal="toggled" from="VBoxContainer/HBoxContainer/FindContainer/FindToolbar/MatchCaseButton" to="." method="_on_match_case_button_toggled"]
|
||||||
|
[connection signal="toggled" from="VBoxContainer/HBoxContainer/FindContainer/FindToolbar/ReplaceToggle" to="." method="_on_replace_toggle_toggled"]
|
||||||
|
[connection signal="text_changed" from="VBoxContainer/HBoxContainer/ReplaceContainer/ReplaceInput" to="." method="_on_replace_input_text_changed"]
|
||||||
|
[connection signal="pressed" from="VBoxContainer/HBoxContainer/ReplaceContainer/ReplaceToolbar/ReplaceSelectedButton" to="." method="_on_replace_selected_button_pressed"]
|
||||||
|
[connection signal="pressed" from="VBoxContainer/HBoxContainer/ReplaceContainer/ReplaceToolbar/ReplaceAllButton" to="." method="_on_replace_all_button_pressed"]
|
218
addons/dialogue_manager/components/search_and_replace.gd
vendored
Normal file
218
addons/dialogue_manager/components/search_and_replace.gd
vendored
Normal file
@@ -0,0 +1,218 @@
|
|||||||
|
@tool
|
||||||
|
extends VBoxContainer
|
||||||
|
|
||||||
|
|
||||||
|
signal open_requested()
|
||||||
|
signal close_requested()
|
||||||
|
|
||||||
|
|
||||||
|
const DialogueConstants = preload("../constants.gd")
|
||||||
|
|
||||||
|
|
||||||
|
@onready var input: LineEdit = $Search/Input
|
||||||
|
@onready var result_label: Label = $Search/ResultLabel
|
||||||
|
@onready var previous_button: Button = $Search/PreviousButton
|
||||||
|
@onready var next_button: Button = $Search/NextButton
|
||||||
|
@onready var match_case_button: CheckBox = $Search/MatchCaseCheckBox
|
||||||
|
@onready var replace_check_button: CheckButton = $Search/ReplaceCheckButton
|
||||||
|
@onready var replace_panel: HBoxContainer = $Replace
|
||||||
|
@onready var replace_input: LineEdit = $Replace/Input
|
||||||
|
@onready var replace_button: Button = $Replace/ReplaceButton
|
||||||
|
@onready var replace_all_button: Button = $Replace/ReplaceAllButton
|
||||||
|
|
||||||
|
# The code edit we will be affecting (for some reason exporting this didn't work)
|
||||||
|
var code_edit: CodeEdit:
|
||||||
|
set(next_code_edit):
|
||||||
|
code_edit = next_code_edit
|
||||||
|
code_edit.gui_input.connect(_on_text_edit_gui_input)
|
||||||
|
code_edit.text_changed.connect(_on_text_edit_text_changed)
|
||||||
|
get:
|
||||||
|
return code_edit
|
||||||
|
|
||||||
|
var results: Array = []
|
||||||
|
var result_index: int = -1:
|
||||||
|
set(next_result_index):
|
||||||
|
result_index = next_result_index
|
||||||
|
if results.size() > 0:
|
||||||
|
var r = results[result_index]
|
||||||
|
code_edit.set_caret_line(r[0])
|
||||||
|
code_edit.select(r[0], r[1], r[0], r[1] + r[2])
|
||||||
|
else:
|
||||||
|
result_index = -1
|
||||||
|
if is_instance_valid(code_edit):
|
||||||
|
code_edit.deselect()
|
||||||
|
|
||||||
|
result_label.text = DialogueConstants.translate(&"n_of_n").format({ index = result_index + 1, total = results.size() })
|
||||||
|
get:
|
||||||
|
return result_index
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
apply_theme()
|
||||||
|
|
||||||
|
input.placeholder_text = DialogueConstants.translate(&"search.placeholder")
|
||||||
|
previous_button.tooltip_text = DialogueConstants.translate(&"search.previous")
|
||||||
|
next_button.tooltip_text = DialogueConstants.translate(&"search.next")
|
||||||
|
match_case_button.text = DialogueConstants.translate(&"search.match_case")
|
||||||
|
$Search/ReplaceCheckButton.text = DialogueConstants.translate(&"search.toggle_replace")
|
||||||
|
replace_button.text = DialogueConstants.translate(&"search.replace")
|
||||||
|
replace_all_button.text = DialogueConstants.translate(&"search.replace_all")
|
||||||
|
$Replace/ReplaceLabel.text = DialogueConstants.translate(&"search.replace_with")
|
||||||
|
|
||||||
|
self.result_index = -1
|
||||||
|
|
||||||
|
replace_panel.hide()
|
||||||
|
replace_button.disabled = true
|
||||||
|
replace_all_button.disabled = true
|
||||||
|
|
||||||
|
hide()
|
||||||
|
|
||||||
|
|
||||||
|
func focus_line_edit() -> void:
|
||||||
|
input.grab_focus()
|
||||||
|
input.select_all()
|
||||||
|
|
||||||
|
|
||||||
|
func apply_theme() -> void:
|
||||||
|
if is_instance_valid(previous_button):
|
||||||
|
previous_button.icon = get_theme_icon("ArrowLeft", "EditorIcons")
|
||||||
|
if is_instance_valid(next_button):
|
||||||
|
next_button.icon = get_theme_icon("ArrowRight", "EditorIcons")
|
||||||
|
|
||||||
|
|
||||||
|
# Find text in the code
|
||||||
|
func search(text: String = "", default_result_index: int = 0) -> void:
|
||||||
|
results.clear()
|
||||||
|
|
||||||
|
if text == "":
|
||||||
|
text = input.text
|
||||||
|
|
||||||
|
var lines = code_edit.text.split("\n")
|
||||||
|
for line_number in range(0, lines.size()):
|
||||||
|
var line = lines[line_number]
|
||||||
|
|
||||||
|
var column = find_in_line(line, text, 0)
|
||||||
|
while column > -1:
|
||||||
|
results.append([line_number, column, text.length()])
|
||||||
|
column = find_in_line(line, text, column + 1)
|
||||||
|
|
||||||
|
if results.size() > 0:
|
||||||
|
replace_button.disabled = false
|
||||||
|
replace_all_button.disabled = false
|
||||||
|
else:
|
||||||
|
replace_button.disabled = true
|
||||||
|
replace_all_button.disabled = true
|
||||||
|
|
||||||
|
self.result_index = clamp(default_result_index, 0, results.size() - 1)
|
||||||
|
|
||||||
|
|
||||||
|
# Find text in a string and match case if requested
|
||||||
|
func find_in_line(line: String, text: String, from_index: int = 0) -> int:
|
||||||
|
if match_case_button.button_pressed:
|
||||||
|
return line.find(text, from_index)
|
||||||
|
else:
|
||||||
|
return line.findn(text, from_index)
|
||||||
|
|
||||||
|
|
||||||
|
#region Signals
|
||||||
|
|
||||||
|
|
||||||
|
func _on_text_edit_gui_input(event: InputEvent) -> void:
|
||||||
|
if event is InputEventKey and event.is_pressed():
|
||||||
|
match event.as_text():
|
||||||
|
"Ctrl+F", "Command+F":
|
||||||
|
open_requested.emit()
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
"Ctrl+Shift+R", "Command+Shift+R":
|
||||||
|
replace_check_button.set_pressed(true)
|
||||||
|
open_requested.emit()
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_text_edit_text_changed() -> void:
|
||||||
|
results.clear()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_search_and_replace_theme_changed() -> void:
|
||||||
|
apply_theme()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_input_text_changed(new_text: String) -> void:
|
||||||
|
search(new_text)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_previous_button_pressed() -> void:
|
||||||
|
self.result_index = wrapi(result_index - 1, 0, results.size())
|
||||||
|
|
||||||
|
|
||||||
|
func _on_next_button_pressed() -> void:
|
||||||
|
self.result_index = wrapi(result_index + 1, 0, results.size())
|
||||||
|
|
||||||
|
|
||||||
|
func _on_search_and_replace_visibility_changed() -> void:
|
||||||
|
if is_instance_valid(input):
|
||||||
|
if visible:
|
||||||
|
input.grab_focus()
|
||||||
|
var selection = code_edit.get_selected_text()
|
||||||
|
if input.text == "" and selection != "":
|
||||||
|
input.text = selection
|
||||||
|
search(selection)
|
||||||
|
else:
|
||||||
|
search()
|
||||||
|
else:
|
||||||
|
input.text = ""
|
||||||
|
|
||||||
|
|
||||||
|
func _on_input_gui_input(event: InputEvent) -> void:
|
||||||
|
if event is InputEventKey and event.is_pressed():
|
||||||
|
match event.as_text():
|
||||||
|
"Enter":
|
||||||
|
search(input.text)
|
||||||
|
"Escape":
|
||||||
|
emit_signal("close_requested")
|
||||||
|
|
||||||
|
|
||||||
|
func _on_replace_button_pressed() -> void:
|
||||||
|
if result_index == -1: return
|
||||||
|
|
||||||
|
# Replace the selection at result index
|
||||||
|
var r: Array = results[result_index]
|
||||||
|
code_edit.begin_complex_operation()
|
||||||
|
var lines: PackedStringArray = code_edit.text.split("\n")
|
||||||
|
var line: String = lines[r[0]]
|
||||||
|
line = line.substr(0, r[1]) + replace_input.text + line.substr(r[1] + r[2])
|
||||||
|
lines[r[0]] = line
|
||||||
|
code_edit.text = "\n".join(lines)
|
||||||
|
code_edit.end_complex_operation()
|
||||||
|
code_edit.text_changed.emit()
|
||||||
|
|
||||||
|
search(input.text, result_index)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_replace_all_button_pressed() -> void:
|
||||||
|
if match_case_button.button_pressed:
|
||||||
|
code_edit.text = code_edit.text.replace(input.text, replace_input.text)
|
||||||
|
else:
|
||||||
|
code_edit.text = code_edit.text.replacen(input.text, replace_input.text)
|
||||||
|
search()
|
||||||
|
code_edit.text_changed.emit()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_replace_check_button_toggled(button_pressed: bool) -> void:
|
||||||
|
replace_panel.visible = button_pressed
|
||||||
|
if button_pressed:
|
||||||
|
replace_input.grab_focus()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_input_focus_entered() -> void:
|
||||||
|
if results.size() == 0:
|
||||||
|
search()
|
||||||
|
else:
|
||||||
|
self.result_index = result_index
|
||||||
|
|
||||||
|
|
||||||
|
func _on_match_case_check_box_toggled(button_pressed: bool) -> void:
|
||||||
|
search()
|
||||||
|
|
||||||
|
|
||||||
|
#endregion
|
@@ -0,0 +1 @@
|
|||||||
|
uid://cijsmjkq21cdq
|
87
addons/dialogue_manager/components/search_and_replace.tscn
Normal file
87
addons/dialogue_manager/components/search_and_replace.tscn
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
[gd_scene load_steps=2 format=3 uid="uid://gr8nakpbrhby"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://cijsmjkq21cdq" path="res://addons/dialogue_manager/components/search_and_replace.gd" id="1_8oj1f"]
|
||||||
|
|
||||||
|
[node name="SearchAndReplace" type="VBoxContainer"]
|
||||||
|
visible = false
|
||||||
|
anchors_preset = 10
|
||||||
|
anchor_right = 1.0
|
||||||
|
offset_bottom = 31.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
script = ExtResource("1_8oj1f")
|
||||||
|
|
||||||
|
[node name="Search" type="HBoxContainer" parent="."]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="Input" type="LineEdit" parent="Search"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
placeholder_text = "Text to search for"
|
||||||
|
metadata/_edit_use_custom_anchors = true
|
||||||
|
|
||||||
|
[node name="MatchCaseCheckBox" type="CheckBox" parent="Search"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Match case"
|
||||||
|
|
||||||
|
[node name="VSeparator" type="VSeparator" parent="Search"]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="PreviousButton" type="Button" parent="Search"]
|
||||||
|
layout_mode = 2
|
||||||
|
tooltip_text = "Previous"
|
||||||
|
flat = true
|
||||||
|
|
||||||
|
[node name="ResultLabel" type="Label" parent="Search"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "0 of 0"
|
||||||
|
|
||||||
|
[node name="NextButton" type="Button" parent="Search"]
|
||||||
|
layout_mode = 2
|
||||||
|
tooltip_text = "Next"
|
||||||
|
flat = true
|
||||||
|
|
||||||
|
[node name="VSeparator2" type="VSeparator" parent="Search"]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="ReplaceCheckButton" type="CheckButton" parent="Search"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Replace"
|
||||||
|
|
||||||
|
[node name="Replace" type="HBoxContainer" parent="."]
|
||||||
|
visible = false
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="ReplaceLabel" type="Label" parent="Replace"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Replace with:"
|
||||||
|
|
||||||
|
[node name="Input" type="LineEdit" parent="Replace"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
|
||||||
|
[node name="ReplaceButton" type="Button" parent="Replace"]
|
||||||
|
layout_mode = 2
|
||||||
|
disabled = true
|
||||||
|
text = "Replace"
|
||||||
|
flat = true
|
||||||
|
|
||||||
|
[node name="ReplaceAllButton" type="Button" parent="Replace"]
|
||||||
|
layout_mode = 2
|
||||||
|
disabled = true
|
||||||
|
text = "Replace all"
|
||||||
|
flat = true
|
||||||
|
|
||||||
|
[connection signal="theme_changed" from="." to="." method="_on_search_and_replace_theme_changed"]
|
||||||
|
[connection signal="visibility_changed" from="." to="." method="_on_search_and_replace_visibility_changed"]
|
||||||
|
[connection signal="focus_entered" from="Search/Input" to="." method="_on_input_focus_entered"]
|
||||||
|
[connection signal="gui_input" from="Search/Input" to="." method="_on_input_gui_input"]
|
||||||
|
[connection signal="text_changed" from="Search/Input" to="." method="_on_input_text_changed"]
|
||||||
|
[connection signal="toggled" from="Search/MatchCaseCheckBox" to="." method="_on_match_case_check_box_toggled"]
|
||||||
|
[connection signal="pressed" from="Search/PreviousButton" to="." method="_on_previous_button_pressed"]
|
||||||
|
[connection signal="pressed" from="Search/NextButton" to="." method="_on_next_button_pressed"]
|
||||||
|
[connection signal="toggled" from="Search/ReplaceCheckButton" to="." method="_on_replace_check_button_toggled"]
|
||||||
|
[connection signal="focus_entered" from="Replace/Input" to="." method="_on_input_focus_entered"]
|
||||||
|
[connection signal="gui_input" from="Replace/Input" to="." method="_on_input_gui_input"]
|
||||||
|
[connection signal="pressed" from="Replace/ReplaceButton" to="." method="_on_replace_button_pressed"]
|
||||||
|
[connection signal="pressed" from="Replace/ReplaceAllButton" to="." method="_on_replace_all_button_pressed"]
|
69
addons/dialogue_manager/components/title_list.gd
vendored
Normal file
69
addons/dialogue_manager/components/title_list.gd
vendored
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
@tool
|
||||||
|
extends VBoxContainer
|
||||||
|
|
||||||
|
signal title_selected(title: String)
|
||||||
|
|
||||||
|
|
||||||
|
const DialogueConstants = preload("../constants.gd")
|
||||||
|
|
||||||
|
|
||||||
|
@onready var filter_edit: LineEdit = $FilterEdit
|
||||||
|
@onready var list: ItemList = $List
|
||||||
|
|
||||||
|
var titles: PackedStringArray:
|
||||||
|
set(next_titles):
|
||||||
|
titles = next_titles
|
||||||
|
apply_filter()
|
||||||
|
get:
|
||||||
|
return titles
|
||||||
|
|
||||||
|
var filter: String:
|
||||||
|
set(next_filter):
|
||||||
|
filter = next_filter
|
||||||
|
apply_filter()
|
||||||
|
get:
|
||||||
|
return filter
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
apply_theme()
|
||||||
|
|
||||||
|
filter_edit.placeholder_text = DialogueConstants.translate(&"titles_list.filter")
|
||||||
|
|
||||||
|
|
||||||
|
func select_title(title: String) -> void:
|
||||||
|
list.deselect_all()
|
||||||
|
for i in range(0, list.get_item_count()):
|
||||||
|
if list.get_item_text(i) == title.strip_edges():
|
||||||
|
list.select(i)
|
||||||
|
|
||||||
|
|
||||||
|
func apply_filter() -> void:
|
||||||
|
list.clear()
|
||||||
|
for title in titles:
|
||||||
|
if filter == "" or filter.to_lower() in title.to_lower():
|
||||||
|
list.add_item(title.strip_edges())
|
||||||
|
|
||||||
|
|
||||||
|
func apply_theme() -> void:
|
||||||
|
if is_instance_valid(filter_edit):
|
||||||
|
filter_edit.right_icon = get_theme_icon("Search", "EditorIcons")
|
||||||
|
if is_instance_valid(list):
|
||||||
|
list.add_theme_stylebox_override("panel", get_theme_stylebox("panel", "Panel"))
|
||||||
|
|
||||||
|
|
||||||
|
### Signals
|
||||||
|
|
||||||
|
|
||||||
|
func _on_theme_changed() -> void:
|
||||||
|
apply_theme()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_filter_edit_text_changed(new_text: String) -> void:
|
||||||
|
self.filter = new_text
|
||||||
|
|
||||||
|
|
||||||
|
func _on_list_item_clicked(index: int, at_position: Vector2, mouse_button_index: int) -> void:
|
||||||
|
if mouse_button_index == MOUSE_BUTTON_LEFT:
|
||||||
|
var title = list.get_item_text(index)
|
||||||
|
title_selected.emit(title)
|
1
addons/dialogue_manager/components/title_list.gd.uid
Normal file
1
addons/dialogue_manager/components/title_list.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://d0k2wndjj0ifm
|
27
addons/dialogue_manager/components/title_list.tscn
Normal file
27
addons/dialogue_manager/components/title_list.tscn
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
[gd_scene load_steps=2 format=3 uid="uid://ctns6ouwwd68i"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://d0k2wndjj0ifm" path="res://addons/dialogue_manager/components/title_list.gd" id="1_5qqmd"]
|
||||||
|
|
||||||
|
[node name="TitleList" type="VBoxContainer"]
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
size_flags_vertical = 3
|
||||||
|
script = ExtResource("1_5qqmd")
|
||||||
|
|
||||||
|
[node name="FilterEdit" type="LineEdit" parent="."]
|
||||||
|
layout_mode = 2
|
||||||
|
placeholder_text = "Filter titles"
|
||||||
|
clear_button_enabled = true
|
||||||
|
|
||||||
|
[node name="List" type="ItemList" parent="."]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_vertical = 3
|
||||||
|
allow_reselect = true
|
||||||
|
|
||||||
|
[connection signal="theme_changed" from="." to="." method="_on_theme_changed"]
|
||||||
|
[connection signal="text_changed" from="FilterEdit" to="." method="_on_filter_edit_text_changed"]
|
||||||
|
[connection signal="item_clicked" from="List" to="." method="_on_list_item_clicked"]
|
125
addons/dialogue_manager/components/update_button.gd
vendored
Normal file
125
addons/dialogue_manager/components/update_button.gd
vendored
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
@tool
|
||||||
|
extends Button
|
||||||
|
|
||||||
|
const DialogueConstants = preload("../constants.gd")
|
||||||
|
const DialogueSettings = preload("../settings.gd")
|
||||||
|
|
||||||
|
const REMOTE_RELEASES_URL = "https://api.github.com/repos/nathanhoad/godot_dialogue_manager/releases"
|
||||||
|
|
||||||
|
|
||||||
|
@onready var http_request: HTTPRequest = $HTTPRequest
|
||||||
|
@onready var download_dialog: AcceptDialog = $DownloadDialog
|
||||||
|
@onready var download_update_panel = $DownloadDialog/DownloadUpdatePanel
|
||||||
|
@onready var needs_reload_dialog: AcceptDialog = $NeedsReloadDialog
|
||||||
|
@onready var update_failed_dialog: AcceptDialog = $UpdateFailedDialog
|
||||||
|
@onready var timer: Timer = $Timer
|
||||||
|
|
||||||
|
var needs_reload: bool = false
|
||||||
|
|
||||||
|
# A lambda that gets called just before refreshing the plugin. Return false to stop the reload.
|
||||||
|
var on_before_refresh: Callable = func(): return true
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
hide()
|
||||||
|
apply_theme()
|
||||||
|
|
||||||
|
# Check for updates on GitHub
|
||||||
|
check_for_update()
|
||||||
|
|
||||||
|
# Check again every few hours
|
||||||
|
timer.start(60 * 60 * 12)
|
||||||
|
|
||||||
|
|
||||||
|
# Convert a version number to an actually comparable number
|
||||||
|
func version_to_number(version: String) -> int:
|
||||||
|
var bits = version.split(".")
|
||||||
|
return bits[0].to_int() * 1000000 + bits[1].to_int() * 1000 + bits[2].to_int()
|
||||||
|
|
||||||
|
|
||||||
|
func apply_theme() -> void:
|
||||||
|
var color: Color = get_theme_color("success_color", "Editor")
|
||||||
|
|
||||||
|
if needs_reload:
|
||||||
|
color = get_theme_color("error_color", "Editor")
|
||||||
|
icon = get_theme_icon("Reload", "EditorIcons")
|
||||||
|
add_theme_color_override("icon_normal_color", color)
|
||||||
|
add_theme_color_override("icon_focus_color", color)
|
||||||
|
add_theme_color_override("icon_hover_color", color)
|
||||||
|
|
||||||
|
add_theme_color_override("font_color", color)
|
||||||
|
add_theme_color_override("font_focus_color", color)
|
||||||
|
add_theme_color_override("font_hover_color", color)
|
||||||
|
|
||||||
|
|
||||||
|
func check_for_update() -> void:
|
||||||
|
if DialogueSettings.get_user_value("check_for_updates", true):
|
||||||
|
http_request.request(REMOTE_RELEASES_URL)
|
||||||
|
|
||||||
|
|
||||||
|
### Signals
|
||||||
|
|
||||||
|
|
||||||
|
func _on_http_request_request_completed(result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray) -> void:
|
||||||
|
if result != HTTPRequest.RESULT_SUCCESS: return
|
||||||
|
|
||||||
|
var current_version: String = Engine.get_meta("DialogueManagerPlugin").get_version()
|
||||||
|
|
||||||
|
# Work out the next version from the releases information on GitHub
|
||||||
|
var response = JSON.parse_string(body.get_string_from_utf8())
|
||||||
|
if typeof(response) != TYPE_ARRAY: return
|
||||||
|
|
||||||
|
# GitHub releases are in order of creation, not order of version
|
||||||
|
var versions = (response as Array).filter(func(release):
|
||||||
|
var version: String = release.tag_name.substr(1)
|
||||||
|
var major_version: int = version.split(".")[0].to_int()
|
||||||
|
var current_major_version: int = current_version.split(".")[0].to_int()
|
||||||
|
return major_version == current_major_version and version_to_number(version) > version_to_number(current_version)
|
||||||
|
)
|
||||||
|
if versions.size() > 0:
|
||||||
|
download_update_panel.next_version_release = versions[0]
|
||||||
|
text = DialogueConstants.translate(&"update.available").format({ version = versions[0].tag_name.substr(1) })
|
||||||
|
show()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_update_button_pressed() -> void:
|
||||||
|
if needs_reload:
|
||||||
|
var will_refresh = on_before_refresh.call()
|
||||||
|
if will_refresh:
|
||||||
|
EditorInterface.restart_editor(true)
|
||||||
|
else:
|
||||||
|
var scale: float = EditorInterface.get_editor_scale()
|
||||||
|
download_dialog.min_size = Vector2(300, 250) * scale
|
||||||
|
download_dialog.popup_centered()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_download_dialog_close_requested() -> void:
|
||||||
|
download_dialog.hide()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_download_update_panel_updated(updated_to_version: String) -> void:
|
||||||
|
download_dialog.hide()
|
||||||
|
|
||||||
|
needs_reload_dialog.dialog_text = DialogueConstants.translate(&"update.needs_reload")
|
||||||
|
needs_reload_dialog.ok_button_text = DialogueConstants.translate(&"update.reload_ok_button")
|
||||||
|
needs_reload_dialog.cancel_button_text = DialogueConstants.translate(&"update.reload_cancel_button")
|
||||||
|
needs_reload_dialog.popup_centered()
|
||||||
|
|
||||||
|
needs_reload = true
|
||||||
|
text = DialogueConstants.translate(&"update.reload_project")
|
||||||
|
apply_theme()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_download_update_panel_failed() -> void:
|
||||||
|
download_dialog.hide()
|
||||||
|
update_failed_dialog.dialog_text = DialogueConstants.translate(&"update.failed")
|
||||||
|
update_failed_dialog.popup_centered()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_needs_reload_dialog_confirmed() -> void:
|
||||||
|
EditorInterface.restart_editor(true)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_timer_timeout() -> void:
|
||||||
|
if not needs_reload:
|
||||||
|
check_for_update()
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user