Add core content definitions
This commit is contained in:
@@ -1,14 +1,16 @@
|
||||
[gd_scene load_steps=5 format=3 uid="uid://b1fxc23gkbqre"]
|
||||
[gd_scene load_steps=6 format=3 uid="uid://b1fxc23gkbqre"]
|
||||
|
||||
[ext_resource type="Script" path="res://scripts/bootstrap/GameRoot.cs" id="1_game_root"]
|
||||
[ext_resource type="Script" path="res://scripts/debug/DebugOverlay.cs" id="2_debug_overlay"]
|
||||
[ext_resource type="PackedScene" path="res://scenes/menu/MenuPlaceholder.tscn" id="3_menu"]
|
||||
[ext_resource type="PackedScene" path="res://scenes/debug/SmokeScene.tscn" id="4_smoke"]
|
||||
[ext_resource type="PackedScene" path="res://scenes/debug/ContentBrowser.tscn" id="5_content_browser"]
|
||||
|
||||
[node name="GameRoot" type="Node"]
|
||||
script = ExtResource("1_game_root")
|
||||
MenuScene = ExtResource("3_menu")
|
||||
SmokeScene = ExtResource("4_smoke")
|
||||
ContentBrowserScene = ExtResource("5_content_browser")
|
||||
|
||||
[node name="DebugOverlay" type="CanvasLayer" parent="."]
|
||||
script = ExtResource("2_debug_overlay")
|
||||
|
||||
39
godot/scenes/debug/ContentBrowser.tscn
Normal file
39
godot/scenes/debug/ContentBrowser.tscn
Normal file
@@ -0,0 +1,39 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://cahv34nq56781"]
|
||||
|
||||
[ext_resource type="Script" path="res://scripts/debug/ContentBrowserController.cs" id="1_content_browser"]
|
||||
|
||||
[node name="ContentBrowser" 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_content_browser")
|
||||
|
||||
[node name="Title" type="Label" parent="."]
|
||||
layout_mode = 1
|
||||
anchors_preset = 10
|
||||
anchor_right = 1.0
|
||||
offset_left = 16.0
|
||||
offset_top = 16.0
|
||||
offset_right = -16.0
|
||||
offset_bottom = 48.0
|
||||
grow_horizontal = 2
|
||||
text = "Content Browser"
|
||||
|
||||
[node name="Scroll" type="ScrollContainer" parent="."]
|
||||
layout_mode = 1
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
offset_left = 16.0
|
||||
offset_top = 56.0
|
||||
offset_right = -16.0
|
||||
offset_bottom = -16.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
|
||||
[node name="Content" type="Label" parent="Scroll"]
|
||||
layout_mode = 2
|
||||
text = "Loading..."
|
||||
@@ -22,8 +22,9 @@ public partial class GameRoot : Node
|
||||
{
|
||||
PackedScene? scene = bootMode switch
|
||||
{
|
||||
DebugBootMode.Smoke => SmokeScene,
|
||||
_ => MenuScene
|
||||
DebugBootMode.Smoke => SmokeScene,
|
||||
DebugBootMode.ContentBrowser => ContentBrowserScene,
|
||||
_ => MenuScene
|
||||
};
|
||||
|
||||
string loadedSceneId = bootMode.ToString();
|
||||
@@ -54,6 +55,9 @@ public partial class GameRoot : Node
|
||||
[Export]
|
||||
public PackedScene? SmokeScene { get; set; }
|
||||
|
||||
[Export]
|
||||
public PackedScene? ContentBrowserScene { get; set; }
|
||||
|
||||
private Node? m_LoadedScene;
|
||||
private DebugSettings? m_Settings;
|
||||
}
|
||||
130
godot/scripts/content/ContentRegistry.cs
Normal file
130
godot/scripts/content/ContentRegistry.cs
Normal file
@@ -0,0 +1,130 @@
|
||||
#nullable enable
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using SideScrollerGame.Content.Definitions;
|
||||
|
||||
namespace SideScrollerGame.Content;
|
||||
|
||||
public sealed class ContentRegistry
|
||||
{
|
||||
public ContentRegistry(IEnumerable<MissionDefinition> missions, IEnumerable<DifficultyDefinition> difficulties, IEnumerable<CameraPathDefinition> cameraPaths, IEnumerable<LevelLayerDefinition> levelLayers, IEnumerable<EnemyTypeDefinition> enemyTypes, IEnumerable<EnemyBehaviorDefinition> enemyBehaviors, IEnumerable<EnemyClusterDefinition> enemyClusters, IEnumerable<CollectibleDefinition> collectibles, IEnumerable<WeaponDefinition> weapons, IEnumerable<SpecialWeaponDefinition> specialWeapons, IEnumerable<SquadronMateTypeDefinition> squadronMateTypes)
|
||||
{
|
||||
MissionDefinitions = missions.ToList();
|
||||
DifficultyDefinitions = difficulties.ToList();
|
||||
CameraPathDefinitions = cameraPaths.ToList();
|
||||
LevelLayerDefinitions = levelLayers.ToList();
|
||||
EnemyTypeDefinitions = enemyTypes.ToList();
|
||||
EnemyBehaviorDefinitions = enemyBehaviors.ToList();
|
||||
EnemyClusterDefinitions = enemyClusters.ToList();
|
||||
CollectibleDefinitions = collectibles.ToList();
|
||||
WeaponDefinitions = weapons.ToList();
|
||||
SpecialWeaponDefinitions = specialWeapons.ToList();
|
||||
SquadronMateTypeDefinitions = squadronMateTypes.ToList();
|
||||
|
||||
Missions = ToDictionary(MissionDefinitions);
|
||||
Difficulties = ToDictionary(DifficultyDefinitions);
|
||||
CameraPaths = ToDictionary(CameraPathDefinitions);
|
||||
LevelLayers = ToDictionary(LevelLayerDefinitions);
|
||||
EnemyTypes = ToDictionary(EnemyTypeDefinitions);
|
||||
EnemyBehaviors = ToDictionary(EnemyBehaviorDefinitions);
|
||||
EnemyClusters = ToDictionary(EnemyClusterDefinitions);
|
||||
Collectibles = ToDictionary(CollectibleDefinitions);
|
||||
Weapons = ToDictionary(WeaponDefinitions);
|
||||
SpecialWeapons = ToDictionary(SpecialWeaponDefinitions);
|
||||
SquadronMateTypes = ToDictionary(SquadronMateTypeDefinitions);
|
||||
}
|
||||
|
||||
public IEnumerable<string> AllDefinitionIds()
|
||||
{
|
||||
return AllDefinitions().Select(definition => definition.Id).OrderBy(id => id);
|
||||
}
|
||||
|
||||
public IReadOnlyList<DefinitionEntry> AllDefinitions()
|
||||
{
|
||||
return MissionDefinitions.Select(definition => new DefinitionEntry("Mission", definition.Id)).Concat(DifficultyDefinitions.Select(definition => new DefinitionEntry("Difficulty", definition.Id))).Concat(CameraPathDefinitions.Select(definition => new DefinitionEntry("CameraPath", definition.Id))).Concat(LevelLayerDefinitions.Select(definition => new DefinitionEntry("LevelLayer", definition.Id))).Concat(EnemyTypeDefinitions.Select(definition => new DefinitionEntry("EnemyType", definition.Id))).Concat(EnemyBehaviorDefinitions.Select(definition => new DefinitionEntry("EnemyBehavior", definition.Id))).Concat(EnemyClusterDefinitions.Select(definition => new DefinitionEntry("EnemyCluster", definition.Id))).Concat(CollectibleDefinitions.Select(definition => new DefinitionEntry("Collectible", definition.Id))).Concat(WeaponDefinitions.Select(definition => new DefinitionEntry("Weapon", definition.Id))).Concat(SpecialWeaponDefinitions.Select(definition => new DefinitionEntry("SpecialWeapon", definition.Id))).Concat(SquadronMateTypeDefinitions.Select(definition => new DefinitionEntry("SquadronMateType", definition.Id))).OrderBy(definition => definition.Id).ToList();
|
||||
}
|
||||
|
||||
public bool TryGetMission(string id, out MissionDefinition? definition)
|
||||
{
|
||||
return Missions.TryGetValue(id, out definition);
|
||||
}
|
||||
|
||||
public bool TryGetDifficulty(string id, out DifficultyDefinition? definition)
|
||||
{
|
||||
return Difficulties.TryGetValue(id, out definition);
|
||||
}
|
||||
|
||||
public bool TryGetEnemyType(string id, out EnemyTypeDefinition? definition)
|
||||
{
|
||||
return EnemyTypes.TryGetValue(id, out definition);
|
||||
}
|
||||
|
||||
public IReadOnlyList<MissionDefinition> MissionDefinitions { get; }
|
||||
|
||||
public IReadOnlyList<DifficultyDefinition> DifficultyDefinitions { get; }
|
||||
|
||||
public IReadOnlyList<CameraPathDefinition> CameraPathDefinitions { get; }
|
||||
|
||||
public IReadOnlyList<LevelLayerDefinition> LevelLayerDefinitions { get; }
|
||||
|
||||
public IReadOnlyList<EnemyTypeDefinition> EnemyTypeDefinitions { get; }
|
||||
|
||||
public IReadOnlyList<EnemyBehaviorDefinition> EnemyBehaviorDefinitions { get; }
|
||||
|
||||
public IReadOnlyList<EnemyClusterDefinition> EnemyClusterDefinitions { get; }
|
||||
|
||||
public IReadOnlyList<CollectibleDefinition> CollectibleDefinitions { get; }
|
||||
|
||||
public IReadOnlyList<WeaponDefinition> WeaponDefinitions { get; }
|
||||
|
||||
public IReadOnlyList<SpecialWeaponDefinition> SpecialWeaponDefinitions { get; }
|
||||
|
||||
public IReadOnlyList<SquadronMateTypeDefinition> SquadronMateTypeDefinitions { get; }
|
||||
|
||||
public IReadOnlyDictionary<string, MissionDefinition> Missions { get; }
|
||||
|
||||
public IReadOnlyDictionary<string, DifficultyDefinition> Difficulties { get; }
|
||||
|
||||
public IReadOnlyDictionary<string, CameraPathDefinition> CameraPaths { get; }
|
||||
|
||||
public IReadOnlyDictionary<string, LevelLayerDefinition> LevelLayers { get; }
|
||||
|
||||
public IReadOnlyDictionary<string, EnemyTypeDefinition> EnemyTypes { get; }
|
||||
|
||||
public IReadOnlyDictionary<string, EnemyBehaviorDefinition> EnemyBehaviors { get; }
|
||||
|
||||
public IReadOnlyDictionary<string, EnemyClusterDefinition> EnemyClusters { get; }
|
||||
|
||||
public IReadOnlyDictionary<string, CollectibleDefinition> Collectibles { get; }
|
||||
|
||||
public IReadOnlyDictionary<string, WeaponDefinition> Weapons { get; }
|
||||
|
||||
public IReadOnlyDictionary<string, SpecialWeaponDefinition> SpecialWeapons { get; }
|
||||
|
||||
public IReadOnlyDictionary<string, SquadronMateTypeDefinition> SquadronMateTypes { get; }
|
||||
|
||||
private static IReadOnlyDictionary<string, TDefinition> ToDictionary<TDefinition>(IEnumerable<TDefinition> definitions) where TDefinition : class
|
||||
{
|
||||
return definitions.GroupBy(GetId).Select(group => group.First()).ToDictionary(GetId);
|
||||
}
|
||||
|
||||
private static string GetId<TDefinition>(TDefinition definition)
|
||||
{
|
||||
return definition switch
|
||||
{
|
||||
MissionDefinition mission => mission.Id,
|
||||
DifficultyDefinition difficulty => difficulty.Id,
|
||||
CameraPathDefinition cameraPath => cameraPath.Id,
|
||||
LevelLayerDefinition levelLayer => levelLayer.Id,
|
||||
EnemyTypeDefinition enemyType => enemyType.Id,
|
||||
EnemyBehaviorDefinition enemyBehavior => enemyBehavior.Id,
|
||||
EnemyClusterDefinition enemyCluster => enemyCluster.Id,
|
||||
CollectibleDefinition collectible => collectible.Id,
|
||||
WeaponDefinition weapon => weapon.Id,
|
||||
SpecialWeaponDefinition specialWeapon => specialWeapon.Id,
|
||||
SquadronMateTypeDefinition squadronMateDefinition => squadronMateDefinition.Id,
|
||||
_ => string.Empty
|
||||
};
|
||||
}
|
||||
}
|
||||
1
godot/scripts/content/ContentRegistry.cs.uid
Normal file
1
godot/scripts/content/ContentRegistry.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://ced4h8gt2he8a
|
||||
3
godot/scripts/content/DefinitionEntry.cs
Normal file
3
godot/scripts/content/DefinitionEntry.cs
Normal file
@@ -0,0 +1,3 @@
|
||||
namespace SideScrollerGame.Content;
|
||||
|
||||
public sealed record DefinitionEntry(string Kind, string Id);
|
||||
1
godot/scripts/content/DefinitionEntry.cs.uid
Normal file
1
godot/scripts/content/DefinitionEntry.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dlbg73ke5c56n
|
||||
@@ -0,0 +1,5 @@
|
||||
#nullable enable
|
||||
|
||||
namespace SideScrollerGame.Content.Definitions;
|
||||
|
||||
public sealed record BehaviorEventDefinition(BehaviorEventKind Kind, double StartSeconds, double DurationSeconds, string? ReferenceId = null);
|
||||
@@ -0,0 +1 @@
|
||||
uid://bfhvtjvxbesyr
|
||||
12
godot/scripts/content/definitions/BehaviorEventKind.cs
Normal file
12
godot/scripts/content/definitions/BehaviorEventKind.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace SideScrollerGame.Content.Definitions;
|
||||
|
||||
public enum BehaviorEventKind
|
||||
{
|
||||
Wait,
|
||||
MovePath,
|
||||
RotatePath,
|
||||
FireProjectile,
|
||||
ChangeSpeed,
|
||||
SpawnChild,
|
||||
TriggerEffect
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://b3k6pfrvkik72
|
||||
@@ -0,0 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SideScrollerGame.Content.Definitions;
|
||||
|
||||
public sealed record BehaviorTrackDefinition(string Id, BehaviorTrackMode Mode, IReadOnlyList<BehaviorEventDefinition> Events);
|
||||
@@ -0,0 +1 @@
|
||||
uid://b8sossiey58sy
|
||||
7
godot/scripts/content/definitions/BehaviorTrackMode.cs
Normal file
7
godot/scripts/content/definitions/BehaviorTrackMode.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace SideScrollerGame.Content.Definitions;
|
||||
|
||||
public enum BehaviorTrackMode
|
||||
{
|
||||
Serial,
|
||||
Parallel
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://cj7orw08m62rq
|
||||
@@ -0,0 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SideScrollerGame.Content.Definitions;
|
||||
|
||||
public sealed record CameraPathDefinition(string Id, string DisplayName, IReadOnlyList<CameraPathPointDefinition> Points, double DefaultSpeed);
|
||||
@@ -0,0 +1 @@
|
||||
uid://1sohi8c0b4p4
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace SideScrollerGame.Content.Definitions;
|
||||
|
||||
public sealed record CameraPathPointDefinition(double TimeSeconds, double X, double Y, double Speed);
|
||||
@@ -0,0 +1 @@
|
||||
uid://n5yggkiyqu3u
|
||||
8
godot/scripts/content/definitions/ClusterEscapeRule.cs
Normal file
8
godot/scripts/content/definitions/ClusterEscapeRule.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace SideScrollerGame.Content.Definitions;
|
||||
|
||||
public enum ClusterEscapeRule
|
||||
{
|
||||
PreventReward,
|
||||
AllowReward,
|
||||
RespawnUntilDestroyed
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://bo12ujllh0vko
|
||||
@@ -0,0 +1,5 @@
|
||||
#nullable enable
|
||||
|
||||
namespace SideScrollerGame.Content.Definitions;
|
||||
|
||||
public sealed record CollectibleDefinition(string Id, string DisplayName, CollectibleKind Kind, int Value, string? ReferencedContentId = null);
|
||||
@@ -0,0 +1 @@
|
||||
uid://dojpxw2gdr0yo
|
||||
12
godot/scripts/content/definitions/CollectibleKind.cs
Normal file
12
godot/scripts/content/definitions/CollectibleKind.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace SideScrollerGame.Content.Definitions;
|
||||
|
||||
public enum CollectibleKind
|
||||
{
|
||||
Points,
|
||||
PrimaryWeapon,
|
||||
SecondaryWeapon,
|
||||
ClearScreen,
|
||||
ShieldCharge,
|
||||
SpecialAmmo,
|
||||
SquadronMate
|
||||
}
|
||||
1
godot/scripts/content/definitions/CollectibleKind.cs.uid
Normal file
1
godot/scripts/content/definitions/CollectibleKind.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bbuwcsopsg4on
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace SideScrollerGame.Content.Definitions;
|
||||
|
||||
public sealed record DifficultyDefinition(string Id, string DisplayName, DifficultyModifierSet Modifiers, int HeroStartingShieldCharges, int HeroRetryCount);
|
||||
@@ -0,0 +1 @@
|
||||
uid://8lf15cyl337v
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace SideScrollerGame.Content.Definitions;
|
||||
|
||||
public sealed record DifficultyModifierSet(double EnemyHealthMultiplier, double EnemyProjectileSpeedMultiplier, double EnemyFireCadenceMultiplier, double EnemySpawnDensityMultiplier, double ClusterSpawnIntervalMultiplier, double BossHealthMultiplier, double BossPhaseTimingMultiplier, double CollectibleDropRateMultiplier, double SpecialWeaponInitialAmmoMultiplier, double ScoreMultiplier);
|
||||
@@ -0,0 +1 @@
|
||||
uid://c3wiaioy3cj2l
|
||||
@@ -0,0 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SideScrollerGame.Content.Definitions;
|
||||
|
||||
public sealed record EnemyBehaviorDefinition(string Id, string DisplayName, IReadOnlyList<BehaviorTrackDefinition> Tracks);
|
||||
@@ -0,0 +1 @@
|
||||
uid://b7qggd0xsyl8v
|
||||
@@ -0,0 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SideScrollerGame.Content.Definitions;
|
||||
|
||||
public sealed record EnemyClusterDefinition(string Id, string DisplayName, IReadOnlyList<SpawnScheduleEntryDefinition> Spawns, int CompletionRewardPoints, ClusterEscapeRule EscapeRule);
|
||||
@@ -0,0 +1 @@
|
||||
uid://c5lqrpv5gc753
|
||||
5
godot/scripts/content/definitions/EnemyTypeDefinition.cs
Normal file
5
godot/scripts/content/definitions/EnemyTypeDefinition.cs
Normal file
@@ -0,0 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SideScrollerGame.Content.Definitions;
|
||||
|
||||
public sealed record EnemyTypeDefinition(string Id, string DisplayName, int Health, int ScoreValue, IReadOnlyList<string> BehaviorIds, IReadOnlyList<string> DropCollectibleIds);
|
||||
@@ -0,0 +1 @@
|
||||
uid://cgsajqr5fvw5g
|
||||
8
godot/scripts/content/definitions/LayerKind.cs
Normal file
8
godot/scripts/content/definitions/LayerKind.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace SideScrollerGame.Content.Definitions;
|
||||
|
||||
public enum LayerKind
|
||||
{
|
||||
Background,
|
||||
Interactive,
|
||||
Foreground
|
||||
}
|
||||
1
godot/scripts/content/definitions/LayerKind.cs.uid
Normal file
1
godot/scripts/content/definitions/LayerKind.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dp41dwk68qlin
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace SideScrollerGame.Content.Definitions;
|
||||
|
||||
public sealed record LevelLayerDefinition(string Id, string DisplayName, LayerKind Kind, double ScrollFactor, bool Repeats);
|
||||
@@ -0,0 +1 @@
|
||||
uid://dfikxuceq8ros
|
||||
5
godot/scripts/content/definitions/MissionDefinition.cs
Normal file
5
godot/scripts/content/definitions/MissionDefinition.cs
Normal file
@@ -0,0 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SideScrollerGame.Content.Definitions;
|
||||
|
||||
public sealed record MissionDefinition(string Id, string DisplayName, string DefaultDifficultyId, string CameraPathId, IReadOnlyList<string> BackgroundLayerIds, IReadOnlyList<string> ForegroundLayerIds, IReadOnlyList<string> ClusterIds, IReadOnlyList<string> CollectibleIds, IReadOnlyList<string> SpecialWeaponIds, IReadOnlyList<string> TimelineMarkers);
|
||||
@@ -0,0 +1 @@
|
||||
uid://d1gwpoe4obx5t
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace SideScrollerGame.Content.Definitions;
|
||||
|
||||
public sealed record SpawnScheduleEntryDefinition(string EnemyTypeId, double SpawnTimeSeconds, double AnchorX, double AnchorY);
|
||||
@@ -0,0 +1 @@
|
||||
uid://crrtalqhddqc3
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace SideScrollerGame.Content.Definitions;
|
||||
|
||||
public sealed record SpecialWeaponDefinition(string Id, string DisplayName, SpecialWeaponKind Kind, int InitialAmmo, int AmmoPickupAmount, int Damage);
|
||||
@@ -0,0 +1 @@
|
||||
uid://b7rncbmm228x6
|
||||
9
godot/scripts/content/definitions/SpecialWeaponKind.cs
Normal file
9
godot/scripts/content/definitions/SpecialWeaponKind.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace SideScrollerGame.Content.Definitions;
|
||||
|
||||
public enum SpecialWeaponKind
|
||||
{
|
||||
Bomb,
|
||||
Crawler,
|
||||
Napalm,
|
||||
BlackHole
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://cr8u8s2r2vlj5
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace SideScrollerGame.Content.Definitions;
|
||||
|
||||
public enum SquadronMateFormationKind
|
||||
{
|
||||
Hug,
|
||||
Orbit,
|
||||
LineFormation,
|
||||
VFormation,
|
||||
Follow
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://trmmvv4tfq0l
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace SideScrollerGame.Content.Definitions;
|
||||
|
||||
public sealed record SquadronMateTypeDefinition(string Id, string DisplayName, SquadronMateFormationKind Formation, double Spacing, double MovementSmoothing);
|
||||
@@ -0,0 +1 @@
|
||||
uid://b0n4orx2ffcu2
|
||||
3
godot/scripts/content/definitions/WeaponDefinition.cs
Normal file
3
godot/scripts/content/definitions/WeaponDefinition.cs
Normal file
@@ -0,0 +1,3 @@
|
||||
namespace SideScrollerGame.Content.Definitions;
|
||||
|
||||
public sealed record WeaponDefinition(string Id, string DisplayName, WeaponKind Kind, int Damage, double FireCadenceSeconds, double ProjectileSpeed, int ProjectileCount, bool ConsumesEnemyProjectiles);
|
||||
@@ -0,0 +1 @@
|
||||
uid://br35s1xq4dspu
|
||||
12
godot/scripts/content/definitions/WeaponKind.cs
Normal file
12
godot/scripts/content/definitions/WeaponKind.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace SideScrollerGame.Content.Definitions;
|
||||
|
||||
public enum WeaponKind
|
||||
{
|
||||
PrimaryBallistic,
|
||||
PrimarySeeking,
|
||||
PrimaryLaser,
|
||||
PrimaryGrenadeCluster,
|
||||
SecondaryVertical,
|
||||
SecondaryDiagonal,
|
||||
SecondaryBackward
|
||||
}
|
||||
1
godot/scripts/content/definitions/WeaponKind.cs.uid
Normal file
1
godot/scripts/content/definitions/WeaponKind.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bbc7rfx8wc610
|
||||
99
godot/scripts/content/samples/SampleContent.cs
Normal file
99
godot/scripts/content/samples/SampleContent.cs
Normal file
@@ -0,0 +1,99 @@
|
||||
using System.Collections.Generic;
|
||||
using SideScrollerGame.Content.Definitions;
|
||||
|
||||
namespace SideScrollerGame.Content.Samples;
|
||||
|
||||
public static class SampleContent
|
||||
{
|
||||
public static ContentRegistry CreateRegistry()
|
||||
{
|
||||
IReadOnlyList<DifficultyDefinition> difficulties =
|
||||
[
|
||||
CreateDifficulty("difficulty.easy", "Easy", 0.8, 4, 4, 0.75),
|
||||
CreateDifficulty("difficulty.normal", "Normal", 1.0, 3, 3, 1.0),
|
||||
CreateDifficulty("difficulty.hard", "Hard", 1.35, 2, 2, 1.5)
|
||||
];
|
||||
|
||||
IReadOnlyList<CameraPathDefinition> cameraPaths =
|
||||
[
|
||||
new("camera.test.path", "Test Camera Path", [
|
||||
new CameraPathPointDefinition(0.0, 0.0, 0.0, 120.0),
|
||||
new CameraPathPointDefinition(10.0, 1200.0, 0.0, 180.0),
|
||||
new CameraPathPointDefinition(20.0, 2400.0, 96.0, 90.0)
|
||||
], 120.0)
|
||||
];
|
||||
|
||||
IReadOnlyList<LevelLayerDefinition> levelLayers =
|
||||
[
|
||||
new("layer.background.stars", "Repeating Background", LayerKind.Background, 0.35, true),
|
||||
new("layer.foreground.clouds", "Repeating Foreground", LayerKind.Foreground, 1.4, true)
|
||||
];
|
||||
|
||||
IReadOnlyList<EnemyBehaviorDefinition> enemyBehaviors =
|
||||
[
|
||||
new("behavior.enemy.serial", "Serial Movement", [
|
||||
new BehaviorTrackDefinition("track.main", BehaviorTrackMode.Serial, [
|
||||
new BehaviorEventDefinition(BehaviorEventKind.MovePath, 0.0, 2.0),
|
||||
new BehaviorEventDefinition(BehaviorEventKind.Wait, 2.0, 0.5),
|
||||
new BehaviorEventDefinition(BehaviorEventKind.FireProjectile, 2.5, 0.1, "weapon.enemy.placeholder")
|
||||
])
|
||||
]),
|
||||
new("behavior.enemy.parallel", "Parallel Movement And Fire", [
|
||||
new BehaviorTrackDefinition("track.move", BehaviorTrackMode.Parallel, [
|
||||
new BehaviorEventDefinition(BehaviorEventKind.MovePath, 0.0, 5.0)
|
||||
]),
|
||||
new BehaviorTrackDefinition("track.fire", BehaviorTrackMode.Parallel, [
|
||||
new BehaviorEventDefinition(BehaviorEventKind.FireProjectile, 0.5, 4.0, "weapon.enemy.placeholder")
|
||||
])
|
||||
])
|
||||
];
|
||||
|
||||
IReadOnlyList<CollectibleDefinition> collectibles =
|
||||
[
|
||||
new("collectible.points.small", "Small Points", CollectibleKind.Points, 100),
|
||||
new("collectible.squadron.orbit", "Orbit Squadron Mate", CollectibleKind.SquadronMate, 1, "squadron.orbit")
|
||||
];
|
||||
|
||||
IReadOnlyList<EnemyTypeDefinition> enemyTypes =
|
||||
[
|
||||
new("enemy.serial", "Serial Enemy", 12, 150, ["behavior.enemy.serial"], ["collectible.points.small"]),
|
||||
new("enemy.parallel", "Parallel Enemy", 18, 250, ["behavior.enemy.parallel"], ["collectible.squadron.orbit"])
|
||||
];
|
||||
|
||||
IReadOnlyList<EnemyClusterDefinition> enemyClusters =
|
||||
[
|
||||
new("cluster.opening", "Opening Cluster", [
|
||||
new SpawnScheduleEntryDefinition("enemy.serial", 1.0, 1.1, 0.25),
|
||||
new SpawnScheduleEntryDefinition("enemy.parallel", 3.0, 1.15, 0.7)
|
||||
], 500, ClusterEscapeRule.PreventReward)
|
||||
];
|
||||
|
||||
IReadOnlyList<WeaponDefinition> weapons =
|
||||
[
|
||||
new("weapon.primary.basic", "Basic Primary", WeaponKind.PrimaryBallistic, 3, 0.18, 640.0, 1, false),
|
||||
new("weapon.secondary.vertical", "Vertical Secondary", WeaponKind.SecondaryVertical, 2, 0.35, 500.0, 1, false)
|
||||
];
|
||||
|
||||
IReadOnlyList<SpecialWeaponDefinition> specialWeapons =
|
||||
[
|
||||
new("weapon.special.bomb", "Bomb Special", SpecialWeaponKind.Bomb, 12, 3, 30)
|
||||
];
|
||||
|
||||
IReadOnlyList<SquadronMateTypeDefinition> squadronMateTypes =
|
||||
[
|
||||
new("squadron.orbit", "Orbit Mate", SquadronMateFormationKind.Orbit, 48.0, 10.0)
|
||||
];
|
||||
|
||||
IReadOnlyList<MissionDefinition> missions =
|
||||
[
|
||||
new("mission.test", "Test Mission", "difficulty.normal", "camera.test.path", ["layer.background.stars"], ["layer.foreground.clouds"], ["cluster.opening"], ["collectible.points.small", "collectible.squadron.orbit"], ["weapon.special.bomb"], ["intro", "cluster.opening", "boss.intro", "outro"])
|
||||
];
|
||||
|
||||
return new ContentRegistry(missions, difficulties, cameraPaths, levelLayers, enemyTypes, enemyBehaviors, enemyClusters, collectibles, weapons, specialWeapons, squadronMateTypes);
|
||||
}
|
||||
|
||||
private static DifficultyDefinition CreateDifficulty(string id, string displayName, double challengeMultiplier, int shields, int retries, double scoreMultiplier)
|
||||
{
|
||||
return new DifficultyDefinition(id, displayName, new DifficultyModifierSet(challengeMultiplier, challengeMultiplier, challengeMultiplier, challengeMultiplier, 1.0 / challengeMultiplier, challengeMultiplier, 1.0 / challengeMultiplier, 1.0, 1.0, scoreMultiplier), shields, retries);
|
||||
}
|
||||
}
|
||||
1
godot/scripts/content/samples/SampleContent.cs.uid
Normal file
1
godot/scripts/content/samples/SampleContent.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://fchmkl772mpk
|
||||
@@ -0,0 +1,5 @@
|
||||
#nullable enable
|
||||
|
||||
namespace SideScrollerGame.Content.Validation;
|
||||
|
||||
public sealed record ContentValidationMessage(ContentValidationSeverity Severity, string Code, string Message, string? DefinitionId = null);
|
||||
@@ -0,0 +1 @@
|
||||
uid://8qcy6e8iuhae
|
||||
16
godot/scripts/content/validation/ContentValidationResult.cs
Normal file
16
godot/scripts/content/validation/ContentValidationResult.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace SideScrollerGame.Content.Validation;
|
||||
|
||||
public sealed class ContentValidationResult
|
||||
{
|
||||
public ContentValidationResult(IEnumerable<ContentValidationMessage> messages)
|
||||
{
|
||||
Messages = messages.ToList();
|
||||
}
|
||||
|
||||
public IReadOnlyList<ContentValidationMessage> Messages { get; }
|
||||
|
||||
public bool HasErrors => Messages.Any(message => message.Severity == ContentValidationSeverity.Error);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://dbi1uj5y5xpa5
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace SideScrollerGame.Content.Validation;
|
||||
|
||||
public enum ContentValidationSeverity
|
||||
{
|
||||
Info,
|
||||
Warning,
|
||||
Error
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://c2kwhf3c7snb7
|
||||
374
godot/scripts/content/validation/ContentValidator.cs
Normal file
374
godot/scripts/content/validation/ContentValidator.cs
Normal file
@@ -0,0 +1,374 @@
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using SideScrollerGame.Content.Definitions;
|
||||
|
||||
namespace SideScrollerGame.Content.Validation;
|
||||
|
||||
public sealed class ContentValidator
|
||||
{
|
||||
public ContentValidationResult Validate(ContentRegistry registry)
|
||||
{
|
||||
List<ContentValidationMessage> messages = new();
|
||||
|
||||
ValidateDuplicateIds(registry, messages);
|
||||
ValidateDifficulties(registry, messages);
|
||||
ValidateCameraPaths(registry, messages);
|
||||
ValidateLayers(registry, messages);
|
||||
ValidateBehaviors(registry, messages);
|
||||
ValidateEnemies(registry, messages);
|
||||
ValidateClusters(registry, messages);
|
||||
ValidateCollectibles(registry, messages);
|
||||
ValidateWeapons(registry, messages);
|
||||
ValidateSpecialWeapons(registry, messages);
|
||||
ValidateSquadronMateTypes(registry, messages);
|
||||
ValidateMissions(registry, messages);
|
||||
|
||||
return new ContentValidationResult(messages);
|
||||
}
|
||||
|
||||
private static void ValidateDuplicateIds(ContentRegistry registry, List<ContentValidationMessage> messages)
|
||||
{
|
||||
foreach (IGrouping<string, DefinitionEntry> group in registry.AllDefinitions().GroupBy(definition => definition.Id))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(group.Key))
|
||||
{
|
||||
AddError(messages, "content.id.empty", "A definition has an empty id.");
|
||||
}
|
||||
else if (group.Count() > 1)
|
||||
{
|
||||
string kinds = string.Join(", ", group.Select(definition => definition.Kind));
|
||||
AddError(messages, "content.id.duplicate", $"Duplicate definition id '{group.Key}' appears in {kinds}.", group.Key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateDifficulties(ContentRegistry registry, List<ContentValidationMessage> messages)
|
||||
{
|
||||
foreach (DifficultyDefinition difficulty in registry.DifficultyDefinitions)
|
||||
{
|
||||
if (difficulty.HeroStartingShieldCharges < 0)
|
||||
{
|
||||
AddError(messages, "difficulty.shields.invalid", $"Difficulty '{difficulty.Id}' has negative starting shields.", difficulty.Id);
|
||||
}
|
||||
|
||||
if (difficulty.HeroRetryCount < 0)
|
||||
{
|
||||
AddError(messages, "difficulty.retries.invalid", $"Difficulty '{difficulty.Id}' has negative retry count.", difficulty.Id);
|
||||
}
|
||||
|
||||
ValidatePositiveMultiplier(messages, difficulty.Id, "EnemyHealthMultiplier", difficulty.Modifiers.EnemyHealthMultiplier);
|
||||
ValidatePositiveMultiplier(messages, difficulty.Id, "EnemyProjectileSpeedMultiplier", difficulty.Modifiers.EnemyProjectileSpeedMultiplier);
|
||||
ValidatePositiveMultiplier(messages, difficulty.Id, "EnemyFireCadenceMultiplier", difficulty.Modifiers.EnemyFireCadenceMultiplier);
|
||||
ValidatePositiveMultiplier(messages, difficulty.Id, "EnemySpawnDensityMultiplier", difficulty.Modifiers.EnemySpawnDensityMultiplier);
|
||||
ValidatePositiveMultiplier(messages, difficulty.Id, "ClusterSpawnIntervalMultiplier", difficulty.Modifiers.ClusterSpawnIntervalMultiplier);
|
||||
ValidatePositiveMultiplier(messages, difficulty.Id, "BossHealthMultiplier", difficulty.Modifiers.BossHealthMultiplier);
|
||||
ValidatePositiveMultiplier(messages, difficulty.Id, "BossPhaseTimingMultiplier", difficulty.Modifiers.BossPhaseTimingMultiplier);
|
||||
ValidatePositiveMultiplier(messages, difficulty.Id, "CollectibleDropRateMultiplier", difficulty.Modifiers.CollectibleDropRateMultiplier);
|
||||
ValidatePositiveMultiplier(messages, difficulty.Id, "SpecialWeaponInitialAmmoMultiplier", difficulty.Modifiers.SpecialWeaponInitialAmmoMultiplier);
|
||||
ValidatePositiveMultiplier(messages, difficulty.Id, "ScoreMultiplier", difficulty.Modifiers.ScoreMultiplier);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateCameraPaths(ContentRegistry registry, List<ContentValidationMessage> messages)
|
||||
{
|
||||
foreach (CameraPathDefinition cameraPath in registry.CameraPathDefinitions)
|
||||
{
|
||||
if (cameraPath.Points.Count == 0)
|
||||
{
|
||||
AddError(messages, "camera.points.empty", $"Camera path '{cameraPath.Id}' has no points.", cameraPath.Id);
|
||||
}
|
||||
|
||||
if (cameraPath.DefaultSpeed <= 0.0)
|
||||
{
|
||||
AddError(messages, "camera.speed.invalid", $"Camera path '{cameraPath.Id}' has non-positive default speed.", cameraPath.Id);
|
||||
}
|
||||
|
||||
foreach (CameraPathPointDefinition point in cameraPath.Points)
|
||||
{
|
||||
if (point.TimeSeconds < 0.0)
|
||||
{
|
||||
AddError(messages, "camera.point.time.invalid", $"Camera path '{cameraPath.Id}' has a point before mission start.", cameraPath.Id);
|
||||
}
|
||||
|
||||
if (point.Speed <= 0.0)
|
||||
{
|
||||
AddError(messages, "camera.point.speed.invalid", $"Camera path '{cameraPath.Id}' has a point with non-positive speed.", cameraPath.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateLayers(ContentRegistry registry, List<ContentValidationMessage> messages)
|
||||
{
|
||||
foreach (LevelLayerDefinition layer in registry.LevelLayerDefinitions)
|
||||
{
|
||||
if (layer.ScrollFactor < 0.0)
|
||||
{
|
||||
AddError(messages, "layer.scroll.invalid", $"Layer '{layer.Id}' has negative scroll factor.", layer.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateBehaviors(ContentRegistry registry, List<ContentValidationMessage> messages)
|
||||
{
|
||||
foreach (EnemyBehaviorDefinition behavior in registry.EnemyBehaviorDefinitions)
|
||||
{
|
||||
if (behavior.Tracks.Count == 0)
|
||||
{
|
||||
AddError(messages, "behavior.tracks.empty", $"Behavior '{behavior.Id}' has no tracks.", behavior.Id);
|
||||
}
|
||||
|
||||
foreach (BehaviorTrackDefinition track in behavior.Tracks)
|
||||
{
|
||||
if (track.Events.Count == 0)
|
||||
{
|
||||
AddError(messages, "behavior.track.events.empty", $"Behavior '{behavior.Id}' track '{track.Id}' has no events.", behavior.Id);
|
||||
}
|
||||
|
||||
foreach (BehaviorEventDefinition behaviorEvent in track.Events)
|
||||
{
|
||||
if (behaviorEvent.StartSeconds < 0.0)
|
||||
{
|
||||
AddError(messages, "behavior.event.start.invalid", $"Behavior '{behavior.Id}' has an event before mission start.", behavior.Id);
|
||||
}
|
||||
|
||||
if (behaviorEvent.DurationSeconds < 0.0)
|
||||
{
|
||||
AddError(messages, "behavior.event.duration.invalid", $"Behavior '{behavior.Id}' has an event with negative duration.", behavior.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateEnemies(ContentRegistry registry, List<ContentValidationMessage> messages)
|
||||
{
|
||||
foreach (EnemyTypeDefinition enemyType in registry.EnemyTypeDefinitions)
|
||||
{
|
||||
if (enemyType.Health <= 0)
|
||||
{
|
||||
AddError(messages, "enemy.health.invalid", $"Enemy '{enemyType.Id}' has non-positive health.", enemyType.Id);
|
||||
}
|
||||
|
||||
if (enemyType.ScoreValue < 0)
|
||||
{
|
||||
AddError(messages, "enemy.score.invalid", $"Enemy '{enemyType.Id}' has negative score value.", enemyType.Id);
|
||||
}
|
||||
|
||||
if (enemyType.BehaviorIds.Count == 0)
|
||||
{
|
||||
AddError(messages, "enemy.behaviors.empty", $"Enemy '{enemyType.Id}' has no behavior references.", enemyType.Id);
|
||||
}
|
||||
|
||||
foreach (string behaviorId in enemyType.BehaviorIds)
|
||||
{
|
||||
if (!registry.EnemyBehaviors.ContainsKey(behaviorId))
|
||||
{
|
||||
AddError(messages, "enemy.behavior.missing", $"Enemy '{enemyType.Id}' references missing behavior '{behaviorId}'.", enemyType.Id);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (string collectibleId in enemyType.DropCollectibleIds)
|
||||
{
|
||||
if (!registry.Collectibles.ContainsKey(collectibleId))
|
||||
{
|
||||
AddError(messages, "enemy.collectible.missing", $"Enemy '{enemyType.Id}' references missing drop collectible '{collectibleId}'.", enemyType.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateClusters(ContentRegistry registry, List<ContentValidationMessage> messages)
|
||||
{
|
||||
foreach (EnemyClusterDefinition cluster in registry.EnemyClusterDefinitions)
|
||||
{
|
||||
if (cluster.Spawns.Count == 0)
|
||||
{
|
||||
AddError(messages, "cluster.spawns.empty", $"Cluster '{cluster.Id}' has no spawns.", cluster.Id);
|
||||
}
|
||||
|
||||
if (cluster.CompletionRewardPoints < 0)
|
||||
{
|
||||
AddError(messages, "cluster.reward.invalid", $"Cluster '{cluster.Id}' has negative completion reward.", cluster.Id);
|
||||
}
|
||||
|
||||
foreach (SpawnScheduleEntryDefinition spawn in cluster.Spawns)
|
||||
{
|
||||
if (spawn.SpawnTimeSeconds < 0.0)
|
||||
{
|
||||
AddError(messages, "cluster.spawn.time.invalid", $"Cluster '{cluster.Id}' has a spawn before mission start.", cluster.Id);
|
||||
}
|
||||
|
||||
if (!registry.EnemyTypes.ContainsKey(spawn.EnemyTypeId))
|
||||
{
|
||||
AddError(messages, "cluster.enemy.missing", $"Cluster '{cluster.Id}' references missing enemy type '{spawn.EnemyTypeId}'.", cluster.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateCollectibles(ContentRegistry registry, List<ContentValidationMessage> messages)
|
||||
{
|
||||
foreach (CollectibleDefinition collectible in registry.CollectibleDefinitions)
|
||||
{
|
||||
if (collectible.Value < 0)
|
||||
{
|
||||
AddError(messages, "collectible.value.invalid", $"Collectible '{collectible.Id}' has negative value.", collectible.Id);
|
||||
}
|
||||
|
||||
if (collectible.Kind == CollectibleKind.PrimaryWeapon && !ReferencedContentExists(registry.Weapons, collectible.ReferencedContentId))
|
||||
{
|
||||
AddError(messages, "collectible.weapon.missing", $"Collectible '{collectible.Id}' references missing primary weapon '{collectible.ReferencedContentId}'.", collectible.Id);
|
||||
}
|
||||
|
||||
if (collectible.Kind == CollectibleKind.SecondaryWeapon && !ReferencedContentExists(registry.Weapons, collectible.ReferencedContentId))
|
||||
{
|
||||
AddError(messages, "collectible.weapon.missing", $"Collectible '{collectible.Id}' references missing secondary weapon '{collectible.ReferencedContentId}'.", collectible.Id);
|
||||
}
|
||||
|
||||
if (collectible.Kind == CollectibleKind.SpecialAmmo && !ReferencedContentExists(registry.SpecialWeapons, collectible.ReferencedContentId))
|
||||
{
|
||||
AddError(messages, "collectible.special.missing", $"Collectible '{collectible.Id}' references missing special weapon '{collectible.ReferencedContentId}'.", collectible.Id);
|
||||
}
|
||||
|
||||
if (collectible.Kind == CollectibleKind.SquadronMate && !ReferencedContentExists(registry.SquadronMateTypes, collectible.ReferencedContentId))
|
||||
{
|
||||
AddError(messages, "collectible.squadron.missing", $"Collectible '{collectible.Id}' references missing squadron mate type '{collectible.ReferencedContentId}'.", collectible.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateWeapons(ContentRegistry registry, List<ContentValidationMessage> messages)
|
||||
{
|
||||
foreach (WeaponDefinition weapon in registry.WeaponDefinitions)
|
||||
{
|
||||
if (weapon.Damage <= 0)
|
||||
{
|
||||
AddError(messages, "weapon.damage.invalid", $"Weapon '{weapon.Id}' has non-positive damage.", weapon.Id);
|
||||
}
|
||||
|
||||
if (weapon.FireCadenceSeconds <= 0.0)
|
||||
{
|
||||
AddError(messages, "weapon.cadence.invalid", $"Weapon '{weapon.Id}' has non-positive fire cadence.", weapon.Id);
|
||||
}
|
||||
|
||||
if (weapon.ProjectileSpeed <= 0.0)
|
||||
{
|
||||
AddError(messages, "weapon.speed.invalid", $"Weapon '{weapon.Id}' has non-positive projectile speed.", weapon.Id);
|
||||
}
|
||||
|
||||
if (weapon.ProjectileCount <= 0)
|
||||
{
|
||||
AddError(messages, "weapon.projectiles.invalid", $"Weapon '{weapon.Id}' has non-positive projectile count.", weapon.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateSpecialWeapons(ContentRegistry registry, List<ContentValidationMessage> messages)
|
||||
{
|
||||
foreach (SpecialWeaponDefinition specialWeapon in registry.SpecialWeaponDefinitions)
|
||||
{
|
||||
if (specialWeapon.InitialAmmo < 0)
|
||||
{
|
||||
AddError(messages, "special.ammo.invalid", $"Special weapon '{specialWeapon.Id}' has negative initial ammo.", specialWeapon.Id);
|
||||
}
|
||||
|
||||
if (specialWeapon.AmmoPickupAmount < 0)
|
||||
{
|
||||
AddError(messages, "special.pickup.invalid", $"Special weapon '{specialWeapon.Id}' has negative pickup ammo.", specialWeapon.Id);
|
||||
}
|
||||
|
||||
if (specialWeapon.Damage <= 0)
|
||||
{
|
||||
AddError(messages, "special.damage.invalid", $"Special weapon '{specialWeapon.Id}' has non-positive damage.", specialWeapon.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateSquadronMateTypes(ContentRegistry registry, List<ContentValidationMessage> messages)
|
||||
{
|
||||
foreach (SquadronMateTypeDefinition squadronMateType in registry.SquadronMateTypeDefinitions)
|
||||
{
|
||||
if (squadronMateType.Spacing < 0.0)
|
||||
{
|
||||
AddError(messages, "squadron.spacing.invalid", $"Squadron mate type '{squadronMateType.Id}' has negative spacing.", squadronMateType.Id);
|
||||
}
|
||||
|
||||
if (squadronMateType.MovementSmoothing < 0.0)
|
||||
{
|
||||
AddError(messages, "squadron.smoothing.invalid", $"Squadron mate type '{squadronMateType.Id}' has negative movement smoothing.", squadronMateType.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateMissions(ContentRegistry registry, List<ContentValidationMessage> messages)
|
||||
{
|
||||
foreach (MissionDefinition mission in registry.MissionDefinitions)
|
||||
{
|
||||
RequireReference(messages, mission.Id, mission.DefaultDifficultyId, registry.Difficulties, "mission.difficulty.missing", "default difficulty");
|
||||
RequireReference(messages, mission.Id, mission.CameraPathId, registry.CameraPaths, "mission.camera.missing", "camera path");
|
||||
RequireReferences(messages, mission.Id, mission.BackgroundLayerIds, registry.LevelLayers, "mission.background.missing", "background layer");
|
||||
RequireReferences(messages, mission.Id, mission.ForegroundLayerIds, registry.LevelLayers, "mission.foreground.missing", "foreground layer");
|
||||
RequireReferences(messages, mission.Id, mission.ClusterIds, registry.EnemyClusters, "mission.cluster.missing", "cluster");
|
||||
RequireReferences(messages, mission.Id, mission.CollectibleIds, registry.Collectibles, "mission.collectible.missing", "collectible");
|
||||
RequireReferences(messages, mission.Id, mission.SpecialWeaponIds, registry.SpecialWeapons, "mission.special.missing", "special weapon");
|
||||
|
||||
if (mission.BackgroundLayerIds.Count == 0)
|
||||
{
|
||||
AddError(messages, "mission.background.empty", $"Mission '{mission.Id}' has no background layers.", mission.Id);
|
||||
}
|
||||
|
||||
if (mission.ForegroundLayerIds.Count == 0)
|
||||
{
|
||||
AddError(messages, "mission.foreground.empty", $"Mission '{mission.Id}' has no foreground layers.", mission.Id);
|
||||
}
|
||||
|
||||
if (mission.ClusterIds.Count == 0)
|
||||
{
|
||||
AddError(messages, "mission.clusters.empty", $"Mission '{mission.Id}' has no clusters.", mission.Id);
|
||||
}
|
||||
|
||||
if (mission.TimelineMarkers.Count == 0)
|
||||
{
|
||||
AddError(messages, "mission.markers.empty", $"Mission '{mission.Id}' has no timeline markers.", mission.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidatePositiveMultiplier(List<ContentValidationMessage> messages, string definitionId, string fieldName, double value)
|
||||
{
|
||||
if (value <= 0.0 || double.IsNaN(value) || double.IsInfinity(value))
|
||||
{
|
||||
AddError(messages, "difficulty.multiplier.invalid", $"Difficulty '{definitionId}' has non-positive {fieldName}.", definitionId);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool ReferencedContentExists<TDefinition>(IReadOnlyDictionary<string, TDefinition> definitions, string? id)
|
||||
{
|
||||
return !string.IsNullOrWhiteSpace(id) && definitions.ContainsKey(id);
|
||||
}
|
||||
|
||||
private static void RequireReferences<TDefinition>(List<ContentValidationMessage> messages, string ownerId, IEnumerable<string> referenceIds, IReadOnlyDictionary<string, TDefinition> definitions, string code, string referenceKind)
|
||||
{
|
||||
foreach (string referenceId in referenceIds)
|
||||
{
|
||||
RequireReference(messages, ownerId, referenceId, definitions, code, referenceKind);
|
||||
}
|
||||
}
|
||||
|
||||
private static void RequireReference<TDefinition>(List<ContentValidationMessage> messages, string ownerId, string referenceId, IReadOnlyDictionary<string, TDefinition> definitions, string code, string referenceKind)
|
||||
{
|
||||
if (!definitions.ContainsKey(referenceId))
|
||||
{
|
||||
AddError(messages, code, $"Definition '{ownerId}' references missing {referenceKind} '{referenceId}'.", ownerId);
|
||||
}
|
||||
}
|
||||
|
||||
private static void AddError(List<ContentValidationMessage> messages, string code, string message, string? definitionId = null)
|
||||
{
|
||||
messages.Add(new ContentValidationMessage(ContentValidationSeverity.Error, code, message, definitionId));
|
||||
}
|
||||
}
|
||||
1
godot/scripts/content/validation/ContentValidator.cs.uid
Normal file
1
godot/scripts/content/validation/ContentValidator.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://chj207coyjf4j
|
||||
80
godot/scripts/debug/ContentBrowserController.cs
Normal file
80
godot/scripts/debug/ContentBrowserController.cs
Normal file
@@ -0,0 +1,80 @@
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Godot;
|
||||
using SideScrollerGame.Content;
|
||||
using SideScrollerGame.Content.Samples;
|
||||
using SideScrollerGame.Content.Validation;
|
||||
|
||||
namespace SideScrollerGame.Debug;
|
||||
|
||||
public partial class ContentBrowserController : Control
|
||||
{
|
||||
public override void _Ready()
|
||||
{
|
||||
ContentRegistry registry = SampleContent.CreateRegistry();
|
||||
ContentValidationResult validation = new ContentValidator().Validate(registry);
|
||||
string report = BuildReport(registry, validation);
|
||||
SetText(report);
|
||||
|
||||
if (!ShouldValidateAndQuit())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
GD.Print(report);
|
||||
GD.Print(validation.HasErrors ? "Content validation failed" : "Content validation succeeded");
|
||||
GetTree().Quit(validation.HasErrors ? 1 : 0);
|
||||
}
|
||||
|
||||
private static string BuildReport(ContentRegistry registry, ContentValidationResult validation)
|
||||
{
|
||||
StringBuilder builder = new();
|
||||
builder.AppendLine("Loaded content definitions:");
|
||||
foreach (string id in registry.AllDefinitionIds())
|
||||
{
|
||||
builder.AppendLine(id);
|
||||
}
|
||||
|
||||
if (validation.Messages.Count > 0)
|
||||
{
|
||||
builder.AppendLine();
|
||||
builder.AppendLine("Validation messages:");
|
||||
foreach (ContentValidationMessage message in validation.Messages)
|
||||
{
|
||||
builder.AppendLine($"{message.Severity}: {message.Code}: {message.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
return builder.ToString().TrimEnd();
|
||||
}
|
||||
|
||||
private static bool ShouldValidateAndQuit()
|
||||
{
|
||||
return IsHeadless() && OS.GetCmdlineUserArgs().Any(argument => argument.Equals(ValidateOnlyArgument, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
private static bool IsHeadless()
|
||||
{
|
||||
return DisplayServer.GetName().Equals("headless", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private void SetText(string report)
|
||||
{
|
||||
Label? title = GetNodeOrNull<Label>("Title");
|
||||
if (title is not null)
|
||||
{
|
||||
title.Text = "Content Browser";
|
||||
}
|
||||
|
||||
Label? content = GetNodeOrNull<Label>("Scroll/Content");
|
||||
if (content is not null)
|
||||
{
|
||||
content.Text = report;
|
||||
}
|
||||
}
|
||||
|
||||
private const string ValidateOnlyArgument = "--content-validate-only";
|
||||
}
|
||||
1
godot/scripts/debug/ContentBrowserController.cs.uid
Normal file
1
godot/scripts/debug/ContentBrowserController.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cfukg3hg4wwpx
|
||||
@@ -3,5 +3,6 @@ namespace SideScrollerGame.Debug;
|
||||
public enum DebugBootMode
|
||||
{
|
||||
Menu,
|
||||
Smoke
|
||||
Smoke,
|
||||
ContentBrowser
|
||||
}
|
||||
@@ -81,7 +81,28 @@ public sealed class DebugSettings
|
||||
|
||||
private static bool TryParseBootMode(string value, out DebugBootMode bootMode)
|
||||
{
|
||||
return Enum.TryParse(value, true, out bootMode);
|
||||
if (Enum.TryParse(value, true, out bootMode))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
string normalizedValue = NormalizeBootModeName(value);
|
||||
foreach (DebugBootMode candidate in Enum.GetValues<DebugBootMode>())
|
||||
{
|
||||
if (NormalizeBootModeName(candidate.ToString()).Equals(normalizedValue, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
bootMode = candidate;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
bootMode = DebugBootMode.Menu;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string NormalizeBootModeName(string value)
|
||||
{
|
||||
return value.Replace("-", string.Empty, StringComparison.Ordinal).Replace("_", string.Empty, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private const string DebugBootModePrefix = "--debug-boot=";
|
||||
|
||||
Reference in New Issue
Block a user