Crafting Systeem
Recepten, crafting interface, en item creatie.
Overzicht
┌─────────────────────────────────────────────────────────────┐
│ Crafting Flow │
├─────────────────────────────────────────────────────────────┤
│ │
│ Recept Selecteren ──► Ingrediënten Check ──► Craft │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ Recipe Database Inventory Create Item │
│ (unlocked recipes) Validation + XP Reward │
│ │
└─────────────────────────────────────────────────────────────┘
Crafting UI
Layout
┌─────────────────────────────────────────────────────────────────┐
│ WERKBANK [X] │
├─────────────────────────────────────────────────────────────────┤
│ [Koken] [Decoratie] [Gereedschap] [Kleding] │
├──────────────────────────┬──────────────────────────────────────┤
│ │ │
│ Recepten: │ Appeltaart │
│ ┌────────────────────┐ │ ──────────────────── │
│ │ 🥧 Appeltaart │ │ "Een heerlijke taart │
│ │ 🍪 Koekjes ✓ │ │ met verse appels" │
│ │ 🍯 Honingbrood │ │ │
│ │ 🥗 Salade ✓ │ │ Ingrediënten: │
│ │ 🧁 Cupcake │ │ ┌───┐ ┌───┐ ┌───┐ │
│ └────────────────────┘ │ │🍎│ │🥚│ │🌾│ │
│ │ │3/3│ │2/2│ │1/1│ │
│ │ └───┘ └───┘ └───┘ │
│ │ │
│ │ Resultaat: │
│ │ ┌─────┐ │
│ │ │ 🥧 │ x1 Appeltaart │
│ │ └─────┘ │
│ │ │
│ │ [Maken] │
│ │ │
└──────────────────────────┴──────────────────────────────────────┘
Scene Structuur
CraftingPanel (Control)
├── Background (NinePatchRect)
├── Header (HBoxContainer)
│ ├── Title (Label)
│ └── CloseButton
├── CategoryTabs (HBoxContainer)
│ └── [CategoryButton instances]
├── HSplitContainer
│ ├── RecipeList (VBoxContainer/ScrollContainer)
│ │ └── [RecipeButton instances]
│ └── RecipeDetail (VBoxContainer)
│ ├── RecipeName (Label)
│ ├── RecipeDescription (Label)
│ ├── IngredientsGrid (GridContainer)
│ ├── ResultPreview (HBoxContainer)
│ └── CraftButton
└── CraftingAnimation (Control)
Crafting Manager
# crafting_manager.gd
extends Node
signal recipe_crafted(recipe_id: String)
signal recipe_unlocked(recipe_id: String)
var unlocked_recipes: Array[String] = []
func _ready() -> void:
_load_unlocked_recipes()
func can_craft(recipe_id: String) -> bool:
if recipe_id not in unlocked_recipes:
return false
var recipe = RecipeDatabase.get_recipe(recipe_id)
return _has_all_ingredients(recipe)
func _has_all_ingredients(recipe: Dictionary) -> bool:
var inventory = get_node("/root/InventoryManager")
for ingredient in recipe.ingredients:
if not inventory.has_item(ingredient.item_id, ingredient.quantity):
return false
return true
func craft(recipe_id: String) -> bool:
if not can_craft(recipe_id):
return false
var recipe = RecipeDatabase.get_recipe(recipe_id)
var inventory = get_node("/root/InventoryManager")
# Remove ingredients
for ingredient in recipe.ingredients:
inventory.remove_item(ingredient.item_id, ingredient.quantity)
# Add result
var added = inventory.add_item(recipe.result.item_id, recipe.result.quantity)
if added > 0:
# XP reward
PlayerStats.add_crafting_xp(recipe.xp_reward)
# Sync to server
SpacetimeDB.call_reducer("craft_item", [recipe_id])
recipe_crafted.emit(recipe_id)
return true
return false
func unlock_recipe(recipe_id: String) -> void:
if recipe_id in unlocked_recipes:
return
unlocked_recipes.append(recipe_id)
_save_unlocked_recipes()
SpacetimeDB.call_reducer("unlock_recipe", [recipe_id])
recipe_unlocked.emit(recipe_id)
NotificationManager.toast("Nieuw recept ontgrendeld!", "success")
Recipe Database
# recipe_database.gd
extends Node
const RECIPES = {
"apple_pie": {
"display_name": "Appeltaart",
"description": "Een heerlijke taart met verse appels",
"category": "cooking",
"ingredients": [
{"item_id": "apple", "quantity": 3},
{"item_id": "egg", "quantity": 2},
{"item_id": "flour", "quantity": 1}
],
"result": {
"item_id": "apple_pie",
"quantity": 1
},
"xp_reward": 25,
"unlock_level": 5,
"icon": "res://assets/items/food/apple_pie.png"
},
"wooden_chair": {
"display_name": "Houten Stoel",
"description": "Een comfortabele stoel voor je treehouse",
"category": "decoration",
"ingredients": [
{"item_id": "wood_plank", "quantity": 4},
{"item_id": "nail", "quantity": 6}
],
"result": {
"item_id": "wooden_chair",
"quantity": 1
},
"xp_reward": 30,
"unlock_level": 3,
"icon": "res://assets/items/furniture/wooden_chair.png"
}
}
func get_recipe(recipe_id: String) -> Dictionary:
return RECIPES.get(recipe_id, {})
func get_recipes_by_category(category: String) -> Array:
var result = []
for id in RECIPES:
if RECIPES[id].category == category:
result.append({"id": id, "data": RECIPES[id]})
return result
func get_all_categories() -> Array:
var categories = []
for id in RECIPES:
var cat = RECIPES[id].category
if cat not in categories:
categories.append(cat)
return categories
Crafting Panel Script
# crafting_panel.gd
extends Control
@onready var category_tabs: HBoxContainer = $Header/CategoryTabs
@onready var recipe_list: VBoxContainer = $Content/RecipeList
@onready var recipe_detail: Control = $Content/RecipeDetail
@onready var craft_button: Button = $Content/RecipeDetail/CraftButton
@onready var ingredients_grid: GridContainer = $Content/RecipeDetail/IngredientsGrid
const RECIPE_BUTTON = preload("res://scenes/ui/recipe_button.tscn")
const INGREDIENT_SLOT = preload("res://scenes/ui/ingredient_slot.tscn")
var current_category: String = "cooking"
var selected_recipe: String = ""
func _ready() -> void:
_setup_category_tabs()
_load_recipes()
craft_button.pressed.connect(_on_craft_pressed)
func _setup_category_tabs() -> void:
for child in category_tabs.get_children():
child.queue_free()
var categories = RecipeDatabase.get_all_categories()
for cat in categories:
var btn = Button.new()
btn.text = _get_category_name(cat)
btn.pressed.connect(_on_category_selected.bind(cat))
category_tabs.add_child(btn)
func _load_recipes() -> void:
# Clear existing
for child in recipe_list.get_children():
child.queue_free()
var recipes = RecipeDatabase.get_recipes_by_category(current_category)
for recipe in recipes:
var btn = RECIPE_BUTTON.instantiate()
btn.setup(recipe.id, recipe.data)
btn.recipe_selected.connect(_on_recipe_selected)
# Check if unlocked
var is_unlocked = CraftingManager.unlocked_recipes.has(recipe.id)
btn.set_locked(not is_unlocked)
# Check if craftable
var can_craft = CraftingManager.can_craft(recipe.id)
btn.set_craftable(can_craft)
recipe_list.add_child(btn)
Recipe Detail Display
func _on_recipe_selected(recipe_id: String) -> void:
selected_recipe = recipe_id
_update_recipe_detail()
func _update_recipe_detail() -> void:
if selected_recipe.is_empty():
recipe_detail.visible = false
return
recipe_detail.visible = true
var recipe = RecipeDatabase.get_recipe(selected_recipe)
# Name and description
$Content/RecipeDetail/Name.text = recipe.display_name
$Content/RecipeDetail/Description.text = recipe.description
# Ingredients
_show_ingredients(recipe.ingredients)
# Result
_show_result(recipe.result)
# Craft button state
craft_button.disabled = not CraftingManager.can_craft(selected_recipe)
func _show_ingredients(ingredients: Array) -> void:
for child in ingredients_grid.get_children():
child.queue_free()
var inventory = get_node("/root/InventoryManager")
for ingredient in ingredients:
var slot = INGREDIENT_SLOT.instantiate()
var has_amount = inventory.count_item(ingredient.item_id)
var need_amount = ingredient.quantity
slot.setup(
ingredient.item_id,
has_amount,
need_amount
)
ingredients_grid.add_child(slot)
func _show_result(result: Dictionary) -> void:
var result_icon = $Content/RecipeDetail/ResultIcon
var result_label = $Content/RecipeDetail/ResultLabel
var item_def = ItemDatabase.get_item(result.item_id)
result_icon.texture = load(item_def.icon)
result_label.text = "x%d %s" % [result.quantity, item_def.display_name]
Craft Execution
func _on_craft_pressed() -> void:
if selected_recipe.is_empty():
return
if not CraftingManager.can_craft(selected_recipe):
NotificationManager.toast("Niet genoeg ingrediënten!", "warning")
return
# Play crafting animation
await _play_crafting_animation()
# Execute craft
var success = CraftingManager.craft(selected_recipe)
if success:
var recipe = RecipeDatabase.get_recipe(selected_recipe)
NotificationManager.toast(
"%s gemaakt!" % recipe.display_name,
"success"
)
# Refresh UI
_update_recipe_detail()
_load_recipes() # Update craftable states
else:
NotificationManager.toast("Crafting mislukt!", "error")
func _play_crafting_animation() -> void:
var anim = $CraftingAnimation
anim.visible = true
# Ingredient items fly to center
var tween = create_tween()
tween.set_parallel(true)
for i in range(ingredients_grid.get_child_count()):
var slot = ingredients_grid.get_child(i)
var target_pos = anim.get_node("Center").global_position
tween.tween_property(slot, "global_position", target_pos, 0.3)
tween.tween_property(slot, "scale", Vector2.ZERO, 0.3)
await tween.finished
# Flash effect
anim.get_node("Flash").visible = true
await get_tree().create_timer(0.2).timeout
anim.get_node("Flash").visible = false
# Show result
var result_anim = anim.get_node("Result")
result_anim.scale = Vector2.ZERO
result_anim.visible = true
var pop_tween = create_tween()
pop_tween.tween_property(result_anim, "scale", Vector2(1.2, 1.2), 0.2)
pop_tween.tween_property(result_anim, "scale", Vector2.ONE, 0.1)
await pop_tween.finished
await get_tree().create_timer(0.5).timeout
anim.visible = false
# Reset ingredient slots
for slot in ingredients_grid.get_children():
slot.scale = Vector2.ONE
Ingredient Slot Component
# ingredient_slot.gd
extends Control
@onready var icon: TextureRect = $Icon
@onready var count_label: Label = $CountLabel
@onready var background: NinePatchRect = $Background
func setup(item_id: String, has_amount: int, need_amount: int) -> void:
var item_def = ItemDatabase.get_item(item_id)
icon.texture = load(item_def.icon)
count_label.text = "%d/%d" % [has_amount, need_amount]
# Color based on availability
if has_amount >= need_amount:
count_label.modulate = Color.GREEN
background.modulate = Color(0.2, 0.4, 0.2)
else:
count_label.modulate = Color.RED
background.modulate = Color(0.4, 0.2, 0.2)
Recipe Unlock System
# In CraftingManager
func check_level_unlocks(new_level: int) -> void:
for recipe_id in RecipeDatabase.RECIPES:
var recipe = RecipeDatabase.get_recipe(recipe_id)
if recipe.unlock_level == new_level:
if recipe_id not in unlocked_recipes:
unlock_recipe(recipe_id)
func unlock_from_item(item_id: String) -> void:
# Recipe scrolls/books
var item_def = ItemDatabase.get_item(item_id)
if item_def.has("unlocks_recipe"):
unlock_recipe(item_def.unlocks_recipe)
Crafting Stations
# Verschillende werkbanken voor verschillende categorieën
const CRAFTING_STATIONS = {
"workbench": ["decoration", "tools"],
"kitchen": ["cooking"],
"sewing_table": ["clothing"],
"forge": ["metal"]
}
func open_crafting(station_type: String) -> void:
var panel = preload("res://scenes/ui/crafting_panel.tscn").instantiate()
panel.set_allowed_categories(CRAFTING_STATIONS[station_type])
get_tree().root.add_child(panel)