@jarrodmedrano/claude-skills 1.0.2 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/bevy/SKILL.md +406 -0
- package/.claude/skills/bevy/references/bevy_specific_tips.md +385 -0
- package/.claude/skills/bevy/references/common_pitfalls.md +217 -0
- package/.claude/skills/bevy/references/ecs_patterns.md +277 -0
- package/.claude/skills/bevy/references/project_structure.md +116 -0
- package/.claude/skills/bevy/references/ui_development.md +147 -0
- package/.claude/skills/domain-driven-design/SKILL.md +459 -0
- package/.claude/skills/domain-driven-design/references/ddd_foundations_and_patterns.md +664 -0
- package/.claude/skills/domain-driven-design/references/rich_hickey_principles.md +406 -0
- package/.claude/skills/domain-driven-design/references/visualization_examples.md +790 -0
- package/.claude/skills/domain-driven-design/references/wlaschin_patterns.md +639 -0
- package/.claude/skills/game-design-theory/SKILL.md +102 -0
- package/.claude/skills/game-design-theory/design-principles.md +308 -0
- package/.claude/skills/game-design-theory/gameplay-elements.md +213 -0
- package/.claude/skills/game-design-theory/player-psychology.md +175 -0
- package/.claude/skills/game-design-theory/playtesting.md +321 -0
- package/.claude/skills/game-design-theory/storytelling.md +219 -0
- package/.claude/skills/game-feel/SKILL.md +305 -0
- package/.claude/skills/game-feel/references/adsr-tuning.md +271 -0
- package/.claude/skills/game-feel/references/classic-profiles.md +279 -0
- package/.claude/skills/game-feel/references/perception-thresholds.md +160 -0
- package/.claude/skills/game-feel/references/polish-effects.md +246 -0
- package/.claude/skills/game-feel/references/simulation-recipes.md +306 -0
- package/.claude/skills/game-feel/references/six-metrics.md +239 -0
- package/.claude/skills/godot/SKILL.md +728 -0
- package/.claude/skills/godot/assets/templates/attribute_template.gd +109 -0
- package/.claude/skills/godot/assets/templates/component_template.gd +76 -0
- package/.claude/skills/godot/assets/templates/interaction_template.gd +108 -0
- package/.claude/skills/godot/assets/templates/item_resource.tres +11 -0
- package/.claude/skills/godot/assets/templates/spell_resource.tres +20 -0
- package/.claude/skills/godot/references/architecture-patterns.md +608 -0
- package/.claude/skills/godot/references/common-pitfalls.md +518 -0
- package/.claude/skills/godot/references/file-formats.md +491 -0
- package/.claude/skills/godot/references/godot4-physics-api.md +302 -0
- package/.claude/skills/godot/scripts/validate_tres.py +145 -0
- package/.claude/skills/godot/scripts/validate_tscn.py +170 -0
- package/.claude/skills/level-design/SKILL.md +249 -0
- package/.claude/skills/level-design/anticipatory-play.md +223 -0
- package/.claude/skills/level-design/hiding-linearity.md +181 -0
- package/.claude/skills/level-design/indie-practices.md +286 -0
- package/.claude/skills/level-design/open-world-planning.md +294 -0
- package/.claude/skills/level-design/play-personas.md +240 -0
- package/.claude/skills/level-design/procedural-handmade.md +271 -0
- package/.claude/skills/level-design/themed-environments.md +264 -0
- package/.claude/skills/react-three-fiber/SKILL.md +2055 -0
- package/.claude/skills/react-three-fiber/scripts/build-scene.ts +171 -0
- package/package.json +3 -1
- package/scripts/install.js +16 -1
- package/templates/github-actions/README.md +36 -0
- /package/.claude/{commands/design-review → agents}/design-review-agent.md +0 -0
- /package/.claude/{commands/code-review → agents}/pragmatic-code-review-subagent.md +0 -0
- /package/{.claude/commands/code-review → templates/github-actions}/claude-code-review-custom.yml +0 -0
- /package/{.claude/commands/code-review → templates/github-actions}/claude-code-review.yml +0 -0
- /package/{.claude/commands/security-review → templates/github-actions}/security.yml +0 -0
|
@@ -0,0 +1,608 @@
|
|
|
1
|
+
# Godot Architecture Patterns
|
|
2
|
+
|
|
3
|
+
Proven architectural patterns for building maintainable Godot projects, especially when working with LLM coding assistants.
|
|
4
|
+
|
|
5
|
+
## Core Principles
|
|
6
|
+
|
|
7
|
+
### 1. Component-Based Design
|
|
8
|
+
Break functionality into small, reusable components attached as Node children.
|
|
9
|
+
|
|
10
|
+
**Benefits:**
|
|
11
|
+
- Each component is a focused, understandable file
|
|
12
|
+
- Easy to add/remove features
|
|
13
|
+
- Clear responsibilities
|
|
14
|
+
- LLM-friendly (small files, clear purpose)
|
|
15
|
+
|
|
16
|
+
### 2. Signal-Driven Communication
|
|
17
|
+
Use signals for loose coupling between systems.
|
|
18
|
+
|
|
19
|
+
**Benefits:**
|
|
20
|
+
- Self-documenting (signals show available events)
|
|
21
|
+
- Easy to connect UI without modifying game logic
|
|
22
|
+
- No complex dependency injection needed
|
|
23
|
+
- Clear event flow
|
|
24
|
+
|
|
25
|
+
### 3. Resource-Based Data
|
|
26
|
+
Separate logic (.gd) from data (.tres).
|
|
27
|
+
|
|
28
|
+
**Benefits:**
|
|
29
|
+
- Data files are simple and safe to edit
|
|
30
|
+
- Easy to create variants
|
|
31
|
+
- Designer-friendly
|
|
32
|
+
- LLM can edit data without touching logic
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Pattern 1: Component-Based Interaction System
|
|
37
|
+
|
|
38
|
+
**Use for:** Objects that can be interacted with in different ways.
|
|
39
|
+
|
|
40
|
+
### Structure
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
BaseInteractable.gd (parent script)
|
|
44
|
+
├─ is_interactable: bool
|
|
45
|
+
├─ interaction_nodes: Array[InteractionComponent]
|
|
46
|
+
└─ interact(player) -> void
|
|
47
|
+
|
|
48
|
+
InteractionComponent.gd (child node script)
|
|
49
|
+
├─ interaction_text: String
|
|
50
|
+
├─ is_enabled: bool
|
|
51
|
+
├─ interact(player) -> bool
|
|
52
|
+
└─ signal: was_interacted_with
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Implementation
|
|
56
|
+
|
|
57
|
+
**BaseInteractable.gd:**
|
|
58
|
+
```gdscript
|
|
59
|
+
extends Node3D
|
|
60
|
+
class_name BaseInteractable
|
|
61
|
+
|
|
62
|
+
@export var is_interactable: bool = true
|
|
63
|
+
var interaction_nodes: Array[InteractionComponent] = []
|
|
64
|
+
|
|
65
|
+
func _ready():
|
|
66
|
+
# Gather all interaction components
|
|
67
|
+
for child in get_children():
|
|
68
|
+
if child is InteractionComponent:
|
|
69
|
+
interaction_nodes.append(child)
|
|
70
|
+
|
|
71
|
+
func interact(player) -> void:
|
|
72
|
+
if not is_interactable:
|
|
73
|
+
return
|
|
74
|
+
|
|
75
|
+
for interaction in interaction_nodes:
|
|
76
|
+
if interaction.is_enabled:
|
|
77
|
+
interaction.interact(player)
|
|
78
|
+
break # Only one interaction per press
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
**InteractionComponent.gd:**
|
|
82
|
+
```gdscript
|
|
83
|
+
extends Node
|
|
84
|
+
class_name InteractionComponent
|
|
85
|
+
|
|
86
|
+
signal was_interacted_with(player)
|
|
87
|
+
|
|
88
|
+
@export var interaction_text: String = "Interact"
|
|
89
|
+
@export var is_enabled: bool = true
|
|
90
|
+
|
|
91
|
+
func interact(player) -> bool:
|
|
92
|
+
if not is_enabled:
|
|
93
|
+
return false
|
|
94
|
+
|
|
95
|
+
_perform_interaction(player)
|
|
96
|
+
was_interacted_with.emit(player)
|
|
97
|
+
return true
|
|
98
|
+
|
|
99
|
+
func _perform_interaction(player) -> void:
|
|
100
|
+
# Override in subclasses
|
|
101
|
+
pass
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
**Example Subclass - PickupInteraction.gd:**
|
|
105
|
+
```gdscript
|
|
106
|
+
extends InteractionComponent
|
|
107
|
+
class_name PickupInteraction
|
|
108
|
+
|
|
109
|
+
@export var item_resource: ItemResource
|
|
110
|
+
|
|
111
|
+
func _perform_interaction(player) -> void:
|
|
112
|
+
if not item_resource:
|
|
113
|
+
push_error("No item resource assigned")
|
|
114
|
+
return
|
|
115
|
+
|
|
116
|
+
player.inventory.add_item(item_resource)
|
|
117
|
+
get_parent().queue_free() # Remove the pickup
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Scene Setup (.tscn)
|
|
121
|
+
|
|
122
|
+
```
|
|
123
|
+
[ext_resource type="Script" path="res://src/base_interactable.gd" id="1"]
|
|
124
|
+
[ext_resource type="Script" path="res://src/pickup_interaction.gd" id="2"]
|
|
125
|
+
[ext_resource type="Resource" path="res://resources/items/key.tres" id="3"]
|
|
126
|
+
|
|
127
|
+
[node name="KeyPickup" type="StaticBody3D"]
|
|
128
|
+
script = ExtResource("1")
|
|
129
|
+
|
|
130
|
+
[node name="PickupInteraction" type="Node" parent="."]
|
|
131
|
+
script = ExtResource("2")
|
|
132
|
+
interaction_text = "Pick up Key"
|
|
133
|
+
item_resource = ExtResource("3")
|
|
134
|
+
|
|
135
|
+
[node name="MeshInstance3D" type="MeshInstance3D" parent="."]
|
|
136
|
+
# ... mesh configuration
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Usage Benefits
|
|
140
|
+
- Single object can have multiple interaction types
|
|
141
|
+
- Easy to add new interaction types (just create new subclass)
|
|
142
|
+
- Interactions can be enabled/disabled dynamically
|
|
143
|
+
- Clear separation of concerns
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## Pattern 2: Attribute System
|
|
148
|
+
|
|
149
|
+
**Use for:** Health, mana, stamina, or any numeric attribute with min/max values.
|
|
150
|
+
|
|
151
|
+
### Structure
|
|
152
|
+
|
|
153
|
+
```
|
|
154
|
+
Attribute.gd (base class)
|
|
155
|
+
├─ attribute_name: String
|
|
156
|
+
├─ value_current: float (with setter)
|
|
157
|
+
├─ value_max: float
|
|
158
|
+
├─ value_start: float
|
|
159
|
+
├─ signal: attribute_changed(name, current, max, increased)
|
|
160
|
+
└─ signal: attribute_reached_zero(name)
|
|
161
|
+
|
|
162
|
+
HealthAttribute.gd (specialized)
|
|
163
|
+
├─ extends Attribute
|
|
164
|
+
├─ signal: damage_taken(amount)
|
|
165
|
+
└─ signal: death()
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Implementation
|
|
169
|
+
|
|
170
|
+
**Attribute.gd:**
|
|
171
|
+
```gdscript
|
|
172
|
+
extends Node
|
|
173
|
+
class_name Attribute
|
|
174
|
+
|
|
175
|
+
signal attribute_changed(attribute_name: String, value_current: float, value_max: float, value_increased: bool)
|
|
176
|
+
signal attribute_reached_zero(attribute_name: String)
|
|
177
|
+
|
|
178
|
+
@export var attribute_name: String = "Attribute"
|
|
179
|
+
@export var value_max: float = 100.0
|
|
180
|
+
@export var value_start: float = 100.0
|
|
181
|
+
|
|
182
|
+
var value_current: float:
|
|
183
|
+
set(value):
|
|
184
|
+
var old_value = value_current
|
|
185
|
+
value_current = clamp(value, 0, value_max)
|
|
186
|
+
var increased = value_current > old_value
|
|
187
|
+
|
|
188
|
+
attribute_changed.emit(attribute_name, value_current, value_max, increased)
|
|
189
|
+
|
|
190
|
+
if value_current <= 0:
|
|
191
|
+
attribute_reached_zero.emit(attribute_name)
|
|
192
|
+
|
|
193
|
+
func _ready():
|
|
194
|
+
value_current = value_start
|
|
195
|
+
|
|
196
|
+
func add(amount: float) -> void:
|
|
197
|
+
value_current += amount
|
|
198
|
+
|
|
199
|
+
func subtract(amount: float) -> void:
|
|
200
|
+
value_current -= amount
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
**HealthAttribute.gd:**
|
|
204
|
+
```gdscript
|
|
205
|
+
extends Attribute
|
|
206
|
+
class_name HealthAttribute
|
|
207
|
+
|
|
208
|
+
signal damage_taken(amount: float)
|
|
209
|
+
signal death()
|
|
210
|
+
|
|
211
|
+
func _ready():
|
|
212
|
+
super._ready()
|
|
213
|
+
attribute_name = "Health"
|
|
214
|
+
attribute_reached_zero.connect(_on_death)
|
|
215
|
+
|
|
216
|
+
func take_damage(amount: float) -> void:
|
|
217
|
+
subtract(amount)
|
|
218
|
+
damage_taken.emit(amount)
|
|
219
|
+
|
|
220
|
+
func heal(amount: float) -> void:
|
|
221
|
+
add(amount)
|
|
222
|
+
|
|
223
|
+
func _on_death() -> void:
|
|
224
|
+
death.emit()
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### Parent Integration
|
|
228
|
+
|
|
229
|
+
**BaseEnemy.gd:**
|
|
230
|
+
```gdscript
|
|
231
|
+
extends CharacterBody3D
|
|
232
|
+
class_name BaseEnemy
|
|
233
|
+
|
|
234
|
+
var attributes: Dictionary = {}
|
|
235
|
+
|
|
236
|
+
func _ready():
|
|
237
|
+
# Gather all attributes
|
|
238
|
+
for child in get_children():
|
|
239
|
+
if child is Attribute:
|
|
240
|
+
attributes[child.attribute_name] = child
|
|
241
|
+
child.attribute_changed.connect(_on_attribute_changed)
|
|
242
|
+
|
|
243
|
+
# Connect to health-specific signals
|
|
244
|
+
if attributes.has("Health"):
|
|
245
|
+
attributes["Health"].death.connect(_on_death)
|
|
246
|
+
|
|
247
|
+
func _on_attribute_changed(attr_name: String, current: float, max_val: float, increased: bool):
|
|
248
|
+
# Update UI or trigger effects
|
|
249
|
+
pass
|
|
250
|
+
|
|
251
|
+
func _on_death():
|
|
252
|
+
# Drop loot, play animation, etc.
|
|
253
|
+
queue_free()
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
### Scene Setup (.tscn)
|
|
257
|
+
|
|
258
|
+
```
|
|
259
|
+
[ext_resource type="Script" path="res://src/base_enemy.gd" id="1"]
|
|
260
|
+
[ext_resource type="Script" path="res://src/health_attribute.gd" id="2"]
|
|
261
|
+
|
|
262
|
+
[node name="Enemy" type="CharacterBody3D"]
|
|
263
|
+
script = ExtResource("1")
|
|
264
|
+
|
|
265
|
+
[node name="HealthAttribute" type="Node" parent="."]
|
|
266
|
+
script = ExtResource("2")
|
|
267
|
+
value_max = 50.0
|
|
268
|
+
value_start = 50.0
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
---
|
|
272
|
+
|
|
273
|
+
## Pattern 3: Resource-Based Effect System
|
|
274
|
+
|
|
275
|
+
**Use for:** Spells, abilities, items, or any combinable effects.
|
|
276
|
+
|
|
277
|
+
### Structure
|
|
278
|
+
|
|
279
|
+
```
|
|
280
|
+
SpellEffect.gd (individual effect)
|
|
281
|
+
├─ effect_type: enum
|
|
282
|
+
├─ magnitude_min/max: float
|
|
283
|
+
├─ duration: float
|
|
284
|
+
└─ apply_effect(target, caster)
|
|
285
|
+
|
|
286
|
+
SpellResource.gd (combines effects)
|
|
287
|
+
├─ effects: Array[SpellEffect]
|
|
288
|
+
├─ mana_cost: float
|
|
289
|
+
├─ projectile_speed: float
|
|
290
|
+
└─ spell_color: Color
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
### Implementation
|
|
294
|
+
|
|
295
|
+
**SpellEffect.gd:**
|
|
296
|
+
```gdscript
|
|
297
|
+
extends Resource
|
|
298
|
+
class_name SpellEffect
|
|
299
|
+
|
|
300
|
+
enum EffectType {
|
|
301
|
+
DAMAGE,
|
|
302
|
+
HEAL,
|
|
303
|
+
RESTORE_MANA,
|
|
304
|
+
DAMAGE_OVER_TIME,
|
|
305
|
+
HEAL_OVER_TIME,
|
|
306
|
+
SLOW,
|
|
307
|
+
SPEED_BOOST,
|
|
308
|
+
STUN,
|
|
309
|
+
# ... more types
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
@export var effect_type: EffectType = EffectType.DAMAGE
|
|
313
|
+
@export var magnitude_min: float = 10.0
|
|
314
|
+
@export var magnitude_max: float = 10.0
|
|
315
|
+
@export var duration: float = 0.0
|
|
316
|
+
@export var tick_rate: float = 1.0 # For over-time effects
|
|
317
|
+
|
|
318
|
+
func apply_effect(target: Node, caster: Node) -> void:
|
|
319
|
+
var magnitude = randf_range(magnitude_min, magnitude_max)
|
|
320
|
+
|
|
321
|
+
match effect_type:
|
|
322
|
+
EffectType.DAMAGE:
|
|
323
|
+
_apply_damage(target, magnitude)
|
|
324
|
+
EffectType.HEAL:
|
|
325
|
+
_apply_heal(target, magnitude)
|
|
326
|
+
EffectType.SLOW:
|
|
327
|
+
_apply_slow(target, magnitude, duration)
|
|
328
|
+
# ... handle other types
|
|
329
|
+
|
|
330
|
+
func _apply_damage(target: Node, amount: float) -> void:
|
|
331
|
+
if target.has_method("take_damage"):
|
|
332
|
+
target.take_damage(amount)
|
|
333
|
+
|
|
334
|
+
func _apply_heal(target: Node, amount: float) -> void:
|
|
335
|
+
if target.has_node("HealthAttribute"):
|
|
336
|
+
target.get_node("HealthAttribute").heal(amount)
|
|
337
|
+
|
|
338
|
+
func _apply_slow(target: Node, percent: float, dur: float) -> void:
|
|
339
|
+
# Apply slow effect logic
|
|
340
|
+
pass
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
**SpellResource.gd:**
|
|
344
|
+
```gdscript
|
|
345
|
+
extends Resource
|
|
346
|
+
class_name SpellResource
|
|
347
|
+
|
|
348
|
+
@export var spell_id: String = ""
|
|
349
|
+
@export var spell_name: String = "Spell"
|
|
350
|
+
@export var mana_cost: float = 10.0
|
|
351
|
+
@export var effects: Array[SpellEffect] = []
|
|
352
|
+
@export var projectile_speed: float = 20.0
|
|
353
|
+
@export var spell_color: Color = Color.WHITE
|
|
354
|
+
|
|
355
|
+
func cast(caster: Node, target_position: Vector3) -> void:
|
|
356
|
+
# Spawn projectile or apply effects immediately
|
|
357
|
+
pass
|
|
358
|
+
|
|
359
|
+
func apply_effects(target: Node, caster: Node) -> void:
|
|
360
|
+
for effect in effects:
|
|
361
|
+
effect.apply_effect(target, caster)
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
### Data File (.tres)
|
|
365
|
+
|
|
366
|
+
```tres
|
|
367
|
+
[gd_resource type="Resource" script_class="SpellResource" load_steps=4 format=3]
|
|
368
|
+
|
|
369
|
+
[ext_resource type="Script" path="res://src/spells/spell_resource.gd" id="1"]
|
|
370
|
+
[ext_resource type="Script" path="res://src/spells/spell_effect.gd" id="2"]
|
|
371
|
+
|
|
372
|
+
[sub_resource type="Resource" id="Effect_damage"]
|
|
373
|
+
script = ExtResource("2")
|
|
374
|
+
effect_type = 0
|
|
375
|
+
magnitude_min = 15.0
|
|
376
|
+
magnitude_max = 25.0
|
|
377
|
+
|
|
378
|
+
[sub_resource type="Resource" id="Effect_slow"]
|
|
379
|
+
script = ExtResource("2")
|
|
380
|
+
effect_type = 12
|
|
381
|
+
magnitude_min = 50.0
|
|
382
|
+
magnitude_max = 50.0
|
|
383
|
+
duration = 3.0
|
|
384
|
+
|
|
385
|
+
[resource]
|
|
386
|
+
script = ExtResource("1")
|
|
387
|
+
spell_name = "Ice Bolt"
|
|
388
|
+
spell_id = "ice_bolt"
|
|
389
|
+
mana_cost = 20.0
|
|
390
|
+
spell_color = Color(0.5, 0.7, 1, 1)
|
|
391
|
+
projectile_speed = 15.0
|
|
392
|
+
effects = Array[ExtResource("2")]([SubResource("Effect_damage"), SubResource("Effect_slow")])
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
### Benefits
|
|
396
|
+
- Create new spells with just data (no code changes)
|
|
397
|
+
- Combinable effects create emergent gameplay
|
|
398
|
+
- Easy for LLM to generate new spell variants
|
|
399
|
+
- Designer-friendly (edit in Inspector)
|
|
400
|
+
|
|
401
|
+
---
|
|
402
|
+
|
|
403
|
+
## Pattern 4: Inventory System
|
|
404
|
+
|
|
405
|
+
**Use for:** Player/enemy inventories, containers, shops.
|
|
406
|
+
|
|
407
|
+
### Structure
|
|
408
|
+
|
|
409
|
+
```
|
|
410
|
+
ItemResource.gd (data)
|
|
411
|
+
├─ item_id: String
|
|
412
|
+
├─ item_name: String
|
|
413
|
+
├─ description: String
|
|
414
|
+
├─ icon: Texture2D
|
|
415
|
+
└─ stackable: bool
|
|
416
|
+
|
|
417
|
+
Inventory.gd (logic)
|
|
418
|
+
├─ items: Array[ItemResource]
|
|
419
|
+
├─ add_item(item)
|
|
420
|
+
├─ remove_item(item_id)
|
|
421
|
+
├─ has_item(item_id) -> bool
|
|
422
|
+
└─ signal: item_added/removed
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
### Implementation
|
|
426
|
+
|
|
427
|
+
**ItemResource.gd:**
|
|
428
|
+
```gdscript
|
|
429
|
+
extends Resource
|
|
430
|
+
class_name ItemResource
|
|
431
|
+
|
|
432
|
+
@export var item_id: String = ""
|
|
433
|
+
@export var item_name: String = "Item"
|
|
434
|
+
@export var description: String = ""
|
|
435
|
+
@export var icon: Texture2D
|
|
436
|
+
@export var stackable: bool = false
|
|
437
|
+
@export var max_stack: int = 99
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
**Inventory.gd:**
|
|
441
|
+
```gdscript
|
|
442
|
+
extends Node
|
|
443
|
+
class_name Inventory
|
|
444
|
+
|
|
445
|
+
signal item_added(item: ItemResource)
|
|
446
|
+
signal item_removed(item_id: String)
|
|
447
|
+
|
|
448
|
+
var items: Array[ItemResource] = []
|
|
449
|
+
|
|
450
|
+
func add_item(item: ItemResource) -> bool:
|
|
451
|
+
if not item:
|
|
452
|
+
return false
|
|
453
|
+
|
|
454
|
+
items.append(item)
|
|
455
|
+
item_added.emit(item)
|
|
456
|
+
return true
|
|
457
|
+
|
|
458
|
+
func remove_item(item_id: String) -> bool:
|
|
459
|
+
for i in range(items.size()):
|
|
460
|
+
if items[i].item_id == item_id:
|
|
461
|
+
var removed = items[i]
|
|
462
|
+
items.remove_at(i)
|
|
463
|
+
item_removed.emit(item_id)
|
|
464
|
+
return true
|
|
465
|
+
return false
|
|
466
|
+
|
|
467
|
+
func has_item(item_id: String) -> bool:
|
|
468
|
+
for item in items:
|
|
469
|
+
if item.item_id == item_id:
|
|
470
|
+
return true
|
|
471
|
+
return false
|
|
472
|
+
|
|
473
|
+
func get_item(item_id: String) -> ItemResource:
|
|
474
|
+
for item in items:
|
|
475
|
+
if item.item_id == item_id:
|
|
476
|
+
return item
|
|
477
|
+
return null
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
---
|
|
481
|
+
|
|
482
|
+
## Pattern 5: State Machine
|
|
483
|
+
|
|
484
|
+
**Use for:** AI behavior, player states, animation control.
|
|
485
|
+
|
|
486
|
+
### Structure
|
|
487
|
+
|
|
488
|
+
```
|
|
489
|
+
StateMachine.gd
|
|
490
|
+
├─ current_state: State
|
|
491
|
+
├─ states: Dictionary
|
|
492
|
+
├─ change_state(state_name)
|
|
493
|
+
└─ _process/_physics_process
|
|
494
|
+
|
|
495
|
+
State.gd (base)
|
|
496
|
+
├─ enter()
|
|
497
|
+
├─ exit()
|
|
498
|
+
├─ process(delta)
|
|
499
|
+
└─ physics_process(delta)
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
### Implementation
|
|
503
|
+
|
|
504
|
+
**State.gd:**
|
|
505
|
+
```gdscript
|
|
506
|
+
extends Node
|
|
507
|
+
class_name State
|
|
508
|
+
|
|
509
|
+
signal state_finished(next_state: String)
|
|
510
|
+
|
|
511
|
+
func enter() -> void:
|
|
512
|
+
pass
|
|
513
|
+
|
|
514
|
+
func exit() -> void:
|
|
515
|
+
pass
|
|
516
|
+
|
|
517
|
+
func process(delta: float) -> void:
|
|
518
|
+
pass
|
|
519
|
+
|
|
520
|
+
func physics_process(delta: float) -> void:
|
|
521
|
+
pass
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
**StateMachine.gd:**
|
|
525
|
+
```gdscript
|
|
526
|
+
extends Node
|
|
527
|
+
class_name StateMachine
|
|
528
|
+
|
|
529
|
+
@export var initial_state: String = ""
|
|
530
|
+
|
|
531
|
+
var states: Dictionary = {}
|
|
532
|
+
var current_state: State = null
|
|
533
|
+
|
|
534
|
+
func _ready():
|
|
535
|
+
# Gather all state children
|
|
536
|
+
for child in get_children():
|
|
537
|
+
if child is State:
|
|
538
|
+
states[child.name] = child
|
|
539
|
+
child.state_finished.connect(_on_state_finished)
|
|
540
|
+
|
|
541
|
+
if initial_state and states.has(initial_state):
|
|
542
|
+
change_state(initial_state)
|
|
543
|
+
|
|
544
|
+
func _process(delta: float):
|
|
545
|
+
if current_state:
|
|
546
|
+
current_state.process(delta)
|
|
547
|
+
|
|
548
|
+
func _physics_process(delta: float):
|
|
549
|
+
if current_state:
|
|
550
|
+
current_state.physics_process(delta)
|
|
551
|
+
|
|
552
|
+
func change_state(new_state_name: String) -> void:
|
|
553
|
+
if current_state:
|
|
554
|
+
current_state.exit()
|
|
555
|
+
|
|
556
|
+
current_state = states.get(new_state_name)
|
|
557
|
+
|
|
558
|
+
if current_state:
|
|
559
|
+
current_state.enter()
|
|
560
|
+
|
|
561
|
+
func _on_state_finished(next_state: String) -> void:
|
|
562
|
+
change_state(next_state)
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
**Example State - IdleState.gd:**
|
|
566
|
+
```gdscript
|
|
567
|
+
extends State
|
|
568
|
+
class_name IdleState
|
|
569
|
+
|
|
570
|
+
@export var idle_time: float = 2.0
|
|
571
|
+
|
|
572
|
+
var timer: float = 0.0
|
|
573
|
+
|
|
574
|
+
func enter() -> void:
|
|
575
|
+
timer = 0.0
|
|
576
|
+
print("Entering idle state")
|
|
577
|
+
|
|
578
|
+
func process(delta: float) -> void:
|
|
579
|
+
timer += delta
|
|
580
|
+
if timer >= idle_time:
|
|
581
|
+
state_finished.emit("Patrol")
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
---
|
|
585
|
+
|
|
586
|
+
## Combining Patterns
|
|
587
|
+
|
|
588
|
+
These patterns work together naturally:
|
|
589
|
+
|
|
590
|
+
```gdscript
|
|
591
|
+
# Enemy with attributes, state machine, and loot
|
|
592
|
+
BaseEnemy (CharacterBody3D)
|
|
593
|
+
├─ HealthAttribute (Attribute component)
|
|
594
|
+
├─ ManaAttribute (Attribute component)
|
|
595
|
+
├─ StateMachine
|
|
596
|
+
│ ├─ IdleState
|
|
597
|
+
│ ├─ PatrolState
|
|
598
|
+
│ ├─ ChaseState
|
|
599
|
+
│ └─ AttackState
|
|
600
|
+
└─ LootDropper (Component)
|
|
601
|
+
└─ loot_table: Array[LootEntry]
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
Each component handles one responsibility, making the system:
|
|
605
|
+
- Easy to understand
|
|
606
|
+
- Simple to modify
|
|
607
|
+
- Clear to debug
|
|
608
|
+
- LLM-friendly to work with
|