This commit is contained in:
Tomasz Boruc 2026-10-16 20:24:12 +02:00
parent e857aa8181
commit 85ba677339
42 changed files with 723 additions and 151 deletions

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 128 128"><defs><linearGradient id="gold" x2=".8" y2="1"><stop stop-color="#fff3b0"/><stop offset=".45" stop-color="#efb83e"/><stop offset="1" stop-color="#966018"/></linearGradient><linearGradient id="face" x2="1" y2="1"><stop stop-color="#644725"/><stop offset="1" stop-color="#241e22"/></linearGradient></defs><path d="M32 18h64l22 23v49l-22 23H32L10 90V41z" fill="#52351f" stroke="#1a1821" stroke-width="5"/><path d="M32 11h64l22 23v49l-22 23H32L10 83V34z" fill="url(#gold)" stroke="#ffe491" stroke-width="2"/><path d="M37 23h54l15 17v37L90 94H38L22 77V40z" fill="url(#face)" stroke="#ffdb73" stroke-width="2"/><path d="M82 42a23 23 0 1 0 0 34" fill="none" stroke="#ffdf7a" stroke-width="9"/><path d="M61 30v58m12-58v58" stroke="#ffdf7a" stroke-width="4"/><path d="M29 37l7-7m56 0 7 7M29 80l7 7m56 0 7-7" stroke="#fff3bc" stroke-width="3"/></svg>

After

Width:  |  Height:  |  Size: 927 B

View File

@ -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

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 128 128"><defs><linearGradient id="shell" x2="1" y2="1"><stop stop-color="#60838f"/><stop offset=".45" stop-color="#203644"/><stop offset="1" stop-color="#0b1827"/></linearGradient><linearGradient id="charge" x2="0" y2="1"><stop stop-color="#c1fff0"/><stop offset="1" stop-color="#18b8a9"/></linearGradient></defs><path d="M50 8h28v16H50z" fill="#99b3bd" stroke="#172937" stroke-width="4"/><path d="M32 20h64l10 12v76l-10 12H32l-10-12V32z" fill="url(#shell)" stroke="#79dedc" stroke-width="3"/><path d="M34 30h60v78H34z" fill="#081a28" stroke="#43616b" stroke-width="2"/><path d="M40 39h48v58H40z" fill="#165c60"/><path d="M68 34L46 70h17l-5 28 25-39H66z" fill="url(#charge)"/><path d="M40 104h12m6 0h12m6 0h12" stroke="#6fffe0" stroke-width="4"/><path d="M28 36v60m72-60v60" stroke="#9bbac2" stroke-width="2"/><circle cx="34" cy="27" r="2" fill="#e0fffa"/><circle cx="94" cy="113" r="2" fill="#e0fffa"/></svg>

After

Width:  |  Height:  |  Size: 988 B

View File

@ -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

View File

@ -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."
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

View File

@ -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

Binary file not shown.

View File

@ -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."

View File

@ -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

View File

@ -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

View File

@ -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] = ["", "", "", "", ""]

View File

@ -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

View File

@ -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:

View File

@ -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:

View File

@ -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

View File

@ -0,0 +1 @@
uid://ekvidemqdxmy

View File

@ -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")):

View File

@ -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:

View File

@ -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()

View File

@ -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

View File

@ -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()

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -0,0 +1 @@
uid://dbhmk2a2i1r8m

View File

@ -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():

View File

@ -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")

View File

@ -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

View File

@ -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

View File

@ -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()

View File

@ -0,0 +1 @@
uid://b56kdfxxfw4y3

View File

@ -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")

View File

@ -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())

View File

@ -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()

View File

@ -0,0 +1 @@
uid://u1hwaxl3xwje

View File

@ -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")

View File

@ -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)

View File

@ -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"

View File

@ -0,0 +1 @@
uid://d4gta8p1xmjma

View File

@ -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")

View File

@ -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