Compare commits

..

59 Commits

Author SHA1 Message Date
5b9de11e5a Add Resume/Exit buttons to pause menu, handle logic for returning to main menu and starting a new game 2025-12-05 20:05:28 -08:00
678916be89 Enemy viewer revamp, loading screen improvement 2025-12-05 16:30:13 -08:00
Pal
f39bd8ecdb Added many player.tscn animated sprite2Ds 2025-12-04 20:21:24 -08:00
Pal
76f4adc5be gallery and options rendered images set to linear filterling 2025-12-04 01:18:03 -08:00
Pal
95227946d1 Merge branch 'main' of https://git.zenny.quest/zenayi/GameJamDungeon 2025-12-03 23:54:32 -08:00
Pal
1ee3e97f85 Overworld Gate object added, visual assetts added, test animation added 2025-12-03 23:54:20 -08:00
6f90a0985a General UI Work 2025-12-03 23:21:29 -08:00
34742d568e Play test animation 2025-12-03 22:29:24 -08:00
Pal
3c369f79f7 Added shadows to enemies 2025-12-01 21:05:33 -08:00
Pal
fe45f0bcf2 Screen renders added, fonts added, altar model updated 2025-12-01 20:13:15 -08:00
edff41af22 Edit dialogue balloons 2025-12-01 01:01:33 -08:00
4ee4e02a51 Take damage/Screen shake 2025-11-30 22:35:26 -08:00
8f8cc217dc Death animation 2025-11-30 21:37:31 -08:00
c491ea5050 Gallery Menu initial implementation 2025-11-30 20:17:08 -08:00
Pal
725547d388 Late November Merge 2025-11-26 02:38:43 -08:00
Pal
539430d112 VFX Assetts Added 2025-11-26 02:37:45 -08:00
46402401b4 Add basic implementation for footsteps
Add disengage to eden pillar
Fix level up sound effect trigger
2025-11-26 02:33:03 -08:00
ed9e611fd9 Work SFX work
Fix up Eden Pillar behavior
2025-11-26 02:12:24 -08:00
db7a1df1f7 Started implementing SFX
Fixed Shield animation jumps and secondary attack
Fixed demon wall stone behavior
Made overworld ambient sounds unpausable
2025-11-25 03:04:07 -08:00
Pal
3e8c11d55d VFX assetts imported 2025-11-22 15:55:13 -08:00
613fc3bf60 Setup player screen FX 2025-11-19 00:05:20 -08:00
f69e219643 Add game tab to options menu, fix focus of GUI elements, add Delete option for save file 2025-11-18 20:28:43 -08:00
Pal
0afbf38bf9 Merge branch 'main' of https://git.zenny.quest/zenayi/GameJamDungeon 2025-11-17 19:18:01 -08:00
Pal
23fdf7309d VFX Assetts Added 2025-11-17 19:17:52 -08:00
fcffdb3b35 Fix shadows 2025-11-16 20:07:44 -08:00
7210133330 Add projectiles to various enemies 2025-11-16 16:35:48 -08:00
Pal
e7bae342c9 SFX additions 2025-11-16 16:12:21 -08:00
Pal
c22fde3bb5 November Merge 2025-11-12 16:14:43 -08:00
Pal
7cb5b20293 weird things I cant undo 2025-11-12 16:14:14 -08:00
a5846e08dc Rewrite and simplify Inventory Menu, various fixes for item effects 2025-11-04 01:12:16 -08:00
7b7fc910bd Button remapping work 2025-11-03 02:48:05 -08:00
9fc875eda5 Add options menu persistence 2025-10-30 23:59:05 -07:00
Pal
dc3c458414 merge 2025-10-30 15:59:01 -07:00
Pal
ba38c6443c merging stuff 2025-10-30 15:58:19 -07:00
575a565a2c Fix spawn point for player, add flee behavior for gold sproingy 2025-10-29 11:03:07 -07:00
21d8c4770d Big fix patch for enemy behaviors 2025-10-29 00:35:15 -07:00
Pal
1a77695155 Merger 2025-10-27 20:10:58 -07:00
Pal
11cbb8c4e5 SFX batch 1 import and some implementation, minimap icons added 2025-10-27 20:08:17 -07:00
eed50bc04e Fix standard enemy logic 2025-10-27 19:41:03 -07:00
2d55ae9cc4 Add secondary attack options 2025-10-27 18:03:14 -07:00
0b909e4e7e Teleport skill added for Chinthe 2025-10-27 17:48:27 -07:00
7e6dca1c29 Rework game over logic and game initialization 2025-10-27 15:04:01 -07:00
720696aed0 Try to get Chinthe to stop jumping upon reaching player 2025-10-26 15:05:18 -07:00
286c221530 Improvements to save and loading
Improvements to Chinthe animation logic
Fix broken Godot Tool system and just use a more manual approach to setting map nodes
Remove ItemDatabase from individual room scenes
2025-10-24 01:33:18 -07:00
f5360adbf1 Basic projectile implementation (put sample version in FilthEaterModelView) 2025-10-23 18:50:05 -07:00
bc161a58b3 Additional refactoring and fixing of equipment data
Add attack data to enemy attacks (might need to rework a little bit for primary/secondary attacks)
2025-10-23 00:05:44 -07:00
f0c4e65783 Refactor stats 2025-10-22 16:24:07 -07:00
6ec45c4805 Refactor Player class to use components, also use components in Enemy class types and fiddle with boss structure 2025-10-22 02:41:08 -07:00
44fd8c82b0 Rework enemy behavior (still in progress but shouldn't crash) 2025-10-20 19:24:50 -07:00
Pal
20b659681a More enemy VFX added and attacks setup, old Shield of Heaven frames removed, player camera rotation fixed 2025-10-15 20:17:04 -07:00
Pal
2786c95c22 re-rendered all frames for chinthe, added teleport in animation as well as VFX for attack and teleport. rough draft of a setup for map glow. 2025-10-09 03:23:27 -07:00
Pal
b0f6aa176e Merge branch 'main' of https://git.zenny.quest/zenayi/GameJamDungeon 2025-10-08 00:59:25 -07:00
e29bef51c5 Move player during load screens to prevent activating minimaps for current position 2025-10-07 23:33:02 -07:00
Pal
9ed5f8600c Area 2 all rooms preliminary lighting setup, a few with unapplied scale but maybe fine. 2025-10-07 21:38:27 -07:00
Pal
4c5281c852 Merge branch 'main' of https://git.zenny.quest/zenayi/GameJamDungeon 2025-10-07 01:18:25 -07:00
Pal
8e030aabcd OW tweaks 2025-10-07 01:18:17 -07:00
d692d5c705 Disable VT timer by default 2025-10-07 01:02:04 -07:00
Pal
9bfb410f99 Merge branch 'main' of https://git.zenny.quest/zenayi/GameJamDungeon 2025-10-07 00:30:31 -07:00
Pal
18cf7e062c Overworld Fixes 2025-10-07 00:30:11 -07:00
3362 changed files with 85061 additions and 21892 deletions

5
.gitignore vendored
View File

@@ -715,3 +715,8 @@ healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
/Zennysoft.Game.Ma/src/map/dungeon/models/Area 2/Puer/A2-Puer.glb.import
/Zennysoft.Game.Ma/src/audio/AMB/amb_beach.wav.import
/Zennysoft.Game.Ma/src/audio/AMB/amb_perlin.wav.import
/Zennysoft.Game.Ma/src/audio/AMB/amb_white_noise.wav.import
/Zennysoft.Game.Ma/src/audio/AMB/amb_wind_loop_altar.wav.import

View File

@@ -10,6 +10,8 @@ public interface IAppRepo : IDisposable
event Action? MainMenuEntered;
event Action? DataViewerExited;
void SkipSplashScreen();
void OnMainMenuEntered();
@@ -19,4 +21,6 @@ public interface IAppRepo : IDisposable
void OnExitGame();
void OnGameOver();
void OnDataViewerExited();
}

View File

@@ -0,0 +1,5 @@
namespace Zennysoft.Game.Abstractions.Entity;
public interface IBehavior
{
}

View File

@@ -0,0 +1,11 @@
namespace Zennysoft.Game.Abstractions.Entity
{
public interface IAction
{
public Task PerformAction();
}
public interface IAction<T>
{
public Task PerformAction(T arg);
}
}

View File

@@ -1,6 +0,0 @@
namespace Zennysoft.Game.Abstractions;
public interface ICanPatrol
{
public void Patrol();
}

View File

@@ -1,15 +1,18 @@
using System.Text.Json;
using System.Text.Json.Serialization.Metadata;
using System.Text.Json.Serialization.Metadata;
namespace Zennysoft.Game.Abstractions;
public interface ISaveFileManager<T>
public interface ISaveFileManager
{
public Task WriteToFile(T gameData, params IJsonTypeInfoResolver?[] resolvers);
public Task WriteToFile<T>(T gameData, params IJsonTypeInfoResolver?[] resolvers);
public Task WriteToFile(T gameData, string filePath, params IJsonTypeInfoResolver?[] resolvers);
public Task WriteToFile<T>(T gameData, string filePath, params IJsonTypeInfoResolver?[] resolvers);
public Task<T?> ReadFromFile(params IJsonTypeInfoResolver?[] resolvers);
public Task<T?> ReadFromFile<T>(params IJsonTypeInfoResolver?[] resolvers);
public Task<T?> ReadFromFile(string filePath, params IJsonTypeInfoResolver?[] resolvers);
public Task<T?> ReadFromFile<T>(string filePath, params IJsonTypeInfoResolver?[] resolvers);
public void DeleteSaveData(string filePath);
public void DeleteSaveData();
}

View File

@@ -9,10 +9,9 @@ using Zennysoft.Game.Abstractions;
namespace Zennysoft.Game.Implementation;
public class SaveFileManager<T> : ISaveFileManager<T>
public class SaveFileManager : ISaveFileManager
{
private readonly IFileSystem _fileSystem;
private readonly JsonSerializerOptions _jsonOptions;
private string _defaultSaveLocation;
public const string DEFAULT_SAVE_FILE_NAME = "game.json";
@@ -23,27 +22,17 @@ public class SaveFileManager<T> : ISaveFileManager<T>
GodotSerialization.Setup();
Serializer.AddConverter(new Texture2DConverter());
var upgradeDependencies = new Blackboard();
_jsonOptions = new JsonSerializerOptions
{
Converters = {
new SerializableTypeConverter(upgradeDependencies)
},
WriteIndented = true
};
}
public Task<T?> ReadFromFile(params IJsonTypeInfoResolver?[] resolvers)
public Task<T?> ReadFromFile<T>(params IJsonTypeInfoResolver?[] resolvers)
{
if (!_fileSystem.File.Exists(_defaultSaveLocation))
throw new FileNotFoundException();
return ReadFromFile(_defaultSaveLocation, resolvers);
return ReadFromFile<T>(_defaultSaveLocation, resolvers);
}
public async Task<T?> ReadFromFile(string filePath, params IJsonTypeInfoResolver?[] resolvers)
public async Task<T?> ReadFromFile<T>(string filePath, params IJsonTypeInfoResolver?[] resolvers)
{
if (!_fileSystem.File.Exists(filePath))
throw new FileNotFoundException();
@@ -51,20 +40,42 @@ public class SaveFileManager<T> : ISaveFileManager<T>
var json = await _fileSystem.File.ReadAllTextAsync(filePath);
var resolver = new SerializableTypeResolver();
_jsonOptions.TypeInfoResolver = JsonTypeInfoResolver.Combine([resolver, .. resolvers]);
return JsonSerializer.Deserialize<T?>(json, _jsonOptions);
var upgradeDependencies = new Blackboard();
var jsonOptions = new JsonSerializerOptions
{
Converters = {
new SerializableTypeConverter(upgradeDependencies)
},
WriteIndented = true
};
jsonOptions.TypeInfoResolver = JsonTypeInfoResolver.Combine([resolver, .. resolvers]);
return JsonSerializer.Deserialize<T?>(json, jsonOptions);
}
public Task WriteToFile(T gameData, params IJsonTypeInfoResolver?[] resolvers)
public Task WriteToFile<T>(T gameData, params IJsonTypeInfoResolver?[] resolvers)
{
return WriteToFile(gameData, _defaultSaveLocation, resolvers);
}
public async Task WriteToFile(T gameData, string filePath, params IJsonTypeInfoResolver?[] resolvers)
public async Task WriteToFile<T>(T gameData, string filePath, params IJsonTypeInfoResolver?[] resolvers)
{
var resolver = new SerializableTypeResolver();
_jsonOptions.TypeInfoResolver = JsonTypeInfoResolver.Combine([resolver, .. resolvers]);
var json = JsonSerializer.Serialize(gameData, _jsonOptions);
var upgradeDependencies = new Blackboard();
var jsonOptions = new JsonSerializerOptions
{
Converters = {
new SerializableTypeConverter(upgradeDependencies)
},
WriteIndented = true
};
jsonOptions.TypeInfoResolver = JsonTypeInfoResolver.Combine([resolver, .. resolvers]);
var json = JsonSerializer.Serialize(gameData, jsonOptions);
await _fileSystem.File.WriteAllTextAsync(filePath, json);
}
public void DeleteSaveData() => DeleteSaveData(_defaultSaveLocation);
public void DeleteSaveData(string filePath) => _fileSystem.File.Delete(filePath);
}

View File

@@ -8,6 +8,7 @@ public class AppRepo : IAppRepo
public event Action? MainMenuEntered;
public event Action? GameEntered;
public event Action? GameExited;
public event Action? DataViewerExited;
private bool _disposedValue;
@@ -21,6 +22,8 @@ public class AppRepo : IAppRepo
public void OnGameOver() => GameExited?.Invoke();
public void OnDataViewerExited() => DataViewerExited?.Invoke();
protected void Dispose(bool disposing)
{
if (!_disposedValue)

View File

@@ -24,6 +24,8 @@ public partial class AppLogic
public readonly record struct EnemyViewerOpened;
public readonly record struct EnemyViewerExited;
public readonly record struct GalleryOpened;
}
}

View File

@@ -24,6 +24,8 @@ public partial class AppLogic
public readonly record struct ShowMainMenu;
public readonly record struct CloseGame;
public readonly record struct ExitGame;
public readonly record struct GameOver;
@@ -32,6 +34,8 @@ public partial class AppLogic
public readonly record struct EnemyViewerOpened;
public readonly record struct EnemyViewerExited;
public readonly record struct GalleryOpened;
}
}

View File

@@ -9,7 +9,7 @@ public partial class AppLogic
public partial record State
{
[Meta]
public partial record EnemyViewer : State
public partial record EnemyViewer : State, IGet<Input.EnemyViewerExited>
{
public EnemyViewer()
{
@@ -18,7 +18,13 @@ public partial class AppLogic
{
Output(new Output.EnemyViewerOpened());
});
this.OnExit(() =>
{
Output(new Output.EnemyViewerExited());
});
}
public Transition On(in Input.EnemyViewerExited input) => To<MainMenu>();
}
}
}

View File

@@ -9,7 +9,7 @@ public partial class AppLogic
public partial record State
{
[Meta]
public partial record GameStarted : State
public partial record GameStarted : State, IGet<Input.QuitGame>
{
public GameStarted()
{
@@ -26,6 +26,11 @@ public partial class AppLogic
OnDetach(() => Get<IAppRepo>().GameExited -= OnGameExited);
}
public Transition On(in Input.QuitGame input)
{
Output(new Output.CloseGame());
return To<MainMenu>();
}
public void OnGameExited() => Input(new Input.QuitGame());
}
}

View File

@@ -7,7 +7,7 @@ public partial class AppLogic
public partial record State
{
[Meta]
public partial record MainMenu : State, IGet<Input.NewGame>, IGet<Input.EnemyViewerOpened>
public partial record MainMenu : State, IGet<Input.NewGame>, IGet<Input.EnemyViewerOpened>, IGet<Input.QuitGame>
{
public MainMenu()
{
@@ -16,6 +16,11 @@ public partial class AppLogic
public Transition On(in Input.NewGame input) => To<GameStarted>();
public Transition On(in Input.EnemyViewerOpened input) => To<EnemyViewer>();
public Transition On(in Input.QuitGame input)
{
Output(new Output.ExitGame());
return ToSelf();
}
}
}
}

View File

@@ -1,30 +0,0 @@
using Godot;
namespace Zennysoft.Ma.Adapter;
public partial class AudioManager : Node
{
#pragma warning disable IDE0044 // Add readonly modifier
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
private static string _sfxPath = $"res://src/audio/sfx";
private AudioStreamPlayer _audioPlayer;
private IDictionary<SoundEffects, AudioStream> _sfxDictionary;
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
#pragma warning restore IDE0044 // Add readonly modifier
public override void _Ready()
{
_audioPlayer = new AudioStreamPlayer();
_sfxDictionary = new Dictionary<SoundEffects, AudioStream>();
var soundEffects = Enum.GetValues(typeof(SoundEffects));
foreach (var effect in soundEffects)
_sfxDictionary.Add((SoundEffects)effect, GD.Load<AudioStream>(_sfxPath + effect + ".ogg"));
}
public void Play(SoundEffects soundEffect)
{
_sfxDictionary.TryGetValue(soundEffect, out var stream);
_audioPlayer.Stream = stream;
_audioPlayer.Play();
}
}

View File

@@ -1,14 +0,0 @@
namespace Zennysoft.Ma.Adapter;
public enum SoundEffects
{
Cancel,
Equip,
Heal,
MenuBack,
MoveThroughOptions,
PlayerAttack,
PlayerHitWall,
Sort,
Unequip
}

View File

@@ -3,50 +3,31 @@ using Zennysoft.Ma.Adapter.Entity;
namespace Zennysoft.Ma.Adapter
{
public class DamageCalculator : IDamageCalculator
public static class DamageCalculator
{
public double CalculateDamage(double damage,
ElementType elementType,
double defense,
ElementalResistanceSet elementalResistanceSet,
bool isCriticalHit = false,
bool ignoreDefense = false,
bool ignoreElementalResistance = false)
public static int CalculateDamage(AttackData damage, double defense, ElementalResistanceSet elementalResistanceSet)
{
var calculatedDamage = damage;
if (!ignoreElementalResistance)
calculatedDamage = CalculateElementalResistance(calculatedDamage, elementType, elementalResistanceSet);
if (!ignoreDefense)
var calculatedDamage = damage.BaseDamage;
if (!damage.IgnoreDefense)
calculatedDamage = CalculateDefenseResistance(calculatedDamage, defense);
if (isCriticalHit)
calculatedDamage *= 2;
if (!damage.IgnoreElementalResistance)
calculatedDamage = CalculateElementalResistance(calculatedDamage, elementalResistanceSet.ElementalResistance[damage.ElementType]);
return calculatedDamage;
return Mathf.Max(1, calculatedDamage);
}
private static double CalculateDefenseResistance(double incomingDamage, double defense)
private static int CalculateDefenseResistance(int incomingDamage, double defense)
{
return Mathf.Max(incomingDamage - defense, 0.0);
var result = incomingDamage - (int)(incomingDamage * (defense / 100));
return result;
}
private static double CalculateElementalResistance(
double incomingDamage,
ElementType incomingElementType,
ElementalResistanceSet elementalResistanceSet)
private static int CalculateElementalResistance(
int incomingDamage,
double elementalResistance)
{
var resistance = elementalResistanceSet.ElementalResistance[incomingElementType];
return Mathf.Max(incomingDamage - (incomingDamage * resistance), 0.0);
var result = incomingDamage - (int)(incomingDamage * (elementalResistance / 100));
return result;
}
}
public interface IDamageCalculator
{
public double CalculateDamage(double damage,
ElementType elementType,
double defense,
ElementalResistanceSet elementalResistanceSet,
bool isCriticalHit = false,
bool ignoreDefense = false,
bool ignoreElementalResistance = false);
}
}

View File

@@ -0,0 +1,18 @@
using Chickensoft.Collections;
namespace Zennysoft.Ma.Adapter;
public interface IAttackComponent : IEntityComponent
{
public IAutoProp<int> CurrentAttack { get; }
public IAutoProp<int> MaximumAttack { get; }
public void Restore(int restoreAmount);
public void Reduce(int reduceAmount);
public void SetAttack(int attack);
public void RaiseMaximumAttack(int raiseAmount);
}

View File

@@ -0,0 +1,18 @@
using Chickensoft.Collections;
namespace Zennysoft.Ma.Adapter;
public interface IDefenseComponent : IEntityComponent
{
public IAutoProp<int> CurrentDefense { get; }
public IAutoProp<int> MaximumDefense { get; }
public void Restore(int restoreAmount);
public void Reduce(int reduceAmount);
public void SetDefense(int attack);
public void RaiseMaximumDefense(int raiseAmount);
}

View File

@@ -0,0 +1,6 @@
namespace Zennysoft.Ma.Adapter;
public interface IEntityComponent
{
public void Reset();
}

View File

@@ -0,0 +1,33 @@
using Chickensoft.Collections;
using Godot;
using Zennysoft.Ma.Adapter.Entity;
namespace Zennysoft.Ma.Adapter;
public interface IEquipmentComponent : IEntityComponent
{
public IAutoProp<EquipableItem> EquippedWeapon { get; }
public IAutoProp<EquipableItem> EquippedArmor { get; }
public IAutoProp<EquipableItem> EquippedAccessory { get; }
public void Equip(EquipableItem equipable);
public void Unequip(EquipableItem equipable);
public bool IsItemEquipped(InventoryItem item);
public int BonusAttack { get; }
public int BonusDefense { get; }
public int BonusHP { get; }
public int BonusVT { get; }
public int BonusLuck { get; }
public ElementalResistanceSet ElementalResistance { get; }
public event Action<EquipableItem> EquipmentChanged;
}

View File

@@ -0,0 +1,18 @@
using Chickensoft.Collections;
namespace Zennysoft.Ma.Adapter;
public interface IExperiencePointsComponent : IEntityComponent
{
public IAutoProp<int> CurrentExp { get; }
public IAutoProp<int> ExpToNextLevel { get; }
public IAutoProp<double> ExpGainRate { get; }
public IAutoProp<int> Level { get; }
public void Gain(int baseExpGain);
public void LevelUp();
}

View File

@@ -0,0 +1,25 @@
using Chickensoft.Collections;
namespace Zennysoft.Ma.Adapter;
public interface IHealthComponent : IEntityComponent
{
public IAutoProp<int> CurrentHP { get; }
public IAutoProp<int> MaximumHP { get; }
public event Action? HealthReachedZero;
public event Action? DamageTaken;
public bool AtFullHealth { get; }
public void Heal(int healAmount);
public void Damage(int damageAmount);
public void SetCurrentHealth(int health);
public void SetMaximumHealth(int health);
public void RaiseMaximumHP(int raiseAmount, bool restoreHP = false);
}

View File

@@ -0,0 +1,10 @@
using Chickensoft.Collections;
namespace Zennysoft.Ma.Adapter;
public interface ILuckComponent : IEntityComponent
{
public IAutoProp<int> Luck { get; }
public void IncreaseLuck(int value);
}

View File

@@ -0,0 +1,22 @@
using Chickensoft.Collections;
namespace Zennysoft.Ma.Adapter;
public interface IVTComponent : IEntityComponent
{
public IAutoProp<int> CurrentVT { get; }
public IAutoProp<int> MaximumVT { get; }
public bool AtFullVT { get; }
public void Restore(int restoreAmount);
public void Reduce(int reduceAmount);
public void SetVT(int vt);
public void RaiseMaximumVT(int raiseAmount, bool restoreVT = true);
public void SetMaximumVT(int vt);
}

View File

@@ -0,0 +1,3 @@
namespace Zennysoft.Ma.Adapter;
public record AttackData(int BaseDamage, ElementType ElementType, bool IgnoreDefense = false, bool IgnoreElementalResistance = false);

View File

@@ -0,0 +1 @@
uid://ci7va4hsq6hyt

View File

@@ -0,0 +1 @@
uid://87d8kluait8y

View File

@@ -0,0 +1,9 @@
[gd_scene load_steps=2 format=3 uid="uid://c7e5g8l6wuph"]
[ext_resource type="Script" uid="uid://87d8kluait8y" path="res://src/enemy/behaviors/PatrolBehavior.cs" id="1_lobva"]
[node name="NavigationAgent" type="NavigationAgent3D"]
avoidance_enabled = true
debug_enabled = true
script = ExtResource("1_lobva")
_patrolSpeed = 100.0

View File

@@ -1,9 +1,14 @@
namespace Zennysoft.Ma.Adapter.Entity
using Chickensoft.Serialization;
namespace Zennysoft.Ma.Adapter.Entity
{
public record ElementalResistanceSet
{
[Save("elemental_resist_set")]
public Dictionary<ElementType, double> ElementalResistance { get; }
public static ElementalResistanceSet None => new ElementalResistanceSet(0, 0, 0, 0, 0);
public ElementalResistanceSet(double aeolicResistance, double hydricResistance, double igneousResistance, double ferrumResistance, double telluricResistance)
{
ElementalResistance = new Dictionary<ElementType, double>
@@ -16,5 +21,15 @@
{ ElementType.Telluric, telluricResistance },
};
}
public static ElementalResistanceSet operator +(ElementalResistanceSet left, ElementalResistanceSet right)
{
return new ElementalResistanceSet(
left.ElementalResistance[ElementType.Aeolic] + right.ElementalResistance[ElementType.Aeolic],
left.ElementalResistance[ElementType.Hydric] + right.ElementalResistance[ElementType.Hydric],
left.ElementalResistance[ElementType.Igneous] + right.ElementalResistance[ElementType.Igneous],
left.ElementalResistance[ElementType.Ferrum] + right.ElementalResistance[ElementType.Ferrum],
left.ElementalResistance[ElementType.Telluric] + right.ElementalResistance[ElementType.Telluric]);
}
}
}

View File

@@ -0,0 +1,39 @@
using Chickensoft.GodotNodeInterfaces;
using System.Collections.Immutable;
using Zennysoft.Game.Ma;
namespace Zennysoft.Ma.Adapter.Entity
{
public interface IEnemy : ICharacterBody3D
{
public void Activate();
public void Idle();
public void Die();
public void PerformAction();
public void ReturnToDefaultState();
public void OnAbsorb();
public void OnMorph();
public IDungeonRoom GetCurrentRoom(ImmutableList<IDungeonRoom> dungeonRooms);
public void MoveEnemyToNewRoom(IDungeonRoom newRoom);
public IHealthComponent HealthComponent { get; }
public IAttackComponent AttackComponent { get; }
public IDefenseComponent DefenseComponent { get; }
public int InitialHP { get; }
public int InitialAttack { get; }
public int InitialDefense { get; }
}
}

View File

@@ -0,0 +1,9 @@
using Godot;
namespace Zennysoft.Ma.Adapter.Entity
{
public interface IKnockbackable
{
void Knockback(float impulse, Vector3 direction);
}
}

View File

@@ -0,0 +1,22 @@
using Chickensoft.Introspection;
using Chickensoft.Serialization;
using Zennysoft.Ma.Adapter.Entity;
namespace Zennysoft.Ma.Adapter;
[Meta, Id("equipable_item")]
public abstract partial class EquipableItem : InventoryItem
{
[Save("bonus_attack_stats")]
public virtual int BonusAttack { get; }
[Save("bonus_defense_stats")]
public virtual int BonusDefense { get; }
[Save("bonus_hp_stats")]
public virtual int BonusHP { get; }
[Save("bonus_vt_stats")]
public virtual int BonusVT { get; }
[Save("bonus_luck_stats")]
public virtual int BonusLuck { get; }
[Save("bonus_elemental_resist_stats")]
public virtual ElementalResistanceSet ElementalResistance { get; } = new ElementalResistanceSet(0, 0, 0, 0, 0);
}

View File

@@ -16,7 +16,7 @@ public abstract partial class InventoryItem : Node3D
[Save("inventory_item_spawn_rate")]
public abstract float SpawnRate { get; }
[Save("inventory_item_throw_damage")]
public abstract double ThrowDamage { get; }
public abstract int ThrowDamage { get; }
[Save("inventory_item_throw_speed")]
public abstract float ThrowSpeed { get; }
[Save("inventory_item_tag")]

View File

@@ -5,5 +5,6 @@ public enum WeaponTag
None,
SelfDamage,
IgnoreAffinity,
IgnoreDefense,
Knockback,
}

View File

@@ -1,18 +0,0 @@
using Chickensoft.Introspection;
using Chickensoft.Serialization;
using Zennysoft.Game.Ma;
namespace Zennysoft.Ma.Adapter;
[Meta, Id("game_data")]
public partial record GameData
{
[Save("player_data")]
public required PlayerData PlayerData { get; init; }
[Save("map_data")]
public required MapData MapData { get; init; }
[Save("rescued_items")]
public required RescuedItemDatabase RescuedItems { get; init; }
}

View File

@@ -1,6 +1,7 @@
using Chickensoft.Collections;
using Godot;
using Zennysoft.Game.Abstractions;
using Zennysoft.Game.Implementation;
namespace Zennysoft.Ma.Adapter;
@@ -52,8 +53,6 @@ public interface IGameRepo : IDisposable
public void OnPlayerAttackedWall();
public void OnPlayerAttackedEnemy();
public void OnRestorativePickedUp(IHealthPack restorative);
public void CloseInventory();
@@ -146,11 +145,6 @@ public class GameRepo : IGameRepo
PlayerAttackedWall?.Invoke();
}
public void OnPlayerAttackedEnemy()
{
PlayerAttackedEnemy?.Invoke();
}
public void OnRestorativePickedUp(IHealthPack restorative)
{
RestorativePickedUp?.Invoke(restorative);

View File

@@ -8,14 +8,14 @@ public partial class GameState
public readonly record struct LoadGame;
public readonly record struct ContinueGame;
public readonly record struct ReturnToMainMenu;
public readonly record struct ExitGame;
public readonly record struct LoadNextFloor;
public readonly record struct InventoryButtonPressed;
public readonly record struct InteractButtonPressed;
public readonly record struct PauseButtonPressed;
public readonly record struct DebugButtonPressed;

View File

@@ -6,6 +6,8 @@ public partial class GameState
{
public readonly record struct InitializeGame;
public readonly record struct ExitGame;
public readonly record struct LoadGameFromFile;
public readonly record struct OpenInventoryMenu;

View File

@@ -16,11 +16,6 @@ public partial class GameState
OnAttach(() => Get<IGameRepo>().Pause());
}
public Transition On(in Input.ReturnToMainMenu input)
{
return To<MainMenu>();
}
public Transition On(in Input.UseTeleport input)
{
Output(new Output.OpenFloorExitScreen());

View File

@@ -8,7 +8,7 @@ public partial class GameState
public partial record State
{
[Meta, LogicBlock(typeof(State), Diagram = true)]
public partial record FloorExitScreen : State, IGet<Input.LoadNextFloor>, IGet<Input.ReturnToMainMenu>
public partial record FloorExitScreen : State, IGet<Input.LoadNextFloor>
{
public FloorExitScreen()
{
@@ -20,11 +20,6 @@ public partial class GameState
Output(new Output.LoadNextFloor());
return To<InGame>();
}
public Transition On(in Input.ReturnToMainMenu input)
{
return To<MainMenu>();
}
}
}
}

View File

@@ -8,18 +8,13 @@ public partial class GameState
public partial record State
{
[Meta, LogicBlock(typeof(State), Diagram = true)]
public partial record GameOver : State, IGet<Input.ContinueGame>, IGet<Input.ReturnToMainMenu>
public partial record GameOver : State, IGet<Input.NewGame>
{
public Transition On(in Input.ContinueGame input)
public Transition On(in Input.NewGame input)
{
Output(new Output.InitializeGame());
return To<InGame>();
}
public Transition On(in Input.ReturnToMainMenu input)
{
return To<MainMenu>();
}
}
}
}

View File

@@ -8,9 +8,9 @@ public partial class GameState
public partial record State
{
[Meta, LogicBlock(typeof(State), Diagram = true)]
public partial record InventoryScreen : State, IGet<Input.InventoryButtonPressed>
public partial record InventoryScreen : State, IGet<Input.InteractButtonPressed>
{
public Transition On(in Input.InventoryButtonPressed input)
public Transition On(in Input.InteractButtonPressed input)
{
Output(new Output.CloseInventoryMenu());
return To<InGame>();

View File

@@ -1,32 +0,0 @@
using Chickensoft.Introspection;
using Chickensoft.LogicBlocks;
namespace Zennysoft.Ma.Adapter;
public partial class GameState
{
public partial record State
{
[Meta, LogicBlock(typeof(State), Diagram = true)]
public partial record MainMenu : State, IGet<Input.NewGame>, IGet<Input.ContinueGame>, IGet<Input.LoadGame>
{
public Transition On(in Input.NewGame input)
{
Output(new Output.InitializeGame());
return To<InGame>();
}
public Transition On(in Input.ContinueGame input)
{
Output(new Output.InitializeGame());
return To<InGame>();
}
public Transition On(in Input.LoadGame input)
{
Output(new Output.LoadGameFromFile());
return To<InGame>();
}
}
}
}

View File

@@ -1,5 +1,6 @@
using Chickensoft.Introspection;
using Chickensoft.LogicBlocks;
using static Zennysoft.Ma.Adapter.GameState.Output;
namespace Zennysoft.Ma.Adapter;
@@ -8,13 +9,20 @@ public partial class GameState
public partial record State
{
[Meta, LogicBlock(typeof(State), Diagram = true)]
public partial record PauseScreen : State, IGet<Input.PauseButtonPressed>
public partial record PauseScreen : State, IGet<Input.PauseButtonPressed>, IGet<Input.ExitGame>
{
public Transition On(in Input.PauseButtonPressed input)
{
Output(new Output.ClosePauseScreen());
return To<InGame>();
}
public Transition On(in Input.ExitGame input)
{
Output(new Output.ClosePauseScreen());
Output(new Output.ExitGame());
return To<State>();
}
}
}
}

View File

@@ -1,11 +0,0 @@
using Chickensoft.Introspection;
using Chickensoft.Serialization;
namespace Zennysoft.Ma.Adapter;
[Meta, Id("equipable_item")]
public abstract partial class EquipableItem : InventoryItem
{
[Save("equipable_item_is_equipped")]
public bool IsEquipped { get; set; }
}

View File

@@ -0,0 +1,9 @@
namespace Zennysoft.Ma.Adapter
{
public interface IDroppedItem
{
void RescueItem();
public InventoryItem Item { get; }
}
}

View File

@@ -2,6 +2,8 @@
public interface IInventory
{
public bool PickUpItem(InventoryItem item);
public List<InventoryItem> Items { get; }
public bool TryAdd(InventoryItem inventoryItem);
@@ -10,5 +12,8 @@ public interface IInventory
public void Remove(InventoryItem inventoryItem);
public void Sort();
public bool Sort(EquipableItem currentWeapon, EquipableItem currentArmor, EquipableItem currentAccessory);
public event Action<string> BroadcastMessage;
public event Action InventoryChanged;
}

View File

@@ -0,0 +1,6 @@
namespace Zennysoft.Ma.Adapter;
public interface IThrownItem
{
public InventoryItem ItemThatIsThrown { get; set; }
}

View File

@@ -1,6 +1,5 @@
using Chickensoft.Introspection;
using Chickensoft.Serialization;
using Zennysoft.Game.Abstractions;
namespace Zennysoft.Ma.Adapter;

View File

@@ -21,4 +21,4 @@ public partial class UsableItemTagEnumContext : JsonSerializerContext;
public partial class BoxItemTagEnumContext : JsonSerializerContext;
[JsonSerializable(typeof(ElementType))]
public partial class ElementTypeEnumContext : JsonSerializerContext;
public partial class ElementTypeEnumContext : JsonSerializerContext;

View File

@@ -1,6 +1,6 @@
using Chickensoft.GodotNodeInterfaces;
using Godot;
using System.Collections.Immutable;
using Zennysoft.Ma.Adapter.Entity;
namespace Zennysoft.Game.Ma;
public interface IDungeonRoom : INode3D

View File

@@ -10,8 +10,8 @@ public class Module
public static void Bootstrap(Container container)
{
container.RegisterSingleton<IFileSystem, FileSystem>();
container.RegisterSingleton<ISaveFileManager<GameData>, SaveFileManager<GameData>>();
container.RegisterSingleton<IMaSaveFileManager<GameData>, MaSaveFileManager<GameData>>();
container.RegisterSingleton<ISaveFileManager, SaveFileManager>();
container.RegisterSingleton<IMaSaveFileManager, MaSaveFileManager>();
container.RegisterSingleton<IGameRepo, GameRepo>();
container.RegisterSingleton<IGameState, GameState>();
container.RegisterSingleton<IDimmableAudioStreamPlayer, DimmableAudioStreamPlayer>();

View File

@@ -1,64 +1,47 @@
using Chickensoft.Collections;
using Chickensoft.GodotNodeInterfaces;
using Godot;
using Zennysoft.Game.Abstractions;
namespace Zennysoft.Ma.Adapter;
public interface IPlayer : IKillable
public interface IPlayer : IKillable, ICharacterBody3D
{
public void InitializePlayerState();
public void ResetPlayerData();
public void Activate();
public void Deactivate();
public void Attack();
public void TakeDamage(double damage, ElementType elementType = ElementType.None, bool isCriticalHit = false);
public void TakeDamage(AttackData damage);
public void Knockback(float impulse);
public void GainExp(double expGained);
public void LevelUp();
public void Move(float delta);
public void TeleportPlayer(Transform3D newTransform);
public void HealHP(int amount);
public void RaiseHP(int amount);
public void HealVT(int amount);
public void RaiseVT(int amount);
public void ModifyBonusAttack(int amount);
public void ModifyBonusDefense(int amount);
public void ModifyMaximumHP(int amount);
public void ModifyMaximumVT(int amount);
public void ModifyBonusLuck(double amount);
public IInventory Inventory { get; }
public PlayerStats Stats { get; }
public Vector3 CurrentPosition { get; }
public Basis CurrentBasis { get; }
public AutoProp<EquipableItem> EquippedWeapon { get; }
public AutoProp<EquipableItem> EquippedArmor { get; }
public AutoProp<EquipableItem> EquippedAccessory { get; }
public void Equip(EquipableItem equipable);
public void Unequip(EquipableItem equipable);
public IInventory Inventory { get; }
public IHealthComponent HealthComponent { get; }
public IVTComponent VTComponent { get; }
public IAttackComponent AttackComponent { get; }
public IDefenseComponent DefenseComponent { get; }
public IExperiencePointsComponent ExperiencePointsComponent { get; }
public ILuckComponent LuckComponent { get; }
public IEquipmentComponent EquipmentComponent { get; }
public void PlayTestAnimation();
public event Action PlayerDied;
public delegate InventoryItem RerollItem(InventoryItem item);
}

View File

@@ -1,20 +0,0 @@
using Chickensoft.Introspection;
using Chickensoft.Serialization;
using Zennysoft.Ma.Adapter;
namespace Zennysoft.Game.Ma;
[Meta, Id("player_data")]
public partial record PlayerData
{
[Save("player_stats")]
public required PlayerStats PlayerStats { get; init; }
[Save("player_inventory")]
public required IInventory Inventory { get; init; }
}
[Meta, Id("map_data")]
public partial record MapData
{
}

View File

@@ -1,115 +0,0 @@
namespace Zennysoft.Ma.Adapter;
using Chickensoft.Collections;
using Godot;
public class PlayerStats
{
public PlayerStats(AutoProp<int> currentHP,
AutoProp<int> maximumHP,
AutoProp<int> currentVT,
AutoProp<int> maximumVT,
AutoProp<int> currentAttack,
AutoProp<int> maxAttack,
AutoProp<int> bonusAttack,
AutoProp<int> currentDefense,
AutoProp<int> maxDefense,
AutoProp<int> bonusDefense,
AutoProp<double> currentExp,
AutoProp<int> expToNextLevel,
AutoProp<int> currentLevel,
AutoProp<double> luck)
{
CurrentHP = currentHP;
MaximumHP = maximumHP;
CurrentVT = currentVT;
MaximumVT = maximumVT;
CurrentAttack = currentAttack;
MaxAttack = maxAttack;
BonusAttack = bonusAttack;
CurrentDefense = currentDefense;
MaxDefense = maxDefense;
BonusDefense = bonusDefense;
CurrentExp = currentExp;
ExpToNextLevel = expToNextLevel;
CurrentLevel = currentLevel;
Luck = luck;
}
public AutoProp<int> CurrentHP { get; init; }
public AutoProp<int> MaximumHP { get; init; }
public AutoProp<int> CurrentVT { get; init; }
public AutoProp<int> MaximumVT { get; init; }
public AutoProp<int> CurrentAttack { get; init; }
public AutoProp<int> MaxAttack { get; init; }
public AutoProp<int> BonusAttack { get; init; }
public AutoProp<int> CurrentDefense { get; init; }
public AutoProp<int> MaxDefense { get; init; }
public AutoProp<int> BonusDefense { get; init; }
public AutoProp<double> CurrentExp { get; init; }
public AutoProp<int> ExpToNextLevel { get; init; }
public AutoProp<int> CurrentLevel { get; init; }
public AutoProp<double> Luck { get; init; }
public void SetCurrentHP(int newValue)
{
var clampedValue = Mathf.Clamp(newValue, 0, MaximumHP.Value);
CurrentHP.OnNext(clampedValue);
}
public void SetMaximumHP(int newValue)
{
MaximumHP.OnNext(newValue);
}
public void SetCurrentVT(int newValue)
{
var clampedValue = Mathf.Clamp(newValue, 0, MaximumVT.Value);
CurrentVT.OnNext(clampedValue);
}
public void SetMaximumVT(int newValue)
{
MaximumVT.OnNext(newValue);
}
public void SetCurrentExp(double newValue)
{
CurrentExp.OnNext(newValue);
}
public void SetCurrentLevel(int newValue)
{
CurrentLevel.OnNext(newValue);
}
public void SetCurrentAttack(int newValue)
{
var clampedValue = Mathf.Clamp(newValue, 0, MaxAttack.Value);
CurrentAttack.OnNext(clampedValue);
}
public void SetBonusAttack(int newValue)
{
BonusAttack.OnNext(newValue);
}
public void SetMaxAttack(int newValue)
{
MaxAttack.OnNext(newValue);
}
public void SetCurrentDefense(int newValue)
{
var clampedValue = Mathf.Clamp(newValue, 0, MaxDefense.Value);
CurrentDefense.OnNext(clampedValue);
}
public void SetBonusDefense(int newValue)
{
BonusDefense.OnNext(newValue);
}
public void SetMaxDefense(int newValue)
{
MaxDefense.OnNext(newValue);
}
public void SetExpToNextLevel(int newValue)
{
ExpToNextLevel.OnNext(newValue);
}
public void SetLuck(double newValue)
{
var clampedValue = Mathf.Clamp(newValue, 0, 1.0);
Luck.OnNext(clampedValue);
}
}

View File

@@ -14,8 +14,6 @@ public partial class PlayerLogic
public readonly record struct Attack;
public readonly record struct AttackAnimationFinished;
public readonly record struct Die;
}
}

View File

@@ -9,5 +9,5 @@ public interface IPlayerLogic : ILogicBlock<PlayerLogic.State>;
[LogicBlock(typeof(State), Diagram = true)]
public partial class PlayerLogic : LogicBlock<PlayerLogic.State>, IPlayerLogic
{
public override Transition GetInitialState() => To<State.Idle>();
public override Transition GetInitialState() => To<State.Alive>();
}

View File

@@ -1,18 +0,0 @@
using Chickensoft.Introspection;
namespace Zennysoft.Ma.Adapter;
public partial class PlayerLogic
{
public partial record State
{
[Meta]
public partial record Attacking : Alive, IGet<Input.AttackAnimationFinished>
{
public Transition On(in Input.AttackAnimationFinished input)
{
return To<Idle>();
}
}
}
}

View File

@@ -1,22 +0,0 @@
using Chickensoft.Introspection;
using Godot;
namespace Zennysoft.Ma.Adapter;
public partial class PlayerLogic
{
public abstract partial record State
{
[Meta, Id("player_logic_state_alive_idle")]
public partial record Idle : Alive, IGet<Input.Attack>
{
public virtual Transition On(in Input.Attack input)
{
GD.Print("Attacking...");
Output(new Output.Animations.Attack());
return To<Attacking>();
}
}
}
}

View File

@@ -8,7 +8,7 @@ public partial class PlayerLogic
public partial record State
{
[Meta, Id("player_logic_alive")]
public abstract partial record Alive : State, IGet<Input.PhysicsTick>, IGet<Input.Die>
public partial record Alive : State, IGet<Input.PhysicsTick>, IGet<Input.Die>
{
public virtual Transition On(in Input.PhysicsTick input)
{

View File

@@ -16,7 +16,7 @@ public partial class PlayerLogic
OnDetach(() => Get<IAppRepo>().GameEntered -= OnGameEntered);
}
public Transition On(in Input.Enable input) => To<Idle>();
public Transition On(in Input.Enable input) => To<Alive>();
public void OnGameEntered() => Input(new Input.Enable());
}

View File

@@ -0,0 +1,11 @@
using Chickensoft.Introspection;
using Chickensoft.Serialization;
namespace Zennysoft.Ma.Adapter;
[Meta, Id("quest_data")]
public partial record QuestData
{
[Save("quest_data_1")]
public bool QuestMarker1 { get; set; } = false;
}

View File

@@ -4,28 +4,28 @@ using Zennysoft.Game.Abstractions;
namespace Zennysoft.Ma.Adapter;
public interface IMaSaveFileManager<T>
public interface IMaSaveFileManager
{
Task Save(T gameData);
Task Save<T>(T gameData);
Task<T?> Load();
Task<T?> Load<T>();
}
public sealed class MaSaveFileManager<T> : IMaSaveFileManager<T>
public sealed class MaSaveFileManager : IMaSaveFileManager
{
private readonly ISaveFileManager<T> _saveFileManager;
private readonly ISaveFileManager _saveFileManager;
private ImmutableList<IJsonTypeInfoResolver> _converters;
public MaSaveFileManager(ISaveFileManager<T> saveFileManager)
public MaSaveFileManager(ISaveFileManager saveFileManager)
{
_saveFileManager = saveFileManager;
_converters = [WeaponTagEnumContext.Default, ItemTagEnumContext.Default, ElementTypeEnumContext.Default, AccessoryTagEnumContext.Default, ThrowableItemTagEnumContext.Default, UsableItemTagEnumContext.Default, BoxItemTagEnumContext.Default];
}
public async Task Save(T gameData)
public async Task Save<T>(T gameData)
{
await _saveFileManager.WriteToFile(gameData, [.. _converters]);
}
public async Task<T?> Load() => await _saveFileManager.ReadFromFile([.. _converters]);
public async Task<T?> Load<T>() => await _saveFileManager.ReadFromFile<T>([.. _converters]);
}

View File

@@ -1,5 +1,6 @@
using Chickensoft.Introspection;
using Chickensoft.LogicBlocks;
using Zennysoft.Game.Implementation;
namespace Zennysoft.Ma.Adapter;

View File

@@ -1,4 +1,5 @@
using Zennysoft.Game.Ma;
using Zennysoft.Game.Implementation;
using Zennysoft.Game.Ma;
namespace Zennysoft.Ma.Adapter;
public partial class InGameUILogic

View File

@@ -7,12 +7,9 @@
</PropertyGroup>
<ItemGroup>
<Compile Remove="Game\state\states\**" />
<Compile Remove="Map\**" />
<EmbeddedResource Remove="Game\state\states\**" />
<EmbeddedResource Remove="Map\**" />
<None Remove="Game\state\states\**" />
<None Remove="Map\**" />
<Compile Remove="Game\state\states\**" />
<EmbeddedResource Remove="Game\state\states\**" />
<None Remove="Game\state\states\**" />
</ItemGroup>
<ItemGroup>
@@ -31,4 +28,8 @@
<ProjectReference Include="..\Zennysoft.Game.Godot.Implementation\Zennysoft.Game.Implementation.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="Actions\" />
</ItemGroup>
</Project>

View File

@@ -1,4 +1,4 @@
<Project Sdk="Godot.NET.Sdk/4.4.1">
<Project Sdk="Godot.NET.Sdk/4.4.0">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<EnableDynamicLoading>true</EnableDynamicLoading>

View File

@@ -0,0 +1,38 @@
<Project Sdk="Godot.NET.Sdk/4.4.0">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<EnableDynamicLoading>true</EnableDynamicLoading>
<WarningsAsErrors>CS9057</WarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Chickensoft.AutoInject" Version="2.5.0" />
<PackageReference Include="Chickensoft.GodotNodeInterfaces" Version="2.4.0" />
<PackageReference Include="Chickensoft.Introspection" Version="2.2.0" />
<PackageReference Include="Chickensoft.Introspection.Generator" Version="2.2.0" />
<PackageReference Include="Chickensoft.LogicBlocks" Version="5.16.0" />
<PackageReference Include="Chickensoft.LogicBlocks.DiagramGenerator" Version="5.16.0" />
<PackageReference Include="Chickensoft.SaveFileBuilder" Version="1.1.0" />
<PackageReference Include="Chickensoft.Serialization.Godot" Version="0.7.6" />
<PackageReference Include="GodotSharp.SourceGenerators" Version="2.6.0-250131-2115.Release" />
<PackageReference Include="SimpleInjector" Version="5.5.0" />
<PackageReference Include="SSH.NET" Version="2024.2.0" />
<PackageReference Include="System.IO.Abstractions" Version="22.0.11" />
<PackageReference Include="Zeroconf" Version="3.7.16" />
</ItemGroup>
<ItemGroup>
<None Include=".editorconfig" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Zennysoft.Game.Godot.Implementation\Zennysoft.Game.Implementation.csproj" />
<ProjectReference Include="..\Zennysoft.Game.Ma.Implementation\Zennysoft.Ma.Adapter.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Update="Godot.SourceGenerators" Version="4.4.0-dev.2" />
</ItemGroup>
<ItemGroup>
<PackageReference Update="GodotSharp" Version="4.4.0-dev.2" />
</ItemGroup>
<ItemGroup>
<PackageReference Update="GodotSharpEditor" Version="4.4.0-dev.2" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,38 @@
<Project Sdk="Godot.NET.Sdk/4.4.0">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<EnableDynamicLoading>true</EnableDynamicLoading>
<WarningsAsErrors>CS9057</WarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Chickensoft.AutoInject" Version="2.5.0" />
<PackageReference Include="Chickensoft.GodotNodeInterfaces" Version="2.4.0" />
<PackageReference Include="Chickensoft.Introspection" Version="2.2.0" />
<PackageReference Include="Chickensoft.Introspection.Generator" Version="2.2.0" />
<PackageReference Include="Chickensoft.LogicBlocks" Version="5.16.0" />
<PackageReference Include="Chickensoft.LogicBlocks.DiagramGenerator" Version="5.16.0" />
<PackageReference Include="Chickensoft.SaveFileBuilder" Version="1.1.0" />
<PackageReference Include="Chickensoft.Serialization.Godot" Version="0.7.6" />
<PackageReference Include="GodotSharp.SourceGenerators" Version="2.6.0-250131-2115.Release" />
<PackageReference Include="SimpleInjector" Version="5.5.0" />
<PackageReference Include="SSH.NET" Version="2024.2.0" />
<PackageReference Include="System.IO.Abstractions" Version="22.0.11" />
<PackageReference Include="Zeroconf" Version="3.7.16" />
</ItemGroup>
<ItemGroup>
<None Include=".editorconfig" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Zennysoft.Game.Godot.Implementation\Zennysoft.Game.Implementation.csproj" />
<ProjectReference Include="..\Zennysoft.Game.Ma.Implementation\Zennysoft.Ma.Adapter.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Update="Godot.SourceGenerators" Version="4.4.0-dev.2" />
</ItemGroup>
<ItemGroup>
<PackageReference Update="GodotSharp" Version="4.4.0-dev.2" />
</ItemGroup>
<ItemGroup>
<PackageReference Update="GodotSharpEditor" Version="4.4.0-dev.2" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,38 @@
<Project Sdk="Godot.NET.Sdk/4.4.1">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<EnableDynamicLoading>true</EnableDynamicLoading>
<WarningsAsErrors>CS9057</WarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Chickensoft.AutoInject" Version="2.5.0" />
<PackageReference Include="Chickensoft.GodotNodeInterfaces" Version="2.4.0" />
<PackageReference Include="Chickensoft.Introspection" Version="2.2.0" />
<PackageReference Include="Chickensoft.Introspection.Generator" Version="2.2.0" />
<PackageReference Include="Chickensoft.LogicBlocks" Version="5.16.0" />
<PackageReference Include="Chickensoft.LogicBlocks.DiagramGenerator" Version="5.16.0" />
<PackageReference Include="Chickensoft.SaveFileBuilder" Version="1.1.0" />
<PackageReference Include="Chickensoft.Serialization.Godot" Version="0.7.6" />
<PackageReference Include="GodotSharp.SourceGenerators" Version="2.6.0-250131-2115.Release" />
<PackageReference Include="SimpleInjector" Version="5.5.0" />
<PackageReference Include="SSH.NET" Version="2024.2.0" />
<PackageReference Include="System.IO.Abstractions" Version="22.0.11" />
<PackageReference Include="Zeroconf" Version="3.7.16" />
</ItemGroup>
<ItemGroup>
<None Include=".editorconfig" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Zennysoft.Game.Godot.Implementation\Zennysoft.Game.Implementation.csproj" />
<ProjectReference Include="..\Zennysoft.Game.Ma.Implementation\Zennysoft.Ma.Adapter.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Update="Godot.SourceGenerators" Version="4.4.0-dev.2" />
</ItemGroup>
<ItemGroup>
<PackageReference Update="GodotSharp" Version="4.4.0-dev.2" />
</ItemGroup>
<ItemGroup>
<PackageReference Update="GodotSharpEditor" Version="4.4.0-dev.2" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,38 @@
<Project Sdk="Godot.NET.Sdk/4.4.0">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<EnableDynamicLoading>true</EnableDynamicLoading>
<WarningsAsErrors>CS9057</WarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Chickensoft.AutoInject" Version="2.5.0" />
<PackageReference Include="Chickensoft.GodotNodeInterfaces" Version="2.4.0" />
<PackageReference Include="Chickensoft.Introspection" Version="2.2.0" />
<PackageReference Include="Chickensoft.Introspection.Generator" Version="2.2.0" />
<PackageReference Include="Chickensoft.LogicBlocks" Version="5.16.0" />
<PackageReference Include="Chickensoft.LogicBlocks.DiagramGenerator" Version="5.16.0" />
<PackageReference Include="Chickensoft.SaveFileBuilder" Version="1.1.0" />
<PackageReference Include="Chickensoft.Serialization.Godot" Version="0.7.6" />
<PackageReference Include="GodotSharp.SourceGenerators" Version="2.6.0-250131-2115.Release" />
<PackageReference Include="SimpleInjector" Version="5.5.0" />
<PackageReference Include="SSH.NET" Version="2024.2.0" />
<PackageReference Include="System.IO.Abstractions" Version="22.0.11" />
<PackageReference Include="Zeroconf" Version="3.7.16" />
</ItemGroup>
<ItemGroup>
<None Include=".editorconfig" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Zennysoft.Game.Godot.Implementation\Zennysoft.Game.Implementation.csproj" />
<ProjectReference Include="..\Zennysoft.Game.Ma.Implementation\Zennysoft.Ma.Adapter.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Update="Godot.SourceGenerators" Version="4.4.0-dev.2" />
</ItemGroup>
<ItemGroup>
<PackageReference Update="GodotSharp" Version="4.4.0-dev.2" />
</ItemGroup>
<ItemGroup>
<PackageReference Update="GodotSharpEditor" Version="4.4.0-dev.2" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,38 @@
<Project Sdk="Godot.NET.Sdk/4.4.1">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<EnableDynamicLoading>true</EnableDynamicLoading>
<WarningsAsErrors>CS9057</WarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Chickensoft.AutoInject" Version="2.5.0" />
<PackageReference Include="Chickensoft.GodotNodeInterfaces" Version="2.4.0" />
<PackageReference Include="Chickensoft.Introspection" Version="2.2.0" />
<PackageReference Include="Chickensoft.Introspection.Generator" Version="2.2.0" />
<PackageReference Include="Chickensoft.LogicBlocks" Version="5.16.0" />
<PackageReference Include="Chickensoft.LogicBlocks.DiagramGenerator" Version="5.16.0" />
<PackageReference Include="Chickensoft.SaveFileBuilder" Version="1.1.0" />
<PackageReference Include="Chickensoft.Serialization.Godot" Version="0.7.6" />
<PackageReference Include="GodotSharp.SourceGenerators" Version="2.6.0-250131-2115.Release" />
<PackageReference Include="SimpleInjector" Version="5.5.0" />
<PackageReference Include="SSH.NET" Version="2024.2.0" />
<PackageReference Include="System.IO.Abstractions" Version="22.0.11" />
<PackageReference Include="Zeroconf" Version="3.7.16" />
</ItemGroup>
<ItemGroup>
<None Include=".editorconfig" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Zennysoft.Game.Godot.Implementation\Zennysoft.Game.Implementation.csproj" />
<ProjectReference Include="..\Zennysoft.Game.Ma.Implementation\Zennysoft.Ma.Adapter.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Update="Godot.SourceGenerators" Version="4.4.0-dev.2" />
</ItemGroup>
<ItemGroup>
<PackageReference Update="GodotSharp" Version="4.4.0-dev.2" />
</ItemGroup>
<ItemGroup>
<PackageReference Update="GodotSharpEditor" Version="4.4.0-dev.2" />
</ItemGroup>
</Project>

View File

@@ -1,68 +0,0 @@
using Godot;
using Godot.Collections;
using System.Linq;
using Zennysoft.Game.Ma;
[Tool]
public partial class DungeonFloorLayout : LayoutType
{
[Export]
public DungeonFloorSetType SetType
{
get => _setType;
set
{
_setType = value;
LayoutWithSpawnRate = [];
NotifyPropertyListChanged();
}
}
[ExportToolButton("Populate Map Data")]
public Callable PopulateMapList => Callable.From(() => PopulateDictionary(SetType));
[Export]
public Dictionary<string, float> LayoutWithSpawnRate { get; private set; }
[Export]
public Dictionary<EnemyType, float> EnemySpawnRates { get; set; } = default!;
private string _floorPath = "res://src/map/dungeon/floors/";
private DungeonFloorSetType _setType;
private void PopulateDictionary(DungeonFloorSetType setType)
{
var floorPath = _floorPath;
var floorType = string.Empty;
if (setType == DungeonFloorSetType.SetA)
floorType = "SetAFloors";
else if (setType == DungeonFloorSetType.SetB)
floorType = "SetBFloors";
var pathToScenes = $"{floorPath}/{floorType}";
var files = DirAccess.GetFilesAt(pathToScenes).Where(x => x.EndsWith(".tscn"));
var newMaps = new Dictionary<string, float>();
foreach (var file in files)
{
if (LayoutWithSpawnRate.ContainsKey($"{floorType}/{file}"))
{
var spawnRate = LayoutWithSpawnRate.TryGetValue($"{floorType}/{file}", out var currentSpawnRate);
newMaps.Add($"{floorType}/{file}", currentSpawnRate);
}
else
newMaps.Add($"{floorType}/{file}", 1.0f);
}
LayoutWithSpawnRate = newMaps;
NotifyPropertyListChanged();
}
public enum DungeonFloorSetType
{
SetA,
SetB
}
}

View File

@@ -1 +0,0 @@
uid://ci7o3nn4mdo8o

View File

@@ -1,22 +0,0 @@
#if TOOLS
using Godot;
using Zennysoft.Game.Ma;
[Tool]
public partial class DungeonFloorLayoutNode : EditorPlugin
{
public override void _EnterTree()
{
// Initialization of the plugin goes here.
var script = GD.Load<Script>("res://addons/dungeon_floor_layout/DungeonFloorLayout.cs");
var texture = GD.Load<Texture2D>("res://addons/dungeon_floor_layout/icon.png");
AddCustomType(nameof(DungeonFloorLayout), nameof(LayoutType), script, texture);
}
public override void _ExitTree()
{
// Clean-up of the plugin goes here.
RemoveCustomType(nameof(DungeonFloorLayout));
}
}
#endif

Binary file not shown.

Before

Width:  |  Height:  |  Size: 161 B

View File

@@ -1,34 +0,0 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cgd2d4fusp4pg"
path="res://.godot/imported/icon.png-f075641bee242eff6dcbc69a1dabede8.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/dungeon_floor_layout/icon.png"
dest_files=["res://.godot/imported/icon.png-f075641bee242eff6dcbc69a1dabede8.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=0

View File

@@ -1,7 +0,0 @@
[plugin]
name="Dungeon Floor Layout"
description=""
author="Zenny"
version=""
script="DungeonFloorLayoutNode.cs"

View File

@@ -0,0 +1,287 @@
using Godot;
using Godot.Collections;
namespace NathanHoad
{
public static class InputHelper
{
public delegate void DeviceChangedEventHandler(string device, int deviceIndex);
public delegate void KeyboardInputChangedEventHandler(string action, InputEvent input);
public delegate void JoypadInputChangedEventHandler(string action, InputEvent input);
public delegate void JoypadChangedEventHandler(int deviceIndex, bool isConnected);
public static DeviceChangedEventHandler? DeviceChanged;
public static KeyboardInputChangedEventHandler? KeyboardInputChanged;
public static JoypadInputChangedEventHandler? JoypadInputChanged;
public static JoypadChangedEventHandler? JoypadChanged;
public const string DEVICE_KEYBOARD = "keyboard";
public const string DEVICE_XBOX_CONTROLLER = "xbox";
public const string DEVICE_SWITCH_CONTROLLER = "switch";
public const string DEVICE_PLAYSTATION_CONTROLLER = "playstation";
public const string DEVICE_STEAMDECK_CONTROLLER = "steamdeck";
public const string DEVICE_GENERIC = "generic";
public const string SUB_DEVICE_XBOX_ONE_CONTROLLER = "xbox_one";
public const string SUB_DEVICE_XBOX_SERIES_CONTROLLER = "xbox_series";
public const string SUB_DEVICE_PLAYSTATION3_CONTROLLER = "playstation3";
public const string SUB_DEVICE_PLAYSTATION4_CONTROLLER = "playstation4";
public const string SUB_DEVICE_PLAYSTATION5_CONTROLLER = "playstation5";
public const string SUB_DEVICE_SWITCH_JOYCON_LEFT_CONTROLLER = "switch_left_joycon";
public const string SUB_DEVICE_SWITCH_JOYCON_RIGHT_CONTROLLER = "switch_right_joycon";
private static Node instance;
public static Node Instance
{
get
{
if (instance == null)
{
instance = (Node)Engine.GetSingleton("InputHelper");
instance.Connect("device_changed", Callable.From((string device, int deviceIndex) => DeviceChanged?.Invoke(device, deviceIndex)));
instance.Connect("keyboard_input_changed", Callable.From((string action, InputEvent input) => KeyboardInputChanged?.Invoke(action, input)));
instance.Connect("joypad_input_changed", Callable.From((string action, InputEvent input) => JoypadInputChanged?.Invoke(action, input)));
instance.Connect("joypad_changed", Callable.From((int deviceIndex, bool isConnected) => JoypadChanged?.Invoke(deviceIndex, isConnected)));
}
return instance;
}
}
public static string Device
{
get => (string)Instance.Get("device");
}
public static int DeviceIndex
{
get => (int)Instance.Get("device_index");
}
public static string LastKnownJoypadDevice
{
get => (string)Instance.Get("last_known_joypad_device");
}
public static string LastKnownJoypadIndex
{
get => (string)Instance.Get("last_known_joypad_index");
}
public static float Deadzone
{
get => (float)Instance.Get("deadzone");
set => Instance.Set("deadzone", value);
}
public static int MouseMotionThreshold
{
get => (int)Instance.Get("mouse_motion_threshold");
set => Instance.Set("mouse_motion_threshold", value);
}
public static string GetSimplifiedDeviceName(string rawName)
{
return (string)Instance.Call("get_simplified_device_name", rawName);
}
public static string GetDeviceFromEvent(InputEvent @event)
{
return (string)Instance.Call("get_device_from_event", @event);
}
public static int GetDeviceIndexFromEvent(InputEvent @event)
{
return (int)Instance.Call("get_device_index_from_event", @event);
}
public static bool HasJoypad()
{
return (bool)Instance.Call("has_joypad");
}
public static string GuessDeviceName()
{
return (string)Instance.Call("guess_device_name");
}
public static void ResetAllActions()
{
Instance.Call("reset_all_actions");
}
public static void SetKeyboardOrJoypadInputForAction(string action, InputEvent input, bool swapIfTaken = true)
{
Instance.Call("set_keyboard_or_joypad_input_for_action", action, input, swapIfTaken);
}
public static InputEvent GetKeyboardOrJoypadInputForAction(string action, InputEvent input, bool swapIfTaken = true)
{
return (InputEvent)Instance.Call("get_keyboard_or_joypad_input_for_action", action, input, swapIfTaken);
}
public static Array<InputEvent> GetKeyboardOrJoypadInputsForAction(string action)
{
return (Array<InputEvent>)Instance.Call("get_keyboard_or_joypad_inputs_for_action", action);
}
public static string GetLabelForInput(InputEvent input)
{
return (string)Instance.Call("get_label_for_input", input);
}
public static string SerializeInputsForAction(string action)
{
return (string)Instance.Call("serialize_inputs_for_action", action);
}
public static string SerializeInputsForActions(Array<string> actions = null)
{
if (actions == null)
{
actions = new Array<string>();
}
return (string)Instance.Call("serialize_inputs_for_actions", actions);
}
public static void DeserializeInputsForAction(string action, string serializedInputs)
{
Instance.Call("desserialize_inputs_for_action", action, serializedInputs);
}
public static void DeserializeInputsForActions(string serializedInputs)
{
Instance.Call("deserialize_inputs_for_actions", serializedInputs);
}
#region Keyboard/Mouse
public static Array<InputEvent> GetKeyboardInputsForAction(string action)
{
return (Array<InputEvent>)Instance.Call("get_keyboard_inputs_for_action", action);
}
public static InputEvent GetKeyboardInputForAction(string action)
{
return (InputEvent)Instance.Call("get_keyboard_input_for_action", action);
}
public static void SetKeyboardInputForAction(string action, InputEvent input, bool swapIfTaken = true)
{
Instance.Call("set_keyboard_input_for_action", action, input, swapIfTaken);
}
public static void ReplaceKeyboardInputForAction(string action, InputEvent currentInput, InputEvent input, bool swapIfTaken = true)
{
Instance.Call("replace_keyboard_input_for_action", action, currentInput, input, swapIfTaken);
}
public static void ReplaceKeyboardInputAtIndex(string action, int index, InputEvent input, bool swapIfTaken = true)
{
Instance.Call("replace_keyboard_input_at_index", action, index, input, swapIfTaken);
}
#endregion
#region Joypad
public static Array<InputEvent> GetJoypadInputsForAction(string action)
{
return (Array<InputEvent>)Instance.Call("get_joypad_inputs_for_action", action);
}
public static InputEvent GetJoypadInputForAction(string action)
{
return (InputEvent)Instance.Call("get_joypad_input_for_action", action);
}
public static void SetJoypadInputForAction(string action, InputEvent input, bool swapIfTaken = true)
{
Instance.Call("set_joypad_input_for_action", action, input, swapIfTaken);
}
public static void ReplaceJoypadInputForAction(string action, InputEvent currentInput, InputEvent input, bool swapIfTaken = true)
{
Instance.Call("replace_joypad_input_for_action", action, currentInput, input, swapIfTaken);
}
public static void ReplaceJoypadInputAtIndex(string action, int index, InputEvent input, bool swapIfTaken = true)
{
Instance.Call("replace_joypad_input_at_index", action, index, input, swapIfTaken);
}
public static void RumbleSmall(int targetDevice = 0)
{
Instance.Call("rumble_small", targetDevice);
}
public static void RumbleMedium(int targetDevice = 0)
{
Instance.Call("rumble_medium", targetDevice);
}
public static void RumbleLarge(int targetDevice = 0)
{
Instance.Call("rumble_large", targetDevice);
}
public static void StartRumbleSmall(int targetDevice = 0)
{
Instance.Call("start_rumble_small", targetDevice);
}
public static void StartRumbleMedium(int targetDevice = 0)
{
Instance.Call("start_rumble_medium", targetDevice);
}
public static void StartRumbleLarge(int targetDevice = 0)
{
Instance.Call("start_rumble_large", targetDevice);
}
public static void StopRumble(int targetDevice = 0)
{
Instance.Call("stop_rumble", targetDevice);
}
#endregion
}
}

View File

@@ -0,0 +1 @@
uid://cfs4dgwrfvb11

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022-present Nathan Hoad
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.

View File

@@ -0,0 +1,230 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="300"
height="80"
viewBox="0 0 79.374999 21.166667"
version="1.1"
id="svg291"
inkscape:version="1.2 (dc2aedaf03, 2022-05-15)"
sodipodi:docname="update.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:zoom="4"
inkscape:cx="121.625"
inkscape:cy="43.25"
inkscape:window-width="2560"
inkscape:window-height="1377"
inkscape:window-x="2552"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="layer1"
inkscape:deskcolor="#d1d1d1"
showguides="true">
<sodipodi:guide
position="-15.575132,19.553027"
orientation="0,-1"
id="guide2089"
inkscape:locked="false" />
</sodipodi:namedview>
<defs
id="defs288" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<g
id="g2335"
transform="matrix(0.93072355,0,0,0.92874157,17.062816,18.409989)"
style="stroke-width:0.85374062;stroke-dasharray:none;stroke:none">
<path
id="rect2095"
style="fill:#349684;fill-opacity:1;stroke:#152f2c;stroke-width:0.56916041;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:0.3;stroke-opacity:1;paint-order:normal"
d="m 24.364404,-16.012976 a 1.7811118,1.7811118 0 0 0 -1.568379,0.938961 1.7811118,1.7811118 0 0 0 -1.557528,-0.918291 1.7811118,1.7811118 0 0 0 -1.383895,0.660942 c -1.816891,-1.243302 -4.115395,-0.49791 -5.162476,1.678967 l -2.844787,5.914368 c -1.050638,2.1842718 -0.431729,4.957652 1.388029,6.2187419 1.819758,1.26108987 4.130958,0.5177068 5.181596,-1.666565 l 1.240234,-2.5786539 h 6.335531 l 1.240234,2.5786539 c 1.050638,2.1842718 3.361322,2.92765487 5.181079,1.666565 1.819758,-1.2610899 2.439184,-4.0344701 1.388546,-6.2187419 L 30.9578,-13.652397 c -1.050637,-2.184271 -3.361838,-2.927654 -5.181596,-1.666565 -1.97e-4,1.37e-4 -3.19e-4,3.8e-4 -5.16e-4,5.17e-4 a 1.7811118,1.7811118 0 0 0 -1.411284,-0.694531 z" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.85374062;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:0.3;stroke-opacity:1;paint-order:markers stroke fill"
id="rect2101"
width="1.403165"
height="4.2796535"
x="17.394386"
y="-13.427068"
ry="0.75541198"
rx="0.75380331" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.85374062;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:0.3;stroke-opacity:1;paint-order:markers stroke fill"
id="rect2103"
width="1.403165"
height="4.2796535"
x="-11.988823"
y="-20.235798"
ry="0.75380331"
rx="0.75541198"
transform="rotate(90)" />
<ellipse
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.85374062;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:0.3;stroke-opacity:1;paint-order:markers stroke fill"
id="ellipse2105"
cx="28.192503"
cy="2.7587082"
rx="0.82257289"
ry="0.79261416"
transform="rotate(-30)" />
<ellipse
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.85374062;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:0.3;stroke-opacity:1;paint-order:markers stroke fill"
id="ellipse2107"
cx="28.216581"
cy="4.7943339"
rx="0.82257289"
ry="0.79261416"
transform="rotate(-30)" />
<ellipse
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.85374062;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:0.3;stroke-opacity:1;paint-order:markers stroke fill"
id="ellipse2109"
cx="30.279696"
cy="2.6987464"
rx="0.82257289"
ry="0.79261416"
transform="rotate(-30)" />
<ellipse
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.85374062;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:0.3;stroke-opacity:1;paint-order:markers stroke fill"
id="ellipse2111"
cx="30.320311"
cy="4.767447"
rx="0.82257289"
ry="0.79261416"
transform="rotate(-30)" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.85374062;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:0.3;stroke-opacity:1;paint-order:markers stroke fill"
id="rect2117"
width="1.109892"
height="2.1669323"
x="22.090807"
y="-10.531444"
ry="0.52878839"
rx="0.52766228" />
</g>
<path
id="rect1625"
style="fill:#50fa7b;fill-opacity:1;stroke:#0f451d;stroke-width:0.593381;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
d="m 50.52517,5.3570989 c -0.643062,0 -1.160653,0.5900216 -1.160653,1.3229166 v 1.0764201 h -1.07642 c -0.732895,0 -1.322916,0.5175908 -1.322916,1.1606533 0,0.643061 0.590021,1.1611691 1.322916,1.1611691 h 1.07642 v 1.076421 c 0,0.732895 0.517591,1.322917 1.160653,1.322917 0.643062,0 1.160653,-0.590022 1.160653,-1.322917 v -1.076421 h 1.076937 c 0.732895,0 1.322916,-0.5181081 1.322916,-1.1611691 0,-0.6430625 -0.590021,-1.1606533 -1.322916,-1.1606533 H 51.685823 V 6.6800155 c 0,-0.732895 -0.517591,-1.3229166 -1.160653,-1.3229166 z" />
<path
id="path2678"
style="fill:#50fa7b;fill-opacity:1;stroke:#0f451d;stroke-width:0.593381;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
d="m 57.791691,5.357099 c -0.643062,0 -1.160653,0.5900216 -1.160653,1.3229166 v 1.0764201 h -1.07642 c -0.732895,0 -1.322916,0.5175908 -1.322916,1.1606522 0,0.643062 0.590021,1.1611701 1.322916,1.1611701 h 1.07642 v 1.076421 c 0,0.732895 0.517591,1.322917 1.160653,1.322917 0.643062,0 1.160653,-0.590022 1.160653,-1.322917 v -1.076421 h 1.076937 c 0.732895,0 1.322916,-0.5181081 1.322916,-1.1611701 0,-0.6430614 -0.590021,-1.1606522 -1.322916,-1.1606522 H 58.952344 V 6.6800156 c 0,-0.732895 -0.517591,-1.3229166 -1.160653,-1.3229166 z" />
<rect
style="fill:#000000;fill-opacity:1;stroke-width:0.72829;stroke-linecap:round;stroke-linejoin:round;stroke-dashoffset:0.3;paint-order:markers stroke fill"
id="rect413"
width="10.200269"
height="8.7847834"
x="-21.316095"
y="3.0372066" />
<rect
style="fill:#000000;fill-opacity:1;stroke-width:0.622475;stroke-linecap:round;stroke-linejoin:round;stroke-dashoffset:0.3;paint-order:markers stroke fill"
id="rect415"
width="7.9928446"
height="15.312933"
x="-20.546837"
y="12.977094"
ry="4.375123"
transform="matrix(0.82192626,0.56959391,-0.43346431,0.90117073,0,0)"
rx="4.375123" />
<rect
style="fill:#000000;fill-opacity:1;stroke-width:0.622475;stroke-linecap:round;stroke-linejoin:round;stroke-dashoffset:0.3;paint-order:markers stroke fill"
id="rect471"
width="7.9928446"
height="15.312933"
x="9.1578636"
y="-5.7980561"
ry="4.375123"
transform="matrix(-0.82192626,0.56959391,0.43346431,0.90117073,0,0)"
rx="4.375123" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1.32292;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:0.3;stroke-opacity:1;paint-order:markers stroke fill"
id="rect1137"
width="1.403165"
height="4.2796535"
x="-22.240166"
y="3.9990206"
ry="0.70158249"
rx="0.70158249" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1.32292;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:0.3;stroke-opacity:1;paint-order:markers stroke fill"
id="rect1295"
width="1.403165"
height="4.2796535"
x="5.4372649"
y="19.398754"
ry="0.70158249"
rx="0.70158249"
transform="rotate(90)" />
<ellipse
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1.32292;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:0.3;stroke-opacity:1;paint-order:markers stroke fill"
id="ellipse1297"
cx="-13.849753"
cy="-1.3102485"
rx="0.82257289"
ry="0.79261416"
transform="rotate(-30)" />
<ellipse
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1.32292;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:0.3;stroke-opacity:1;paint-order:markers stroke fill"
id="ellipse1299"
cx="-13.825675"
cy="0.72537726"
rx="0.82257289"
ry="0.79261416"
transform="rotate(-30)" />
<ellipse
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1.32292;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:0.3;stroke-opacity:1;paint-order:markers stroke fill"
id="ellipse1301"
cx="-11.762562"
cy="-1.3702103"
rx="0.82257289"
ry="0.79261416"
transform="rotate(-30)" />
<ellipse
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1.32292;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:0.3;stroke-opacity:1;paint-order:markers stroke fill"
id="ellipse1303"
cx="-11.721946"
cy="0.69849032"
rx="0.82257289"
ry="0.79261416"
transform="rotate(-30)" />
<circle
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.32292;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:0.3;stroke-opacity:1;paint-order:markers stroke fill"
id="path2085"
cx="-17.863199"
cy="3.3752983"
r="1.7811118" />
<circle
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.32292;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:0.3;stroke-opacity:1;paint-order:markers stroke fill"
id="circle2087"
cx="-14.737003"
cy="3.3542202"
r="1.7811118" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1.32292;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:0.3;stroke-opacity:1;paint-order:markers stroke fill"
id="rect2091"
width="0.98221546"
height="1.917659"
x="-16.946886"
y="7.1795278"
ry="0.49110773"
rx="0.49110773" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -0,0 +1,37 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://ddixs2ish5bi6"
path="res://.godot/imported/update.svg-3137f1f7d53c08c0ae65aabe138d898b.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/input_helper/assets/update.svg"
dest_files=["res://.godot/imported/update.svg-3137f1f7d53c08c0ae65aabe138d898b.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

View File

@@ -0,0 +1,83 @@
@tool
extends Control
signal failed()
signal updated(updated_to_version: String)
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: String = "":
set(next_next_version):
next_version = next_next_version
label.text = "Version %s is available for download!" % next_version
get:
return next_version
func save_zip(bytes: PackedByteArray) -> void:
var file: FileAccess = FileAccess.open(TEMP_FILE_NAME, FileAccess.WRITE)
file.store_buffer(bytes)
file.flush()
### Signals
func _on_download_button_pressed() -> void:
# Safeguard the actual input helper repo from accidentally updating itself
if FileAccess.file_exists("res://examples/device_tester.gd"):
prints("You can't update the input helper from within itself.")
failed.emit()
return
http_request.request("https://github.com/nathanhoad/godot_input_helper/archive/refs/tags/v%s.zip" % next_version)
download_button.disabled = true
download_button.text = "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
save_zip(body)
OS.move_to_trash(ProjectSettings.globalize_path("res://addons/input_helper"))
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)
func _on_notes_button_pressed() -> void:
OS.shell_open("https://github.com/nathanhoad/godot_input_helper/releases/tag/v%s" % next_version)

View File

@@ -0,0 +1 @@
uid://dcff0mowkn6km

View File

@@ -0,0 +1,60 @@
[gd_scene load_steps=3 format=3 uid="uid://b7mst0qu7vjk1"]
[ext_resource type="Script" uid="uid://dcff0mowkn6km" path="res://addons/input_helper/components/download_update_panel.gd" id="1_4tm1k"]
[ext_resource type="Texture2D" uid="uid://ddixs2ish5bi6" path="res://addons/input_helper/assets/update.svg" id="2_j7shv"]
[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_j7shv")
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 and install 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"]

View File

@@ -0,0 +1,601 @@
extends Node
signal device_changed(device: String, device_index: int)
signal keyboard_input_changed(action: String, input: InputEvent)
signal joypad_input_changed(action: String, input: InputEvent)
signal joypad_changed(device_index: int, is_connected: bool)
const DEVICE_KEYBOARD = "keyboard"
const DEVICE_XBOX_CONTROLLER = "xbox"
const DEVICE_SWITCH_CONTROLLER = "switch"
const DEVICE_PLAYSTATION_CONTROLLER = "playstation"
const DEVICE_STEAMDECK_CONTROLLER = "steamdeck"
const DEVICE_GENERIC = "generic"
const SUB_DEVICE_XBOX_ONE_CONTROLLER = "xbox_one"
const SUB_DEVICE_XBOX_SERIES_CONTROLLER = "xbox_series"
const SUB_DEVICE_PLAYSTATION3_CONTROLLER = "playstation3"
const SUB_DEVICE_PLAYSTATION4_CONTROLLER = "playstation4"
const SUB_DEVICE_PLAYSTATION5_CONTROLLER = "playstation5"
const SUB_DEVICE_SWITCH_JOYCON_LEFT_CONTROLLER = "switch_left_joycon"
const SUB_DEVICE_SWITCH_JOYCON_RIGHT_CONTROLLER = "switch_right_joycon"
const XBOX_BUTTON_LABELS = ["A", "B", "X", "Y", "Back", "Guide", "Start", "Left Stick", "Right Stick", "LB", "RB", "Up", "Down", "Left", "Right", "Share", "Paddle 1", "Paddle 2", "Paddle 3", "Paddle 4"]
const XBOX_ONE_BUTTON_LABELS = ["A", "B", "X", "Y", "View", "Guide", "Menu", "Left Stick", "Right Stick", "LB", "RB", "Up", "Down", "Left", "Right", "Share", "Paddle 1", "Paddle 2", "Paddle 3", "Paddle 4"]
const XBOX_SERIES_BUTTON_LABELS = ["A", "B", "X", "Y", "View", "Guide", "Menu", "Left Stick", "Right Stick", "LB", "RB", "Up", "Down", "Left", "Right", "Share", "Paddle 1", "Paddle 2", "Paddle 3", "Paddle 4"]
const STEAMDECK_BUTTON_LABELS = ["A", "B", "X", "Y", "View", "?", "Options", "Left Stick", "Right Stick", "L1", "R1", "Up", "Down", "Left", "Right", "", "", "", "", ""]
# Note: share and home buttons are not recognized
const SWITCH_BUTTON_LABELS = ["B", "A", "Y", "X", "Minus", "", "Plus", "Left Stick", "Right Stick", "LS", "RS", "Up", "Down", "Left", "Right", "Capture"]
# Mapping for left and right joypad connected together (extended gamepad)
# Left Stick is Axis 0 and 1
# Right Stick is Axis 2 and 3
# ZL and ZR are Axis 4 and 5
const SWITCH_EXTENDED_GAMEPAD_BUTTON_LABELS = ["B", "A", "Y", "X", "Minus", "", "Plus", "Left Stick", "Right Stick", "L", "R", "Up", "Down", "Left", "Right", "Capture"]
const PLAYSTATION_3_4_BUTTON_LABELS = ["Cross", "Circle", "Square", "Triangle", "Share", "PS", "Options", "L3", "R3", "L1", "R1", "Up", "Down", "Left", "Right", "Microphone", "", "", "", "", "Touchpad"]
# Note: Microphone does not work on PC / touchpad is not recognized
const PLAYSTATION_5_BUTTON_LABELS = ["Cross", "Circle", "Square", "Triangle", "Create", "PS", "Options", "L3", "R3", "L1", "R1", "Up", "Down", "Left", "Right", "Microphone", "", "", "", "", "Touchpad"]
const SERIAL_VERSION = 1
## The deadzone to ignore for joypad motion
var deadzone: float = 0.5
## The mouse distance to ignore before movement is assumed
var mouse_motion_threshold: int = 100
## The last known joypad device name (or "" if no joypad detected)
var last_known_joypad_device: String = get_simplified_device_name(Input.get_joy_name(0))
## The last known joypad index
var last_known_joypad_index: int = 0 if Input.get_connected_joypads().size() > 0 else -1
## Used internally
var device_last_changed_at: int = 0
var _last_known_granular_joypad_device: String = get_simplified_device_name(Input.get_joy_name(0), true)
@onready var device: String = guess_device_name()
@onready var device_index: int = 0 if has_joypad() else -1
func _ready() -> void:
process_mode = Node.PROCESS_MODE_ALWAYS
if not Engine.has_singleton("InputHelper"):
Engine.register_singleton("InputHelper", self)
Input.joy_connection_changed.connect(func(device_index, is_connected): joypad_changed.emit(device_index, is_connected))
func _input(event: InputEvent) -> void:
var next_device: String = device
var next_device_index: int = device_index
# Did we just press a key on the keyboard or move the mouse?
if event is InputEventKey \
or event is InputEventMouseButton \
or (event is InputEventMouseMotion and (event as InputEventMouseMotion).relative.length_squared() > mouse_motion_threshold):
next_device = DEVICE_KEYBOARD
next_device_index = -1
# Did we just use a joypad?
elif event is InputEventJoypadButton \
or (event is InputEventJoypadMotion and abs(event.axis_value) > deadzone):
next_device = get_simplified_device_name(get_joy_name(event.device))
last_known_joypad_device = next_device
next_device_index = event.device
last_known_joypad_index = next_device_index
_last_known_granular_joypad_device = get_simplified_device_name(get_joy_name(event.device), true)
# Debounce changes for 1 second because some joypads register twice in Windows for some reason
var not_changed_in_last_second = Engine.get_frames_drawn() - device_last_changed_at > Engine.get_frames_per_second()
if (next_device != device or next_device_index != device_index) and not_changed_in_last_second:
device_last_changed_at = Engine.get_frames_drawn()
device = next_device
device_index = next_device_index
device_changed.emit(device, device_index)
## Get the name of a joypad
func get_joy_name(at_device_index: int) -> String:
var joy_name: String = Input.get_joy_name(at_device_index)
if joy_name == "" and Input.get_joy_info(at_device_index).size() > 0 and "xinput" in Input.get_joy_info(at_device_index).keys()[0]:
joy_name = "XInput"
return joy_name
## Get the device name for an event
func get_device_from_event(event: InputEvent) -> String:
if event is InputEventKey or event is InputEventMouseButton or event is InputEventMouseMotion:
return DEVICE_KEYBOARD
elif event is InputEventJoypadButton or event is InputEventJoypadMotion:
return get_simplified_device_name(get_joy_name(event.device))
else:
return DEVICE_GENERIC
## Get the device name for an event
func get_device_index_from_event(event: InputEvent) -> int:
if event is InputEventJoypadButton or event is InputEventJoypadMotion:
return event.device
else:
return -1
## Convert a Godot device identifier to a simplified string
func get_simplified_device_name(raw_name: String, force_granular_identifier: bool = false) -> String:
var use_granular_identifier: bool = force_granular_identifier or InputHelperSettings.get_setting(InputHelperSettings.USE_GRANULAR_DEVICE_IDENTIFIERS, false)
var keywords: Dictionary = {
SUB_DEVICE_XBOX_ONE_CONTROLLER: ["Xbox One Controller"],
SUB_DEVICE_XBOX_SERIES_CONTROLLER: ["Xbox Series Controller", "Xbox Wireless Controller"],
DEVICE_XBOX_CONTROLLER: ["XInput", "XBox"],
SUB_DEVICE_PLAYSTATION3_CONTROLLER: ["PS3"],
SUB_DEVICE_PLAYSTATION4_CONTROLLER:["Nacon Revolution Unlimited Pro Controller", "PS4", "DUALSHOCK 4"],
SUB_DEVICE_PLAYSTATION5_CONTROLLER:["Sony DualSense", "PS5", "DualSense Wireless Controller"],
DEVICE_STEAMDECK_CONTROLLER: ["Steam"],
DEVICE_SWITCH_CONTROLLER: ["Switch", "Joy-Con (L/R)", "PowerA Core Controller"],
SUB_DEVICE_SWITCH_JOYCON_LEFT_CONTROLLER: ["Joy-Con (L)"],
SUB_DEVICE_SWITCH_JOYCON_RIGHT_CONTROLLER: ["joy-Con (R)"],
} if use_granular_identifier else {
DEVICE_XBOX_CONTROLLER: ["XBox", "XInput"],
DEVICE_PLAYSTATION_CONTROLLER: ["Sony", "PS3", "PS5", "PS4", "DUALSHOCK 4", "DualSense", "Nacon Revolution Unlimited Pro Controller"],
DEVICE_STEAMDECK_CONTROLLER: ["Steam"],
DEVICE_SWITCH_CONTROLLER: ["Switch", "Joy-Con", "PowerA Core Controller"],
}
for device_key in keywords:
for keyword in keywords[device_key]:
if keyword.to_lower() in raw_name.to_lower():
return device_key
return DEVICE_GENERIC
## Check if there is a connected joypad
func has_joypad() -> bool:
return Input.get_connected_joypads().size() > 0
## Guess the initial input device
func guess_device_name() -> String:
if has_joypad():
return get_simplified_device_name(get_joy_name(0))
else:
return DEVICE_KEYBOARD
#region Mapping
func reset_all_actions() -> void:
InputMap.load_from_project_settings()
for action in InputMap.get_actions():
var input: InputEvent = get_joypad_input_for_action(action)
if input != null:
joypad_input_changed.emit(action, input)
input = get_keyboard_input_for_action(action)
if input != null:
keyboard_input_changed.emit(action, input)
## Set the key or button for an action
func set_keyboard_or_joypad_input_for_action(action: String, event: InputEvent, swap_if_taken: bool = true) -> void:
if event is InputEventKey or event is InputEventMouse:
set_keyboard_input_for_action(action, event, swap_if_taken)
elif event is InputEventJoypadButton:
set_joypad_input_for_action(action, event, swap_if_taken)
## Get the key or button for a given action depending on the current device
func get_keyboard_or_joypad_input_for_action(action: String) -> InputEvent:
if device == DEVICE_KEYBOARD:
return get_keyboard_input_for_action(action)
else:
return get_joypad_input_for_action(action)
## Get the key or button for a given action depending on the current device
func get_keyboard_or_joypad_inputs_for_action(action: String) -> Array[InputEvent]:
if device == DEVICE_KEYBOARD:
return get_keyboard_inputs_for_action(action)
else:
return get_joypad_inputs_for_action(action)
## Get a text label for a given input
func get_label_for_input(input: InputEvent) -> String:
if input == null: return ""
if input is InputEventKey:
if input.physical_keycode > 0 :
var keycode: Key = DisplayServer.keyboard_get_keycode_from_physical(input.physical_keycode) if DisplayServer.keyboard_get_current_layout() > -1 else input.physical_keycode
return OS.get_keycode_string(keycode)
elif input.keycode > 0:
return OS.get_keycode_string(input.keycode)
else:
return input.as_text()
elif input is InputEventMouseButton:
match input.button_index:
MOUSE_BUTTON_LEFT:
return "Mouse Left Button"
MOUSE_BUTTON_MIDDLE:
return "Mouse Middle Button"
MOUSE_BUTTON_RIGHT:
return "Mouse Right Button"
return "Mouse Button %d" % input.button_index
elif input is InputEventJoypadButton:
var labels = []
match _last_known_granular_joypad_device:
DEVICE_XBOX_CONTROLLER, DEVICE_GENERIC:
labels = XBOX_BUTTON_LABELS
SUB_DEVICE_XBOX_ONE_CONTROLLER:
labels = XBOX_ONE_BUTTON_LABELS
SUB_DEVICE_XBOX_SERIES_CONTROLLER:
labels = XBOX_SERIES_BUTTON_LABELS
SUB_DEVICE_SWITCH_JOYCON_LEFT_CONTROLLER, SUB_DEVICE_SWITCH_JOYCON_RIGHT_CONTROLLER:
labels = SWITCH_BUTTON_LABELS
DEVICE_SWITCH_CONTROLLER:
labels = SWITCH_EXTENDED_GAMEPAD_BUTTON_LABELS
SUB_DEVICE_PLAYSTATION3_CONTROLLER, SUB_DEVICE_PLAYSTATION4_CONTROLLER:
labels = PLAYSTATION_3_4_BUTTON_LABELS
DEVICE_PLAYSTATION_CONTROLLER, SUB_DEVICE_PLAYSTATION5_CONTROLLER:
labels = PLAYSTATION_5_BUTTON_LABELS
DEVICE_STEAMDECK_CONTROLLER:
labels = STEAMDECK_BUTTON_LABELS
if input.button_index < labels.size():
return "%s Button" % labels[input.button_index]
else:
return "Button %d" % input.button_index
elif input is InputEventJoypadMotion:
var motion: InputEventJoypadMotion = input as InputEventJoypadMotion
match motion.axis:
JOY_AXIS_LEFT_X:
return "Left Stick %s" % ("Left" if motion.axis_value < 0 else "Right")
JOY_AXIS_LEFT_Y:
return "Left Stick %s" % ("Up" if motion.axis_value < 0 else "Down")
JOY_AXIS_RIGHT_X:
return "Right Stick %s" % ("Left" if motion.axis_value < 0 else "Right")
JOY_AXIS_RIGHT_Y:
return "Right Stick %s" % ("Up" if motion.axis_value < 0 else "Down")
JOY_AXIS_TRIGGER_LEFT:
return "Left Trigger"
JOY_AXIS_TRIGGER_RIGHT:
return "Right Trigger"
return input.as_text()
## Serialize a single action's inputs.
func serialize_inputs_for_action(action: StringName) -> String:
var action_inputs: PackedStringArray = []
var inputs: Array[InputEvent] = InputMap.action_get_events(action)
for input in inputs:
if input is InputEventKey:
var s: String = get_label_for_input(input)
var modifiers: Array[String] = []
if input.alt_pressed:
modifiers.append("alt")
if input.shift_pressed:
modifiers.append("shift")
if input.ctrl_pressed:
modifiers.append("ctrl")
if input.meta_pressed:
modifiers.append("meta")
if not modifiers.is_empty():
s += "|" + ",".join(modifiers)
action_inputs.append("key:%s" % s)
elif input is InputEventMouseButton:
action_inputs.append("mouse:%d" % input.button_index)
elif input is InputEventJoypadButton:
action_inputs.append("joypad:%d" % input.button_index)
elif input is InputEventJoypadMotion:
action_inputs.append("joypad:%d|%f" % [input.axis, input.axis_value])
return ";".join(action_inputs)
## Serialize a list of action inputs to string. If actions is empty then it will serialize
## all actions.
func serialize_inputs_for_actions(actions: PackedStringArray = []) -> String:
if actions == null or actions.is_empty():
actions = InputMap.get_actions()
var map: Dictionary = {}
for action in actions:
map[action] = serialize_inputs_for_action(action)
return JSON.stringify({
version = SERIAL_VERSION,
map = map
})
## Deserialize a single action's inputs.
func deserialize_inputs_for_action(action: String, string: String) -> void:
InputMap.action_erase_events(action)
var action_inputs: PackedStringArray = string.split(";")
for action_input in action_inputs:
var bits: PackedStringArray = action_input.split(":")
# Ignore any empty actions
if bits.size() < 2: continue
var input_type: String = bits[0]
var input_details: String = bits[1]
match input_type:
"key":
var keyboard_input = InputEventKey.new()
if "|" in input_details:
var detail_bits = input_details.split("|")
keyboard_input.keycode = OS.find_keycode_from_string(detail_bits[0])
detail_bits = detail_bits[1].split(",")
if detail_bits.has("alt"):
keyboard_input.alt_pressed = true
if detail_bits.has("shift"):
keyboard_input.shift_pressed = true
if detail_bits.has("ctrl"):
keyboard_input.ctrl_pressed = true
if detail_bits.has("meta"):
keyboard_input.meta_pressed = true
else:
keyboard_input.keycode = OS.find_keycode_from_string(input_details)
InputMap.action_add_event(action, keyboard_input)
keyboard_input_changed.emit(action, keyboard_input)
"mouse":
var mouse_input = InputEventMouseButton.new()
mouse_input.button_index = int(input_details)
InputMap.action_add_event(action, mouse_input)
keyboard_input_changed.emit(action, mouse_input)
"joypad":
if "|" in str(input_details):
var joypad_motion_input = InputEventJoypadMotion.new()
var joypad_bits = input_details.split("|")
joypad_motion_input.axis = int(joypad_bits[0])
joypad_motion_input.axis_value = float(joypad_bits[1])
InputMap.action_add_event(action, joypad_motion_input)
joypad_input_changed.emit(action, joypad_motion_input)
else:
var joypad_input = InputEventJoypadButton.new()
joypad_input.button_index = int(input_details)
InputMap.action_add_event(action, joypad_input)
joypad_input_changed.emit(action, joypad_input)
## Deserialise a list of actions' inputs.
func deserialize_inputs_for_actions(string: String) -> void:
var data: Dictionary = JSON.parse_string(string)
# Use legacy deserialization
if not data.has("version"):
_deprecated_deserialize_inputs_for_actions(string)
return
# Version 1
for action in data.map.keys():
deserialize_inputs_for_action(action, data.map[action])
# Load inputs from a serialized string. [deprecated]
func _deprecated_deserialize_inputs_for_actions(string: String) -> void:
var map: Dictionary = JSON.parse_string(string)
for action in map.keys():
InputMap.action_erase_events(action)
for key in map[action]["keyboard"]:
var keyboard_input = InputEventKey.new()
if "|" in key:
var bits = key.split("|")
keyboard_input.keycode = OS.find_keycode_from_string(bits[0])
bits = bits[1].split(",")
if bits.has("alt"):
keyboard_input.alt_pressed = true
if bits.has("shift"):
keyboard_input.shift_pressed = true
if bits.has("ctrl"):
keyboard_input.ctrl_pressed = true
if bits.has("meta"):
keyboard_input.meta_pressed = true
else:
keyboard_input.keycode = OS.find_keycode_from_string(key)
InputMap.action_add_event(action, keyboard_input)
for button_index in map[action]["mouse"]:
var mouse_input = InputEventMouseButton.new()
mouse_input.button_index = int(button_index)
InputMap.action_add_event(action, mouse_input)
for button_index_or_motion in map[action]["joypad"]:
if "|" in str(button_index_or_motion):
var joypad_motion_input = InputEventJoypadMotion.new()
var bits = button_index_or_motion.split("|")
joypad_motion_input.axis = int(bits[0])
joypad_motion_input.axis_value = float(bits[1])
InputMap.action_add_event(action, joypad_motion_input)
else:
var joypad_input = InputEventJoypadButton.new()
joypad_input.button_index = int(button_index_or_motion)
InputMap.action_add_event(action, joypad_input)
#endregion
#region Keyboard/mouse input
## Get all of the keys/mouse buttons used for an action.
func get_keyboard_inputs_for_action(action: String) -> Array[InputEvent]:
return InputMap.action_get_events(action).filter(func(event):
return event is InputEventKey or event is InputEventMouseButton
)
## Get the first key for an action
func get_keyboard_input_for_action(action: String) -> InputEvent:
var inputs: Array[InputEvent] = get_keyboard_inputs_for_action(action)
return null if inputs.is_empty() else inputs[0]
## Set the key used for an action
func set_keyboard_input_for_action(action: String, input: InputEvent, swap_if_taken: bool = true) -> Error:
return _update_keyboard_input_for_action(action, input, swap_if_taken, null)
## Replace a specific key with another key
func replace_keyboard_input_for_action(action: String, current_input: InputEvent, input: InputEvent, swap_if_taken: bool = true) -> Error:
return _update_keyboard_input_for_action(action, input, swap_if_taken, current_input)
## Replace a specific key, given its index
func replace_keyboard_input_at_index(action: String, index: int, input: InputEvent, swap_if_taken: bool = true) -> Error:
var inputs: Array[InputEvent] = get_keyboard_inputs_for_action(action)
var replacing_input = InputEventKey.new() if (inputs.is_empty() or inputs.size() <= index) else inputs[index]
return _update_keyboard_input_for_action(action, input, swap_if_taken, replacing_input)
func _update_keyboard_input_for_action(action: String, input: InputEvent, swap_if_taken: bool, replacing_input: InputEvent = null) -> Error:
if not (input is InputEventKey or input is InputEventMouseButton): return ERR_INVALID_DATA
var is_valid_keyboard_event = func(event):
return event is InputEventKey or event is InputEventMouseButton
return _update_input_for_action(action, input, swap_if_taken, replacing_input, is_valid_keyboard_event, keyboard_input_changed)
#endregion
#region Joypad input
## Get all buttons used for an action
func get_joypad_inputs_for_action(action: String) -> Array[InputEvent]:
return InputMap.action_get_events(action).filter(func(event):
return event is InputEventJoypadButton or event is InputEventJoypadMotion
)
## Get the first button for an action
func get_joypad_input_for_action(action: String) -> InputEvent:
var buttons: Array[InputEvent] = get_joypad_inputs_for_action(action)
return null if buttons.is_empty() else buttons[0]
## Set the button for an action
func set_joypad_input_for_action(action: String, input: InputEvent, swap_if_taken: bool = true) -> Error:
return _update_joypad_input_for_action(action, input, swap_if_taken, null)
## Replace a specific button for an action
func replace_joypad_input_for_action(action: String, current_input: InputEvent, input: InputEventJoypadButton, swap_if_taken: bool = true) -> Error:
return _update_joypad_input_for_action(action, input, swap_if_taken, current_input)
## Replace a button, given its index
func replace_joypad_input_at_index(action: String, index: int, input: InputEvent, swap_if_taken: bool = true) -> Error:
var inputs: Array[InputEvent] = get_joypad_inputs_for_action(action)
var replacing_input
if inputs.is_empty() or inputs.size() <= index:
replacing_input = InputEventJoypadButton.new()
replacing_input.button_index = JOY_BUTTON_INVALID
else:
replacing_input = inputs[index]
return _update_joypad_input_for_action(action, input, swap_if_taken, replacing_input)
## Set the action used for a button
func _update_joypad_input_for_action(action: String, input: InputEvent, swap_if_taken: bool = true, replacing_input: InputEvent = null) -> Error:
var is_valid_keyboard_event = func(event):
return event is InputEventJoypadButton or event is InputEventJoypadMotion
return _update_input_for_action(action, input, swap_if_taken, replacing_input, is_valid_keyboard_event, joypad_input_changed)
func _update_input_for_action(action: String, input: InputEvent, swap_if_taken: bool, replacing_input: InputEvent, check_is_valid: Callable, did_change_signal: Signal) -> Error:
# Find any action that is already mapped to this input
var clashing_action = ""
var clashing_event
if swap_if_taken:
for other_action in InputMap.get_actions():
if other_action == action: continue
for event in InputMap.action_get_events(other_action):
if event.is_match(input):
clashing_action = other_action
clashing_event = event
# Find the key based event for the target action
var action_events: Array[InputEvent] = InputMap.action_get_events(action)
var is_replacing: bool = false
for i in range(0, action_events.size()):
var event: InputEvent = action_events[i]
if check_is_valid.call(event):
if replacing_input != null and not event.is_match(replacing_input):
continue
# Remap the other event if there is a clashing one
if clashing_action:
_update_input_for_action(clashing_action, event, false, clashing_event, check_is_valid, did_change_signal)
# Replace the event
action_events[i] = input
is_replacing = true
break
# If we were trying to replace something but didn't find it then just add it to the end
if not is_replacing:
action_events.append(input)
# Apply the changes
InputMap.action_erase_events(action)
for event in action_events:
if event != null:
InputMap.action_add_event(action, event)
did_change_signal.emit(action, input)
return OK
#endregion
#region Rumbling
func rumble_small(target_device: int = 0) -> void:
Input.start_joy_vibration(target_device, 0.4, 0, 0.1)
func rumble_medium(target_device: int = 0) -> void:
Input.start_joy_vibration(target_device, 0, 0.7, 0.1)
func rumble_large(target_device: int = 0) -> void:
Input.start_joy_vibration(target_device, 0, 1, 0.1)
func start_rumble_small(target_device: int = 0) -> void:
Input.start_joy_vibration(target_device, 0.4, 0, 0)
func start_rumble_medium(target_device: int = 0) -> void:
Input.start_joy_vibration(target_device, 0, 0.7, 0)
func start_rumble_large(target_device: int = 0) -> void:
Input.start_joy_vibration(target_device, 0, 1, 0)
func stop_rumble(target_device: int = 0) -> void:
Input.stop_joy_vibration(target_device)
#endregion

View File

@@ -0,0 +1 @@
uid://cholww48njaeh

View File

@@ -0,0 +1,7 @@
[plugin]
name="Input Helper"
description="Detect which input device the player is using and manage input actions"
author="Nathan Hoad"
version="4.7.0"
script="plugin.gd"

View File

@@ -0,0 +1,112 @@
@tool
extends EditorPlugin
const REMOTE_RELEASES_URL = "https://api.github.com/repos/nathanhoad/godot_input_helper/releases"
const LOCAL_CONFIG_PATH = "res://addons/input_helper/plugin.cfg"
const DownloadDialogScene = preload("res://addons/input_helper/views/download_dialog.tscn")
var http_request: HTTPRequest = HTTPRequest.new()
var next_version: String = ""
func _enter_tree():
add_autoload_singleton("InputHelper", "res://addons/input_helper/input_helper.gd")
# Configure settings
InputHelperSettings.prepare()
# Check for updates on GitHub
get_editor_interface().get_base_control().add_child(http_request)
http_request.request_completed.connect(_on_http_request_request_completed)
http_request.request(REMOTE_RELEASES_URL)
func _exit_tree():
remove_autoload_singleton("InputHelper")
if next_version != "":
remove_tool_menu_item("Update Input Helper to v%s" % next_version)
# Get the current version
func get_version() -> String:
var config: ConfigFile = ConfigFile.new()
config.load(LOCAL_CONFIG_PATH)
return config.get_value("plugin", "version")
# 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()
### Signals
func _on_http_request_request_completed(result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray) -> void:
http_request.queue_free()
if result != HTTPRequest.RESULT_SUCCESS: return
var current_version: String = 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)
return version_to_number(version) > version_to_number(current_version)
)
if versions.size() > 0:
next_version = versions[0].tag_name.substr(1)
add_tool_menu_item("Update Input Helper to v%s" % next_version, _update_input_helper)
func _update_input_helper() -> void:
var download_dialog := DownloadDialogScene.instantiate()
download_dialog.next_version = next_version
var scale: float = get_editor_interface().get_editor_scale()
download_dialog.min_size = Vector2(300, 250) * scale
download_dialog.update_finished.connect(_on_download_dialog_update_finished)
download_dialog.update_failed.connect(_on_download_dialog_update_failed)
get_editor_interface().get_base_control().add_child(download_dialog)
download_dialog.show()
func _on_download_dialog_update_finished() -> void:
remove_tool_menu_item("Update Input Helper to v%s" % next_version)
get_editor_interface().get_resource_filesystem().scan()
print_rich("\n[b]Updated Input Helper to v%s[/b]\n" % next_version)
var finished_dialog: AcceptDialog = AcceptDialog.new()
finished_dialog.dialog_text = "Your Input Helper is now up to date."
var restart_addon = func():
finished_dialog.queue_free()
get_editor_interface().call_deferred("set_plugin_enabled", "input_helper", true)
get_editor_interface().set_plugin_enabled("input_helper", false)
finished_dialog.canceled.connect(restart_addon)
finished_dialog.confirmed.connect(restart_addon)
get_editor_interface().get_base_control().add_child(finished_dialog)
finished_dialog.popup_centered()
func _on_download_dialog_update_failed() -> void:
var failed_dialog: AcceptDialog = AcceptDialog.new()
failed_dialog.dialog_text = "There was a problem downloading the update."
failed_dialog.canceled.connect(func(): failed_dialog.queue_free())
failed_dialog.confirmed.connect(func(): failed_dialog.queue_free())
get_editor_interface().get_base_control().add_child(failed_dialog)
failed_dialog.popup_centered()

View File

@@ -0,0 +1 @@
uid://cul6evy00vr55

View File

@@ -0,0 +1,42 @@
class_name InputHelperSettings extends Node
const USE_GRANULAR_DEVICE_IDENTIFIERS = "devices/use_granular_device_identifiers"
const SETTINGS_CONFIGURATION = {
USE_GRANULAR_DEVICE_IDENTIFIERS: {
value = false,
type = TYPE_BOOL,
is_advanced = true
},
}
static func prepare() -> void:
for key: String in SETTINGS_CONFIGURATION:
var setting_config: Dictionary = SETTINGS_CONFIGURATION[key]
var setting_name: String = "input_helper/%s" % key
if not ProjectSettings.has_setting(setting_name):
ProjectSettings.set_setting(setting_name, setting_config.value)
ProjectSettings.set_initial_value(setting_name, setting_config.value)
ProjectSettings.add_property_info({
"name" = setting_name,
"type" = setting_config.type,
"hint" = setting_config.get("hint", PROPERTY_HINT_NONE),
"hint_string" = setting_config.get("hint_string", "")
})
ProjectSettings.set_as_basic(setting_name, not setting_config.has("is_advanced"))
ProjectSettings.set_as_internal(setting_name, setting_config.has("is_hidden"))
static func set_setting(key: String, value) -> void:
if get_setting(key, value) != value:
ProjectSettings.set_setting("input_helper/%s" % key, value)
ProjectSettings.set_initial_value("input_helper/%s" % key, SETTINGS_CONFIGURATION[key].value)
ProjectSettings.save()
static func get_setting(key: String, default):
if ProjectSettings.has_setting("input_helper/%s" % key):
return ProjectSettings.get_setting("input_helper/%s" % key)
else:
return default

Some files were not shown because too many files have changed in this diff Show More