Instruction file imported from DMINATOR/supreme-system (
.github/instructions/godot-scripting.instructions.md). Copyright stays with the author.
Godot Scripting Conventions
Node Scripts
- All Godot node scripts must be
partial class - Do not use namespaces — the Godot autoload/scene system expects top-level types
- Don't put game logic directly in node scripts — delegate to
SupremeEngine - Never use generic base classes for Godot node scripts — Godot 4's C# script registry cannot handle open generic types (e.g.
class Foo<T> : Node) and will throw a duplicate-keyArgumentExceptionwhen reloading the assembly; if you need a typed base, use a non-generic abstract base class and cast at the call site in the concrete subclass
Logging
- Use
GD.Printfor logging — neverConsole.WriteLine
_Ready Structure
_ReadycallsLoadNodes()and thenPrepareNodes()— omitPrepareNodes()only if the scene has no signal wiring or state initializationLoadNodes(): everyGetNode<T>(...)assignment goes here and nowhere elsePrepareNodes(): signal wiring, initial state setup, and any calls that rely on loaded nodes go here- Both are
private voidmethods placed in the private methods section of the class
Setup Methods
- Scenes that require external data before display must expose a
Setup(...)method - Never call
SetupbeforeAddChild—_Readymust have already fired so thatLoadNodeshas run and nodes are available;Setupthen populates them directly [Export]properties are the opposite — they must be set beforeAddChildso they are readable when_Readyfires; this is a different pattern fromSetup()and the two must not be confused:[Export]values → beforeAddChild;Setup()calls → afterAddChild- For Util helpers that instantiate scenes, the helper must call
parent.AddChild(scene)first, thenscene.Setup(...)— this guarantees_Readyhas fired by the timeSetupruns - Do not use null checks on node fields inside
Setupto detect whether_Readyhas fired; that is a design flaw — fix the call order instead
Node References
- Every
GetNode<T>(...)call must be stored in a private field inLoadNodes— never chain.Pressed,.Text, etc. directly off aGetNodecall - This applies to both autoloads and scene-tree nodes; storing in a field makes null-reference issues easy to spot and debug
- Autoloads are retrieved using path constants from
AutoloadPath— never hard-code"/root/..."strings inline- Current autoloads:
AutoloadPath.SceneManager,AutoloadPath.SaveManager,AutoloadPath.WorldManager - When a new autoload is added, add its constant to
Managers/AutoloadPath.csfirst
- Current autoloads:
Scene Files (.tscn) and Resources (.tres)
- Every
.csscript attached to a scene or resource has a matching.cs.uidfile generated by Godot - Never invent a UID — Godot UIDs are base62-encoded random values; any manually written value will be rejected at runtime with an "invalid UID" warning
- This applies equally to
.tscnscene files,.tresresource files, and.cs.uidscript UID files — do not writeuid=values in any of them manually - When creating a
.tscnor.tresmanually for a script that already has a.cs.uidfile: read the.cs.uidfile and copy the exact value into the[ext_resource]line - When creating a
.tscnor.tresmanually for a new script (no.cs.uidexists yet): omit theuid=attribute from[ext_resource]entirely — Godot will use the path as a fallback and generate the.cs.uidand assign the UID on next project load - Never create
.cs.uidfiles manually — always let Godot generate them - Never use a
.cs.uidvalue as theuid=for aPackedSceneext_resource —.cs.uidfiles hold script UIDs, not scene UIDs; scene UIDs live in theuid=attribute on the[gd_scene]header line of the.tscnfile itself; if the.tscnwas created manually and has no header UID, omituid=from the[ext_resource]reference entirely - UID quick-reference when creating a
.tscnmanually:Situation What to write [gd_scene]header of the new fileOmit uid=— Godot assigns on loadScript [ext_resource]—.cs.uidexistsCopy exact value from .cs.uidfileScript [ext_resource]— no.cs.uidyetOmit uid=entirelyPackedScene[ext_resource](sub-scene)Omit uid=— never copy a.cs.uidvalue here - When embedding a sub-scene inside a
.tscn, useinstance=ExtResource("...")on the node line — never combinetype=andinstance=on the same node;type=overridesinstance=and the script won't be attached:[node name="CardScene" parent="VBoxContainer" instance=ExtResource("2_card")] - Scene/Node names in the editor: PascalCase
- To change the entry-point scene, update
run/main_sceneinproject.godot
Prefab Scenes
- Prefab scenes are reusable scene fragments that are instantiated programmatically and embedded inside other scenes — they are not navigated to via
SceneManager - They live in
Scenes/Prefabs/and their filenames end withPrefabScene(e.g.CardPrefabScene.tscn,CardOfferPrefabScene.tscn) - Their C# class names also end with
PrefabScene(e.g.CardPrefabScene,CardOfferPrefabScene) - Their path constants in
SceneManagerend withPrefabScene— noGoTomethod is added for them - They are always instantiated via a
PrefabFactory-style helper inUtil/, never directly from a scene script - Never call
SetupbeforeAddChildon a prefab scene —_Ready(and thereforeLoadNodes) must fire first; the helper is responsible for the correct order - When removing a dynamically instantiated prefab scene, unsubscribe from all C# events before calling
QueueFree()to prevent dangling references
Navigation Buttons
- Always use
SceneButtonPrefabScenefor any button that navigates to another scene — embed it as aninstance=ExtResource(...)in the.tscnand set its[Export] TargetSceneproperty inline; never wire a plainButtontoSceneManager.GoToin C# code - The correct
.tscnpattern:[ext_resource type="PackedScene" path="res://Scenes/Prefabs/Control/SceneButtonPrefabScene.tscn" id="N_id"] ... [node name="BackButton" parent="Hud" instance=ExtResource("N_id")] text = "Back" TargetScene = 9TargetSceneis aGameSceneenum export; Godot serializes enum exports as integers in.tscnfiles (e.g.9forRegionMapScene) — seeSceneManager.GameScenefor the full list of values - Before adding any navigation button, check
Scenes/Prefabs/Control/for the right prefab — do not invent a new approach
Scene Structure — Static vs Dynamic
- All static controls a scene needs must be declared in its
.tscnfile with sensible default values set inline — never construct them in C# - Never call
new Label(),new Button(),new HBoxContainer(), or any other bare Godot control constructor in a scene script — these nodes have no.tscnbacking, cannot be themed, inspected, or resized in the editor - Every element that must be added dynamically at runtime must be a prefab scene, instantiated through
PrefabFactory— if no matching factory method exists, add one - This rule applies to structural layout containers too — a
VBoxContainerwrapping companion tabs is not exempt; it must also be a prefab, notnew VBoxContainer() - The one-sentence test: if a control cannot be pointed to in a
.tscnfile or its prefab's.tscn, it should not exist
Asset Binding — Prefer Exported Fields Over Hardcoded Paths
- Never hardcode
res://resource paths inside C# scripts — use[Export]fields and bind the resources in the.tscnfile instead - This keeps resource references deterministic and editor-visible: the scene file is the single source of truth for which assets are used
- When a sub-component needs to select from a fixed set of assets based on data (e.g. a header image per rarity), extract it into its own prefab scene with one
[Export]field per variant, bind all variants in that scene's.tscn, and expose aSetup(...)method that picks the correct one:// CardPrefabHeaderScene.cs [Export] public Texture2D CommonHeader { get; set; } [Export] public Texture2D RareHeader { get; set; } public void Setup(CardRarity rarity) { Texture = rarity switch { CardRarity.Common => CommonHeader, _ => RareHeader }; } - The parent scene embeds the sub-scene via
instance=ExtResource(...)and callsSetup(...)from its ownSetupmethod — no path strings appear anywhere in C#
Util
godot/supreme-godot/Util/contains shared Godot-layer helpers — check here before writing one-off boilerplate in a scene script- Current helpers:
DialogHelper.ShowConfirm(Node parent, string message, Action onConfirmed)— shows aConfirmationDialog, wires confirm/cancel, and callsQueueFreeautomaticallyDialogHelper.ShowError(Node parent, string message)— logs viaGD.PushErrorand shows anAcceptDialogPrefabFactory.CreateCardSlotScenes(Node parent, ICardCollection collection, bool enableDragAndDrop, Action<Card> onCardPressed)— creates all card slot scenes for every slot incollection, enables drag-and-drop if requested, and wiresCardPressedtoonCardPressedon each; use this instead of a manual loopPrefabFactory.CreateBagScene(Node parent, ICardCollection bag)— instantiatesCardCollectionPrefabScene.tscn, adds it toparent, callsSetup(bag, "Bag"), and returns the ready nodePrefabFactory.CreateCompanionDeckScene(Node parent, ICardCollection deck)— instantiatesCardCollectionPrefabScene.tscn, adds it toparent, callsSetup(deck, "Deck"), and returns the ready node; the caller resolves the deck fromWorldManagerPrefabFactory.CreateCatalogueScene(Node parent)— instantiatesCardCollectionPrefabScene.tscn, loadsCardTemplateLibrary, builds aCardCollectionfrom all templates, callsSetupwith D&D disabled, and returns the ready node; exposesCardSelectedevent viaCardCollectionPrefabScenePrefabFactory.CreateCardTemplateRowScene(Node parent, string displayText)— instantiatesCardTemplateRowPrefabScene.tscn, adds it toparent, callsSetup(displayText), and returns the ready node; exposesCreatePressedevent for the caller to subscribe toPrefabFactory.CreateCardTemplateRowScene(Node parent, string displayText, Action onCreate)— same as above but also wiresCreatePressedin one call; prefer this overload when the callback is known at creation timePrefabFactory.CreateSaveSlotRowScene(Node parent, SlotSummary summary)— instantiatesSaveSlotRowPrefabScene.tscn, adds it toparent, callsSetup(summary), and returns the ready node; exposesNewPressed,LoadPressed, andDeletePressedeventsPrefabFactory.CreateSaveSlotRowScene(Node parent, SlotSummary summary, Action onNew, Action onLoad, Action onDelete)— same as above but also wires all three events in one call; prefer this overload when the callbacks are known at creation timePrefabFactory.CreateCompanionMemberTabScene(Node parent, string companionId)— instantiatesCompanionMemberTabPrefabScene.tscn, setsCompanionIdbeforeAddChild, adds it toparent, and returns the ready node; the scene self-loads its deck and equipment fromWorldManagerin_ReadyPrefabFactory.CreateCompanionEquipmentScene(Node parent, string companionId)— instantiatesCompanionEquipmentSlotsPrefabScene.tscnscoped to a companionPrefabFactory.CreateRegionCellScene(Node parent, Region region, Action<Region> onSelected)— instantiatesRegionCellPrefabScene.tscn, adds it toparent, callsSetup(region), wiresRegionSelectedtoonSelected, and returns the ready nodePrefabFactory.CreateLocationRowScene(Node parent, RegionLocation location)— instantiatesLocationRowPrefabScene.tscn, adds it toparent, callsSetup(location), and returns the ready nodeRegionDetailPrefabScene— static embed (not factory-instantiated); embedded viainstance=ExtResource(...)inWorldMapScene.tscn; callSetup(region)to populate it; exposespublic event Action ClosePressed
- All prefab instantiation helpers live in
PrefabFactory— do not create separate helper classes per prefab type - Self-loading prefab scenes use
[Export]properties to identify their data source; set these properties on the instantiated node before callingAddChildso they are available when_Readyfires — this is intentionally opposite toSetup(), which must always be called afterAddChild - Static prefab instances (non-companion) are embedded directly in the parent
.tscnusinginstance=ExtResource(...)with their[Export]values set inline — no runtime instantiation needed for fixed slots InventoryPrefabSceneis the prefab that owns the memberTabContainer; it embeds static Player tab sub-scenes in its.tscnand adds dynamic companion tabs inPrepareNodes— embed it in parent scenes viainstance=ExtResource(...)rather than instantiating it in code- When adding new reusable Godot UI/node utilities, place them in
Util/asstaticclasses - When a helper needs a scene path, reference the
public constonSceneManager— do not declare a duplicate path string in the helper
Signals vs C# Events
- Use
[Signal]delegates only when the event needs to cross the GDScript boundary or be visible in the Godot editor [Signal]delegates only support Godot Variant-compatible types (built-in Godot types,GodotObjectsubclasses) — never use pure C# types likeCard,Bag, etc. as signal parameters- When communicating between C#-only nodes with pure C# payloads, use a standard C#
eventinstead:public event Action<Card> Accepted; public event Action Declined; - Never use a plain
Action<T>field — always declarepublic event Action<T>so callers use+=/-=and cannot overwrite existing subscribers with= - Wire C# events in
PrepareNodesthe same way as Godot signals
Error Handling
-
Never silently swallow missing state — use
GD.PushErrorto make programming mistakes visible in the Godot debugger -
For optional/recoverable failures (e.g. missing save data): call
GD.PushErrorwith a descriptive message, then return early -
For missing required pre-conditions set before
_Ready(e.g.Setup()not called): useGD.PushErrorand return — never silently continue with null state -
Do not use C# exceptions for Godot-layer errors;
GD.PushErroris the idiomatic equivalent -
All scene transitions go through
SceneManager— do not callGetTree().ChangeSceneToFile(...)directly from node scripts -
Navigation-only buttons (whose sole purpose is to navigate to a scene) must use
SceneButtonPrefabScene— embedinstance=ExtResource(...)in the.tscnwithtextandTargetSceneset inline; do NOT add aprivate Buttonfield,GetNode, or_sceneManager.GoTo*wiring in C#SceneButtonPrefabScenelives atres://Scenes/Prefabs/Control/SceneButtonPrefabScene.tscnTargetSceneis aGameSceneenum export — set it to the integer value of the enum in the.tscn(e.g.TargetScene = 0forGameScene.MainMenu); theGameSceneenum is defined insideSceneManagerand lists all navigable scenes in order- Each
GameScenevalue is decorated with[ScenePath("res://...")]—SceneManager.GoTo(GameScene)resolves the path via reflection; no switch statement is needed - When a new navigable scene is added, add its value to
GameScenewith a[ScenePath(...)]attribute and aGoTo<SceneName>()convenience method toSceneManager - Only use a plain
Button+_sceneManager.GoTo*when navigation is conditional (e.g. guarded by a load result or confirmation dialog)
-
When a scene is created, renamed, or removed, always update all of the following without being asked:
SceneManager.cs: path constant andGoTo<SceneName>()method (add, rename, or remove)DebugScene.tscn:SceneButtonPrefabSceneinstance node inVBoxContainer/TabContainer/Scenes, withtextandTargetScenebound inline (add, rename, or remove)DebugScene.cs: no C# changes needed for navigation buttons — handled entirely by the.tscn