diff --git a/assets/icons/resources/credits.svg b/assets/icons/resources/credits.svg
new file mode 100644
index 0000000..a156429
--- /dev/null
+++ b/assets/icons/resources/credits.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/assets/icons/resources/credits.svg.import b/assets/icons/resources/credits.svg.import
new file mode 100644
index 0000000..f0521e1
--- /dev/null
+++ b/assets/icons/resources/credits.svg.import
@@ -0,0 +1,43 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://cu3axmjxclbnu"
+path="res://.godot/imported/credits.svg-f8d3e258536ac8d1209ad161871ccdce.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://assets/icons/resources/credits.svg"
+dest_files=["res://.godot/imported/credits.svg-f8d3e258536ac8d1209ad161871ccdce.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/uastc_level=0
+compress/rdo_quality_loss=0.0
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/channel_remap/red=0
+process/channel_remap/green=1
+process/channel_remap/blue=2
+process/channel_remap/alpha=3
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/assets/icons/resources/energy_bank.svg b/assets/icons/resources/energy_bank.svg
new file mode 100644
index 0000000..79c5d9d
--- /dev/null
+++ b/assets/icons/resources/energy_bank.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/assets/icons/resources/energy_bank.svg.import b/assets/icons/resources/energy_bank.svg.import
new file mode 100644
index 0000000..9899c54
--- /dev/null
+++ b/assets/icons/resources/energy_bank.svg.import
@@ -0,0 +1,43 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://ixmsdytq8a4q"
+path="res://.godot/imported/energy_bank.svg-8c8e30dd3b3aa92f057a91b42959e0d5.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://assets/icons/resources/energy_bank.svg"
+dest_files=["res://.godot/imported/energy_bank.svg-8c8e30dd3b3aa92f057a91b42959e0d5.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/uastc_level=0
+compress/rdo_quality_loss=0.0
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/channel_remap/red=0
+process/channel_remap/green=1
+process/channel_remap/blue=2
+process/channel_remap/alpha=3
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/assets/icons/resources/prompts.json b/assets/icons/resources/prompts.json
index bff9772..2f088a3 100644
--- a/assets/icons/resources/prompts.json
+++ b/assets/icons/resources/prompts.json
@@ -41,6 +41,11 @@
"key": "missile",
"prompt": "Generate one high-quality isolated game inventory resource icon. Square image with a genuine transparent background. Detailed stylized 3D science-fiction game item render, crisp metal and mineral materials, broad readable silhouette, three-quarter view, controlled highlights. Center the object with 12 percent transparent padding on all sides; keep every part completely inside the frame. Must read clearly as a 32px HUD icon. No scene, no floor, no drop shadow, no border, no text, no lettering, no labels. Subject: a single compact spaceship missile with pointed silver nose, dark steel casing, two small red fins and a restrained orange rear nozzle, diagonal three-quarter view",
"asset": "assets/icons/resources/missile.png"
+ },
+ {
+ "key": "sulfur",
+ "asset": "assets/icons/resources/sulfur.png",
+ "prompt": "Create one premium game inventory icon of sulfur ore for a dark sci-fi space mining game. A compact irregular charcoal rock encrusted with richly yellow sulfur crystals, natural opaque golden yellow mineral facets, realistic detailed painted 3D game item render, dramatic upper left light, crisp silhouette legible at 48 pixels, object centered filling 80% of square with small safe margins. Transparent background. No text, letters, UI, frame, multiple icons or floor. Match a realistic inventory resource icon set with chunky dark rocks and colored mineral veins. Save the image as sulfur.png if possible."
}
]
}
diff --git a/assets/icons/resources/sulfur.png b/assets/icons/resources/sulfur.png
new file mode 100644
index 0000000..8d623d7
Binary files /dev/null and b/assets/icons/resources/sulfur.png differ
diff --git a/assets/icons/resources/sulfur.png.import b/assets/icons/resources/sulfur.png.import
new file mode 100644
index 0000000..55e27a1
--- /dev/null
+++ b/assets/icons/resources/sulfur.png.import
@@ -0,0 +1,40 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://drp2s1khtvtn4"
+path="res://.godot/imported/sulfur.png-2f60f314b66456b06aeadc4c9514b477.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://assets/icons/resources/sulfur.png"
+dest_files=["res://.godot/imported/sulfur.png-2f60f314b66456b06aeadc4c9514b477.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/uastc_level=0
+compress/rdo_quality_loss=0.0
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/channel_remap/red=0
+process/channel_remap/green=1
+process/channel_remap/blue=2
+process/channel_remap/alpha=3
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/assets/sounds/music/music2.mp3 b/assets/sounds/music/music2.mp3
new file mode 100644
index 0000000..34e431a
Binary files /dev/null and b/assets/sounds/music/music2.mp3 differ
diff --git a/resources/items/energy_bank.tres b/resources/items/energy_bank.tres
new file mode 100644
index 0000000..790adc6
--- /dev/null
+++ b/resources/items/energy_bank.tres
@@ -0,0 +1,10 @@
+[gd_resource type="Resource" script_class="ItemDefinition" load_steps=3 format=3]
+[ext_resource type="Script" path="res://scripts/items/item_definition.gd" id="1"]
+[ext_resource type="Texture2D" path="res://assets/icons/resources/energy_bank.svg" id="icon"]
+[resource]
+script = ExtResource("1")
+icon = ExtResource("icon")
+item_id = "energy_bank"
+display_name = "Bank energii"
+category = 7
+description = "Jednorazowy powerbank. Przywraca 10% maksymalnej energii statku, do jego aktualnego limitu. Użyj klawiszem R podczas lotu."
diff --git a/resources/items/gamma_cannon.tres b/resources/items/gamma_cannon.tres
index dd56998..7e72f5c 100644
--- a/resources/items/gamma_cannon.tres
+++ b/resources/items/gamma_cannon.tres
@@ -14,7 +14,7 @@ stat_suffix = " DMG"
weapon_kind = 4
fire_interval = 0.8
projectile_speed = 430.0
-weapon_range = 800.0
+weapon_range = 400.0
damage_lower_roll_min = 10
damage_lower_roll_max = 14
damage_spread_roll_min = 4
diff --git a/scenes/ui/captain_panel.tscn b/scenes/ui/captain_panel.tscn
index f49b1e0..8e3a7dd 100644
--- a/scenes/ui/captain_panel.tscn
+++ b/scenes/ui/captain_panel.tscn
@@ -65,12 +65,3 @@ size_flags_horizontal = 3
theme_override_colors/font_color = Color(0.98, 0.78, 0.43, 1)
theme_override_font_sizes/font_size = 11
-[node name="Points" type="Label" parent="Contents/Wallet"]
-layout_mode = 2
-tooltip_text = "Punkty nauki: +1 za każdy zdobyty poziom kapitana."
-theme_override_colors/font_color = Color(0.47, 0.91, 0.74, 1)
-theme_override_font_sizes/font_size = 11
-
-[node name="Skills" type="VBoxContainer" parent="Contents"]
-layout_mode = 2
-theme_override_constants/separation = 2
diff --git a/scripts/captain/captain_progress.gd b/scripts/captain/captain_progress.gd
index e878f4b..b9ccd6b 100644
--- a/scripts/captain/captain_progress.gd
+++ b/scripts/captain/captain_progress.gd
@@ -9,7 +9,7 @@ const ACTIVE_SKILL_SLOTS := 5
@export var experience: int = 0
@export var credits: int = 0
@export var parts: float = 0.0
-@export var minerals: Dictionary = {"uranium": 0, "titanium": 0, "iron": 0, "aluminium": 0}
+@export var minerals: Dictionary = {"uranium": 0, "titanium": 0, "iron": 0, "aluminium": 0, "sulfur": 0}
@export var learning_points: int = 0
@export var skills: Dictionary = {}
@export var equipped_skills: Array[String] = ["", "", "", "", ""]
diff --git a/scripts/game/enemy_ship.gd b/scripts/game/enemy_ship.gd
index fcb509c..156ba7e 100644
--- a/scripts/game/enemy_ship.gd
+++ b/scripts/game/enemy_ship.gd
@@ -201,6 +201,16 @@ func _reward_captain() -> void:
get_parent().call_deferred("add_child", credits)
func _drop_loot() -> void:
+ # Roll independently of equipment drops.
+ var bank_chance := 0.1
+ if randf() < bank_chance:
+ var bank := ItemInstance.new()
+ bank.definition = preload("res://resources/items/energy_bank.tres")
+ var bank_pickup := Node2D.new()
+ bank_pickup.set_script(LOOT_PICKUP)
+ bank_pickup.set("item", bank)
+ bank_pickup.position = position + Vector2(0, -42)
+ get_parent().call_deferred("add_child", bank_pickup)
var chance := 0.65 if loot_tier == 0 else (0.9 if loot_tier == 1 else 1.0)
if randf() > chance:
return
diff --git a/scripts/game/flight_hud.gd b/scripts/game/flight_hud.gd
index 4608dc3..ccb3bd4 100644
--- a/scripts/game/flight_hud.gd
+++ b/scripts/game/flight_hud.gd
@@ -6,6 +6,7 @@ extends CanvasLayer
@onready var inventory_button: Button = $HUDRoot/ShipEquipmentPanel/Contents/InventoryButton
@onready var inventory_panel: Control = $HUDRoot/ShipInventoryPanel
+var _energy_bank_button: Button
var _debug_expanded := false
var _ability_buttons: Dictionary = {}
var _ability_bar: HBoxContainer
@@ -36,7 +37,7 @@ func _create_navigation() -> void:
navigation.offset_top = 12
navigation.offset_bottom = 64
navigation.add_theme_constant_override("separation", 8)
- _crafting_window = _create_window("CraftingWindow", "TWÓRZ", Vector2(440, 250))
+ _crafting_window = _create_window("CraftingWindow", "WARSZTAT", Vector2(704, 420))
var crafting := PanelContainer.new()
crafting.name = "CraftingPanel"
crafting.set_script(preload("res://scripts/ui/ship_crafting_panel.gd"))
@@ -73,6 +74,7 @@ func _create_navigation() -> void:
func _create_window(node_name: String, title: String, dimensions: Vector2) -> PanelContainer:
var window := PanelContainer.new()
window.name = node_name
+ window.z_index = 100
var style := StyleBoxFlat.new()
style.bg_color = Color("0b1520fa")
style.border_color = Color("376779")
@@ -298,7 +300,50 @@ func _layout_inventory() -> void:
var equipment := _sidebar.get_node("Scroll/Contents/ShipEquipmentPanel") as Control
_inventory_clip.position = Vector2(maxf(12.0, _sidebar.position.x - _inventory_clip.size.x - 8), minf(equipment.global_position.y, maxf(8.0, $HUDRoot.size.y - inventory_panel.size.y * EQUIPMENT_UI_SCALE - 8)))
+func _update_energy_bank_button() -> void:
+ if _energy_bank_button == null:
+ _energy_bank_button = Button.new()
+ _energy_bank_button.name = "EnergyBankButton"
+ _energy_bank_button.set_script(preload("res://scripts/ui/captain_skill_button.gd"))
+ _energy_bank_button.call("configure", preload("res://assets/icons/resources/energy_bank.svg"), Color("70f5df"))
+ _energy_bank_button.custom_minimum_size = Vector2(48, 48)
+ _energy_bank_button.size = Vector2(48, 48)
+ $HUDRoot.add_child(_energy_bank_button)
+ var key := Label.new()
+ key.text = "R"
+ key.mouse_filter = Control.MOUSE_FILTER_IGNORE
+ key.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
+ key.vertical_alignment = VERTICAL_ALIGNMENT_TOP
+ key.add_theme_font_size_override("font_size", 11)
+ key.add_theme_color_override("font_color", Color.WHITE)
+ key.add_theme_color_override("font_outline_color", Color("07141e"))
+ key.add_theme_constant_override("outline_size", 7)
+ _energy_bank_button.add_child(key)
+ key.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
+ key.offset_right = -5
+ key.offset_top = 2
+ var amount := Label.new()
+ amount.name = "Amount"
+ amount.mouse_filter = Control.MOUSE_FILTER_IGNORE
+ amount.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
+ amount.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
+ amount.add_theme_font_size_override("font_size", 22)
+ amount.add_theme_color_override("font_color", Color.WHITE)
+ amount.add_theme_color_override("font_outline_color", Color("07141e"))
+ amount.add_theme_constant_override("outline_size", 7)
+ _energy_bank_button.add_child(amount)
+ amount.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
+ _energy_bank_button.pressed.connect(func() -> void: player.call("use_energy_bank"))
+ _energy_bank_button.position = Vector2(_cockpit.position.x + _cockpit.size.x * _cockpit.scale.x * 0.5 - 24.0, _cockpit.position.y - 56.0)
+ var inventory := player.get_node("ShipInventory") as ShipInventory
+ var count := inventory.get_energy_bank_count()
+ (_energy_bank_button.get_node("Amount") as Label).text = str(count)
+ _energy_bank_button.tooltip_text = "Bank energii: %d\nR • Przywraca 10%% maksymalnej energii" % count
+ _energy_bank_button.disabled = count == 0 or float(player.get("energy")) >= float(player.get("max_energy")) or bool(player.get("_is_destroyed")) or bool(player.get("is_docked"))
+ _energy_bank_button.call("set_visual_state", 0.0, false, _energy_bank_button.disabled)
+
func _process(_delta: float) -> void:
+ _update_energy_bank_button()
_layout_inventory()
_update_ability_bar()
if not _debug_expanded:
diff --git a/scripts/game/ship_controller.gd b/scripts/game/ship_controller.gd
index 5f3f9a3..8a2733a 100644
--- a/scripts/game/ship_controller.gd
+++ b/scripts/game/ship_controller.gd
@@ -95,6 +95,7 @@ func apply_equipment_bonuses(bonuses: Dictionary) -> void:
func _ready() -> void:
for index in ShipInventory.EXTENSION_SLOT_COUNT:
_ensure_input_action(StringName("ability_%d" % (index + 1)), (KEY_1 + index) as Key)
+ _ensure_input_action(&"use_energy_bank", KEY_R)
_ensure_input_action(&"ship_thrust", KEY_W)
_ensure_input_action(&"ship_mini_boost", KEY_TAB)
_ensure_input_action(&"ship_brake", KEY_S)
@@ -402,7 +403,19 @@ func activate_ability(slot_index: int) -> bool:
get_active_ability(slot_index).activate(self)
return true
+func use_energy_bank() -> void:
+ if $ShipInventory.use_energy_bank():
+ var popup := DAMAGE_POPUP_SCENE.instantiate() as Label
+ popup.text = "+10% ENERGII"
+ popup.set("text_color", Color("70f5df"))
+ popup.position = position + Vector2(-48, -60)
+ get_parent().add_child(popup)
+
func _unhandled_key_input(event: InputEvent) -> void:
+ if event.is_action_pressed("use_energy_bank") and not event.is_echo():
+ use_energy_bank()
+ get_viewport().set_input_as_handled()
+ return
if event is InputEventKey and event.echo:
return
for index in ShipInventory.EXTENSION_SLOT_COUNT:
diff --git a/scripts/items/crafting_recipes.gd b/scripts/items/crafting_recipes.gd
new file mode 100644
index 0000000..d99d22b
--- /dev/null
+++ b/scripts/items/crafting_recipes.gd
@@ -0,0 +1,17 @@
+extends RefCounted
+
+const MINERALS := ["iron", "titanium", "uranium", "aluminium", "sulfur"]
+const NAMES := {"iron": "Żelazo", "titanium": "Tytan", "uranium": "Uran", "aluminium": "Aluminium", "sulfur": "Siarka"}
+const COLORS := {"iron": Color("c5b5a0"), "titanium": Color("9ecbe9"), "uranium": Color("a9ef83"), "aluminium": Color("dce8ef"), "sulfur": Color("f4d466")}
+const IDS := ["parts", "ammunition", "missile", "energy_bank", "irradiated_ammunition"]
+const DATA := {
+ "parts": {"name": "Części", "amount": 1, "cost": {"titanium": 1, "iron": 1, "aluminium": 1}},
+ "ammunition": {"name": "Amunicja", "amount": 10, "cost": {"titanium": 1, "sulfur": 1}},
+ "missile": {"name": "Rakieta", "amount": 1, "cost": {"iron": 1, "sulfur": 1, "uranium": 1}},
+ "energy_bank": {"name": "Powerbank", "amount": 1, "cost": {"iron": 1, "aluminium": 1}},
+ "irradiated_ammunition": {"name": "Amunicja radiacyjna", "amount": 10, "cost": {"titanium": 1, "uranium": 1}},
+}
+
+static func icon(id: String) -> Texture2D:
+ var extension := "svg" if id == "energy_bank" else "png"
+ return load("res://assets/icons/resources/%s.%s" % [id, extension]) as Texture2D
diff --git a/scripts/items/crafting_recipes.gd.uid b/scripts/items/crafting_recipes.gd.uid
new file mode 100644
index 0000000..f638481
--- /dev/null
+++ b/scripts/items/crafting_recipes.gd.uid
@@ -0,0 +1 @@
+uid://ekvidemqdxmy
diff --git a/scripts/items/credit_pickup.gd b/scripts/items/credit_pickup.gd
index a39bc9e..2d6fb7e 100644
--- a/scripts/items/credit_pickup.gd
+++ b/scripts/items/credit_pickup.gd
@@ -13,10 +13,10 @@ func _ready() -> void:
z_index = 18
func _draw() -> void:
- draw_circle(Vector2.ZERO, 13.0, Color("46351b"))
- draw_arc(Vector2.ZERO, 13.0, 0, TAU, 32, Color("ffd56a"), 2.0, true)
- draw_arc(Vector2.ZERO, 6.0, PI * 0.3, PI * 1.7, 20, Color("ffd56a"), 2.0, true)
- draw_line(Vector2(-2, -8), Vector2(-2, 8), Color("ffd56a"), 1.5, true)
+ var gold := Color("ffd56a")
+ for radius in [27.0, 22.0, 18.0]:
+ draw_circle(Vector2.ZERO, radius, Color(gold, 0.06))
+ draw_texture_rect(preload("res://assets/icons/resources/credits.svg"), Rect2(-18, -18, 36, 36), false)
func _physics_process(delta: float) -> void:
if _collected or not is_instance_valid(_player) or bool(_player.get("_is_destroyed")):
diff --git a/scripts/items/item_definition.gd b/scripts/items/item_definition.gd
index 18bae6b..f623b13 100644
--- a/scripts/items/item_definition.gd
+++ b/scripts/items/item_definition.gd
@@ -1,7 +1,7 @@
extends Resource
class_name ItemDefinition
-enum Category { WEAPON, SHIELD, ARMOR, REACTOR, ENGINE, ACTIVE_EXTENSION, COMPUTER }
+enum Category { WEAPON, SHIELD, ARMOR, REACTOR, ENGINE, ACTIVE_EXTENSION, COMPUTER, CONSUMABLE }
enum WeaponKind { PLASMA, MACHINE_GUN, LASER, MISSILE, GAMMA }
func get_damage_type() -> int:
diff --git a/scripts/items/item_instance.gd b/scripts/items/item_instance.gd
index a05ecaa..cc60a24 100644
--- a/scripts/items/item_instance.gd
+++ b/scripts/items/item_instance.gd
@@ -104,6 +104,8 @@ func get_type_color() -> Color:
func get_tooltip_text() -> String:
if definition == null:
return ""
+ if definition.category == ItemDefinition.Category.CONSUMABLE:
+ return "%s\n\n%s\nR • Zużywa jedną sztukę z zasobów." % [definition.display_name, definition.description]
var primary_text := "%s: +%.1f%s" % [stat_name(definition.primary_stat), get_stat_total(definition.primary_stat), definition.stat_suffix]
if definition.category == ItemDefinition.Category.WEAPON:
primary_text = "Obrażenia: %s" % get_damage_range_text()
diff --git a/scripts/items/loot_pickup.gd b/scripts/items/loot_pickup.gd
index 700f54a..6efbe6d 100644
--- a/scripts/items/loot_pickup.gd
+++ b/scripts/items/loot_pickup.gd
@@ -5,6 +5,7 @@ const ATTRACTION_RADIUS := 150.0
var item: ItemInstance
var _player: CharacterBody2D
var _label: Label
+var _glow_time := 0.0
func _ready() -> void:
add_to_group("loot")
@@ -17,16 +18,43 @@ func _ready() -> void:
_label.add_theme_color_override("font_color", Color("f1f4f8"))
_label.text = item.definition.display_name
add_child(_label)
+ z_index = 18
+ set_process(item.get_addon_count() >= 5)
+ queue_redraw()
+
+func _process(delta: float) -> void:
+ _glow_time += delta
queue_redraw()
func _draw() -> void:
if item == null:
return
- var background := item.get_background_color()
- var border := item.get_type_color()
- draw_circle(Vector2.ZERO, 15.0, background)
- draw_arc(Vector2.ZERO, 15.0, 0.0, TAU, 32, border, 2.0, true)
- draw_polyline(PackedVector2Array([Vector2(0, -8), Vector2(7, 0), Vector2(0, 8), Vector2(-7, 0), Vector2(0, -8)]), Color("f1f4f8"), 1.0, true)
+ if item.get_addon_count() >= 5:
+ _draw_legendary_glow()
+ var border := Color("70f5df") if item.definition.category == ItemDefinition.Category.CONSUMABLE else item.get_type_color()
+ for radius in [28.0, 23.0, 19.0]:
+ draw_circle(Vector2.ZERO, radius, Color(border, 0.07))
+ draw_circle(Vector2.ZERO, 18.0, item.get_background_color())
+ draw_arc(Vector2.ZERO, 19.0, 0.15, PI - 0.15, 32, border, 1.5, true)
+ draw_arc(Vector2.ZERO, 19.0, PI + 0.15, TAU - 0.15, 32, border, 1.5, true)
+ if item.definition.icon != null:
+ draw_texture_rect(item.definition.icon, Rect2(-16, -16, 32, 32), false)
+
+func _draw_legendary_glow() -> void:
+ var gold := Color("ffbd68")
+ var pulse := 0.85 + 0.15 * sin(_glow_time * 2.0)
+ # Soft layered halo remains visible without requiring viewport bloom.
+ for layer in range(12, 0, -1):
+ var radius := 20.0 + float(layer) * 3.5
+ draw_circle(Vector2.ZERO, radius, Color(gold, 0.012 * pulse))
+ # A tapered pillar and a slowly turning broken ring mark a maximum-addon drop.
+ for layer in range(6, 0, -1):
+ var width := float(layer) * 2.2
+ draw_polygon(PackedVector2Array([Vector2(-width, 0), Vector2(-width * 0.2, -94), Vector2(width * 0.2, -94), Vector2(width, 0)]), PackedColorArray([Color(gold, 0.025 * pulse), Color(gold, 0), Color(gold, 0), Color(gold, 0.025 * pulse)]))
+ var angle := _glow_time * 0.25
+ for segment in 3:
+ var start := angle + float(segment) * TAU / 3.0
+ draw_arc(Vector2.ZERO, 29.0, start, start + 1.25, 20, Color(gold, 0.42 * pulse), 1.0, true)
func _physics_process(delta: float) -> void:
if not is_instance_valid(_player) or bool(_player.get("_is_destroyed")):
@@ -35,7 +63,7 @@ func _physics_process(delta: float) -> void:
if distance > ATTRACTION_RADIUS:
return
var inventory := _player.get_node("ShipInventory") as ShipInventory
- if inventory.get_cargo_used() >= ShipInventory.CARGO_SIZE:
+ if item.definition.item_id != "energy_bank" and inventory.get_cargo_used() >= ShipInventory.CARGO_SIZE:
_label.text = "%s\nMagazyn pełny" % item.definition.display_name
return
_label.text = item.definition.display_name
diff --git a/scripts/items/ship_inventory.gd b/scripts/items/ship_inventory.gd
index 85b0efd..3badeca 100644
--- a/scripts/items/ship_inventory.gd
+++ b/scripts/items/ship_inventory.gd
@@ -4,10 +4,10 @@ class_name ShipInventory
signal inventory_changed
signal ammunition_changed
+const RECIPES := preload("res://scripts/items/crafting_recipes.gd")
const CARGO_SIZE := 20
const WEAPON_SLOT_COUNT := 3
const STARTING_MISSILE_AMMUNITION := 2
-const MISSILE_CRAFT_PARTS_COST := 50.0
const MISSILE_BUY_BASE_COST := 10
const MISSILE_DEFINITIONS := [preload("res://resources/items/missile_launcher.tres"), preload("res://resources/items/unguided_missile.tres")]
const MODULE_SLOT_COUNT := 5
@@ -15,11 +15,11 @@ const EXTENSION_SLOT_COUNT := 5
const MODULE_CATEGORIES := [ItemDefinition.Category.SHIELD, ItemDefinition.Category.ARMOR, ItemDefinition.Category.ENGINE, ItemDefinition.Category.REACTOR, ItemDefinition.Category.COMPUTER]
const MODULE_NAMES := ["Tarcze", "Pancerz", "Silnik", "Reaktor", "Komputer"]
const STARTING_AMMUNITION := 50
-const AMMUNITION_CRAFT_AMOUNT := 100
-const AMMUNITION_CRAFT_PARTS_COST := 50.0
+const AMMUNITION_CRAFT_AMOUNT := 10
const AMMUNITION_BUY_AMOUNT := 100
const AMMUNITION_BUY_BASE_COST := 20
+var energy_banks := 0
var irradiated_ammunition := 0
var ammunition: int = STARTING_AMMUNITION
@@ -48,7 +48,9 @@ func _ready() -> void:
ammunition = int(GameState.restart_inventory.get("ammunition", STARTING_AMMUNITION))
missile_ammunition = int(GameState.restart_inventory.get("missile_ammunition", STARTING_MISSILE_AMMUNITION))
irradiated_ammunition = int(GameState.restart_inventory.get("irradiated_ammunition", 0))
+ energy_banks = int(GameState.restart_inventory.get("energy_banks", 0))
GameState.restart_inventory.clear()
+ _convert_legacy_energy_banks()
_remove_extension_addons()
_recalculate_ship_stats()
inventory_changed.emit()
@@ -68,10 +70,32 @@ func preserve_for_restart() -> void:
"module": module_slots.duplicate(),
"extension": extension_slots.duplicate(),
"irradiated_ammunition": irradiated_ammunition,
+ "energy_banks": energy_banks,
"ammunition": ammunition,
"missile_ammunition": missile_ammunition,
}
+func _convert_legacy_energy_banks() -> void:
+ for index in cargo.size():
+ if cargo[index] != null and cargo[index].definition != null and cargo[index].definition.item_id == "energy_bank":
+ energy_banks += 1
+ cargo[index] = null
+
+func get_energy_bank_count() -> int:
+ return energy_banks
+
+func use_energy_bank() -> bool:
+ if bool(ship.get("_is_destroyed")) or bool(ship.get("is_docked")) or get_tree().paused or energy_banks <= 0:
+ return false
+ var maximum := float(ship.get("max_energy"))
+ var current := float(ship.get("energy"))
+ if current >= maximum:
+ return false
+ ship.set("energy", minf(maximum, current + maximum * 0.1))
+ energy_banks -= 1
+ inventory_changed.emit()
+ return true
+
func consume_ammunition() -> bool:
if ammunition <= 0:
return false
@@ -80,18 +104,47 @@ func consume_ammunition() -> bool:
ammunition_changed.emit()
return true
-func can_craft_ammunition() -> bool:
- return GameState.captain.parts >= AMMUNITION_CRAFT_PARTS_COST
-
-func craft_ammunition() -> bool:
- if not can_craft_ammunition():
+func can_craft(recipe_id: String) -> bool:
+ if not RECIPES.DATA.has(recipe_id):
return false
- GameState.captain.parts -= AMMUNITION_CRAFT_PARTS_COST
- ammunition += AMMUNITION_CRAFT_AMOUNT
+ for mineral in RECIPES.DATA[recipe_id]["cost"]:
+ if int(GameState.captain.minerals.get(mineral, 0)) < int(RECIPES.DATA[recipe_id]["cost"][mineral]):
+ return false
+ return true
+
+func craft(recipe_id: String) -> bool:
+ if not can_craft(recipe_id):
+ return false
+ var recipe: Dictionary = RECIPES.DATA[recipe_id]
+ for mineral in recipe["cost"]:
+ GameState.captain.minerals[mineral] = int(GameState.captain.minerals.get(mineral, 0)) - int(recipe["cost"][mineral])
+ var amount := int(recipe["amount"])
+ match recipe_id:
+ "parts": GameState.captain.parts += amount
+ "ammunition": ammunition += amount
+ "missile": missile_ammunition += amount
+ "energy_bank": energy_banks += amount
+ "irradiated_ammunition": irradiated_ammunition += amount
GameState.captain.emit_changed()
ammunition_changed.emit()
+ inventory_changed.emit()
return true
+func get_crafted_stock(recipe_id: String) -> int:
+ match recipe_id:
+ "parts": return int(GameState.captain.parts)
+ "ammunition": return ammunition
+ "missile": return missile_ammunition
+ "energy_bank": return energy_banks
+ "irradiated_ammunition": return irradiated_ammunition
+ return 0
+
+func can_craft_ammunition() -> bool:
+ return can_craft("ammunition")
+
+func craft_ammunition() -> bool:
+ return craft("ammunition")
+
func get_item(slot_kind: String, index: int) -> ItemInstance:
var slots := _get_slots(slot_kind)
if index < 0 or index >= slots.size():
@@ -116,6 +169,10 @@ func buy_ammunition() -> bool:
func add_item(item: ItemInstance) -> bool:
if item == null or item.definition == null:
return false
+ if item.definition.item_id == "energy_bank":
+ energy_banks += 1
+ inventory_changed.emit()
+ return true
if item.definition.category == ItemDefinition.Category.ACTIVE_EXTENSION:
item.additional_modifiers.clear()
item.rarity = 0
@@ -180,7 +237,7 @@ func consume_missile() -> bool:
return true
func can_craft_missile() -> bool:
- return GameState.captain.parts >= MISSILE_CRAFT_PARTS_COST
+ return can_craft("missile")
func get_missile_buy_cost() -> int:
return GameState.captain.discounted_price(MISSILE_BUY_BASE_COST * maxi(1, GameState.system_level))
@@ -189,13 +246,7 @@ func can_buy_missile() -> bool:
return GameState.captain.credits >= get_missile_buy_cost()
func craft_missile() -> bool:
- if not can_craft_missile():
- return false
- GameState.captain.parts -= MISSILE_CRAFT_PARTS_COST
- missile_ammunition += 1
- GameState.captain.emit_changed()
- ammunition_changed.emit()
- return true
+ return craft("missile")
func buy_missile() -> bool:
if not can_buy_missile():
@@ -352,17 +403,10 @@ func consume_irradiated_ammunition() -> bool:
return true
func can_craft_irradiated_ammunition() -> bool:
- return int(GameState.captain.minerals.get("uranium", 0)) >= 1 and GameState.captain.parts >= 10.0
+ return can_craft("irradiated_ammunition")
func craft_irradiated_ammunition() -> bool:
- if not can_craft_irradiated_ammunition():
- return false
- GameState.captain.minerals["uranium"] -= 1
- GameState.captain.parts -= 10.0
- irradiated_ammunition += 10
- GameState.captain.emit_changed()
- ammunition_changed.emit()
- return true
+ return craft("irradiated_ammunition")
func can_buy_irradiated_ammunition() -> bool:
return GameState.captain.credits >= get_irradiated_ammunition_buy_cost()
diff --git a/scripts/levels/exploration_poi.gd b/scripts/levels/exploration_poi.gd
index 5999d33..c0c5c1e 100644
--- a/scripts/levels/exploration_poi.gd
+++ b/scripts/levels/exploration_poi.gd
@@ -23,7 +23,7 @@ var _asteroid_spin := 0.0
func _ready() -> void:
add_to_group("exploration_pois")
_rng.seed = reward_seed
- _mineral = ["iron", "aluminium", "titanium", "uranium"][_rng.randi_range(0, 3)]
+ _mineral = ["iron", "aluminium", "titanium", "uranium", "sulfur"][_rng.randi_range(0, 4)]
hull = 20.0 * reward_scale()
collision_layer = 4 if kind == "asteroid" else 0
collision_mask = 0
@@ -80,7 +80,7 @@ func _update_label(near: bool) -> void:
if kind == "ruin":
_label.text = "?"
elif kind == "asteroid":
- _label.text = "Asteroida · %s\nHP: %.0f" % [{"iron": "Żelazo", "aluminium": "Aluminium", "titanium": "Tytan", "uranium": "Uran"}[_mineral], hull]
+ _label.text = "Asteroida · %s\nHP: %.0f" % [{"iron": "Żelazo", "aluminium": "Aluminium", "titanium": "Tytan", "uranium": "Uran", "sulfur": "Siarka"}[_mineral], hull]
else:
_label.text = ("Wrak statku" if kind == "wreck" else "Antyczne znalezisko") + ("\n[E] Przeszukaj" if near else "\nPodleć, aby przeszukać")
@@ -115,7 +115,7 @@ func claim() -> bool:
if kind == "asteroid":
var amount := maxi(1, roundi(_rng.randi_range(1, 3) * scale)) + roundi(GameState.captain.get_skill_bonus("miner"))
GameState.captain.minerals[_mineral] = int(GameState.captain.minerals.get(_mineral, 0)) + amount
- summary = "+%d %s" % [amount, {"iron": "żelaza", "aluminium": "aluminium", "titanium": "tytanu", "uranium": "uranu"}[_mineral]]
+ summary = "+%d %s" % [amount, {"iron": "żelaza", "aluminium": "aluminium", "titanium": "tytanu", "uranium": "uranu", "sulfur": "siarki"}[_mineral]]
elif kind == "ruin":
var xp := roundi(_rng.randi_range(10, 20) * scale)
GameState.captain.add_rewards(xp, 0)
diff --git a/scripts/ui/captain_panel.gd b/scripts/ui/captain_panel.gd
index e669e59..29dcb3f 100644
--- a/scripts/ui/captain_panel.gd
+++ b/scripts/ui/captain_panel.gd
@@ -1,16 +1,11 @@
extends PanelContainer
-const BALANCE := preload("res://scripts/captain/captain_balance.gd")
-
@onready var player: CharacterBody2D = get_node("../../../PlayerShip")
@onready var level_label: Label = $Contents/Header/Level
@onready var xp_label: Label = $Contents/XPLabel
@onready var xp_bar: ProgressBar = $Contents/XPBar
@onready var credits_label: Label = $Contents/Wallet/Credits
-@onready var points_label: Label = $Contents/Wallet/Points
-var _collapsed := false
-var _collapse_button: Button
var _captain: CaptainProgress
func _ready() -> void:
@@ -20,8 +15,6 @@ func _ready() -> void:
_captain.changed.connect(_refresh)
player.get_node("ShipInventory").connect("inventory_changed", _refresh)
_refresh()
- if bool(GameState.hud_panel_state.get("captain_collapsed", true)):
- _toggle_collapsed()
func _refresh() -> void:
level_label.text = "POZ. %d" % _captain.level
@@ -29,7 +22,6 @@ func _refresh() -> void:
xp_bar.max_value = _captain.get_xp_required()
xp_bar.value = _captain.experience
credits_label.text = "Kredyty: %d" % _captain.credits
- points_label.text = "Punkty: %d" % _captain.learning_points
func _build_portrait() -> void:
custom_minimum_size = Vector2(320, 0)
@@ -40,14 +32,6 @@ func _build_portrait() -> void:
style.set_corner_radius_all(8)
style.set_content_margin_all(10)
add_theme_stylebox_override("panel", style)
- _collapse_button = Button.new()
- _collapse_button.name = "CollapseCaptain"
- _collapse_button.text = "−"
- _collapse_button.flat = true
- _collapse_button.focus_mode = Control.FOCUS_NONE
- _collapse_button.custom_minimum_size = Vector2(24, 24)
- _collapse_button.pressed.connect(_toggle_collapsed)
- $Contents/Header.add_child(_collapse_button)
var row := HBoxContainer.new()
row.name = "PortraitRow"
row.add_theme_constant_override("separation", 10)
@@ -82,11 +66,3 @@ func _build_portrait() -> void:
xp_bar.reparent(info, false)
xp_bar.custom_minimum_size.y = 5
-func _toggle_collapsed() -> void:
- _collapsed = not _collapsed
- GameState.hud_panel_state["captain_collapsed"] = _collapsed
- for child in $Contents.get_children():
- if child.name not in ["Header", "PortraitRow"]:
- child.visible = not _collapsed
- _collapse_button.text = "+" if _collapsed else "−"
- size.y = get_combined_minimum_size().y
diff --git a/scripts/ui/crafting_recipe_button.gd b/scripts/ui/crafting_recipe_button.gd
new file mode 100644
index 0000000..86b0ee6
--- /dev/null
+++ b/scripts/ui/crafting_recipe_button.gd
@@ -0,0 +1,109 @@
+extends Button
+
+const RECIPES := preload("res://scripts/items/crafting_recipes.gd")
+var recipe_id := ""
+var inventory: ShipInventory
+var _stock: Label
+var _availability: Label
+var _picture: TextureRect
+
+func _ready() -> void:
+ custom_minimum_size = Vector2(120, 166)
+ size_flags_horizontal = Control.SIZE_EXPAND_FILL
+ focus_mode = Control.FOCUS_NONE
+ mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
+ tooltip_text = "Receptura"
+ for state in ["normal", "hover", "pressed", "disabled"]:
+ var style := StyleBoxFlat.new()
+ style.bg_color = Color("172b3b") if state == "hover" else Color("0c1925")
+ style.border_color = Color("e7bb73") if state == "hover" else Color("314b60")
+ style.set_border_width_all(1)
+ style.set_corner_radius_all(7)
+ style.set_content_margin_all(8)
+ add_theme_stylebox_override(state, style)
+ var contents := VBoxContainer.new()
+ contents.mouse_filter = Control.MOUSE_FILTER_IGNORE
+ contents.add_theme_constant_override("separation", 4)
+ add_child(contents)
+ contents.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
+ contents.offset_left = 8
+ contents.offset_right = -8
+ contents.offset_top = 8
+ contents.offset_bottom = -8
+ var artwork := Control.new()
+ artwork.custom_minimum_size.y = 60
+ artwork.mouse_filter = Control.MOUSE_FILTER_IGNORE
+ contents.add_child(artwork)
+ _picture = TextureRect.new()
+ _picture.texture = RECIPES.icon(recipe_id)
+ _picture.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
+ _picture.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
+ _picture.mouse_filter = Control.MOUSE_FILTER_IGNORE
+ artwork.add_child(_picture)
+ _picture.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
+ var output := _label("+%d" % RECIPES.DATA[recipe_id]["amount"], 12, Color("efc681"))
+ output.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
+ artwork.add_child(output)
+ output.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
+ var title := _label(RECIPES.DATA[recipe_id]["name"], 11, Color("e5edf4"))
+ title.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
+ title.custom_minimum_size.y = 36
+ title.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
+ contents.add_child(title)
+ _stock = _label("", 10, Color("8ea9bc"))
+ contents.add_child(_stock)
+ _availability = _label("", 10, Color.WHITE)
+ contents.add_child(_availability)
+ refresh()
+
+func _label(caption: String, font_size: int, color: Color) -> Label:
+ var label := Label.new()
+ label.text = caption
+ label.mouse_filter = Control.MOUSE_FILTER_IGNORE
+ label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
+ label.add_theme_font_size_override("font_size", font_size)
+ label.add_theme_color_override("font_color", color)
+ return label
+
+func refresh() -> void:
+ if _stock == null:
+ return
+ var available := inventory.can_craft(recipe_id)
+ _stock.text = "Zapas: %d" % inventory.get_crafted_stock(recipe_id)
+ _availability.text = "WYTWÓRZ" if available else "BRAK SUROWCÓW"
+ _availability.add_theme_color_override("font_color", Color("85deb1") if available else Color("d08c8e"))
+ _picture.modulate = Color.WHITE if available else Color("8b939f")
+ # Keep the card hoverable so unavailable recipes still show their costs.
+ mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND if available else Control.CURSOR_HELP
+
+func _make_custom_tooltip(_text: String) -> Object:
+ var panel := PanelContainer.new()
+ var style := StyleBoxFlat.new()
+ style.bg_color = Color("0a1522")
+ style.border_color = Color("bc975f")
+ style.set_border_width_all(1)
+ style.set_corner_radius_all(7)
+ style.set_content_margin_all(14)
+ panel.add_theme_stylebox_override("panel", style)
+ var contents := VBoxContainer.new()
+ contents.add_theme_constant_override("separation", 8)
+ panel.add_child(contents)
+ contents.add_child(_label("%s ×%d" % [RECIPES.DATA[recipe_id]["name"], RECIPES.DATA[recipe_id]["amount"]], 13, Color("efc681")))
+ contents.add_child(HSeparator.new())
+ for mineral in RECIPES.DATA[recipe_id]["cost"]:
+ var needed := int(RECIPES.DATA[recipe_id]["cost"][mineral])
+ var owned := int(GameState.captain.minerals.get(mineral, 0))
+ var row := HBoxContainer.new()
+ row.add_theme_constant_override("separation", 10)
+ contents.add_child(row)
+ var icon := TextureRect.new()
+ icon.texture = RECIPES.icon(mineral)
+ icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
+ icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
+ icon.custom_minimum_size = Vector2(28, 28)
+ row.add_child(icon)
+ var line := _label("%s %d / %d" % [RECIPES.NAMES[mineral], owned, needed], 12, Color("85deb1") if owned >= needed else Color("f08087"))
+ line.horizontal_alignment = HORIZONTAL_ALIGNMENT_LEFT
+ row.add_child(line)
+ contents.add_child(_label("Kliknij, aby wytworzyć" if inventory.can_craft(recipe_id) else "Zdobądź brakujące surowce z asteroid", 10, Color("8ea9bc")))
+ return panel
diff --git a/scripts/ui/crafting_recipe_button.gd.uid b/scripts/ui/crafting_recipe_button.gd.uid
new file mode 100644
index 0000000..5685749
--- /dev/null
+++ b/scripts/ui/crafting_recipe_button.gd.uid
@@ -0,0 +1 @@
+uid://dbhmk2a2i1r8m
diff --git a/scripts/ui/equipment_slot_view.gd b/scripts/ui/equipment_slot_view.gd
index 8f97932..f0e354b 100644
--- a/scripts/ui/equipment_slot_view.gd
+++ b/scripts/ui/equipment_slot_view.gd
@@ -407,10 +407,14 @@ func _category_name(category: int) -> String:
return "KOMPUTER"
ItemDefinition.Category.ACTIVE_EXTENSION:
return "ROZSZERZENIE"
+ ItemDefinition.Category.CONSUMABLE:
+ return "ZUŻYWALNY"
return "PRZEDMIOT"
func _stat_text(item: ItemInstance) -> String:
var definition := item.definition
+ if definition.category == ItemDefinition.Category.CONSUMABLE:
+ return "[R] +10% energii"
if definition.category == ItemDefinition.Category.WEAPON:
return "%s · Poz. %d" % [item.get_damage_range_text(), item.item_level]
if definition.primary_stat.is_empty():
diff --git a/scripts/ui/ship_crafting_panel.gd b/scripts/ui/ship_crafting_panel.gd
index 61b625d..5cbba8c 100644
--- a/scripts/ui/ship_crafting_panel.gd
+++ b/scripts/ui/ship_crafting_panel.gd
@@ -1,80 +1,79 @@
extends PanelContainer
+const RECIPES := preload("res://scripts/items/crafting_recipes.gd")
@export var standalone := false
var _inventory: ShipInventory
-var _expanded := false
-var _toggle: Button
-var _details: VBoxContainer
-var _parts: Label
-var _stock: Label
-var _ammo: Button
-var _gamma: Button
-var _missile: Button
+var _resource_cards: Dictionary = {}
+var _recipe_buttons: Dictionary = {}
+var _status: Label
func _ready() -> void:
add_theme_stylebox_override("panel", StyleBoxEmpty.new())
_inventory = get_tree().get_first_node_in_group("ship_inventory") as ShipInventory
var contents := VBoxContainer.new()
- contents.name = "Contents"
- contents.add_theme_constant_override("separation", 6)
+ contents.add_theme_constant_override("separation", 14)
add_child(contents)
- _toggle = Button.new()
- _toggle.name = "CreateToggle"
- _toggle.text = "TWÓRZ ▾"
- _toggle.custom_minimum_size.y = 28
- _toggle.add_theme_font_size_override("font_size", 12)
- preload("res://scripts/ui/hud_button_style.gd").apply(_toggle, Color("efb568"))
- _toggle.pressed.connect(_toggle_expanded)
- contents.add_child(_toggle)
- _details = VBoxContainer.new()
- _details.name = "CraftingDetails"
- _details.add_theme_constant_override("separation", 6)
- contents.add_child(_details)
- _parts = _label(Color("efb568"))
- _stock = _label(Color("91dbea"))
- _ammo = _button("CraftAmmunition", "+%d AMUNICJI · %.0f CZĘŚCI" % [ShipInventory.AMMUNITION_CRAFT_AMOUNT, ShipInventory.AMMUNITION_CRAFT_PARTS_COST])
- _ammo.tooltip_text = "Tworzy amunicję do działka maszynowego."
- _ammo.pressed.connect(func() -> void: _inventory.craft_ammunition())
- _missile = _button("CraftMissile", "+1 RAKIETA · %.0f CZĘŚCI" % ShipInventory.MISSILE_CRAFT_PARTS_COST)
- _missile.tooltip_text = "Tworzy rakietę do dowolnej wyrzutni."
- _missile.pressed.connect(func() -> void: _inventory.craft_missile())
- _gamma = _button("CraftGamma", "+10 NAPROMIENIOWANEJ · 1 URAN + 10 CZĘŚCI")
- _gamma.pressed.connect(func() -> void: _inventory.craft_irradiated_ammunition())
- _details.visible = standalone
- _toggle.visible = not standalone
- if not standalone and bool(GameState.hud_panel_state.get("crafting_expanded", false)):
- _toggle_expanded()
+ contents.add_child(_heading("SUROWCE Z ASTEROID", Color("8faabc")))
+ var minerals := HBoxContainer.new()
+ minerals.name = "Minerals"
+ minerals.add_theme_constant_override("separation", 10)
+ contents.add_child(minerals)
+ for mineral in RECIPES.MINERALS:
+ var card := PanelContainer.new()
+ card.set_script(preload("res://scripts/ui/inventory_resource_card.gd"))
+ card.name = mineral.to_pascal_case()
+ card.set("resource_name", RECIPES.NAMES[mineral])
+ card.set("description", "Wydobywany z asteroid. Składnik receptur warsztatu.")
+ card.set("accent", RECIPES.COLORS[mineral])
+ card.set("icon_texture", RECIPES.icon(mineral))
+ minerals.add_child(card)
+ card.custom_minimum_size = Vector2(120, 52)
+ _resource_cards[mineral] = card
+ contents.add_child(HSeparator.new())
+ var heading := HBoxContainer.new()
+ contents.add_child(heading)
+ var title := _heading("PRODUKCJA", Color("efc681"))
+ title.size_flags_horizontal = Control.SIZE_EXPAND_FILL
+ heading.add_child(title)
+ heading.add_child(_heading("NAJEDŹ NA IKONĘ, ABY ZOBACZYĆ KOSZT", Color("7893a6"), 9))
+ var recipes := HBoxContainer.new()
+ recipes.name = "Recipes"
+ recipes.add_theme_constant_override("separation", 10)
+ contents.add_child(recipes)
+ for id in RECIPES.IDS:
+ var button := Button.new()
+ button.set_script(preload("res://scripts/ui/crafting_recipe_button.gd"))
+ button.name = id.to_pascal_case()
+ button.set("recipe_id", id)
+ button.set("inventory", _inventory)
+ recipes.add_child(button)
+ button.pressed.connect(_craft.bind(id))
+ _recipe_buttons[id] = button
+ _status = _heading("1 KLIKNIĘCIE = 1 RECEPTURA • PRODUKCJA BEZPOŚREDNIO Z MINERAŁÓW", Color("7893a6"), 9)
+ _status.custom_minimum_size.y = 24
+ contents.add_child(_status)
+ _inventory.inventory_changed.connect(_refresh)
_inventory.ammunition_changed.connect(_refresh)
GameState.captain.changed.connect(_refresh)
_refresh()
-func _label(color: Color) -> Label:
+func _heading(caption: String, color: Color, font_size: int = 11) -> Label:
var label := Label.new()
- label.add_theme_font_size_override("font_size", 10)
+ label.text = caption
+ label.add_theme_font_size_override("font_size", font_size)
label.add_theme_color_override("font_color", color)
- _details.add_child(label)
return label
-func _button(node_name: String, caption: String) -> Button:
- var button := Button.new()
- button.name = node_name
- button.text = caption
- button.custom_minimum_size.y = 32
- button.add_theme_font_size_override("font_size", 10)
- button.focus_mode = Control.FOCUS_NONE
- _details.add_child(button)
- return button
-
-func _toggle_expanded() -> void:
- _expanded = not _expanded
- GameState.hud_panel_state["crafting_expanded"] = _expanded
- _details.visible = _expanded
- _toggle.text = "TWÓRZ ▴" if _expanded else "TWÓRZ ▾"
+func _craft(id: String) -> void:
+ if _inventory.craft(id):
+ _status.text = "WYTWORZONO: %s ×%d" % [RECIPES.DATA[id]["name"], RECIPES.DATA[id]["amount"]]
+ _status.add_theme_color_override("font_color", Color("85deb1"))
+ else:
+ _status.text = "BRAKUJE SUROWCÓW — NAJEDŹ NA RECEPTURĘ, ABY SPRAWDZIĆ KOSZT"
+ _status.add_theme_color_override("font_color", Color("f08087"))
func _refresh() -> void:
- _parts.text = "DOSTĘPNE CZĘŚCI: %.1f" % GameState.captain.parts
- _stock.text = "AMUNICJA: %d / RAKIETY: %d" % [_inventory.ammunition, _inventory.missile_ammunition]
- _ammo.disabled = not _inventory.can_craft_ammunition()
- _gamma.disabled = not _inventory.can_craft_irradiated_ammunition()
- _stock.text += "\nNAPROMIENIOWANA: %d · URAN: %d" % [_inventory.irradiated_ammunition, int(GameState.captain.minerals.get("uranium", 0))]
- _missile.disabled = not _inventory.can_craft_missile()
+ for mineral in _resource_cards:
+ _resource_cards[mineral].call("set_amount", float(GameState.captain.minerals.get(mineral, 0)))
+ for button in _recipe_buttons.values():
+ button.call("refresh")
diff --git a/scripts/ui/ship_inventory_panel.gd b/scripts/ui/ship_inventory_panel.gd
index 06378f3..18eee30 100644
--- a/scripts/ui/ship_inventory_panel.gd
+++ b/scripts/ui/ship_inventory_panel.gd
@@ -32,7 +32,7 @@ func _ready() -> void:
_refresh_title()
func _refresh_title() -> void:
- for key in ["iron", "titanium", "aluminium", "uranium"]:
+ for key in ["iron", "titanium", "aluminium", "uranium", "sulfur"]:
_resource_cards[key].set_amount(float(GameState.captain.minerals.get(key, 0)))
_resource_cards["parts"].set_amount(GameState.captain.parts)
if _inventory:
@@ -40,6 +40,7 @@ func _refresh_title() -> void:
_resource_cards["ammunition"].set_amount(_inventory.ammunition)
_resource_cards["irradiated_ammunition"].set_amount(_inventory.irradiated_ammunition)
_resource_cards["missile"].set_amount(_inventory.get_missile_count())
+ _resource_cards["energy_bank"].set_amount(_inventory.get_energy_bank_count())
func _create_resource_section() -> void:
var frame := PanelContainer.new()
@@ -63,13 +64,15 @@ func _create_resource_section() -> void:
["titanium", "Tytan", "Surowiec pozyskiwany z asteroid.", Color("9ecbe9")],
["aluminium", "Aluminium", "Surowiec pozyskiwany z asteroid.", Color("dce8ef")],
["uranium", "Uran", "Surowiec z asteroid. Do produkcji napromieniowanej amunicji.", Color("a9ef83")],
- ["parts", "Części", "Odzyskiwane z przedmiotów i wraków. Do produkcji amunicji i napraw.", Color("efb568")],
+ ["sulfur", "Siarka", "Surowiec z asteroid. Do produkcji amunicji i rakiet.", Color("f4d466")],
]:
_add_resource_card(minerals, data)
- var ammunition := _resource_row(section, "AMUNICJA", Color("91dbea"))
+ var ammunition := _resource_row(section, "ZAPASY", Color("91dbea"))
for data in [
+ ["parts", "Części", "Wytwarzane z minerałów, odzyskiwane z przedmiotów i wraków. Do napraw.", Color("efb568")],
["ammunition", "Zwykła amunicja", "Pociski do działka maszynowego. Obrażenia kinetyczne.", Color("efb568")],
["irradiated_ammunition", "Napromieniowana amunicja", "Kapsuły do działka gamma. Obrażenia radiacyjne.", Color("a9ef83")],
+ ["energy_bank", "Powerbanki", "Przywracają 10% maksymalnej energii. Użyj klawiszem R.", Color("70f5df")],
["missile", "Rakiety", "Amunicja do wyrzutni rakiet. Obrażenia kinetyczne.", Color("ffac8b")],
]:
_add_resource_card(ammunition, data)
@@ -98,7 +101,7 @@ func _add_resource_card(row: HBoxContainer, data: Array) -> void:
card.set("resource_name", data[1])
card.set("description", data[2])
card.set("accent", data[3])
- card.set("icon_texture", load("res://assets/icons/resources/%s.png" % data[0]))
+ card.set("icon_texture", preload("res://scripts/items/crafting_recipes.gd").icon(data[0]))
row.add_child(card)
_resource_cards[data[0]] = card
diff --git a/tests/abilities/missile_system_test.gd b/tests/abilities/missile_system_test.gd
index 60ff70c..d458bd4 100644
--- a/tests/abilities/missile_system_test.gd
+++ b/tests/abilities/missile_system_test.gd
@@ -49,8 +49,8 @@ func _ready() -> void:
assert(inventory.weapon_slots[2] == missile and ship.get("energy") == energy_before)
GameState.captain.parts = 49.0
assert(not inventory.craft_missile())
- GameState.captain.parts = 50.0
- assert(inventory.craft_missile() and GameState.captain.parts == 0.0)
+ GameState.captain.minerals.merge({"iron": 1, "sulfur": 1, "uranium": 1}, true)
+ assert(inventory.craft_missile() and GameState.captain.parts == 49.0)
var unguided := ItemInstance.new()
unguided.definition = ShipInventory.MISSILE_DEFINITIONS[1]
assert(inventory.add_item(unguided))
@@ -86,7 +86,8 @@ func _ready() -> void:
if inventory.cargo[index] == null:
inventory.cargo[index] = missile
var ammo_before := inventory.missile_ammunition
- assert(inventory.craft_missile() and GameState.captain.parts == 50.0)
+ GameState.captain.minerals.merge({"iron": 1, "sulfur": 1, "uranium": 1}, true)
+ assert(inventory.craft_missile() and GameState.captain.parts == 100.0)
assert(inventory.buy_missile() and GameState.captain.credits == 70)
assert(inventory.missile_ammunition == ammo_before + 2)
addon.value = 100.0
diff --git a/tests/items/crafting_recipes_test.gd b/tests/items/crafting_recipes_test.gd
new file mode 100644
index 0000000..f579c7f
--- /dev/null
+++ b/tests/items/crafting_recipes_test.gd
@@ -0,0 +1,79 @@
+extends Node2D
+
+const PLAYER := preload("res://scenes/game/player_ship.tscn")
+const RECIPES := preload("res://scripts/items/crafting_recipes.gd")
+
+func _ready() -> void:
+ GameState.start_new_run()
+ var ship := PLAYER.instantiate() as CharacterBody2D
+ ship.name = "PlayerShip"
+ add_child(ship)
+ ship.set_physics_process(false)
+ ship.get_node("WeaponController").set_physics_process(false)
+ var inventory := ship.get_node("ShipInventory") as ShipInventory
+ var cargo_item := ItemInstance.new()
+ cargo_item.definition = preload("res://resources/items/plasma_cannon.tres")
+ inventory.cargo.fill(cargo_item)
+ GameState.captain.parts = 100
+ GameState.captain.credits = 500
+ for id in RECIPES.IDS:
+ var cost: Dictionary = RECIPES.DATA[id]["cost"]
+ # Each missing ingredient must block the whole transaction.
+ for missing in cost:
+ GameState.captain.minerals.clear()
+ for key in RECIPES.MINERALS:
+ GameState.captain.minerals[key] = 5
+ GameState.captain.minerals[missing] = 0
+ var before := GameState.captain.minerals.duplicate()
+ var stock_before := inventory.get_crafted_stock(id)
+ assert(not inventory.craft(id))
+ assert(GameState.captain.minerals == before)
+ assert(inventory.get_crafted_stock(id) == stock_before)
+ for key in RECIPES.MINERALS:
+ GameState.captain.minerals[key] = 5
+ var stock_before := inventory.get_crafted_stock(id)
+ assert(inventory.craft(id))
+ assert(inventory.get_crafted_stock(id) == stock_before + int(RECIPES.DATA[id]["amount"]))
+ for key in RECIPES.MINERALS:
+ assert(GameState.captain.minerals[key] == 5 - int(cost.get(key, 0)))
+ assert(inventory.get_cargo_used() == ShipInventory.CARGO_SIZE)
+ assert(GameState.captain.credits == 500)
+ assert(not inventory.craft("unknown"))
+ # Sulfur is obtainable from an asteroid and uses the normal mining reward path.
+ var asteroid := StaticBody2D.new()
+ asteroid.set_script(preload("res://scripts/levels/exploration_poi.gd"))
+ asteroid.set("kind", "asteroid")
+ asteroid.set("poi_id", "crafting_sulfur_test")
+ asteroid.set("player", ship)
+ add_child(asteroid)
+ asteroid.set("_mineral", "sulfur")
+ # Isolate reward verification from the unrelated asteroid debris renderer.
+ asteroid.set("_asteroid_sprite", null)
+ var sulfur_before := int(GameState.captain.minerals["sulfur"])
+ assert(asteroid.call("claim"))
+ assert(int(GameState.captain.minerals["sulfur"]) > sulfur_before)
+ assert(not asteroid.call("claim"))
+ # Exercise the actual workshop buttons and mixed-availability cost tooltip.
+ var panel := PanelContainer.new()
+ panel.set_script(preload("res://scripts/ui/ship_crafting_panel.gd"))
+ add_child(panel)
+ assert(panel.get("_resource_cards").size() == 5)
+ assert(panel.get("_recipe_buttons").size() == 5)
+ GameState.captain.minerals.merge({"titanium": 2, "iron": 0, "aluminium": 1}, true)
+ GameState.captain.emit_changed()
+ var button: Button = panel.get("_recipe_buttons")["parts"]
+ var tooltip: PanelContainer = button.call("_make_custom_tooltip", "")
+ var rows := tooltip.get_child(0)
+ assert(rows.get_child(2).get_child(1).get_theme_color("font_color") == Color("85deb1"))
+ assert(rows.get_child(3).get_child(1).get_theme_color("font_color") == Color("f08087"))
+ tooltip.free()
+ var parts_before := GameState.captain.parts
+ button.pressed.emit()
+ assert(GameState.captain.parts == parts_before)
+ GameState.captain.minerals["iron"] = 1
+ GameState.captain.emit_changed()
+ button.pressed.emit()
+ assert(GameState.captain.parts == parts_before + 1)
+ assert(GameState.captain.minerals["iron"] == 0)
+ print("CRAFTING_RECIPES_TEST: PASS (all recipes, each missing ingredient, exact costs and yields, no partial charge, full cargo, sulfur mining, UI clicks, red/green tooltip)")
+ get_tree().quit()
diff --git a/tests/items/crafting_recipes_test.gd.uid b/tests/items/crafting_recipes_test.gd.uid
new file mode 100644
index 0000000..ab23a7a
--- /dev/null
+++ b/tests/items/crafting_recipes_test.gd.uid
@@ -0,0 +1 @@
+uid://b56kdfxxfw4y3
diff --git a/tests/items/crafting_recipes_test.tscn b/tests/items/crafting_recipes_test.tscn
new file mode 100644
index 0000000..be121ec
--- /dev/null
+++ b/tests/items/crafting_recipes_test.tscn
@@ -0,0 +1,4 @@
+[gd_scene load_steps=2 format=3]
+[ext_resource type="Script" path="res://tests/items/crafting_recipes_test.gd" id="1"]
+[node name="CraftingRecipesTest" type="Node2D"]
+script = ExtResource("1")
diff --git a/tests/items/damage_types_test.gd b/tests/items/damage_types_test.gd
index 46cfd5e..25c13b1 100644
--- a/tests/items/damage_types_test.gd
+++ b/tests/items/damage_types_test.gd
@@ -38,6 +38,7 @@ func _ready() -> void:
assert(ship.absorption == Vector3.ZERO)
GameState.captain.parts = 10
GameState.captain.minerals["uranium"] = 1
+ GameState.captain.minerals["titanium"] = 1
assert(inventory.craft_irradiated_ammunition())
assert(inventory.irradiated_ammunition == 10)
assert(not inventory.craft_irradiated_ammunition())
diff --git a/tests/items/energy_bank_test.gd b/tests/items/energy_bank_test.gd
new file mode 100644
index 0000000..d5f3c4f
--- /dev/null
+++ b/tests/items/energy_bank_test.gd
@@ -0,0 +1,65 @@
+extends Node2D
+
+const PLAYER := preload("res://scenes/game/player_ship.tscn")
+const BANK := preload("res://resources/items/energy_bank.tres")
+
+func _ready() -> void:
+ GameState.start_new_run()
+ var ship := PLAYER.instantiate() as CharacterBody2D
+ ship.name = "PlayerShip"
+ add_child(ship)
+ ship.set_physics_process(false)
+ ship.get_node("WeaponController").set_physics_process(false)
+ var inventory := ship.get_node("ShipInventory") as ShipInventory
+ inventory.cargo.fill(null)
+ var item := ItemInstance.new()
+ item.definition = BANK
+ var pickup := Node2D.new()
+ pickup.set_script(preload("res://scripts/items/loot_pickup.gd"))
+ pickup.set("item", item)
+ add_child(pickup)
+ pickup.set_physics_process(false)
+ var equipment := ItemInstance.new()
+ equipment.definition = preload("res://resources/items/plasma_cannon.tres")
+ inventory.cargo.fill(equipment)
+ pickup.call("_physics_process", 0.1)
+ assert(pickup.is_queued_for_deletion())
+ assert(inventory.get_cargo_used() == ShipInventory.CARGO_SIZE)
+ assert(inventory.get_energy_bank_count() == 1)
+ assert(not inventory.use_energy_bank())
+ assert(inventory.get_energy_bank_count() == 1)
+ ship.set("max_energy", 250.0)
+ ship.set("energy", 100.0)
+ assert(inventory.use_energy_bank())
+ assert(is_equal_approx(float(ship.get("energy")), 125.0))
+ assert(inventory.get_energy_bank_count() == 0)
+ assert(not inventory.use_energy_bank())
+ assert(inventory.add_item(item))
+ ship.set("energy", 240.0)
+ ship.set("is_docked", true)
+ assert(not inventory.use_energy_bank())
+ ship.set("is_docked", false)
+ var press := InputEventAction.new()
+ press.action = "use_energy_bank"
+ press.pressed = true
+ ship.call("_unhandled_key_input", press)
+ assert(is_equal_approx(float(ship.get("energy")), 250.0))
+ assert(inventory.get_energy_bank_count() == 0)
+ assert(inventory.add_item(item))
+ inventory.preserve_for_restart()
+ assert(GameState.restart_inventory["energy_banks"] == 1)
+ assert(GameState.restart_inventory["cargo"][0].definition != BANK)
+ # Old slot-based banks are converted once when restoring a ship.
+ GameState.restart_inventory["cargo"][0] = item
+ var restored := PLAYER.instantiate() as CharacterBody2D
+ restored.name = "RestoredShip"
+ add_child(restored)
+ restored.set_physics_process(false)
+ restored.get_node("WeaponController").set_physics_process(false)
+ var restored_inventory := restored.get_node("ShipInventory") as ShipInventory
+ assert(restored_inventory.get_energy_bank_count() == 2)
+ assert(restored_inventory.cargo[0] == null)
+ assert(restored_inventory.get_cargo_used() == ShipInventory.CARGO_SIZE - 1)
+ assert(is_equal_approx(preload("res://resources/items/gamma_cannon.tres").weapon_range, 400.0))
+ print("ENERGY_BANK_TEST: PASS (pickup, full cargo, 10% maximum, cap, full energy, empty cargo, docked, R, persistence, gamma range)")
+ get_tree().quit()
diff --git a/tests/items/energy_bank_test.gd.uid b/tests/items/energy_bank_test.gd.uid
new file mode 100644
index 0000000..39b2c37
--- /dev/null
+++ b/tests/items/energy_bank_test.gd.uid
@@ -0,0 +1 @@
+uid://u1hwaxl3xwje
diff --git a/tests/items/energy_bank_test.tscn b/tests/items/energy_bank_test.tscn
new file mode 100644
index 0000000..252b535
--- /dev/null
+++ b/tests/items/energy_bank_test.tscn
@@ -0,0 +1,4 @@
+[gd_scene load_steps=2 format=3]
+[ext_resource type="Script" path="res://tests/items/energy_bank_test.gd" id="1"]
+[node name="EnergyBankTest" type="Node2D"]
+script = ExtResource("1")
diff --git a/tests/items/weapon_damage_test.gd b/tests/items/weapon_damage_test.gd
index f116a5d..81656fa 100644
--- a/tests/items/weapon_damage_test.gd
+++ b/tests/items/weapon_damage_test.gd
@@ -88,26 +88,27 @@ func _ready() -> void:
GameState.captain.parts = 49.0
assert(not inventory.craft_ammunition())
assert(inventory.ammunition == 0 and GameState.captain.parts == 49.0)
- GameState.captain.parts = 50.0
+ GameState.captain.minerals["titanium"] = 1
+ GameState.captain.minerals["sulfur"] = 1
assert(inventory.craft_ammunition())
- assert(inventory.ammunition == 100 and GameState.captain.parts == 0.0)
+ assert(inventory.ammunition == 10 and GameState.captain.parts == 49.0)
GameState.system_level = 3
var buy_cost := ShipInventory.AMMUNITION_BUY_BASE_COST * 3
assert(inventory.get_ammunition_buy_cost() == buy_cost)
GameState.captain.credits = buy_cost - 1
assert(not inventory.buy_ammunition())
- assert(inventory.ammunition == 100 and GameState.captain.credits == buy_cost - 1)
+ assert(inventory.ammunition == 10 and GameState.captain.credits == buy_cost - 1)
GameState.captain.credits = buy_cost * 2
assert(inventory.buy_ammunition())
- assert(inventory.ammunition == 200 and GameState.captain.credits == buy_cost)
+ assert(inventory.ammunition == 110 and GameState.captain.credits == buy_cost)
assert(inventory.buy_ammunition())
- assert(inventory.ammunition == 300 and GameState.captain.credits == 0)
+ assert(inventory.ammunition == 210 and GameState.captain.credits == 0)
assert(not inventory.buy_ammunition())
GameState.system_level = 1
inventory.preserve_for_restart()
var restored_player := PLAYER_SCENE.instantiate() as CharacterBody2D
add_child(restored_player)
- assert(restored_player.get_node("ShipInventory").get("ammunition") == 300)
+ assert(restored_player.get_node("ShipInventory").get("ammunition") == 210)
restored_player.queue_free()
var target := StaticBody2D.new()
target.position = Vector2(200, -28)
diff --git a/tests/previews/legendary_loot_preview.gd b/tests/previews/legendary_loot_preview.gd
new file mode 100644
index 0000000..dacdcbc
--- /dev/null
+++ b/tests/previews/legendary_loot_preview.gd
@@ -0,0 +1,23 @@
+extends "res://scripts/game/game.gd"
+
+func _ready() -> void:
+ super._ready()
+ # Keep the demonstration safe and the drops outside the attraction radius.
+ player.set_physics_process(false)
+ player.get_node("WeaponController").set_physics_process(false)
+ for enemy in get_tree().get_nodes_in_group("enemies"):
+ enemy.set_physics_process(false)
+ for index in 2:
+ var item := ItemInstance.new()
+ item.definition = preload("res://resources/items/plasma_cannon.tres")
+ for addon in (5 if index == 1 else 3):
+ var modifier := ItemStatModifier.new()
+ modifier.stat_key = "damage_percent"
+ modifier.value = 1.0
+ item.additional_modifiers.append(modifier)
+ var pickup := Node2D.new()
+ pickup.set_script(preload("res://scripts/items/loot_pickup.gd"))
+ pickup.set("item", item)
+ pickup.position = player.position + Vector2(-190 if index == 0 else 190, -70)
+ add_child(pickup)
+ (pickup.get("_label") as Label).text = "3 ADDONY" if index == 0 else "5 ADDONÓW • MAKSIMUM"
diff --git a/tests/previews/legendary_loot_preview.gd.uid b/tests/previews/legendary_loot_preview.gd.uid
new file mode 100644
index 0000000..d8076d4
--- /dev/null
+++ b/tests/previews/legendary_loot_preview.gd.uid
@@ -0,0 +1 @@
+uid://d4gta8p1xmjma
diff --git a/tests/previews/legendary_loot_preview.tscn b/tests/previews/legendary_loot_preview.tscn
new file mode 100644
index 0000000..ed3115e
--- /dev/null
+++ b/tests/previews/legendary_loot_preview.tscn
@@ -0,0 +1,5 @@
+[gd_scene load_steps=3 format=3]
+[ext_resource type="PackedScene" path="res://scenes/game/game.tscn" id="1"]
+[ext_resource type="Script" path="res://tests/previews/legendary_loot_preview.gd" id="2"]
+[node name="LegendaryLootPreview" instance=ExtResource("1")]
+script = ExtResource("2")
diff --git a/tests/ships/captain_ship_drawer_test.gd b/tests/ships/captain_ship_drawer_test.gd
index caf5a8e..9a9c3b4 100644
--- a/tests/ships/captain_ship_drawer_test.gd
+++ b/tests/ships/captain_ship_drawer_test.gd
@@ -51,7 +51,7 @@ func _ready() -> void:
await get_tree().process_frame
await get_tree().process_frame
assert(crafting._details.visible and drawer.size.y > closed_height)
- GameState.captain.parts = ShipInventory.AMMUNITION_CRAFT_PARTS_COST + ShipInventory.MISSILE_CRAFT_PARTS_COST
+ GameState.captain.minerals.merge({"titanium": 1, "sulfur": 2, "iron": 1, "uranium": 1}, true)
GameState.captain.emit_changed()
var inv: ShipInventory = ship.get_node("ShipInventory")
var ammo_before := inv.ammunition